Учитывая, как часто это происходит, я думаю, что это заслуживает немного большего изложения, помимо полезного ответа, данного Джошем О'Брайеном выше.
В дополнение к S убсетуаббревиатура D ata, обычно цитируемая / созданная Джошем, я думаю, что также полезно рассмотреть букву «S», обозначающую «Само собой» или «Самоссылка» - .SD
в самом основномПриведите рефлексивную ссылку к самому data.table
- как мы увидим в примерах ниже, это особенно полезно для объединения воедино «запросов» (извлечения / подмножества / и т.д., используя [
).В частности, это также означает, что .SD
сам по себе является data.table
(с оговоркой, что он не позволяет присваивать с :=
).
Упрощенное использование .SD
для поднабора столбцов (т. е. когда указано .SDcols
);Я думаю, что эту версию гораздо проще понять, поэтому сначала мы рассмотрим ее ниже.Интерпретация .SD
во втором его использовании, сценарии группирования (т. Е. Когда задано by =
или keyby =
), концептуально немного отличается (хотя по сути это то же самое, поскольку, в конце концов, не группируетсяоперация - крайний случай группировки только с одной группой).
Вот несколько иллюстративных примеров и некоторые другие примеры использования, которые я сам часто использую:
Загрузка данных Лахмана
Чтобы придать этому более реалистичный видВместо того, чтобы составлять данные, давайте загрузим некоторые наборы данных о бейсболе из 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
Первый способ повлиять на то, что .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 интерпретировать это как имена столбцов,вместо того, чтобы пытаться присвоить RHS имя fkt
.
Гибкость .SDcols
(и :=
) для принятия character
vector или и 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 питчера (Earned Runs Average, показатель производительности), используя небольшой набор ковариат, доступных в таблице 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)
Коэффициент всегда имеет ожидаемый знак (лучшие кувшины, как правило, имеют больше побед и меньше разрешенных пробегов), но величинаможет существенно различаться в зависимости от того, что еще мы контролируем.
Условные объединения * Синтаксис
data.table
прекрасен своей простотой и надежностью.Синтаксис x[i]
гибко обрабатывает два распространенных подхода к подмножеству - когда i
является вектором logical
, x[i]
возвращает те строки x
, которые соответствуют где i
равно TRUE
;когда i
равен , другой data.table
, выполняется join
(в простом виде, используя key
s 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
разделен на множество компонент data.table
с, каждый из которых соответствует одному значению вашей by
переменной (переменных):
В этом случае .SD
является множественным по природе - оно относится к каждому из этих под data.table
с, по одному (слегкаТочнее, область действия .SD
- это единица (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
относится к общему количеству строк в группе (оно равно * 1237).* внутри каждой группы), поэтому .SD[.N]
возвращает полное значение .SD
для последней строки, связанной с каждым teamID
.
Другой распространенной версией этого является использование .SD[1L]
вместо этого, чтобы получить первое наблюдение для каждой группы.
Группа Optima
Предположим, что мы хотим вернуть лучший год для каждой команды, измеренный по их общему количеству забитых запусков (R
; конечно, мы могли бы легко откорректировать это для ссылки на другие метрики).Вместо того чтобы брать элемент fixed из каждого подэлемента 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')]
Несмотря на то, что существует значительная степень неоднородности, существует четкая концентрация вокруг наблюдаемогообщая ценность
Надеюсь, это прояснило силу .SD
в создании красивого и эффективного кода в data.table
!