更安全的purrr::map2函数用于名称顺序不一致的列表

10

这是一个我之前在我的代码中编写了故障保护的问题,但我想知道是否有一些更简单的方法被我忽略了。

有时我有2个(或更多)包含不同类型信息的列表,需要使用 map2 这样的函数协同工作——例如,一个命名的 ggplot 对象列表和一个保存每个对象输出文件路径的命名列表。是否有一种内置或轻松添加到管道工作流的方法,以确保列表项通过名称而不是位置进行匹配?

考虑一个简单的例子:

library(purrr)

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

map2 返回与 第一个 列表相同的名称列表,并按位置进行迭代。因此,odds 中项目 bd 的交换未得到解决,这两个调用的结果不同:

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"

我过去所做的是使用imap,并使用第一个列表的名称提取另一个列表中适当的项目,但这意味着不再在我的函数参数中具有第二个列表:

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"
如果我想感觉自己在两个列表上运作更加平稳,我可以按名称对它们进行排序,但这种方式感觉很笨重:
map2(
  evens[order(names(evens))],
  odds[order(names(odds))],
  function(l1, l2) paste(l1, l2)
)
# same output as previous

或者更加笨重的方法是,先将这两个列表组成一个新列表,然后对这个新列表再进行排序,并使用另一个map函数。最后,将结果传递给pmap函数,因为它需要一个列表的列表作为输入:

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

理想情况下,我希望将imap选项的安全性与map2的整洁性结合起来。


也许可以使用类似 transpose(lst(odds, evens)) 的方法,并在其上使用标准的 map 函数?或者,如果这符合您的用例,则将路径设置为 ggplot 对象的属性。 - moodymudskipper
5个回答

5
我们可以做。
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"

如果两个列表的名称都是无序的,则循环遍历其中一个列表中排序后的名称,提取两个元素并将它们连接起来。

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

或者为 order 创建一个标识符,然后对 list 中的元素和 map2 进行排序并连接。

i1 <- order(names(evens)) # not sure if this should be avoided
map2(evens[i1], odds[i1], str_c, sep=" ")

2
我喜欢只将一个列表子集到另一个列表的名称中的解决方案。如果使用 order,则会得到具有不在另一个列表中的名称的欺骗性结果。 - IceCreamToucan
1
@IceCreamToucan 根据这个例子,有一些基于“假设”的条件。如果例子发生变化,可能会变得有点复杂。 - akrun

5
只需编写一个辅助函数来清理它。
namemap <- function(.x, .y, .f, ...) {
  n <- order(unique(names(.x), names(.y)))
  map2(.x[n], .y[n], .f, ...)
}
namemap(odds, evens, paste)

基本上,在 purrr 中没有原语会自动为您执行此操作。而且当这么容易做到时,似乎没有什么意义。


谢谢,我不知道为什么我经常对编写辅助函数持谨慎态度——不想在 R 脚本的混沌中失去它们。但既然我已经为工作编写了两个实用程序包,这可能是最安全和可持续的方法。 - camille

5

bind_rows函数按名称匹配,因此你可以通过bind_rows函数将列表合并,然后再使用map函数(但这会对列表中的内容施加额外的限制)。

library(tidyverse)

bind_rows(evens, odds) %>% 
  map(paste, collapse = ' ')

# $`a`
# [1] "2 11"
# 
# $b
# [1] "4 5"
# 
# $c
# [1] "6 7"
# 
# $d
# [1] "8 9"

2

transpose() 似乎是通过名称进行匹配的。尽管它并没有被文档记录(编辑: .names 参数的解释提供了上下文,并且有示例),而且在某些地方文档似乎不准确(purrr v. 0.3.1)。

它被称为 transpose 是因为 x[[1]][[2]] 等价于 transpose(x)[[2]][[1]]

^ 在这种情况下似乎不准确,因为 list(evens, odds)[[2]][[4]]5,而 transpose(list(evens, odds))[[4]][[2]]9

另外

请注意,transpose() 是其自身的逆操作,就像矩阵上的转置操作一样。你可以通过两次转置来恢复原始输入。

并不完全准确,但我们可以利用它:

list(evens, odds) %>% 
  transpose() %>% 
  transpose()
#> [[1]]
#> [[1]]$a
#> [1] 2
#> 
#> [[1]]$b
#> [1] 4
#> 
#> [[1]]$c
#> [1] 6
#> 
#> [[1]]$d
#> [1] 8
#> 
#> 
#> [[2]]
#> [[2]]$a
#> [1] 11
#> 
#> [[2]]$b
#> [1] 5
#> 
#> [[2]]$c
#> [1] 7
#> 
#> [[2]]$d
#> [1] 9

此文档由reprex package (v0.2.1)于2019年04月23日创建

OP的第一个示例 ("想象一下一个ggplot对象的命名列表和一个保存每个输出文件路径的命名列表.") 可以看起来像:

  list(paths, plots) # or list(filename = paths, plot = plots) to match args of ggsave
  transpose() %>%
  walk(lift(ggsave))

OP的第二个例子可以是:

list(evens = evens, odds = odds) %>% # or tibble::lst(evens, odds) but lst() is in the questioning stage
  transpose() %>% 
  map(lift(paste)) # or map(paste, collapse = " ") 
#> $a
#> [1] "2 11"
#> 
#> $b
#> [1] "4 5"
#> 
#> $c
#> [1] "6 7"
#> 
#> $d
#> [1] "8 9"

创建于2019年4月23日,使用reprex包(v0.2.1)
注意:我没有检查是否存在关于此行为的 Github 问题,并且我也不知道是否有任何可能性改变这一点,或者增加额外的参数以进行更精细的控制。

1
如果列表名称只有部分重叠,可以使用以下修改版的@MrFlick的答案。应用的函数必须忽略NULL参数。
namedmap2 <- function(.x, .y, .f, ...) {
    set <- unique(c(names(.x), names(.y)))
    lst <- map2(.x[set], .y[set], .f, ...)
    names(lst) <- set
    lst
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接