如何按组加速子集

Jul*_*rre 20 performance r dplyr data.table

我曾经用dplyr实现我的数据争论,但有些计算是"慢"的.特别是按组子集,我读到dplyr很慢,当有很多组并且基于这个基准数据时.表可能更快,所以我开始学习data.table.

以下是如何使用250k行和大约230k组重现与我的实际数据接近的内容.我想按id1,id2进行分组,并将max(datetime)每个组的行子集化.

DATAS

# random datetime generation function by Dirk Eddelbuettel
# https://stackoverflow.com/questions/14720983/efficiently-generate-a-random-sample-of-times-and-dates-between-two-dates
rand.datetime <- function(N, st = "2012/01/01", et = "2015/08/05") {
  st <- as.POSIXct(as.Date(st))
  et <- as.POSIXct(as.Date(et))
  dt <- as.numeric(difftime(et,st,unit="sec"))
  ev <- sort(runif(N, 0, dt))
  rt <- st + ev
}

set.seed(42)
# Creating 230000 ids couples
ids <- data.frame(id1 = stringi::stri_rand_strings(23e4, 9, pattern = "[0-9]"), 
                  id2 = stringi::stri_rand_strings(23e4, 9, pattern = "[0-9]"))
# Repeating randomly the ids[1:2000, ] to create groups
ids <- rbind(ids, ids[sample(1:2000, 20000, replace = TRUE), ])
# Adding random datetime variable and dummy variables to reproduce real datas
datas <- transform(ids, 
                   datetime = rand.datetime(25e4), 
                   var1 = sample(LETTERS[1:6], 25e4, rep = TRUE), 
                   var2 = sample(c(1:10, NA), 25e4, rep = TRUE), 
                   var3 = sample(c(1:10, NA), 25e4, rep = TRUE), 
                   var4 = rand.datetime(25e4), 
                   var5 = rand.datetime(25e4))

datas.tbl <- tbl_df(datas)
datas.dt <- data.table(datas, key = c("id1", "id2"))
Run Code Online (Sandbox Code Playgroud)

我无法找到通过data.table分组的直接方式,所以我问了这个问题:按组使用data.table过滤行

我们建议我使用.SD:

datas.dt[, .SD[datetime == max(datetime)], by = c("id1", "id2")]
Run Code Online (Sandbox Code Playgroud)

