214

我在理解data.table. 一些操作似乎“破坏”了参考,我想确切地了解正在发生的事情。

data.table在从另一个创建一个data.table(通过<-,然后通过 更新新表时:=,原始表也被更改。这是预期的,根据:

?data.table::copystackoverflow:按引用传递操作符在数据表包中

这是一个例子:

library(data.table)

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

newDT <- DT        # reference, not copy
newDT[1, a := 100] # modify new DT

print(DT)          # DT is modified too.
#        a  b
# [1,] 100 11
# [2,]   2 12

但是,如果我在分配和上面的行之间插入非:=基于修改,现在不再修改:<-:=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)
#      a  b
# [1,] 1 11
# [2,] 2 12

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

如果有人可以向我解释这一点,我将不胜感激。

4

2 回答 2

149

是的,它是 R 中使用<-(或=->)的子赋值,它制作了整个对象的副本。您可以使用tracemem(DT)和进行跟踪.Internal(inspect(DT)),如下所示。data.table功能:=set()通过引用分配给它们传递的任何对象。因此,如果该对象先前被复制(通过子分配<-或显式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向量是如何被复制的(不同的十六进制值表示向量的新副本),即使a没有更改。甚至整个都b被复制了,而不是仅仅改变需要改变的元素。对于大数据,避免这一点很重要,以及:=为什么set()引入data.table.

现在,使用我们的副本,newDT我们可以通过引用对其进行修改:

newDT
#      a   b
# [1,] 1  11
# [2,] 2 200

newDT[2, b := 400]
#      a   b        # 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 个十六进制值(列点向量和 2 列中的每一列)保持不变。所以它是真正通过引用修改的,根本没有副本。

或者,我们可以参考修改原文DT

DT[2, b := 600]
#      a   b
# [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..

这些十六进制值与我们在上面看到的原始值相同DT。键入以获取更多使用和比较的example(copy)示例。tracememdata.frame

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

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

于 2012-04-19T10:49:24.990 回答
113

简单总结一下。

<-withdata.table就像基础一样;即,在随后使用<-(例如更改列名或更改元素,例如DT[i,j]<-v)完成子分配之前,不会获取任何副本。然后它像base一样获取整个对象的副本。这就是所谓的写时复制。我认为,最好将其称为子分配复制!:=当您使用特殊运算符或set*提供的功能时,它不会复制data.table。如果您有大量数据,您可能希望使用它们。:=并且set*不会复制data.table,即使在函数内。

鉴于此示例数据:

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

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

DT2 <- DT

这从不复制,也从不复制基础。它只是标记数据对象,以便 R 知道两个不同的名称 (DT2DT) 指向同一个对象。因此,如果之后将任何一个对象分配给R,则 R 将需要复制该对象。

这也很适合data.table:=不是为了这样做。因此,以下是故意的错误:=,不仅仅是绑定对象名称:

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

:=用于通过引用进行子分配。但是您不会像在 base 中那样使用它:

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

你像这样使用它:

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

DT是通过参考改变的。假设您通过引用数据对象添加新列new,则无需这样做:

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用于大型数据集,请记住。如果您有 20GBdata.table的内存,那么您需要一种方法来执行此操作。这是一个非常深思熟虑的设计决定data.table

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

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

为避免复制,请不要使用基本类型分配或更新:

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

如果您想确保通过引用使用更新.Internal(inspect(x))并查看成分的内存地址值(请参阅 Matthew Dowle 的回答)。

像这样写可以让您按 group 按引用:=进行分配。您可以按组通过引用添加新列。所以这就是为什么在里面这样做:j:=[...]

DT[, newcol:=mean(x), by=group]
于 2013-01-12T11:52:14.317 回答