【问题标题】:performance considerations get() in data.tabledata.table 中的性能注意事项 get()
【发布时间】:2020-07-21 09:39:02
【问题描述】:

我一直在循环中使用 get() 来通过 i 参考多个其他列来操作列 j。

我想知道是否有更快/更有效的方法?有什么性能方面的考虑?

这是我想到的操作类型的一个最小示例:

require(data.table) # version 1.12.8
dt = data.table(v1=c(1,2,NA),v2=c(0,0,1),v3=c(0,0,0))
for (i in 1:2){
     dt[ is.na(get(paste0('v',i))), (paste0('v',i)):= get(paste0('v',i+1))+2 ][]
}

我这样做的实际表要大得多(~5 mio 行,~300 列)。

如果有任何想法,我将不胜感激。

【问题讨论】:

  • 尝试对较大的数据使用apply函数,它会比循环使用更少的内存空间

标签: r loops data.table


【解决方案1】:

我们可以使用set,它会就地分配

library(data.table)
for(j in 1:2) {
    i1 <- which(is.na(dt[[j]]))
    set(dt, i = i1, j = j, value = dt[[j+1]][i1]+ 2)
 }

dt
#   v1 v2 v3
#1:  1  0  0
#2:  2  0  0
#3:  3  1  0

for 循环或lapply(如果两者都使用get)之间没有太大区别。为了性能提升,最好使用set


base R,我们也可以这样做

setDF(dt)
i1 <- is.na(dt[-length(dt)])
dt[-length(dt)][i1] <- dt[-1][i1] + 2
dt
#  v1 v2 v3
#1  1  0  0
#2  2  0  0
#3  3  1  0

【讨论】:

  • 这似乎可行,谢谢。诚然不知道 set() 将向量作为参数 i ..
【解决方案2】:

是的,您的for 循环大大减慢了您的速度。即使是简单的lapply(可能还有更优雅的方法),也会为您带来显着的性能提升:

library(data.table)
dt <- data.table(v1 = rnorm(100), v2 = sample(c(NA,1:5)), v3 = sample(c(NA,1:5)), v4 = sample(c(NA,1:5)))
dt2 <- copy(dt)
dt3 <- copy(dt)
dt4 <- copy(dt)

microbenchmark::microbenchmark(
  for (i in 1:2){
    dt[ is.na(get(paste0('v',i))), (paste0('v',i)):= get(paste0('v',i+1))+2 ]
  },
  for (i in 1:2){ 
    dt2[ is.na(get(paste0('v',i))), (paste0('v',i)):= get(paste0('v',i+1))+2 ][]
  },
  lapply(1:2, function(i) dt3[ is.na(get(paste0('v',i))), (paste0('v',i)):= get(paste0('v',i+1))+2 ]),
  for(j in 1:2) {
    i1 <- which(is.na(dt4[[j]]))
    set(dt4, i = i1, j = j, value = dt[[j+1]][i1]+ 2)
  }  
)

Unit: milliseconds
                                                                                                                   expr      min       lq      mean   median
    for (i in 1:2) {     dt[is.na(get(paste0("v", i))), `:=`((paste0("v", i)), get(paste0("v",          i + 1)) + 2)] } 8.439924 8.651308 10.257670 8.900500
 for (i in 1:2) {     dt2[is.na(get(paste0("v", i))), `:=`((paste0("v", i)), get(paste0("v",          i + 1)) + 2)][] } 8.902435 9.098875 10.469305 9.262659
     lapply(1:2, function(i) dt3[is.na(get(paste0("v", i))), `:=`((paste0("v",      i)), get(paste0("v", i + 1)) + 2)]) 1.032788 1.144117  1.561741 1.224858
           for (j in 1:2) {     i1 <- which(is.na(dt4[[j]]))     set(dt4, i = i1, j = j, value = dt[[j + 1]][i1] + 2) } 6.216452 6.392754  7.970074 6.502356
       uq       max neval
 9.588571 35.259060   100
 9.729876 23.245224   100
 1.349337  9.467026   100
 7.046646 30.857044   100

检查结果是等价的:

identical(dt,dt2)
# [1] TRUE
identical(dt,dt3)
# [1] TRUE
identical(dt,dt4)
# [1] TRUE

可能有更优雅的方法可以做到这一点,但是对于只需要几秒钟的编程时间除以 10 的平均计算时间是一个很好的收益;)

【讨论】:

  • 感谢 linog。似乎也比上面提出的“set()”方法更快。 dt 对象似乎在执行 lapply(..) 片段时直接更改,所以似乎我什至不需要像这样提取输出。 (我只想继续直接处理 dt 中的操作列)。
  • 不错!是的,你是对的,我没想过回头看dt:= 确实通过引用您的原始 data.table 进行更改。我将@akrun 提出的set 解决方案添加到基准测试中,因为它也可以引起其他人的兴趣。如果答案适合你,你能接受吗?
  • 对不起,我想我必须支持@akrun。我还在基准测试中添加了 set() 并得到了与您相同的结果。但是:然后我在我的真实数据集上对其进行了测试,而 set() 在“大数据”上似乎要快得多。直觉上这似乎是有道理的,因为你的“lapply”方法似乎会产生很多开销(想想每个元素 lapply 的所有 dt 副本都经过)。如果数据非常大,这似乎加起来了。很遗憾,因为 get() 语法(与循环或 lapply 类似)对我来说似乎更具可读性/直观性......无论如何,非常感谢!
  • 好的,没问题,很高兴知道。正如我所说,有比lapply 更优雅的解决方案;)
猜你喜欢
  • 2011-07-09
  • 2017-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-16
  • 1970-01-01
  • 1970-01-01
  • 2012-02-03
相关资源
最近更新 更多