在R的data.table中,.SD代表什么?

193

.SD看起来很有用,但我并不真的知道我在做什么。 它代表什么?为什么前面有一个句点(句号)?当我使用它时会发生什么?

我读到:.SD是一个data.table,包含每个组的x数据的子集,不包括组列。它可以在按i分组,按by分组,按键by临时 by分组时使用。

这是否意味着对于下一个操作,子data.table保存在内存中?


34
感谢这个问题,data.table在v1.7.10中得到了改进。现在根据被接受的答案,它解释了名为.SD的含义。 - Matt Dowle
3个回答

228

.SD代表着“Data.table的子集”。初始的点号"."没有实际意义,只是为了避免与用户自定义的列名重名。

如果这是你的data.table:

DT = data.table(x=rep(c("a","b","c"),each=2), y=c(1,3), v=1:6)
setkey(DT, y)
DT
#    x y v
# 1: a 1 1
# 2: b 1 3
# 3: c 1 5
# 4: a 3 2
# 5: b 3 4
# 6: c 3 6

这样做可以帮助你了解什么是.SD

DT[ , .SD[ , paste(x, v, sep="", collapse="_")], by=y]
#    y       V1
# 1: 1 a1_b3_c5
# 2: 3 a2_b4_c6

基本上,by=y 语句将原始的 data.table 拆分成这两个子 data.tables

DT[ , print(.SD), by=y]
# <1st sub-data.table, called '.SD' while it's being operated on>
#    x v
# 1: a 1
# 2: b 3
# 3: c 5
# <2nd sub-data.table, ALSO called '.SD' while it's being operated on>
#    x v
# 1: a 2
# 2: b 4
# 3: c 6
# <final output, since print() doesn't return anything>
# Empty data.table (0 rows) of 1 col: y

它逐个操作这些数据。在操作其中一个时,您可以使用别名/句柄/符号.SD引用当前的子data.table。这非常方便,因为您可以访问并像使用单个名为.SD的data.table一样操作列…除此之外,data.table将对由键组合定义的每个子data.table执行这些操作,“粘合”它们并将结果返回到单个data.table中!


15
没问题,.SD 的另一个理解方式是DT[,print(.SD),by=y] - Matt Dowle
7
@MatthewDowle -- 在这里,有一个问题想问你。执行DT [,print(.SD [ ,y ]),by = y],表明我可以访问y的值,即使它不是.SD的一部分。y的值是从哪里作用域而来的?它是否可用因为它是“by”的当前值? - Josh O'Brien
8
@Josh 很好。是的。.SD[,y] 是一个普通的 data.table 子集,因此由于 y 不是 .SD 的列,它会在调用它的环境中查找,也就是 DT 查询的 j 环境,在那里 by 变量是可用的。如果在那里没有找到,它会在父级、其父级等等中以通常的R方式查找(当然,也通过继承范围加入了连接,但这些示例中没有使用,因为没有 i)。 - Matt Dowle
3
因为组变量在j中也是可用的,并且长度为1。by=list(x,y,z)意味着xyz也可以在j中使用。为了通用的访问,它们也被封装在.BY中。FAQ 2.10上有一些历史记录,但?data.table可能需要增加一些说明。很好,文档帮助将非常受欢迎。如果您想直接加入项目并进行更改,那就更好了。 - Matt Dowle
1
除了 DT[ , print(.SD), by = y] 之外,另一个选项是 DT[ , dput(.SD), by = y],我发现对于简单的 data.table 更有帮助。 - MichaelChirico
显示剩余3条评论

118

编辑:

考虑到这个答案的好评,我已经将它转化为一个软件包的介绍,现在可以在这里找到。


考虑到这经常出现,我认为需要更多的解释,除了由Josh O'Brien提供的有用答案。

