拆分应用组合 returns 多个变量的函数

Split-apply-combine with function that returns multiple variables

我需要将 myfun 应用于数据框的子集,并将结果作为新列包含在返回的数据框中。在过去,我使用 ddply。但是在 dplyr 中,我相信 summarise 是用来做这个的,像这样:

myfun<- function(x,y) {
  df<- data.frame( a= mean(x)*mean(y), b= mean(x)-mean(y) )           
  return (df)
}

mtcars %>%
  group_by(cyl) %>%
  summarise(a = myfun(cyl,disp)$a, b = myfun(cyl,disp)$b)

上面的代码有效,但是我将使用的 myfun 在计算上非常昂贵,所以我希望它 只被调用一次 而不是单独调用ab 列。在 dplyr 中有没有办法做到这一点?

因为你的函数 returns 是一个数据框,你可以在 group_by %>% do 中调用你的函数,它将函数应用到每个单独的组和 rbind 返回的数据框在一起:

mtcars %>% group_by(cyl) %>% do(myfun(.$cyl, .$disp))

# A tibble: 3 x 3
# Groups:   cyl [3]
#    cyl         a         b
#  <dbl>     <dbl>     <dbl>
#1     4  420.5455 -101.1364
#2     6 1099.8857 -177.3143
#3     8 2824.8000 -345.1000

我们可以使用data.table

library(data.table)
setDT(mtcars)[, myfun(cyl, disp), cyl] 
#    cyl         a         b
#1:   6 1099.8857 -177.3143
#2:   4  420.5455 -101.1364
#3:   8 2824.8000 -345.1000

do不一定会提高速度。在此 post 中,我将介绍一种设计执行相同任务的函数的方法,然后进行基准测试以比较每种方法的性能。

这是定义函数的另一种方法。

myfun2 <- function(dt, x, y){
  x <- enquo(x)
  y <- enquo(y)

  dt2 <- dt %>%
    summarise(a = mean(!!x) * mean(!!y), b = mean(!!x) - mean(!!y))
  return(dt2)
}

注意myfun2的第一个参数是dt,这是输入数据框。通过这样做,myfun2 可以作为管道操作的一部分成功实施。

mtcars %>%
  group_by(cyl) %>%
  myfun2(x = cyl, y = disp)
# A tibble: 3 x 3
    cyl         a         b
  <dbl>     <dbl>     <dbl>
1     4  420.5455 -101.1364
2     6 1099.8857 -177.3143
3     8 2824.8000 -345.1000

通过这样做,我们不必在每次要创建新列时都调用 my_fun。所以这个方法可能比my_fun.

更有效率

这是使用 microbenchmark 的性能比较。我比较的方法如下。我运行模拟1000次

m1: OP's original way to apply `myfun`  
m2: Psidom's method, using `do`to apply `myfun`.  
m3: My approach, using `myfun2`  
m4: Using `do` to apply `myfun2`  
m5: Z.Lin's suggestion, directly calculating the values without defining a function.
m6: akrun's `data.table` approach with `myfun`

这是基准测试代码。

microbenchmark(m1 = (mtcars %>%
                       group_by(cyl) %>%
                       summarise(a = myfun(cyl, disp)$a, b = myfun(cyl, disp)$b)),
               m2 = (mtcars %>% 
                       group_by(cyl) %>% 
                       do(myfun(.$cyl, .$disp))),
               m3 = (mtcars %>%
                       group_by(cyl) %>%
                       myfun2(x = cyl, y = disp)),
               m4 = (mtcars %>%
                       group_by(cyl) %>%
                       do(myfun2(., x = cyl, y = disp))),
               m5 = (mtcars %>% 
                       group_by(cyl) %>% 
                       summarise(a = mean(cyl) * mean(disp), b = mean(cyl) - mean(disp))),
               m6 = (as.data.table(mtcars)[, myfun(cyl, disp), cyl]),
               times = 1000)

这是基准测试的结果。

Unit: milliseconds
 expr       min        lq      mean    median        uq        max neval
   m1  7.058227  7.692654  9.429765  8.375190 10.570663  28.730059  1000
   m2  8.559296  9.381996 11.643645 10.500100 13.229285  27.585654  1000
   m3  6.817031  7.445683  9.423832  8.085241 10.415104 193.878337  1000
   m4 21.787298 23.995279 28.920262 26.922683 31.673820 177.004151  1000
   m5  5.337132  5.785528  7.120589  6.223339  7.810686  23.231274  1000
   m6  1.320812  1.540199  1.919222  1.640270  1.935352   7.622732  1000

结果显示 do 方法(m2m4)实际上比对应方法(m1m3)慢。在这种情况下,应用 myfun (m1) 和 myfun2 (m3) 比使用 do 更快。 myfun2 (m3) 比 myfun (m1) 略快。但是,不定义任何函数(m5)实际上比所有函数定义的方法(m1m4)更快,这表明对于这种特殊情况,实际上没有必要定义一个函数。最后,如果没有必要留在tidyverse,或者数据集的大小是巨大的。我们可以考虑 data.table 方法 (m6),它比此处列出的所有 tidyverse 解决方案快得多。