确切地了解data.table何时是另一个data.table的引用(vs另一个副本)

我在理解data.table的传递引用属性时遇到了一些麻烦。 一些操作似乎“打破”了参考,我想要明白到底发生了什么事情。

从另一个data.table创build一个data.table (通过<- ,然后更新新的表:= ,原来的表也被改变,这是预期的,按照:

?data.table::copy和stackoverflow:通过引用传入数据表中的操作符包

这是一个例子:

 library(data.table) DT <- data.table(a=c(1,2), b=c(11,12)) print(DT) # ab # [1,] 1 11 # [2,] 2 12 newDT <- DT # reference, not copy newDT[1, a := 100] # modify new DT print(DT) # DT is modified too. # ab # [1,] 100 11 # [2,] 2 12 

但是,如果我在上面的<- assignment和<- :=行之间插入非基于:=的修改,现在不再修改DT

 DT = data.table(a=c(1,2), b=c(11,12)) newDT <- DT newDT$b[2] <- 200 # new operation newDT[1, a := 100] print(DT) # ab # [1,] 1 11 # [2,] 2 12 

因此,似乎新的newDT$b[2] <- 200线以某种方式“打破”了参考。 我想这会调用一个副本,但我想完全理解R是如何处理这些操作,以确保我不会在我的代码中引入潜在的错误。

如果有人能向我解释这一点,我将非常感激。

是的,它在R中使用<- (或=-> )进行次级分配,使整个对象的副本。 您可以使用tracemem(DT).Internal(inspect(DT))来跟踪,如下所示。 data.table特点:=set()通过引用来分配它们传递的任何对象。 因此,如果该对象先前被复制(通过copy(DT) <-或显式copy(DT) ),那么它是通过引用修改的副本。

 DT <- data.table(a = c(1, 2), b = c(11, 12)) newDT <- DT .Internal(inspect(DT)) # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100) # @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2 # @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12 # ATTRIB: # ..snip.. .Internal(inspect(newDT)) # precisely the same object at this point # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100) # @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2 # @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12 # ATTRIB: # ..snip.. tracemem(newDT) # [1] "<0x0000000003b7e2a0" newDT$b[2] <- 200 # tracemem[0000000003B7E2A0 -> 00000000040ED948]: # tracemem[00000000040ED948 -> 00000000040ED830]: .Call copy $<-.data.table $<- .Internal(inspect(DT)) # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),TR,ATT] (len=2, tl=100) # @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2 # @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12 # ATTRIB: # ..snip.. .Internal(inspect(newDT)) # @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100) # @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2 # @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,200 # ATTRIB: # ..snip.. 

注意a向量是如何被复制的(不同的hex值表示向量的新副本),尽pipea没有被改变。 即使是整个b也是被复制的,而不是仅仅改变需要改变的元素。 这对于避免大数据很重要,为什么:=set()被引入data.table

现在,通过我们复制的newDT我们可以通过引用来修改它:

 newDT # ab # [1,] 1 11 # [2,] 2 200 newDT[2, b := 400] # ab # See FAQ 2.21 for why this prints newDT # [1,] 1 11 # [2,] 2 400 .Internal(inspect(newDT)) # @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100) # @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2 # @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,400 # ATTRIB: # ..snip .. 

请注意,所有3个hex值(列点向量和2列中的每一个)保持不变。 所以这是真正的修改,参考没有任何副本。

或者,我们可以通过参考修改原始DT

 DT[2, b := 600] # ab # [1,] 1 11 # [2,] 2 600 .Internal(inspect(DT)) # @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100) # @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2 # @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,600 # ATTRIB: # ..snip.. 

这些hex值与我们在DT上面看到的原始值相同。 键入example(copy)以获取更多使用tracemem和比较data.frame

顺便说一句,如果你tracemem(DT) DT[2,b:=600]你会看到一个副本报告。 这是print方法的前10行的副本。 当用invisible()包装或者在函数或脚本中调用时, print方法不会被调用。

所有这些也适用于函数内部。 即:=set()不要在写入时拷贝,即使在函数中也是如此。 如果您需要修改本地副本,请在函数的开头调用x=copy(x) 。 但是,记住data.table是用于大数据(以及对于小数据更快的编程优势)。 我们故意不想复制大对象(永远)。 因此,我们不需要考虑通常的3 *工作记忆因素经验法则。 我们试图只需要一列大的工作记忆(即工作记忆因子1 / ncol而不是3)。

只是一个快速的总结。

data.table和base一样。 也就是说,直到用<- (例如改变列名称或者改变一个元素,比如DT[i,j]<-v )完成一个子分配之后才能进行复制。 然后它就像基地一样需要整个对象的副本。 这就是所谓的写入复制。 将更好地称为副本上的副本,我想! 当你使用特殊的:=操作符或data.table提供的set*函数时,它不会被复制。 如果你有大量的数据,你可能想要使用它们。 :=并且set*不会复制data.table ,即使在函数内。

给出这个例子的数据:

 DT <- data.table(a=c(1,2), b=c(11,12)) 

以下只是将另一个名称“ DT2 ”绑定到当前绑定到名称DT的同一个数据对象上:

 DT2 <- DT 

这从来没有复制,也从来没有在基地复制。 它只是标记数据对象,以便R知道两个不同的名称( DT2DT )指向同一个对象。 所以如果任何一个被分配后面 ,R将需要复制该对象。

对于data.table也是完美的。 :=不是为了做到这一点。 所以下面是一个故意的错误:=不仅仅是绑定对象名称:

 DT2 := DT # not what := is for, not defined, gives a nice error 

:=用于通过引用进行分配 。 但是你不会像在基地那样使用它:

 DT[3,"foo"] := newvalue # not like this 

你这样使用它:

 DT[3,foo:=newvalue] # like this 

这通过引用改变了DT 。 假设你通过引用数据对象添加一个新的列,不需要这样做:

 DT <- DT[,new:=1L] 

因为RHS已经通过引用改变了DT 。 额外的DT <-是误解什么:=做的。 你可以写在那里,但是这是多余的。

DT通过引用被改变:= ,即使在函数内:

 f <- function(X){ X[,new2:=2L] return("something else") } f(DT) # will change DT DT2 <- DT f(DT) # will change both DT and DT2 (they're the same data object) 

data.table是为大数据集记住的。 如果你在内存中有一个20GB的data.table ,那么你需要一种方法来做到这一点。 这是一个非常慎重的data.tabledevise决定。

当然可以复印。 您只需要通过使用copy()函数来告诉data.table您确定要复制20GB数据集:

 DT3 <- copy(DT) # rather than DT3 <- DT DT[,new3:=3L] # now, this just changes DT3 because it's a copy, not DT too. 

为避免复制,请勿使用基本types分配或更新:

 DT$new4 <- 1L # will make a copy so use := attr(DT,"sorted") <- "a" # will make a copy use setattr() 

如果你想确定你是通过引用来更新的话,可以使用.Internal(inspect(x))并查看组成部分的内存地址值(参见Matthew Dowle的答案)。

写作:=j就像允许你通过组参考进行分配。 您可以按组引用添加新列。 所以这就是为什么:=在内部完成: [...]

 DT[, newcol:=mean(x), by=group]