优化`dplyr` group_by /总结

时间:2019-07-18 15:17:34

标签: r optimization dplyr data.table

我正在寻找一种技巧/技术来优化如下操作:

library(dplyr)

n <- 1e5

d <- tibble(x=sample(800, n, TRUE),
            y=sample(2000, n, TRUE) %>% as.Date(origin='1970-01-01'),
            z=sample(5, n, TRUE),
            val=runif(n))

system.time({
  y_dp <- d %>%
    group_by(x, y) %>%
    summarize(w = val[which.max(z)])
})
#     user   system  elapsed 
# 1014.918    9.760 1027.845 

这很漂亮-按2个变量分组,然后基于另外2个变量为每个组计算标量摘要。

data.table能够以这种大小的数据更有效地处理10000倍:

library(data.table)
system.time({
  y_dt <- data.table(d, key=c("x", "y")) %>%
    `[`(, .(w=val[which.max(z)]), by=list(x, y)) %>%
    as_tibble()
})
#    user  system elapsed 
#   0.109   0.003   0.112 

all.equal(y_dt, y_dp)
# TRUE

大概可以通过基于键索引(在这种情况下为排序),然后线性遍历结构来实现这一点; dplyr可能必须为每个组合(x, y)的结构构造单独的索引。

通过(x, y)进行预排序也无法解决dplyr的情况,因为它似乎无法“记住”数据是按照其分组依据进行排序的:

system.time({
  y3 <- d %>%
    arrange(x, y) %>%
    group_by(x, y) %>%
    summarize(w = val[which.max(z)])
})
#     user   system  elapsed 
# 1048.983   13.616 1070.929 

实际上,由于小标题的类和属性在排序后不会改变,因此看来以后没有办法利用排序。

有想法吗?

编辑:当计时实际上是用n <-1e5完成时,我错误地写了n <-5e4,我只是将其固定在编辑中。另外,这是我的规格:

> sessionInfo()
R version 3.6.0 (2019-04-26)
Platform: x86_64-apple-darwin17.7.0 (64-bit)
Running under: macOS High Sierra 10.13.6

Matrix products: default
BLAS/LAPACK: /usr/local/Cellar/openblas/0.3.6_1/lib/libopenblasp-r0.3.6.dylib

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] dplyr_0.8.2

loaded via a namespace (and not attached):
 [1] tidyselect_0.2.5 compiler_3.6.0   magrittr_1.5     assertthat_0.2.1
 [5] R6_2.4.0         pillar_1.4.2     glue_1.3.1       tibble_2.1.3    
 [9] crayon_1.3.4     Rcpp_1.0.1       pkgconfig_2.0.2  rlang_0.4.0     
[13] purrr_0.3.2     

2 个答案:

答案 0 :(得分:3)

这是由版本0.8.2中的回归引起的:

  

https://github.com/tidyverse/dplyr/issues/4458

性能损失是非线性的,因此在第4458期中,它是500倍,在我的示例中是10000倍,在我的真实数据集中,我可能不得不等待宇宙的热死对其进行测量。 / p>

升级到dplyr 0.8.3可以解决我的问题:

> install.packages('dplyr')
# Installing package into ‘/private/tmp/lib’
# ...

n <- 1e5

d <- tibble(x=sample(800, n, TRUE),
            y=sample(2000, n, TRUE) %>% as.Date(origin='1970-01-01'),
            z=sample(5, n, TRUE),
            val=runif(n))
system.time({
  y_dp <- d %>%
    group_by(x, y) %>%
    summarize(w = val[which.max(z)])
})
#   user  system elapsed 
#  0.447   0.050   0.500 

答案 1 :(得分:1)

以下是您的data.table代码的可读性更高的版本。

您可以通过magittr将DT.符号进行管道传输。 另一个细节是您可以使用.(x, y)代替list(x,y)。 排序与聚合中的data.table无关,但与联接有关。

library(data.table)

system.time({
  y_dt <- data.table(d) %>% 
    .[, .(w = val[which.max(z)]), .(x,y)]
    as_tibble()
})

另一个变种,它删除了第一个管道,因此删除了注释中提到的magittr .

system.time({
  y_dt <- as.data.table(d)[, .(w = val[which.max(z)]), .(x,y)] %>%
    as_tibble()
})

请注意,我使用as.data.table(d)代替了setDT,因为这将通过引用更改d

基准:

       user  system elapsed 
dplyr  2.643   0.000   2.642
DT     0.158   0.000   0.092

在此示例中,data.table似乎比dplyr快28倍。