在data.table操作中,.SD的作用是什么?

3
我有一个关于data.table的问题。 我喜欢它,但我认为我有时会误用 .SD,我希望能够澄清何时在data.table中使用它是有意义的。以下是两个例子,让我认识到我可能误用了.SD: 第一个例子是在这里讨论的(感谢Henry的评论)。
library(microbenchmark)
library(data.table)

DTlength <- 2000
DT <-
  data.table(
    id = rep(sapply(combn(LETTERS, 6, simplify = FALSE), function(x) {
      paste(x, collapse = "")
    }), each = 4)[1:DTlength],
    replicate(10, sample(1001, DTlength, replace = TRUE)),
    Answer = sample(c("Yes", "No"), DTlength, TRUE)
  )

microbenchmark(
  "without SD" = {
    b <- DT[, Answer[1], by = id][, V1]
  },
  "without SD alternative" = {
    b <- DT[DT[, .I[1], by = id][, V1], Answer]
  },
  "with SD" = {
    b <- DT[, .SD[1, Answer], by = id][, V1]
  }
)

Unit: microseconds
                   expr        min         lq        mean     median         uq        max neval
             without SD    455.795    493.949    569.4979    529.847    558.564   2323.283   100
 without Sd alternative    961.231   1010.667   1160.9114   1060.513   1113.641   7783.798   100
                with SD 121217.691 123557.590 131071.5699 127495.437 130340.977 240317.227   100

与分组操作中的替代方法相比,.SD 操作速度较慢。 即使您想对整个数据表进行分组,使用其他方法稍微快一些(尽管在这里的时间差异可能不值得以语法清晰为代价):

microbenchmark(
  "with SD" = {b <-DT[,.SD[1], by = id]},
  "Without SD" = {b <- DT[DT[,.I[1],by = id][,V1]]}
)

Unit: milliseconds
       expr      min       lq     mean   median       uq      max neval
    with SD 1.058872 1.361436 1.560866 1.643078 1.741540 1.960206   100
 Without SD 1.067898 1.169642 1.279443 1.233437 1.348719 1.781334   100

第二个示例说明了在组内使用条件将新变量赋值给一个值时,你不能真正使用 .SD(或者我没有找到方法):

DT[, .SD[V1 - V1[1] > 100][, plouf2 := Answer], by = id] # doesn't assign plouf2
DT[DT[, .I[V1 - V1[1] > 100], by = id][, V1], plouf2 := Answer] # this does

我发现使用.SD有两种情况非常有用:一种是在DT[,lapply(.SD,fun),.SDcols = ]中使用,非常方便;另一种是当我们想要将组内所有值分配给满足特定条件的特定值时:

DT[, plouf3 := .SD[V1 - V1[1] > 100, Answer][1], by = id] 
# all values are assigned, which is actually different from 
DT[DT[, .I[V1 - V1[1] > 100][1], by = id][, V1], plouf2 := Answer] 
# where only the values that match the condition V1-V1[1]>100 are assigned

所以我的问题是:是否还有其他需要/有趣使用.SD的情况?
非常感谢您的帮助。

1
第一个问题,可能是Subset by group with data.table的重复: "OP很慢的主要原因不仅仅是它有.SD,而且是它以一种特定的方式使用它 - 通过调用[.data.table,目前它具有巨大的开销,所以在循环中运行它(当一个人进行by时)会累积非常大的惩罚。" 另请参见Optimize .SD query to keep the elegance but make it faster及其中的链接。 - Henrik
1
通常情况下,当您想要操作多个列(就像您已经展示的那样)或在某个操作之后返回少量(或所有)列时,您将希望使用.SD,例如 DT[, if(any(x > 2)) .SD, by = y]DT[, .SD[1L], by = x]。您还可以将其用于条件连接,例如 DT[x > 2, .SD[DT, x, on = .(y)]]。除此之外,我真的看不出有什么理由使用它,您可能会使用实际向量。 - David Arenburg
1
这个问题太宽泛了,如果可以的话我会投票关闭它;当.SD有用时,我会使用它,而这种情况相当普遍——这不是一个好答案,但主要是因为这不是一个好问题。 - eddi
2
可能是What does .SD stand for in data.table in R的重复问题。特别是考虑到@MichaelChirico最近非常详细的回答 - Henrik
感谢@Henrik提供的链接。是的,MichaelChirico的回答非常好,解决了我的问题。非常感谢。 - denis
显示剩余3条评论
1个回答

1

关于您的第一个问题

只有当三种方法都生成相同的输出时,基准才是公平的。"无SD替代"方法生成不同的结果,所以我们将其搁置。

"有SD"和"无SD"方法生成相同的输出,但后者更有效率。原因如下:当您执行... .SD[1, Answer] ...时,您基本上正在对匹配行的所有列进行子集划分,然后在该子集上执行下一个操作(即获取向量Answer的第一个值)。然而,在"无SD"方法中,您仅对一个向量进行子集划分(而不是所有向量),然后获取该一个向量的第一个值。在"有SD"方法中不必要地子集划分额外未使用的列是导致其变慢的原因。

关于您的第二个问题

此命令不会将值分配给DT:

DT[, .SD[V1 - V1[1] > 100][, plouf2 := Answer], by = id]

原因在于.SD操作符是单向操作符,也就是说如果你在.SD返回的子集中更改了某些内容,它不会将更改应用于较大的数据表,而只会应用于内存中子集的副本。不能称其为内存中的副本,因为.SD实际上并没有复制数据(它只是指向保存感兴趣子集的内存的相关部分),但关键是对它的赋值只会应用于这个内存指针,而不是原始的底层数据。
注意:你可以争论它不应该支持任何赋值。我不知道Matt Dowle的想法,但在我看来,赋值实际上是一个有用的特性!例如:DT.2 <- DT[, .SD[V1 - V1[1] > 100][, plouf2 := Answer], by = id] 这样做可以生成我想要的输出并将其存储在新的数据表中,而不修改原始数据表!我能想到的任何其他生成此精确输出的方法都需要使用.SD且不能触及原始数据表,但代码会更长。
关于您的最后一个问题:
当您想处理多个或所有数据表列而不仅仅是一列或几列时,.SD很有用。(这就是为什么您在第一部分中使用的“with SD”方法不是完成您想做的事情的适当方式)。 What does .SD stand for in data.table in R 中提供的示例非常有用,可说明何时.SD非常方便。在我看来,.SD的主要优点不在于代码运行的效率,而在于您可以将概念转化为R代码的效率以及该段代码的可读性。

感谢您的回答。在https://dev59.com/C2oy5IYBdhLWcg3wguSS中提供的示例确实非常好。感谢您的解释。 - denis
顺便提一句,不同的方法生成相同的结果。不使用SD的替代方法生成字符向量,而其他两种方法生成数据表,但结果是相同的。我编辑了帖子,以确保结果完全相同。 - denis
嗯,它们在语义上对我们来说是相同的,但在技术上是不同的(制作数据表的处理和内存占用与向量不同)。一个好的基准是所有方法的结果都完全相同 - Merik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接