使用 .SD 时 R data.table 聚合缓慢

Posted

技术标签:

【中文标题】使用 .SD 时 R data.table 聚合缓慢【英文标题】:R data.table slow aggregation when using .SD 【发布时间】:2013-03-07 14:17:22 【问题描述】:

我正在对 data.table 进行一些聚合(出色的包!!!),我发现 .SD 变量对很多事情都非常有用。但是,当有很多组时,使用它会显着减慢计算速度。举个例子:

# A moderately big data.table
x = data.table(id=sample(1e4,1e5,replace=T),
               code=factor(sample(2,1e5,replace=T)),
               z=runif(1e5)
              )

setkey(x,id,code)

system.time(x[,list(code2=nrow(.SD[code==2]), total=.N), by=id])
##  user  system elapsed 
##  6.226   0.000   6.242

system.time(x[,list(code2=sum(code==2), total=.N), by=id])
## user  system elapsed 
## 0.497   0.000   0.498

system.time(x[,list(code2=.SD[code==2,.N], total=.N), by=id])
## user  system elapsed 
## 6.152   0.000   6.168

我做错了吗?我应该避免使用 .SD 来支持单个列吗?提前致谢。

【问题讨论】:

【参考方案1】:

我是不是做错了什么,即我应该避免.SD 以支持个别列吗?

是的,没错。如果您确实在使用.SD 中的所有数据,请仅使用.SD。您可能还会发现对nrow() 的调用和对j 内部的[.data.table 的子查询调用也是罪魁祸首:使用Rprof 进行确认。

见FAQ 2.1的最后几句:

FAQ 2.1 如何避免写一个很长的 j 表达式?你说过我应该使用列名,但我有很多列。 分组时,j 表达式可以使用列名作为变量,如 你知道,但它也可以使用保留符号.SD,它指的是 每个组的 Data.table 的子集(不包括分组 列)。所以总结一下你所有的专栏 DT[,lapply(.SD,sum),by=grp]。这可能看起来很棘手,但它很快 编写和快速运行。请注意,您不必创建匿名 功能。请参阅时间插图和 wiki 以与其他人进行比较 方法。 .SD 对象在内部高效实现等等 比将参数传递给函数更有效。 请不要这样做 虽然:DT[,sum(.SD[,"sales",with=FALSE]),by=grp]。这行得通,但非常 低效且不优雅。这就是本意: DT[,sum(sales),by=grp] 并且可能快 100 倍。

另见常见问题解答 3.1 的第一个项目符号:

FAQ 3.1 我有 20 列和大量行。为什么一列的表达式这么快? 几个原因: -- 只有那一列是 分组后,其他 19 个被忽略,因为 data.table 检查 j 表达式并意识到它不使用其他列。

data.table 检查j 并看到.SD 符号时,效率增益就消失了。即使您不使用所有列,它也必须为每个组填充整个 .SD 子集。 data.table 很难知道您真正使用了 .SD 的哪些列(例如,j 可能包含 ifs)。但是,如果您无论如何都需要它们,那当然没关系,例如DT[,lapply(.SD,sum),by=...]。这是.SD 的理想用法。

所以,是的,尽可能避免.SD。直接使用列名给data.table优化j的最佳机会。 j 中符号 .SD 的存在很重要。

这就是引入.SDcols 的原因。因此,如果您只想要一个子集,您可以告诉data.table 哪些列应该在.SD 中。否则,data.table 将使用所有列填充.SD,以防j 需要它们。

【讨论】:

非常感谢!我被“.SD对象在内部有效地实现并且比将参数传递给函数更有效”这句话欺骗了我,并且不理解“请不要这样做:DT [,sum(.SD [,” sales",with=FALSE]),by=grp]" 由于 with=FALSE。这将大大加快我的代码速度! @vsalmendra 啊,是的,它可能更清楚。这是过去留给社区讨论的那种事情。最终,我们希望改进j 优化,让用户不需要知道这样的事情。 @vsalmendra 我现在为下一个版本改进了常见问题解答 2.1。【参考方案2】:

尝试通过将计算分为两个步骤来解决此问题,然后合并生成的数据框:

system.time(
  x2 <- x[code==2, list(code2=.N), by=id]
  xt <- x[, list(total=.N), by=id]
  print(x2[xt])
)

在我的机器上,它的运行时间为 0.04 秒,而不是 7.42 秒,即比原始代码快约 200 倍:

         id code2 total
   1:     1     6    14
   2:     2     8    10
   3:     3     7    13
   4:     4     5    13
   5:     5     9    18
  ---                  
9995:  9996     4     9
9996:  9997     3     6
9997:  9998     6    10
9998:  9999     3     4
9999: 10000     3     6
   user  system elapsed 
   0.05    0.00    0.04 

【讨论】:

(+1) 我会将x2 替换为x2 &lt;- x[J(unique(id), "2"), list(code2 = .N)][, code := NULL, keyby="id"]。您的代码会删除给定 id 的 code != 2 的行。 +1 实际上这比x[,list(sum(code==2),.N),by=id](有问题的示例2)快得多,不是吗!也许是因为您避免为每个组重复调用==(这些小向量的关联分配等)。 @Arun 你确定吗? x2[xt][is.na(code2)] 有一些行。你只会得到NA 而不是0。可能是错的,只是快速看了一下。 @MatthewDowle,不,我只是指x2 的步骤。 x2 有 9921 行而不是 9999。当然,最后,使用 x[y],这些行得到 NA。我认为使用J(key columns) 进行索引应该比logical indexing 更快...我弄错了吗? @Arun 单个列上的单个向量扫描是一种特殊情况:unique(id) 调用无论如何也必须扫描每个项目,然后加上加入的时间。只是检查一下,x2 有 9921 行而不是 9999 行不是问题吗? (这就是你上面的评论似乎表明它是的)。

以上是关于使用 .SD 时 R data.table 聚合缓慢的主要内容,如果未能解决你的问题,请参考以下文章

r data.table 围绕 ad-hoc 连接的函数包装器(在链中聚合)

使用聚合操作时如何避免在 data.table 中创建重复项

data.table中的R rowsum崩溃R.

从R中的data.table中删除带有NA的行[重复]

在 R 中聚合超过 80K 的唯一 ID

R语言进行数据聚合统计(Aggregating transforms)实战:使用R原生方法data.tabledplyr等方案计算分组均值并添加到可视化结果中