除了通常由Josh引用 / 创建的“数据子集”(Subset of the Data)缩写外,我认为考虑将"S"表示为“同样的”或“自我参照”也很有帮助 - .SD在其最基本的形式下是一个对data.table本身的反身参考 - 正如我们将在下面的示例中看到的那样,这对于链式连接“查询”(使用[进行提取/子集等)特别有帮助。特别地,这也意味着.SD本身是一个data.table(有一个警告是它不允许使用进行赋值)。

.SDcols指定时,.SD的简单用法是用于列子集;我认为这个版本更加直接明了,因此我们将首先涵盖这个版本。第二个用法的.SD解释,分组场景(即,当指定by = keyby = 时),在概念上略有不同(尽管本质上相同,因为毕竟,一个未分组的操作是仅有一个组进行分组的边缘情况)。


以下是一些说明性示例和我经常使用的其他用法示例:

加载Lahman数据

为了使这更具现实感,而不是编造数据,让我们从Lahman加载一些关于棒球的数据集:

library(data.table) 
library(magrittr) # some piping can be beautiful
library(Lahman)
Teams = as.data.table(Teams)
# *I'm selectively suppressing the printed output of tables here*
Teams
Pitching = as.data.table(Pitching)
# subset for conciseness
Pitching = Pitching[ , .(playerID, yearID, teamID, W, L, G, ERA)]
Pitching

裸露的 .SD

为了说明我所说的关于.SD反身性质的含义,考虑它最平凡的用法:

Pitching[ , .SD]
#         playerID yearID teamID  W  L  G   ERA
#     1: bechtge01   1871    PH1  1  2  3  7.96
#     2: brainas01   1871    WS3 12 15 30  4.50
#     3: fergubo01   1871    NY2  0  0  1 27.00
#     4: fishech01   1871    RC1  4 16 24  4.35
#     5: fleetfr01   1871    NY2  0  1  1 10.00
#    ---                                       
# 44959: zastrro01   2016    CHN  1  0  8  1.13
# 44960: zieglbr01   2016    ARI  2  3 36  2.82
# 44961: zieglbr01   2016    BOS  2  4 33  1.52
# 44962: zimmejo02   2016    DET  9  7 19  4.87
# 44963:  zychto01   2016    SEA  1  0 12  3.29

也就是说,我们刚刚返回了Pitching,也就是说,这是一种过于冗长的写法,等同于PitchingPitching[]

identical(Pitching, Pitching[ , .SD])
# [1] TRUE

在子集方面,.SD仍然是数据的一个子集,只是一个微不足道的子集(即集合本身)。

列子集:.SDcols

影响.SD内容的第一种方法是使用.SDcols参数限制包含在.SD中的

Pitching[ , .SD, .SDcols = c('W', 'L', 'G')]
#         W  L  G
#     1:  1  2  3
#     2: 12 15 30
#     3:  0  0  1
#     4:  4 16 24
#     5:  0  1  1
# ---         
# 44959:  1  0  8
# 44960:  2  3 36
# 44961:  2  4 33
# 44962:  9  7 19
# 44963:  1  0 12

这只是为了说明而已,相当无聊。但即使这样的简单用法也可以用于各种高度有益/普遍的数据操作:

列类型转换

列类型转换是数据处理中必不可少的一部分--截至本文撰写时,fwrite 无法自动读取 DatePOSIXct,在字符/因子/数字之间进行转换是很常见的。我们可以使用 .SD.SDcols 批量转换此类列。

我们注意到以下列在 Teams 数据集中以 character 形式存储:

# see ?Teams for explanation; these are various IDs
#   used to identify the multitude of teams from
#   across the long history of baseball
fkt = c('teamIDBR', 'teamIDlahman45', 'teamIDretro')
# confirm that they're stored as `character`
Teams[ , sapply(.SD, is.character), .SDcols = fkt]
# teamIDBR teamIDlahman45    teamIDretro 
#     TRUE           TRUE           TRUE 

如果你对这里使用的 sapply 感到困惑,请注意它与基础 R 中的 data.frames 是相同的:

setDF(Teams) # convert to data.frame for illustration
sapply(Teams[ , fkt], is.character)
# teamIDBR teamIDlahman45    teamIDretro 
#     TRUE           TRUE           TRUE 
setDT(Teams) # convert back to data.table

理解这个语法的关键是要记住,一个data.table(以及一个data.frame)可以被视为一个list,其中每个元素都是一列——因此,sapply/lapplyFUN应用于每个column,并像通常一样返回结果(这里,FUN == is.character返回长度为1的logical,因此sapply返回一个向量)。
将这些列转换为factor的语法非常相似——只需添加:=赋值运算符即可。
Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]

