2014-09-20 79 views
8

[.data.table中的函数set或表达式:=意味着data.table通过引用进行更新。我不太了解的是,这种行为与将操作结果重新分配给原始data.frame的方式不同。“通过引用更新”vs浅拷贝

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,:=]不能浅拷贝的期限来表示 - 所以我真的只是要求有关创建/删除新的列或行我。

回答

12

data.table:=全部set*函数通过引用更新对象。这是在2012年IIRC左右推出的。而在这个时候,基地R 没有浅拷贝,但拷贝。 浅层副本自3.1.0开始引入。


这是一个罗嗦/冗长的答案,但我觉得这回答你的前两个问题:

这是怎么回事基础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. DF2DF3y单独不得不被复制/重新分配,但x得到再次复制。
  3. DF2DF4,与(2)相同。

也就是说,只要列保持不变,列就被浅拷贝 - 在某种程度上,复制被延迟,除非绝对必要。

data.table,我们修改就地。即使在DF3DF4y也不会被复制。

DT2[y >= 8L, y := 1L] ## (a) 
DT2[, y := 2L] 

这里,因为y已经是一个整数列,而我们是通过整数修改它,通过引用,这里有所有无新的内存分配。

当您想分号作为参考(标记为上面的(a))时,这也特别有用。这是我们在data.table中非常喜欢的一个便利功能。

附带免费的(我来自我们的互动就知道了)是另一个优势当我们已经,比如说,转换data.table的所有列到numeric类型,比如从,character类型:

DT[, (cols) := lapply(.SD, as.numeric), .SDcols = cols] 

在这里,因为我们通过引用更新,每个字符列得到通过参考取代与它的数字对应。在更换之后,不再需要较早的字符列,并且可以用于垃圾收集。但是,如果你要做到这一点使用基础R:

DF[] = lapply(DF, as.numeric) 

所有列将被转换为数字,而必须在临时变量举行,最后将被分配回到DF。这意味着,如果你已经10列有100万行,每种性格类型的,那么你的DF需要的空间:

10 * 100e6 * 4/1024^3 = ~ 3.7GB 

而且由于numeric类型是两倍大小为多,我们需要共为美国太空7.4GB + 3.7GB使用基本R.

进行转换但要注意在DF1data.table副本DF2。那就是:

DT2 = DT1[, c("x", "y"), with=FALSE] 

导致复制,因为我们不能通过在拷贝参考子分配。它会更新所有的克隆。

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


对于你最后一个问题: “什么时候是最可观的区别”

  1. 还有谁有权使用旧版本的R,在无法避免深副本的人。

  2. 这取决于有多少列正在被复制,因为您对它执行的操作。最糟糕的情况是你复制了所有的列,当然。

  3. 有像this这样的情况,浅拷贝不会受益。

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

  5. 当你想更新基于与联接的发言权,data.table DT1列另一个data.table DT2 - 这是可以做到的:

    DT1[DT2, col := i.val] 
    

    其中i.指值为(i参数)的val列,用于匹配行。此语法允许非常有效地执行此操作,而不必首先加入整个结果,然后更新所需的列。

总而言之,有很强的论据,通过引用更新会节省很多时间,并且速度很快。但是人们有时不喜欢更新对象,并且愿意为它牺牲速度/内存。除了现有的参考更新之外,我们还试图找出如何最好地提供此功能。

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