“通过引用更新”vs浅拷贝

时间:2014-09-20 04:56:02

标签: r data.table

set中的函数:=或表达式[.data.table表示data.table通过引用更新。我不太了解的是这种行为与将操作结果重新分配给原始data.frame的方式不同。

keepcols<-function(DF,cols){
  eval.parent(substitute(DF<-DF[,cols,with=FALSE]))  
}
keeprows<-function(DF,i){
   eval.parent(substitute(DF<-DF[i,]))
}

因为表达式<-中的RHS是最近版本的R中初始数据帧的浅表副本,所以这些函数看起来非常有效。这个基本R方法与data.table等价物有何不同?差异仅与速度或内存使用有关吗?什么时候差异最大?

一些(速度)基准。当数据集只有两个变量时,速度差异似乎可以忽略不计,并且随着变量越多,速度差异越大。

library(data.table)

# Long dataset
N=1e7; K=100
DT <- data.table(
  id1 = sample(sprintf("id%03d",1:K), N, TRUE),     
   v1 =  sample(5, N, TRUE)                                         
)
system.time(DT[,a_inplace:=mean(v1)])
 user  system elapsed 
 0.060   0.013   0.077 
system.time(DT[,a_inplace:=NULL])
 user  system elapsed 
0.044   0.010   0.060 


system.time(DT <- DT[,c(.SD,a_usual=mean(v1)),.SDcols=names(DT)])
user  system elapsed 
0.132   0.025   0.161  
system.time(DT <- DT[,list(id1,v1)])
user  system elapsed 
0.124   0.026   0.153 


# Wide dataset
N=1e7; K=100
DT <- data.table(
  id1 = sample(sprintf("id%03d",1:K), N, TRUE),      
  id2 = sample(sprintf("id%03d",1:K), N, TRUE),      
  id3 = sample(sprintf("id%010d",1:(N/K)), N, TRUE), 
   v1 =  sample(5, N, TRUE),                          
   v2 =  sample(1e6, N, TRUE),                        
   v3 =  sample(round(runif(100,max=100),4), N, TRUE)                    
)
system.time(DT[,a_inplace:=mean(v1)])
 user  system elapsed 
  0.057   0.014   0.089 
system.time(DT[,a_inplace:=NULL])
 user  system elapsed 
  0.038   0.009   0.061 

system.time(DT <- DT[,c(.SD,a_usual=mean(v1)),.SDcols=names(DT)])
user  system elapsed 
2.483   0.146   2.602 
system.time(DT <- DT[,list(id1,id2,id3,v1,v2,v3)])
 user  system elapsed 
 1.143   0.088   1.220 

现在我明白setkeyX[Y,:=]无法用浅拷贝来表达 - 所以我真的只是要求创建/删除新的列或行。

1 个答案:

答案 0 :(得分:15)

data.table中,:=所有 set*函数通过引用更新对象。这是在2012 IIRC附近推出的。此时,基础R 没有浅拷贝,但被复制。自3.1.0以来引入了 Shallow 副本。


这是一个冗长/冗长的答案,但我认为这回答了你的前两个问题:

  

这个基本R方法与data.table等价物有何不同?差异仅与速度或内存使用有关吗?

在我做的基础R v3.1.0 +中:

DF1 = data.frame(x=1:5, y=6:10, z=11:15)
DF2 = DF1[, c("x", "y")]
DF3 = transform(DF2, y = ifelse(y>=8L, 1L, y))
DF4 = transform(DF2, y = 2L)
  1. DF1DF2,两列都只是复制。
  2. DF2DF3,必须复制/重新分配列y,但x再次复制
  3. DF2DF4,与(2)相同。
  4. 也就是说,只要列保持不变,就会对列进行浅层复制 - 在某种程度上,除非绝对必要,否则副本会被延迟。

    data.table中,我们修改就地。 <!1}}和DF3DF4期间的含义不会被复制。

    y

    这里,因为DT2[y >= 8L, y := 1L] ## (a) DT2[, y := 2L] 已经是一个整数列,并且我们通过引用修改整数,所以根本没有新的内存分配。

    当您想要通过引用子分配时(标记为上面的(a)),这也特别有用。这是我们在y中非常喜欢的一个方便功能。

    免费提供的另一个优势(我从我们的互动中了解到)是,当我们要将data.table的所有列转换为data.table类型时,例如,{ {1}}输入:

    numeric

    在这里,由于我们通过引用进行更新,因此每个字符列都会通过引用将替换为,并使用它的数字对应项。在替换之后,不再需要早期的字符列,并且可以用于垃圾收集。但是如果你使用基数R来做这个:

    character

    所有列都必须转换为数字,并且必须保存在临时变量中,然后最终将分配回DT[, (cols) := lapply(.SD, as.numeric), .SDcols = cols] 。这意味着,如果你有10列,每行有1亿行字符类型,那么你的DF[] = lapply(DF, as.numeric) 占用的空间为:

    DF

    由于DF类型的大小是其两倍,因此我们需要总共10 * 100e6 * 4 / 1024^3 = ~ 3.7GB 个空间才能使用基数R进行转换。

    但请注意,numeric期间7.4GB + 3.7GB会复制data.table。那就是:

    DF1

    会产生副本,因为我们无法通过副本上的引用子分配。它会更新所有克隆。

    如果我们可以无缝地集成浅拷贝特征,但是跟踪特定对象的列是否具有多个引用,并且尽可能通过引用进行更新,那将是多么美妙。在这方面,R的升级引用计数功能可能非常有用。无论如何,我们正在努力实现它。


    关于你的上一个问题:

      

    “差异何时最大?”

    1. 仍然有人必须使用较旧版本的R,无法避免使用深层副本。

    2. 这取决于要复制的列数,因为您对其执行的操作。最糟糕的情况是你复制了所有列,当然。

    3. 有些像this这样的情况,浅层复制不会受益。

    4. 如果您想更新每个组的data.frame列,并且组太多了。

    5. 当您想根据与其他data.table DF2的联接更新say,data.table DT2 = DT1[, c("x", "y"), with=FALSE] 列时,可以这样做:

      DT1

      其中DT2引用DT1[DT2, col := i.val] i.列的val列中的值(DT2参数)以匹配行。此语法允许非常有效地执行此操作,而不必首先连接整个结果,然后更新所需的列。

    6. 总而言之,有强有力的论据,通过引用更新可以节省大量时间,并且速度很快。但人们有时不喜欢就地更新对象,并愿意为此牺牲速度/内存。除了已经存在的引用更新之外,我们还试图弄清楚如何最好地提供此功能。

      希望这会有所帮助。这已经是一个很长的答案了。我会留下你可能留给别人的任何问题,或者让你弄清楚(除了这个答案中任何明显的误解)。