请注意,我们必须将fkt用括号()括起来,以强制R将其解释为列名,而不是尝试将名称fkt分配给RHS。 .SDcols(和:=)的灵活性接受列位置的字符向量或整数向量,这对于基于模式的列名转换也非常有用。我们可以将所有factor列转换为character
fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]

然后将所有包含team的列转换回factor

team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]

** 显式地使用列号(例如DT [,(1):= rnorm(.N)])是不好的做法,并且如果列位置发生变化,随着时间的推移可能会导致代码默默地损坏。即使隐含地使用数字,如果我们在创建编号索引和使用它的顺序上没有保持智能/严格控制,也可能是危险的。

控制模型的RHS

变化的模型规范是强大的统计分析的核心特征。让我们尝试使用Pitching表中可用的小数据集来预测投手的ERA(平均失分率,一种性能度量)。当包括哪些其他协变量在规范中时,W(胜利)和ERA之间的(线性)关系如何变化?

这里是一个利用.SD功能探索这个问题的简短脚本:

# this generates a list of the 2^k possible extra variables
#   for models of the form ERA ~ G + (...)
extra_var = c('yearID', 'teamID', 'G', 'L')
models =
  lapply(0L:length(extra_var), combn, x = extra_var, simplify = FALSE) %>%
  unlist(recursive = FALSE)

# here are 16 visually distinct colors, taken from the list of 20 here:
#   https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/
col16 = c('#e6194b', '#3cb44b', '#ffe119', '#0082c8', '#f58231', '#911eb4',
          '#46f0f0', '#f032e6', '#d2f53c', '#fabebe', '#008080', '#e6beff',
          '#aa6e28', '#fffac8', '#800000', '#aaffc3')

par(oma = c(2, 0, 0, 0))
sapply(models, function(rhs) {
  # using ERA ~ . and data = .SD, then varying which
  #   columns are included in .SD allows us to perform this
  #   iteration over 16 models succinctly.
  #   coef(.)['W'] extracts the W coefficient from each model fit
  Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', rhs)]
}) %>% barplot(names.arg = sapply(models, paste, collapse = '/'),
               main = 'Wins Coefficient with Various Covariates',
               col = col16, las = 2L, cex.names = .8)

fit OLS coefficient on W, various specifications, depicted as bars with distinct colors.

系数始终具有预期的符号(更好的投手倾向于赢得更多比赛并减少失分),但其大小可以根据我们控制的其他因素而大幅变化。

条件连接

data.table 语法美丽简单而且强大。语法 x[i] 灵活处理两种常见的子集方法 - 当 i 是一个 logical 向量时,x[i] 将返回 x 中对应于 iTRUE 的行;当 i 是另一个 data.table 时,将执行一次 join 操作(在普通形式下,使用 xikey,否则,当指定 on = 时,使用这些列的匹配项)。

总体上来说这是很棒的,但当我们希望执行 条件连接 时就会遇到问题,其中表之间的确切关系取决于一个或多个列中行的某些特性。

这个例子有点牵强,但说明了这个想法;请参阅此处 (1, 2) 获取更多信息。

目标是向Pitching表添加一列team_performance,记录每个团队最佳投手的团队表现(排名),该表现由至少有6场比赛记录的投手中最低的防御率衡量。
# to exclude pitchers with exceptional performance in a few games,
#   subset first; then define rank of pitchers within their team each year
#   (in general, we should put more care into the 'ties.method'
Pitching[G > 5, rank_in_team := frank(ERA), by = .(teamID, yearID)]
Pitching[rank_in_team == 1, team_performance := 
           # this should work without needing copy(); 
           #   that it doesn't appears to be a bug: 
           #   https://github.com/Rdatatable/data.table/issues/1926
           Teams[copy(.SD), Rank, .(teamID, yearID)]]

