Что делает .SD в data.table в R

.SD выглядит полезным, но я не знаю, что с ним делаю. Что это означает? Почему существует предшествующий период (полная остановка). Что происходит, когда я его использую?

Я читал: .SD - это data.table, содержащее подмножество данных x для каждой группы, исключая столбцы (группы) группы. Он может использоваться при группировке i при группировке по by, с клавишей by и _ad hoc_ by

Означает ли это, что дочь data.table удерживается в памяти для следующей операции?

Ответ 1

.SD означает что-то вроде S ubset D ata.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.tables

DT[ , print(.SD), by=y]
# <1st sub-data.table, called '.SD' while it being operated on>
#    x v
# 1: a 1
# 2: b 3
# 3: c 5
# <2nd sub-data.table, ALSO called '.SD' while it 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

и работает по ним поочередно.

Пока он работает на одном, он позволяет ссылаться на текущий суб- data.table, используя псевдоним/дескриптор/символ .SD. Это очень удобно, так как вы можете получить доступ к столбцам и работать с ними так же, как если бы вы сидели в командной строке, работающей с одной таблицей data.table с именем .SD... кроме того, здесь data.table будет выполнять эти операции на каждый под-data.table, определяемый комбинациями ключа, "вставляя" их обратно вместе и возвращая результаты в один data.table!

Ответ 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 !

Ответ 3

Я сделал видео об этом после разговора с Мэттом Доулом о .SD, вы можете посмотреть его на YouTube: https://www.youtube.com/watch?v=DwEzQuYfMsI