Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Safer purrr::map2 for lists with names out of order

Tags:

r

purrr

This is a question for which I've written failsafes in my code before, but I'm wondering if there's something more straightforward that I've missed.

I sometimes have 2 (or more) lists that contain different types of information that need to work together with a function such as map2—think a named list of ggplot objects and a named list of file paths for saving output of each. Is there a way built-in or easily added to a piped workflow to make sure list items are matched by name rather than by position?

Consider a simple example:

library(purrr)

evens <- list(a = 2, b = 4, c = 6, d = 8)
odds <- list(a = 11, d = 9, c = 7, b = 5)

map2 returns a list with the same names as the first list, and iterates by position. So the fact that items b and d are switched in odds isn't addressed, and these two calls come out with different results:

map2(evens, odds, function(l1, l2) {
  paste(l1, l2)
})
#> $a
#> [1] "2 11"
#> 
#> $b
#> [1] "4 9"
#> 
#> $c
#> [1] "6 7"
#> 
#> $d
#> [1] "8 5"

map2(odds, evens, function(l1, l2) {
  paste(l1, l2)
})
#> $a
#> [1] "11 2"
#> 
#> $d
#> [1] "9 4"
#> 
#> $c
#> [1] "7 6"
#> 
#> $b
#> [1] "5 8"

What I've done in the past is to instead use imap and use the names of the first list to extract the appropriate item in the other list, but that means no longer having that second list in my function arguments:

imap(evens, function(l1, name) {
  paste(l1, odds[[name]])
})
#> $a
#> [1] "2 11"
#> 
#> $b
#> [1] "4 5"
#> 
#> $c
#> [1] "6 7"
#> 
#> $d
#> [1] "8 9"

If I want to feel like I'm operating more evenly over both lists, I could order them each by name, but this feels clunky:

map2(
  evens[order(names(evens))],
  odds[order(names(odds))],
  function(l1, l2) paste(l1, l2)
)
# same output as previous

Or clunkier still, make a list of the two lists, order them each in another map, then pipe that into pmap since it takes a list of lists:

list(evens, odds) %>%
  map(~.[order(names(.))]) %>%
  pmap(function(l1, l2) paste(l1, l2))
# same output as previous

Ideally, I'd like to combine the safety of the imap option with the cleanliness of map2.

like image 234
camille Avatar asked Apr 22 '19 17:04

camille


2 Answers

Just write a helper function to clean it up

namemap <- function(.x, .y, .f, ...) {
  n <- order(unique(names(.x), names(.y)))
  map2(.x[n], .y[n], .f, ...)
}
namemap(odds, evens, paste)

Basically there's no primitive in purrr that will do this automatically for you. And when it's this easy to do, there doesn't seem to be much point.

like image 105
MrFlick Avatar answered Nov 02 '22 22:11

MrFlick


We can do

library(tidyverse)
map2(evens, odds[names(evens)], str_c, sep=' ')
#$a
#[1] "2 11"

#$b
#[1] "4 5"

#$c
#[1] "6 7"

#$d
#[1] "8 9"

If both the list names are unordered, loop through the sorted names of one of the list, extract both the elements and concatenate

map(sort(names(evens)), ~ str_c(evens[[.x]], odds[[.x]], sep= ' '))

Or create an identifier for the order, then order the list elements in both the list and concatenate with map2

i1 <- order(names(evens)) # not sure if this should be avoided
map2(evens[i1], odds[i1], str_c, sep=" ")
like image 5
akrun Avatar answered Nov 02 '22 23:11

akrun