如何按组加速子集
Posted
技术标签:
【中文标题】如何按组加速子集【英文标题】:How to speed up subset by groups 【发布时间】:2015-10-29 09:31:47 【问题描述】:我曾经用 dplyr 来实现我的数据整理,但有些计算“慢”。特别是按组划分的子集,我读到当有很多组时 dplyr 很慢,并且基于this benchmark data.table 可能会更快,所以我开始学习 data.table。
这里是如何用 250k 行和大约 230k 组重现接近我的真实数据的东西。我想按 id1、id2 分组,并为每个组使用 max(datetime)
对行进行子集化。
数据
# random datetime generation function by Dirk Eddelbuettel
# https://***.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"))
我找不到使用 data.table 按组划分子集的直接方法,所以我问了这个问题:Filter rows by groups with data.table
我们建议我使用 .SD:
datas.dt[, .SD[datetime == max(datetime)], by = c("id1", "id2")]
但是我有两个问题,它适用于日期但不适用于 POSIXct ("Error in 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
所以我找到了其他更快的方法来实现这一点(并保持日期时间) 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]
但后来我认为 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
我是否遗漏/误用了 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’
【问题讨论】:
您想获取等于 max 的所有值,还是只获取第一个值,例如which.max
返回? datas.dt[, .SD[as.Date(datetime) == max(as.Date(datetime))], by = c("id1", "id2")]
也是一种不好的做法。您应该在子集之前将 date
转换为 IDate
类。
只是为了好玩,您可以将x %>% group_by(id1, id2) %>% slice(which(datetime == max(datetime)))
添加到您的比较中吗?
另外,datas.dt[, datetime := as.IDate(datetime)] ; system.time(datas.dt[datas.dt[, .I[datetime == max(datetime)], by = c("id1", "id2")]$V1])
只运行 5 秒,而使用 .SD
时为 200 秒,所以我很难相信你的基准测试。
@DavidArenburg,恭喜,虽然这不是我想要的比较。无论如何,我只是出于好奇而问。
@docendodiscimus 我不是在吹牛什么的,所以不确定你在祝贺我什么。 OP 正在寻找 data.table
解决方案,因为他认为它会比 dplyr
更快 - 这就是为什么我将您的建议与 data.table
进行比较,以防他的假设是错误的。
【参考方案1】:
好问题!
我假设 df
和 dt
是对象的名称,以便于/快速键入。
df = datas.tbl
dt = datas.dt
-O3
级别优化的比较:
首先,这是我的系统在当前 CRAN 版本 dplyr
和开发版本 data.table
上的时间。 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
我运行了很多次,并且似乎发生了变化。但是,我用-O3
优化标志编译所有包(通过适当地设置~/.R/Makevars
)。而且我观察到data.table
的性能比我在-O3
比较过的其他软件包要好得多。
分组速度比较
其次,了解这种缓慢的原因很重要。首先让我们将时间与 group 进行比较。
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
即使总共有 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
因此,仅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))'
而且它是即时的。为什么?因为max()
在内部被优化为gmax()
,并且230K 组中的每一个都没有eval()
调用。
那么为什么datetime == max(datetime)
不是即时的?因为这样的表达式解析和内部优化比较复杂,我们还没有搞定。
解决方法
既然我们知道了这个问题,以及解决它的方法,让我们使用它。
dt.agg = dt[, .(datetime = max(datetime)), by = .(id1, id2)]
dt[dt.agg, on = c("id1", "id2", "datetime")] # v1.9.5+
这在我的 Mac 上大约需要 0.14 秒。
请注意,这只是快因为表达式被优化为gmax()
。比较一下:
dt[, .(datetime = base::max(datetime)), by = .(id1, id2)]
我同意优化更复杂的表达式以避免eval()
惩罚是理想的解决方案,但我们还没有。
【讨论】:
感谢您的启发性回答。你给了我一个将执行时间除以 100 的解决方案,但也帮助我理解了这个计算的瓶颈!谢谢。【参考方案2】:总结一下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
正确过滤数据
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
附录
如果您使用data.table
的devel version,则setkey
参数是多余的(感谢@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')]
)
【讨论】:
在开发版本中,您不需要setkey
。 datas.dt[datas1, on=c('id1', 'id2')]
应该可以工作。虽然没有经过时间测试。
@akrun,谢谢。我对data.table
的具体细节一无所知。
您应该保留这两个版本,因为您的编辑仅适用于开发版本。
@akrun,是的,一个未解决的问题on GH。这是我认为我们应该保留这两种选择的另一个原因。顺便说一句,很好的解决方案 Kashaa,您可能只是重新定义了此类任务的规范解决方案,而不是 this
@Khashaa 看看this answer 我想我已经解释得很好了。虽然根据 Aruns 的精彩回答,我开始怀疑这个解决方案是否会更好地适用于 所有 功能,而不仅仅是 sum
、mean
、min
和 max
以上是关于如何按组加速子集的主要内容,如果未能解决你的问题,请参考以下文章