【问题标题】:What does .SD stand for in data.table in R.SD 在 R 中的 data.table 中代表什么
【发布时间】:2012-01-20 11:07:46
【问题描述】:

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

我读到: .SD 是一个data.table,包含每个组的x 数据的子集,不包括组列。可用于i分组、by、keyedby、_ad hoc_by分组时使用

这是否意味着女儿data.tables 被保存在内存中以备下一次操作?

【问题讨论】:

  • ?data.table 在 v1.7.10 中得到了改进,感谢这个问题。它现在根据接受的答案解释名称 .SD

标签: r data.table


【解决方案1】:

.SD 代表类似“Subset of 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中返回结果!

【讨论】:

  • 是的,完全正确。查看.SD 的另一种方式是DT[,print(.SD),by=y]
  • @MatthewDowle -- 当你在这里时,问你一个问题。执行DT[,print(.SD[,y]),by=y] 表示我确实可以访问y 的值,即使它不是.SD 的一部分。 y 的值是从哪里确定的?是否可用 b/c 它是 by 的当前值?
  • @Josh Nice。是的。 .SD[,y] 是常规的 data.table 子集,因此由于 y 不是 .SD 的列,它在调用它的环境中查找,在这种情况下是 j 环境(DT查询),其中by 变量可用。如果在那里找不到,它会以通常的 R 方式在父级及其父级中查找,依此类推。 (好吧,也可以通过连接继承范围,因为没有is,所以在这些示例中没有使用它)。
  • @Josh FAQ 2.8 对此有更多内容,可能需要改进(欢迎提出建议)。
  • @Josh 因为组变量在 j 中也可用,并且长度为 1。by=list(x,y,z) 意味着 xyz 可用于 j。对于通用访问,它们也包含在 .BY 中。 FAQ 2.10 有一些历史,但可以在?data.table 中添加一些清晰的内容。太好了,非常欢迎文档帮助。如果您想加入项目并直接进行更改,那就更好了。
【解决方案2】:

编辑:

鉴于这个答案的受欢迎程度,我已将其转换为现在可用的包小插图here


考虑到这种情况出现的频率,我认为除了上面 Josh O'Brien 给出的有用答案之外,还需要进行更多说明。

除了 Josh 通常引用/创建的 Data 首字母缩略词的 S子集之外,我认为考虑“S”代表“”也很有帮助Selfsame”或“Self-reference”——.SD 在其最基本的形式中是对data.table 本身的反身引用——正如我们将在下面的示例中看到的那样,这特别有用用于将“查询”链接在一起(使用[ 的提取/子集/等)。特别是,这也意味着.SD本身就是一个data.table(需要注意的是它不允许使用:= 赋值)。

.SD 的更简单用法是用于列子集(即,当指定 .SDcols 时);我认为这个版本更容易理解,所以我们将在下面首先介绍。 .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 cannot automatically read Date or POSIXct columnscharacter/factor/numeric 之间的来回转换很常见。我们可以使用.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 感到困惑,请注意它与base 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/@987654384 @ 将 FUN 应用于每个 并返回结果为 sapply/lapply 通常会(这里,FUN == is.character 返回长度为 1 的 logical,所以 sapply 返回向量)。

将这些列转换为factor 的语法非常相似——只需添加:= 赋值运算符

Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]

请注意,我们必须将 fkt 括在括号 () 中以强制 R 将其解释为列名,而不是尝试将名称 fkt 分配给 RHS。

.SDcols(和:=)可以灵活地接受character 向量integer 列位置向量也可以用于基于模式的列转换名称*。我们可以将所有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)

系数总是有预期的符号(更好的投手往往有更多的胜利和更少的跑动),但大小可能会根据我们控制的其他因素而有很大差异。

条件连接

data.table 语法因其简单性和健壮性而美丽。语法x[i] 灵活地处理了两种常见的子集化方法——当ilogical 向量时,x[i] 将返回x 的那些行,对应于iTRUE 的位置;当i 是另一个data.table时,执行join(以普通形式,使用xikeys,否则,当on = 指定,使用这些列的匹配)。

这通常很好,但是当我们希望执行一个条件连接时就不够用了,其中表之间关系的确切性质取决于一列或多列中的行的某些特征。

这个例子有点做作,但说明了这个想法;请参阅此处(12)了解更多信息。

目标是在Pitching 表中添加一列team_performance,以记录每支球队中最佳投手的球队表现(排名)(以最低ERA 衡量,在至少有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) 值,这就是.SDTeams[.SD] 右侧的原因(因为在这种情况下:= 的RHS 需要nrow(Pitching[rank_in_team == 1]) 值。

分组 .SD 操作

通常,我们希望在组级别对我们的数据执行一些操作。当我们指定by =(或keyby = )时,当data.table 处理j 时发生的事情的心理模型是将您的data.table 视为被拆分为许多组件子data.tables,每个这对应于您的 by 变量的单个值:

在这种情况下,.SD 本质上是多重的——它指代每个子data.tables,一次一个(稍微更准确地说,范围.SD 的一个子 data.table)。这使我们能够简洁地表达我们希望在将重新组装的结果返回给我们之前对每个 sub-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] 返回 整个.SD用于与每个teamID关联的最后一行。

另一个常见的版本是使用.SD[1L] 来获取每个组的first观察结果。

组最优

假设我们想要返回每支球队的最佳年,以他们的总得分来衡量(R;当然,我们可以轻松地调整它以参考其他指标) .我们现在不是从每个子data.table 中获取 fixed 元素,而是动态地 定义所需的索引,如下所示:

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

请注意,这种方法当然可以与.SDcols 结合使用,以仅返回每个.SDdata.table 的一部分(需要注意的是,.SDcols 应该在各个子集中固定)

NB.SD[1L] 目前由 GForce (see also)、data.table 内部优化,可大幅加速最常见的分组操作,如 summean - - 有关更多详细信息,请参阅?GForce,并密切关注/语音支持以获取有关这方面更新的功能改进请求:123456

分组回归

回到上面关于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')]

虽然存在相当多的异质性,但在观察到的总体值周围存在明显的集中度

希望这阐明了.SD 在促进data.table 中漂亮、高效的代码方面的力量!

【讨论】:

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

在与 Matt Dowle 讨论 .SD 后,我制作了一个视频,您可以在 YouTube 上看到它:https://www.youtube.com/watch?v=DwEzQuYfMsI

【讨论】:

猜你喜欢
  • 2015-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-27
  • 1970-01-01
  • 1970-01-01
  • 2018-05-12
  • 1970-01-01
相关资源
最近更新 更多