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
现在我明白setkey
或X[Y,:=
]无法用浅拷贝来表达 - 所以我真的只是要求创建/删除新的列或行。
答案 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)
DF1
到DF2
,两列都只是浅复制。 DF2
到DF3
,必须复制/重新分配列y
,但x
再次复制浅。 DF2
到DF4
,与(2)相同。 也就是说,只要列保持不变,就会对列进行浅层复制 - 在某种程度上,除非绝对必要,否则副本会被延迟。
在data.table
中,我们修改就地。 <!1}}和DF3
列DF4
期间的含义不会被复制。
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的升级引用计数功能可能非常有用。无论如何,我们正在努力实现它。
关于你的上一个问题:
“差异何时最大?”
仍然有人必须使用较旧版本的R,无法避免使用深层副本。
这取决于要复制的列数,因为您对其执行的操作。最糟糕的情况是你复制了所有列,当然。
有些像this这样的情况,浅层复制不会受益。
如果您想更新每个组的data.frame列,并且组太多了。
当您想根据与其他data.table DF2
的联接更新say,data.table DT2 = DT1[, c("x", "y"), with=FALSE]
列时,可以这样做:
DT1
其中DT2
引用DT1[DT2, col := i.val]
i.
列的val
列中的值(DT2
参数)以匹配行。此语法允许非常有效地执行此操作,而不必首先连接整个结果,然后更新所需的列。
总而言之,有强有力的论据,通过引用更新可以节省大量时间,并且速度很快。但人们有时不喜欢就地更新对象,并愿意为此牺牲速度/内存。除了已经存在的引用更新之外,我们还试图弄清楚如何最好地提供此功能。
希望这会有所帮助。这已经是一个很长的答案了。我会留下你可能留给别人的任何问题,或者让你弄清楚(除了这个答案中任何明显的误解)。