Ответ 2
Учитывая, как часто это происходит, я думаю, что это заслуживает немного большего изложения, помимо полезного ответа Джоша О'Брайена выше.
В дополнение к S ubset от D ата аббревиатурой обычно цитируемой/созданный Джошем, я думаю, что это также полезно рассмотреть "S" стоять "или" тот же самый "самоссылки" - .SD
находится в самом базовом data.table
рефлексивную ссылку на сам data.table
- как мы увидим в примерах ниже, это особенно полезно для объединения воедино "запросов" (извлечения/подмножества/и т.д. с использованием [
). В частности, это также означает, что .SD
сам по себе является data.table
(с оговоркой, что он не позволяет присваивать с :=
).
Более простое использование .SD
для поднабора столбцов (т. .SDcols
Когда .SDcols
); Я думаю, что эту версию гораздо проще понять, поэтому сначала мы рассмотрим ее ниже. Интерпретация .SD
при его втором использовании, группировании сценариев (т. keyby =
задано значение 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
, то есть это был слишком многословный способ написания Pitching
или Pitching[]
:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
С точки зрения подмножества .SD
по-прежнему является подмножеством данных, оно просто тривиальное (сам набор).
.SDcols
столбцов: .SDcols
Первый способ повлиять на то, что .SD
- это ограничить столбцы, содержащиеся в .SD
используя аргумент .SDcols
до [
:
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
не может автоматически читать столбцы Date
или POSIXct
, и преобразования между character
/factor
/numeric
встречаются часто. Мы можем использовать .SD
и .SDcols
для пакетного преобразования групп таких столбцов.
Мы замечаем, что следующие столбцы хранятся в виде character
в наборе данных Teams
:
# 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
/lapply
применяет FUN
к каждому столбцу и возвращает результат как обычно sapply
/lapply
(здесь FUN == is.character
возвращает logical
длины 1, поэтому 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
Различная спецификация модели является основной характеристикой надежного статистического анализа. Давайте попробуем и спрогнозировать ERA питчера (среднее число заработанных ходов, мера производительности), используя небольшой набор ковариат, доступных в таблице Pitching
. Как (линейные) отношения между 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]()
Коэффициент всегда имеет ожидаемый знак (у лучших кувшинов, как правило, больше побед и меньше разрешенных пробегов), но величина может существенно варьироваться в зависимости от того, что еще мы контролируем.
Условные объединения
Синтаксис data.table
прекрасен своей простотой и надежностью. Синтаксис x[i]
гибко обрабатывает два распространенных подхода к подмножеству - когда i
является logical
вектором, x[i]
будет возвращать те строки x
соответствуют где i
- TRUE
; когда i
- другой data.table
, выполняется join
(в простом виде, используя key
x
и i
, в противном случае, когда указано on =
, используя совпадения этих столбцов).
В целом это хорошо, но не получается, когда мы хотим выполнить условное соединение, в котором точный характер отношений между таблицами зависит от некоторых характеристик строк в одном или нескольких столбцах.
Этот пример немного придуман, но иллюстрирует идею; см. здесь (1, 2) для получения дополнительной информации.
Цель состоит в том, чтобы добавить столбец team_performance
к Pitching
таблице, которая записывает производительность команды (ранг) лучшего кувшин на каждую команде (как измерено самой низкой 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)
, поэтому .SD
находится справа в Teams[.SD]
(поскольку RHS для :=
в этом случае требует nrow(Pitching[rank_in_team == 1])
значения.
Сгруппированные .SD
операции
Часто мы хотели бы выполнить некоторые операции с нашими данными на уровне группы. Когда мы указываем с by =
(или keyby =
), ментальная модель того, что происходит, когда data.table
обрабатывает j
заключается в том, чтобы думать о том, что ваш data.table
разделен на множество компонентов sub- data.table
s, каждый из которых соответствует одно значение из ваших by
переменной (ы):
![grouping illustrated]()
В этом случае .SD
является множественным по своей природе - он относится к каждому из этих sub- data.table
s, по одному за раз (немного точнее, область действия .SD
представляет собой одиночные sub- data.table
). Это позволяет нам кратко выразить операцию, которую мы хотели бы выполнить с каждой таблицей sub- data.table
до того, как нам будет возвращен data.table
результат.
Это полезно в различных настройках, наиболее распространенные из которых представлены здесь:
Подмножество групп
Позвольте получить самый последний сезон данных для каждой команды в данных Лахмана. Это можно сделать довольно просто с помощью:
# 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]
чтобы получить первое наблюдение для каждой группы.
Группа Оптима
Предположим, что мы хотим вернуть лучший год для каждой команды, измеренный по их общему количеству забитых запусков (R
; конечно, мы могли бы легко откорректировать это для ссылки на другие показатели). Вместо того, чтобы брать фиксированный элемент из каждого sub- data.table
, теперь мы определим искомый индекс динамически следующим образом:
Teams[ , .SD[which.max(R)], by = teamID]
Обратите внимание, что этот подход, конечно, можно комбинировать с .SDcols
для возврата только части data.table
для каждого .SD
(с оговоркой, что .SDcols
должен быть зафиксирован в различных подмножествах)
NB: .SD[1L]
в настоящее время оптимизируется с помощью GForce
(см. Также), внутренностей data.table
которые значительно ускоряют наиболее распространенные сгруппированные операции, такие как sum
или mean
- смотрите ?GForce
для получения дополнительной информации и отслеживания/голосовой поддержки для запросов на улучшение функций для обновлений на этом фронте: 1, 2, 3, 4, 5, 6
Группированная регрессия
Возвращаясь к приведенному выше запросу относительно отношений между ERA
и W
, предположим, что мы ожидаем, что эти отношения будут различаться в зависимости от команды (т.е. Для каждой команды будет свой наклон). Мы можем легко перезапустить эту регрессию, чтобы исследовать неоднородность в этом отношении следующим образом (отмечая, что стандартные ошибки этого подхода, как правило, неверны - спецификация 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')]
![distribution of fitted coefficients]()
Несмотря на то, что существует значительная степень неоднородности, вокруг наблюдаемой общей стоимости имеется четкая концентрация
Надеемся, что это объяснило возможности .SD
в .SD
красивого и эффективного кода в data.table
!