但我有两个问题,它适用于日期而不是POSIXct("UseMethod中的错误("as.data.table"):没有适用于"as.data.table"的方法应用于类"c"的对象POSIXct','POSIXt')""),这很慢.比如日期:

> system.time({
+   datas.dt[, .SD[as.Date(datetime) == max(as.Date(datetime))], by = c("id1", "id2")]
+ })
 utilisateur     système      écoulé 
      207.03        0.00      207.48 
Run Code Online (Sandbox Code Playgroud)

所以我发现使用data.table实现这一目标(以及保持日期时间)要快得多:

功能

f.dplyr <- function(x) x %>% group_by(id1, id2) %>% filter(datetime == max(datetime))
f.dt.i <- function(x) x[x[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1]
f.dt <- function(x) x[x[, datetime == max(datetime), by = c("id1", "id2")]$V1]
Run Code Online (Sandbox Code Playgroud)

但后来我认为data.table会快得多,与dplyr的时差也没有意义.

微基准

mbm <- microbenchmark(
  dplyr = res1 <- f.dplyr(datas.tbl), 
  data.table.I = res2 <- f.dt.i(datas.dt), 
  data.table = res3 <- f.dt(datas.dt), 
  times = 50L)

Unit: seconds
         expr      min       lq     mean   median       uq      max neval
        dplyr 31.84249 32.24055 32.59046 32.61311 32.88703 33.54226    50
 data.table.I 30.02831 30.94621 31.19660 31.17820 31.42888 32.16521    50
   data.table 30.28923 30.84212 31.09749 31.04851 31.40432 31.96351    50
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

我是否遗漏/误用了data.table?你有想加快这个计算吗?

任何帮助将非常感谢!谢谢


编辑:有关用于微基准测试的系统和软件包版本的一些精确性.(电脑不是战争机器,12Go i5)

系统

sessionInfo()
R version 3.1.3 (2015-03-09)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 7 x64 (build 7601) Service Pack 1

locale:
  [1] LC_COLLATE=French_France.1252  LC_CTYPE=French_France.1252   
[3] LC_MONETARY=French_France.1252 LC_NUMERIC=C                  
[5] LC_TIME=French_France.1252    

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

other attached packages:
  [1] readr_0.1.0          ggplot2_1.0.1        microbenchmark_1.4-2
[4] data.table_1.9.4     dplyr_0.4.1          plyr_1.8.2          

loaded via a namespace (and not attached):
  [1] assertthat_0.1   chron_2.3-45     colorspace_1.2-6 DBI_0.3.1       
[5] digest_0.6.8     grid_3.1.3       gtable_0.1.2     lazyeval_0.1.10 
[9] magrittr_1.5     MASS_7.3-39      munsell_0.4.2    parallel_3.1.3  
[13] proto_0.3-10     Rcpp_0.11.5      reshape2_1.4.1   scales_0.2.4    
[17] stringi_0.4-1    stringr_0.6.2    tools_3.1.3 

> packageVersion("data.table")
[1] ‘1.9.4’
> packageVersion("dplyr")
[1] ‘0.4.1’
Run Code Online (Sandbox Code Playgroud)

Aru*_*run 23

好问题!

我将假设dfdt成为易于/快速输入的对象的名称.

df = datas.tbl
dt = datas.dt
Run Code Online (Sandbox Code Playgroud)

-O3级别优化比较:

首先,这是我的系统在当前CRAN版本dplyr和devel版本上的时间data.table.devel版本dplyr似乎遭受了性能回归(并且由Romain修复).

system.time(df %>% group_by(id1, id2) %>% filter(datetime == max(datetime)))
#  25.291   0.128  25.610 

system.time(dt[dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1])
#  17.191   0.075  17.349 
Run Code Online (Sandbox Code Playgroud)

我跑了好几次,而且似乎改变了.但是,我使用-O3优化标志编译所有包(通过~/.R/Makevars适当设置).而且我观察到data.table性能比我用它比较的其他软件包要好得多-O3.

分组速度比较

其次,了解这种缓慢的原因很重要.首先让我们将时间与组合进行比较.

system.time(group_by(df, id1, id2))
#   0.303   0.007   0.311 
system.time(data.table:::forderv(dt, by = c("id1", "id2"), retGrp = TRUE))
#   0.002   0.000   0.002 
Run Code Online (Sandbox Code Playgroud)

即使总共有250,000行,您的数据大小约为38MB.在这个尺寸下,分组速度不太可能出现明显差异.

data.table>100x这里的分组速度更快,显然不是这种缓慢的原因......

它为什么慢?

那是什么原因?让我们打开datatable.verbose选项并再次检查:

options(datatable.verbose = TRUE)
dt[dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1]
# Detected that j uses these columns: datetime 
# Finding groups (bysameorder=TRUE) ... done in 0.002secs. bysameorder=TRUE and o__ is length 0
# lapply optimization is on, j unchanged as '.I[datetime == max(datetime)]'
# GForce is on, left j unchanged
# Old mean optimization is on, left j unchanged.
# Starting dogroups ... 
#   memcpy contiguous groups took 0.097s for 230000 groups
#   eval(j) took 17.129s for 230000 calls
# done dogroups in 17.597 secs
Run Code Online (Sandbox Code Playgroud)

所以eval(j)单独占用了大约97%的时间!我们j每个组评估了我们提供的表达式.由于你有230,000个团体,而且这个eval()电话会受到惩罚,这就相加了.

避免eval()罚款

既然我们已经知道了这个点球的,我们已经先行开始实施的一些常用功能的内部版本:sum,mean,min,max.这将/应该扩展到尽可能多的其他功能(当我们找到时间).

那么,让我们尝试计算刚刚获得的时间max(datetime):

dt.agg = dt[, .(datetime = max(datetime)), by = .(id1, id2)]
# Detected that j uses these columns: datetime 
# Finding groups (bysameorder=TRUE) ... done in 0.002secs. bysameorder=TRUE and o__ is length 0
# lapply optimization is on, j unchanged as 'list(max(datetime))'
# GForce optimized j to 'list(gmax(datetime))'
Run Code Online (Sandbox Code Playgroud)

而且它是即时的.为什么?因为max()内部优化,gmax()并且没有eval()呼叫每个230K组.

那么为什么不是datetime == max(datetime)即时的呢?因为解析这些表达式并在内部进行优化会更复杂,而我们还没有完成它.

解决方法

所以现在我们知道了这个问题,以及一种绕过它的方法,让我们使用它.

dt.agg = dt[, .(datetime = max(datetime)), by = .(id1, id2)]
dt[dt.agg, on = c("id1", "id2", "datetime")] # v1.9.5+
Run Code Online (Sandbox Code Playgroud)

我的Mac需要大约0.14秒.

请注意,这只是快速,因为表达式被优化为gmax().比较它:

dt[, .(datetime = base::max(datetime)), by = .(id1, id2)]
Run Code Online (Sandbox Code Playgroud)

我同意优化更复杂的表达式以避免eval()惩罚是理想的解决方案,但我们还没有.


Kha*_*haa 10

如何总结data.table和join原始数据

system.time({
  datas1 <- datas.dt[, list(datetime=max(datetime)), by = c("id1", "id2")] #summarize the data
  setkey(datas1, id1, id2, datetime)
  setkey(datas.dt, id1, id2, datetime)
  datas2 <- datas.dt[datas1]
})
#  user  system elapsed 
# 0.083   0.000   0.084 
Run Code Online (Sandbox Code Playgroud)

这正确地过滤了数据

system.time(dat1 <- datas.dt[datas.dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1])
#   user  system elapsed 
# 23.226   0.000  23.256 
all.equal(dat1, datas2)
# [1] TRUE
Run Code Online (Sandbox Code Playgroud)

附录

setkey参数是多余的,如果你使用的是开发人员版本中的data.table(感谢@akrun的指针)

system.time({
  datas1 <- datas.dt[, list(datetime=max(datetime)), by = c("id1", "id2")] #summarize the data
  datas2 <- datas.dt[datas1, on=c('id1', 'id2', 'datetime')]
})
Run Code Online (Sandbox Code Playgroud)

  • @akrun,是[关于GH]的公开问题(https://github.com/Rdatatable/data.table/issues/1232).这是我认为我们应该保留两种选择的另一个原因.顺便说一句,很好的解决方案Kashaa,你可能只是重新定义了这些任务的规范解决方案而不是[this](http://stackoverflow.com/questions/16573995/subset-by-group-with-data-table) (2认同)