Что означает .SD в data.table в R

173

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

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

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

Фаррел
источник
30
?data.tableбыл улучшен в v1.7.10, благодаря этому вопросу. Это теперь объясняет имя .SDсогласно принятому ответу.
Мэтт Доул

Ответы:

205

.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

и действует на них по очереди.

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

Джош О'Брайен
источник
12
Да, точно. Еще один способ увидеть .SDэто DT[,print(.SD),by=y].
Мэтт Доул
6
@ MatthewDowle - пока ты здесь, вопрос к тебе. «Делать» DT[,print(.SD[,y]),by=y]означает, что у меня есть доступ к значению y, даже если оно не является частью .SD. Откуда ценность области yвидимости? Это доступно, потому что это текущее значение by?
Джош О'Брайен
8
@ Джош Хорошо. Да. Это .SD[,y]обычное data.tableподмножество, так как yстолбец этого столбца .SDне выглядит в среде, которая его вызвала, которая в данном случае является jсредой ( DTзапроса), в byкоторой доступны переменные. Если его там нет, он выглядит в родительском, родительском и т. Д. Обычным способом. (Ну, через унаследованную область соединения тоже, которая не используется в этих примерах, потому что нет is).
Мэтт Доул
1
@Josh FAQ 2.8 имеет больше об этом, возможно, нуждается в улучшении (предложения приветствуются).
Мэтт Доул
3
@Josh Поскольку переменные группы доступны у, тоже, и длина 1. by=list(x,y,z)будет означать x, yи zдоступны j. Для общего доступа они также включены .BY. FAQ 2.10 имеет некоторую историю, но некоторую ясность можно добавить к ?data.table. Отлично, документальная помощь будет очень кстати. Еще лучше, если вы хотите присоединиться к проекту и изменить напрямую.
Мэтт Доул
98

Редактировать:

Учитывая, насколько хорошо был получен этот ответ, я преобразовал его в виньетку пакета, доступную здесь


Учитывая, как часто это происходит, я думаю, что это заслуживает немного большего изложения, помимо полезного ответа Джоша О'Брайена выше.

В дополнении к S ubset от D ату аббревиатуры обычно цитируемой / создатель Джош, я думаю , что это также полезно рассмотреть «S» стоять « тот же самый» или «Self-эталонным» - .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, то есть это был слишком многословный способ написания 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

Ключом к пониманию этого синтаксиса является напоминание о том, что a data.table(а также a data.frame) можно рассматривать как a, listгде каждый элемент является столбцом - таким образом, sapply/ lapplyприменяется FUNк каждому столбцу и возвращает результат, как sapply/ lapplyобычно будет (здесь FUN == is.characterвозвращается logicalдлиной 1, такsapply возвращает вектор).

Синтаксис для преобразования этих столбцов factorочень похож - просто добавьте :=оператор присваивания

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

Обратите внимание, что мы должны заключить fktв скобки, ()чтобы заставить R интерпретировать это как имена столбцов, вместо того, чтобы пытаться присвоить имя fktRHS.

Гибкость .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)

подходит коэффициент OLS по W, различные спецификации

Коэффициент всегда имеет ожидаемый знак (у лучших кувшинов, как правило, больше побед и меньше разрешенных пробегов), но величина может существенно варьироваться в зависимости от того, что еще мы контролируем.

Условные объединения

data.tableсинтаксис прекрасен своей простотой и надежностью. Синтаксис x[i]гибко обрабатывает два общие подходы к Подменят - когда iэто logicalвектор, x[i]будет возвращать эти строки , xсоответствующие где iнаходится TRUE; когда iявляется другимdata.table , joinвыполняется a (в простой форме, используя keys of 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 прежде чем нам будет возвращен пересобранный результат.

Это полезно в различных настройках, наиболее распространенные из которых представлены здесь:

Подмножество групп

Давайте получим самый последний сезон данных для каждой команды в данных 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]вместо этого, чтобы получить первое наблюдение для каждой группы.

Группа Оптима

Предположим, что мы хотим вернуть лучший год для каждой команды, измеренный по их общему количеству забитых запусков ( Rразумеется, мы могли бы легко откорректировать это для ссылки на другие показатели). Вместо того, чтобы брать фиксированный элемент из каждого подпункта 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!

MichaelChirico
источник
5
Хорошее объяснение. Только один комментарий: вместо команд [, .SD [which.max (R)], by = teamID] вы можете использовать функцию быстрого заказа data.table: через команды [order (teamID, -R), .SD [ 1L], keyby = teamID], это должно быть быстрее.
Бартлби
@ bartleby спасибо, действительно, это правильно, но общий подход все еще полезно знать - например, если нам нужно упорядочить по количеству, зависящему от группы. возможно я изменю пример.
MichaelChirico
3

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

Sharon
источник
2
К сожалению, переполнение стека обычно не приветствует ответы, которые состоят в основном из внешних ссылок. Может быть, вы хотите отредактировать в нем больше текста и информации о коде? Fyi Matt добавил ваше видео в вики: github.com/Rdatatable/data.table/wiki/Presentations
Фрэнк