请注意,x[y] 语法返回 nrow(y) 个值,这就是为什么在 Teams[.SD].SD 在右边的原因(因为在这种情况下 := 的 RHS 需要 nrow(Pitching[rank_in_team == 1]) 个值)。

分组的 .SD 操作

通常,我们希望在数据的分组级别上执行某些操作。当我们指定 by =(或 keyby = )时,处理 j 时,data.table 的心理模型是将您的 data.table 分成许多组件子 data.table,每个子表对应于您的 by 变量的一个单独的值:

A visual depiction of how grouping works. on the left is a grid. The first column is titled "ID COLUMN" with values the capital letters A through G, and the rest of the data is unlabelled, but is in a darker color and simply has "Data" written to indicate that's arbitrary. A right arrow shows how this data is split into groups. Each capital letter A through G has a grid on the right-hand side; the grid on the left has been subdivided to create that on the right.

在这种情况下,.SD 具有多重性质--它分别指向每个子 data.table一个接一个地(稍微更准确地说,.SD 的作用范围是单个子 data.table)。这使我们能够简洁地表达我们想要在重新组装结果返回给我们之前,在每个子 data.table上执行的操作。
这在各种设置中都很有用,其中最常见的是:

群组子集

让我们获取Lahman数据中每个球队最近的赛季数据。这可以通过以下简单的方式完成:

# the data is already sorted by year; if it weren't
#   we could do Teams[order(yearID), .SD[.N], by = teamID]
Teams[ , .SD[.N], by = teamID]

请注意,.SD本身就是一个data.table.N指的是每个组中的总行数(在每个组内等于nrow(.SD)),因此.SD[.N]返回与每个teamID相关联的最后一行的.SD 整体

另一个常见的版本是改用.SD[1L]以获取每个组的第一条观测结果。

群组最优解

假设我们想要按照每个团队得分的总数(R;当然,我们可以轻松调整此内容以引用其他度量标准)返回每个团队的最佳年份。现在,我们定义所需的索引动态地如下:

Teams[ , .SD[which.max(R)], by = teamID]

请注意,这种方法当然可以与.SDcols结合使用,以仅返回每个.SDdata.table部分(但要注意.SDcols在各个子集中应保持固定)。
注:GForce参见)目前已经优化了.SD[1L],它是data.table内部的一种机制,可大幅加快最常见的分组操作,如summean——有关更多详细信息,请参阅?GForce,并关注/支持功能改进请求的更新:1, 2, 3, 4, 5, 6

分组回归

回到上面有关ERAW之间关系的问题,假设我们希望这种关系因团队而异(即每个团队都有不同的斜率)。我们可以轻松地重新运行此回归以探索这种关系中的异质性,方法如下(请注意,此方法的标准误差通常不正确--规范化ERA ~ W * teamID将更好--此方法更易于阅读且系数是可以的):

# use the .N > 20 filter to exclude teams with few observations
Pitching[ , if (.N > 20) .(w_coef = coef(lm(ERA ~ W))['W']), by = teamID
          ][ , hist(w_coef, 20, xlab = 'Fitted Coefficient on W',
                    ylab = 'Number of Teams', col = 'darkgreen',
                    main = 'Distribution of Team-Level Win Coefficients on ERA')]

A histogram depicting the distribution of fitted coefficients. It is vaguely bell-shaped and concentrated around -.2

虽然存在一定的异质性,但观察到整体值存在明显的集中趋势。
希望这能阐明在data.table中,.SD的强大作用,帮助编写优美高效的代码!

6
好的解释。只有一个评论:您可以利用快速的data.table排序函数,而不是Teams [ , .SD[which.max(R)], by = teamID] :通过Teams [order(teamID,-R) , .SD[1L], keyby = teamID],这应该更快。 - bartleby
@bartleby 谢谢,确实如此,但是了解一般的方法仍然很有用--例如,如果我们需要按组相关数量排序。也许我会改变这个例子。 - MichaelChirico

5

3
很遗憾,Stack Overflow通常不欢迎基本上只包含外部链接的回答。也许你想编辑更多的文本和代码信息进去吗?顺便说一下,Matt已经将你的视频添加到维基中了:https://github.com/Rdatatable/data.table/wiki/Presentations - Frank

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