将函数列表应用于值列表

31

参考此问题,我试图找出将函数列表应用于值列表的最简单方法。基本上是嵌套的lapply。例如,这里我们将sdmean应用于内置数据集trees

funs <- list(sd=sd, mean=mean)
sapply(funs, function(x) sapply(trees, x))

获得:

              sd     mean
Girth   3.138139 13.24839
Height  6.371813 76.00000
Volume 16.437846 30.17097

但我希望避免使用内部的function,并想要类似这样的代码:

sapply(funs, sapply, X=trees)

由于X匹配了第一个sapply而不是第二个,所以它不起作用。我们可以使用functional::Curry来解决:

sapply(funs, Curry(sapply, X=trees))

但我希望也许有一种聪明的方法可以使用位置和名称匹配来完成这个任务,而我可能没有想到它。


5
Hadley在这个主题上写了一整章节:http://adv-r.had.co.nz/Functional-programming.html#lists-of-functions,因为我比他聪明不了多少,所以我不知道有什么更好的方法来做到这一点。 - grrgrrbla
如果你想要一个整洁的数据框,这里提供一种不简单但很好的方法:library(purrr) ; map_df(funs, ~map_df(trees, .x), .id = 'statistic') - alistaire
4个回答

34

因为mapply使用省略号...来传递向量(原子或列表),而不是像sapply,lapply等中的命名参数X,所以如果您使用mapply而不是sapply,则不需要命名参数X = trees

funs <- list(sd = sd, mean = mean)

x <- sapply(funs, function(x) sapply(trees, x))

y <- sapply(funs, mapply, trees)

> y
              sd     mean
Girth   3.138139 13.24839
Height  6.371813 76.00000
Volume 16.437846 30.17097
> identical(x, y)
[1] TRUE

你离得到你想要的只有一个字母之遥! :)

请注意,我在funs中使用了列表,因为我无法创建函数数据框,会报错。

> R.version.string
[1] "R version 3.1.3 (2015-03-09)"

3
非常聪明,以后一定会用到;我认为关键特性在于 mapply 恰好接受函数参数作为第一个参数,因此这个方法能够奏效。 - BrodieG

15

你基本上需要一个匿名函数,因为没有其他方法可以区分两个不同的sapply调用中的命名参数。你已经展示了一个显式的匿名函数和Curry方法。你也可以使用magrittr

 library(magrittr)
 sapply(funs, . %>%  sapply(trees, .))
 # or .. funs %>% sapply(. %>%  sapply(trees, .))

但关键是你需要有某些东西来进行拆分。问题在于sapply调用了一个内部函数lapply,该函数似乎决定将更改的值放在函数调用的开头。你需要一些东西来重新排序参数,由于存在相同的参数名称集,无法通过拆分来实现,必须使用辅助函数来处理消歧。

mapply函数允许你向"MoreArgs"传递一个列表,从而绕过命名参数冲突。这旨在区分你应该对其进行向量化和那些固定不变的项。因此,你可以执行以下操作:

mapply(sapply, funs, MoreArgs=list(X=trees))
#               sd     mean
# Girth   3.138139 13.24839
# Height  6.371813 76.00000
# Volume 16.437846 30.17097

1
对于 MoreArgs 的应用很棒。我猜 magrittr 可以是 funs %>% sapply(. %>% sapply(X=trees))?当看到管道中的第一元素是 . 时,肯定会让人有些摸不着头脑。 - BrodieG
1
是的,我也添加了,尽管我认为第一个版本更清晰。不过说实话,我认为最好的方法就是像你第一次那样使用显式匿名函数:sapply(funs, function(x) sapply(trees, x)) - MrFlick
同意;我再次编辑以删除多余的“。”,但不确定是否完全遵循自己的逻辑... - BrodieG
1
@BrodieG 去掉额外的 . 的工作做得很好。它的功能和外观都很“简单”,但我个人认为对于程序员来说,快速阅读和理解它可能会太困难了。sapply 调用自然具有一定的节奏,而这种写法打破了这种节奏。但是每个人都有自己的喜好。 - MrFlick

6

使用purrr的另一种方法是:

require(purrr)

funs <- list(sd=sd, mean=mean)
trees %>% map_df(~invoke_map(funs, ,.), .id="id")

重要提示:注意invoke_map的第二个参数为空,以匹配位置。请参阅?purrr::invoke_map示例。

这样做可以给你提供:

Source: local data frame [3 x 3]

      id        sd     mean
   <chr>     <dbl>    <dbl>
1  Girth  3.138139 13.24839
2 Height  6.371813 76.00000
3 Volume 16.437846 30.17097

与其使用行名,这种方法给你提供了一个包含原始列的列 id


当使用 purrr 0.2.2(以及可能的早期版本,我没有检查)时,需要使用 invoke_map_df() 而不是 invoke_map() 才能获得所示结果。 - egnha
@egnha,这很奇怪。对我来说,使用purrr_0.2.2完全正常。使用invoke_map_df会导致“错误:无法将对象转换为数据框”...你使用的R版本是什么? - Rentrop
这很令人困惑。我正在使用R 3.3.0;在一个新的会话中只加载了purrr(没有被R加载的init文件)。道义上,invoke_map_df是正确的invoke_map*应用程序(并且在我的机器上正常工作),因为map_df通过绑定行来创建数据框(除非我误解了什么)。 - egnha
你使用的dplyr版本是什么?(map_df使用dplyr::bind_rows) - egnha

1

虽然不如@Floo0提出的解决方案那么启发人心,也不够优雅,但这里还是有另一种方法使用tidyrdplyr

library(dplyr)
library(tidyr)

fns <- funs(sd = sd, mean = mean)
trees %>% 
    gather(property, value, everything()) %>% 
    group_by(property) %>% 
    summarise_all(fns)

#   A tibble: 3 x 3
#   property        sd     mean
#      <chr>     <dbl>    <dbl>
# 1    Girth  3.138139 13.24839
# 2   Height  6.371813 76.00000
# 3   Volume 16.437846 30.17097

这些操作的顺序在表达意图方面做得不错,但代价是冗余过多。

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