Wofür steht .SD in data.table in R.

172

.SDsieht nützlich aus, aber ich weiß nicht wirklich, was ich damit mache. Für was steht das? Warum gibt es eine vorhergehende Periode (Punkt). Was passiert, wenn ich es benutze?

I gelesen: .SDein data.tabledie Teilmenge von enthaltende xs - Daten für jede Gruppe, mit Ausnahme der Spalte Gruppe (n). Es kann beim Gruppieren nach i, beim Gruppieren nach by, Keyed byund _ad hoc_ verwendet werden.by

Bedeutet das, dass die Tochter data.tables für die nächste Operation gespeichert wird?

Farrel
quelle
30
?data.tablewurde dank dieser Frage in Version 1.7.10 verbessert. Es erklärt nun den Namen .SDgemäß der akzeptierten Antwort.
Matt Dowle

Antworten:

205

.SDsteht für so etwas wie " Subset of Data.table". Die Initiale "."hat keine Bedeutung , außer dass es noch unwahrscheinlicher ist, dass es zu einem Konflikt mit einem benutzerdefinierten Spaltennamen kommt.

Wenn dies Ihre data.table ist:

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

Auf diese Weise können Sie möglicherweise erkennen, was .SDist:

DT[ , .SD[ , paste(x, v, sep="", collapse="_")], by=y]
#    y       V1
# 1: 1 a1_b3_c5
# 2: 3 a2_b4_c6

Grundsätzlich unterteilt die by=yAnweisung die ursprüngliche Datentabelle in diese beiden Unterabschnittedata.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

und arbeitet wiederum auf sie.

Während eines der beiden Vorgänge können Sie data.tablemithilfe des Spitznamens / Handles / Symbols auf das aktuelle Sub zugreifen .SD. Dies ist sehr praktisch, da Sie auf die Spalten zugreifen und sie bearbeiten können, als ob Sie an der Befehlszeile sitzen und mit einer einzelnen Datentabelle namens .SD... arbeiten würden, mit der Ausnahme, dass hier data.tablediese Operationen für jede einzelne data.tabledurch definierte Tabelle ausgeführt werden Kombinationen des Schlüssels, "zusammenfügen" und die Ergebnisse in einem einzigen zurückgeben data.table!

Josh O'Brien
quelle
12
Ja genau. Eine andere Art zu sehen .SDist DT[,print(.SD),by=y].
Matt Dowle
6
@MatthewDowle - Während du hier bist, eine Frage an dich. Wenn Sie dies tun DT[,print(.SD[,y]),by=y], bedeutet dies, dass ich Zugriff auf den Wert von habe y, obwohl dies kein Teil von ist .SD. yWoher kommt der Wert des Umfangs? Ist es verfügbar, weil es der aktuelle Wert von ist by?
Josh O'Brien
8
@ Josh Schön. Ja. Das .SD[,y]ist eine reguläre data.tableTeilmenge, da in der Umgebung, in der es aufgerufen wurde, ykeine Spalte .SDdavon angezeigt wird. In diesem Fall handelt es sich um die jUmgebung (der DTAbfrage), in der die byVariablen verfügbar sind. Wenn es dort nicht gefunden wird, sieht es im übergeordneten Element und seinem übergeordneten Element usw. auf die übliche R-Weise aus. (Nun, auch über Join geerbten Bereich, der in diesen Beispielen nicht verwendet wird, weil es keine is gibt).
Matt Dowle
1
@Josh FAQ 2.8 hat mehr dazu, muss möglicherweise verbessert werden (Vorschläge willkommen).
Matt Dowle
3
@ Josh Weil die Gruppengrößen sind in j verfügbar, auch, und sind die Länge 1 by=list(x,y,z)würde bedeuten x, yund zsind zur Verfügung j. Für den generischen Zugriff sind sie auch eingepackt .BY. FAQ 2.10 enthält eine gewisse Historie, aber es könnte etwas Klarheit hinzugefügt werden ?data.table. Tolle, Doku-Hilfe wäre sehr willkommen. Noch besser, wenn Sie dem Projekt beitreten und direkt ändern möchten.
Matt Dowle
96

Bearbeiten:

Angesichts der guten Resonanz dieser Antwort habe ich sie in eine Paketvignette umgewandelt, die jetzt hier verfügbar ist


Angesichts der Häufigkeit, mit der dies geschieht, ist dies meiner Meinung nach eine etwas ausführlichere Darstellung erforderlich, die über die hilfreiche Antwort von Josh O'Brien hinausgeht.

Neben dem S ubset der D ata Akronym in der Regel von Josh zitiert / erstellt, ich denke , es ist auch hilfreich , um die „S“ zu prüfen , für „selbigen“ oder „Selbstreferenz“ stehen - .SDist in seiner einfachsten Gestalt ein reflexiver Verweis auf sich data.tableselbst - wie wir in den folgenden Beispielen sehen werden, ist dies besonders hilfreich, um "Abfragen" (Extraktionen / Teilmengen / usw. mit [) zu verketten . Dies bedeutet insbesondere auch, dass .SDes sich selbst um eine handeltdata.table (mit der Einschränkung, dass keine Zuordnung zulässig ist :=).

Die einfachere Verwendung von .SDist für die Spaltenuntermenge (dh wenn .SDcolsangegeben); Ich denke, diese Version ist viel einfacher zu verstehen, deshalb werden wir das zuerst unten behandeln. Die Interpretation von .SDGruppierungsszenarien (dh wann by =oder keyby =angegeben) in ihrer zweiten Verwendung ist konzeptionell etwas anders (obwohl es im Kern dieselbe ist, da eine nicht gruppierte Operation schließlich ein Randfall der Gruppierung mit just ist Eine Gruppe).


Hier sind einige veranschaulichende Beispiele und einige andere Beispiele für Verwendungen, die ich selbst oft implementiere:

Laden von Lahman-Daten

Um dies realistischer zu gestalten, anstatt Daten zu erstellen, laden wir einige Datensätze über Baseball von 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

Nackt .SD

Um zu veranschaulichen, was ich über die Reflexivität von meine .SD, betrachten Sie die banalste Verwendung:

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

Das heißt, wir sind gerade zurückgekehrt Pitching, dh dies war eine übermäßig ausführliche Schreibweise Pitchingoder Pitching[]:

identical(Pitching, Pitching[ , .SD])
# [1] TRUE

In Bezug auf die Teilmenge .SDist es immer noch eine Teilmenge der Daten, es ist nur eine triviale (die Menge selbst).

Spaltenuntermenge: .SDcols

Die erste Möglichkeit, Auswirkungen auf das zu haben, .SDbesteht darin, die in der Verwendung des Arguments enthaltenen Spalten auf Folgendes zu beschränken :.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

Dies ist nur zur Veranschaulichung und war ziemlich langweilig. Aber selbst diese einfache Verwendung eignet sich für eine Vielzahl von äußerst nützlichen / allgegenwärtigen Datenmanipulationsvorgängen:

Spaltentypkonvertierung

Spalte Typumwandlung ist eine Tatsache des Lebens für die Daten munging - als dies geschrieben wurde , fwritekann nicht automatisch gelesen Dateoder POSIXctSpalten und Conversions zurück und her unter character/ factor/ numericsind üblich. Wir können Gruppen solcher Spalten verwenden .SDund .SDcolsstapelweise konvertieren.

Wir stellen fest, dass die folgenden Spalten wie characterim TeamsDatensatz gespeichert sind :

# 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 

Wenn Sie durch die Verwendung von sapplyhier verwirrt sind, beachten Sie, dass es dasselbe ist wie für Basis 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

Der Schlüssel zum Verständnis dieser Syntax zu erinnern ist , dass ein data.table(sowie data.frame) als ein in Betracht gezogen werden , listwobei jedes Element eine Spalte - also sapply/ lapplygilt FUNfür jede Spalte und gibt das Ergebnis als sapply/ lapplyin der Regel würde (hier FUN == is.characterkehrt eine logicalvon Länge 1, sapplygibt also einen Vektor zurück).

Die Syntax zum Konvertieren dieser Spalten factorist sehr ähnlich - fügen Sie einfach den :=Zuweisungsoperator hinzu

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

Beachten Sie, dass wir fktin Klammern setzen müssen (), um R zu zwingen, dies als Spaltennamen zu interpretieren, anstatt zu versuchen, den Namen fktder RHS zuzuweisen .

Die Flexibilität von .SDcols(und :=), einen characterVektor oder einen integerVektor von Spaltenpositionen zu akzeptieren, kann auch für die musterbasierte Konvertierung von Spaltennamen * nützlich sein. Wir könnten alle factorSpalten konvertieren in character:

fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]

Und konvertieren Sie dann alle Spalten, die zurück enthalten, teamin factor:

team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]

** Die explizite Verwendung von Spaltennummern (wie DT[ , (1) := rnorm(.N)]) ist eine schlechte Praxis und kann im Laufe der Zeit zu unbemerkt beschädigtem Code führen, wenn sich die Spaltenpositionen ändern. Selbst die implizite Verwendung von Zahlen kann gefährlich sein, wenn wir nicht genau kontrollieren, wann wir den nummerierten Index erstellen und wann wir ihn verwenden.

Steuerung der RHS eines Modells

Unterschiedliche Modellspezifikationen sind ein Kernmerkmal einer robusten statistischen Analyse. Versuchen wir, die ERA eines Pitchers (Earned Runs Average, ein Maß für die Leistung) anhand der in der PitchingTabelle verfügbaren kleinen Menge von Kovariaten vorherzusagen . Wie variiert die (lineare) Beziehung zwischen W(gewinnt) und ERAhängt davon ab, welche anderen Kovariaten in der Spezifikation enthalten sind?

Hier ist ein kurzes Skript, das die Kraft .SDdieser Frage nutzt :

# 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)

Passen Sie den OLS-Koeffizienten an W an, verschiedene Spezifikationen

Der Koeffizient hat immer das erwartete Vorzeichen (bessere Pitcher haben tendenziell mehr Siege und weniger erlaubte Läufe), aber die Größe kann erheblich variieren, je nachdem, was wir sonst noch kontrollieren.

Bedingte Verknüpfungen

data.tableDie Syntax ist schön für ihre Einfachheit und Robustheit. Die Syntax x[i]verarbeitet flexibel zwei gemeinsame Ansätze zu subsetting - wenn ia logicalvector, x[i]kehrt die Zeilen der dem xentspricht, wo iist TRUE; Wenn ies sich um ein anderes handeltdata.table , joinwird a ausgeführt (in der einfachen Form unter Verwendung des keys von xund iandernfalls, wenn on =angegeben, unter Verwendung der Übereinstimmungen dieser Spalten).

Dies ist im Allgemeinen großartig, wird jedoch nicht ausgeführt, wenn eine bedingte Verknüpfung durchgeführt werden soll , bei der die genaue Art der Beziehung zwischen Tabellen von einigen Merkmalen der Zeilen in einer oder mehreren Spalten abhängt.

Dieses Beispiel ist ein bisschen erfunden, veranschaulicht aber die Idee; siehe hier ( 1 , 2 ) für mehr.

Ziel ist es team_performance, der PitchingTabelle eine Spalte hinzuzufügen, in der die Leistung (Rang) des Teams als bester Pitcher in jedem Team aufgezeichnet wird (gemessen an der niedrigsten ERA unter Pitchern mit mindestens 6 aufgezeichneten Spielen).

# 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)]]

Beachten Sie, dass die x[y]Syntax nrow(y)Werte zurückgibt , weshalb sie .SDrechts in steht Teams[.SD](da die RHS von :=in diesem Fall nrow(Pitching[rank_in_team == 1])Werte erfordert .

Gruppierte .SDOperationen

Oft möchten wir unsere Daten auf Gruppenebene bearbeiten . Wenn wir angeben by =(oder keyby =), besteht das mentale Modell für das, was bei data.tableProzessen passiert , jdarin, sich vorzustellen, dass Sie data.tablein viele Komponenten unterteilt sind data.table, von denen jede einem einzelnen Wert Ihrer byVariablen entspricht:

Gruppierung dargestellt

In diesem Fall .SDist es, mehrere in der Natur - bezieht er sich auf jede dieser sub- data.tables, one-at-a-time (etwas genauer, der Umfang der .SDein einzelner sub- data.table). Auf diese Weise können wir eine Operation, die wir für jedes Subdata.table ausführen möchten, präzise ausdrücken, bevor das neu zusammengestellte Ergebnis an uns zurückgegeben wird.

Dies ist in einer Vielzahl von Einstellungen nützlich, von denen die häufigsten hier vorgestellt werden:

Gruppenuntermenge

Lassen Sie uns die neuesten Daten für jedes Team in den Lahman-Daten abrufen. Dies kann ganz einfach gemacht werden mit:

# 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]

Denken Sie daran, dass dies .SDselbst ein data.tableist und .Nsich auf die Gesamtzahl der Zeilen in einer Gruppe bezieht (dies entspricht der Anzahl nrow(.SD)innerhalb jeder Gruppe), sodass .SD[.N]die Gesamtheit.SD für die letzte Zeile zurückgegeben wird, die jeder Zeile zugeordnet ist teamID.

Eine andere übliche Version davon ist die Verwendung .SD[1L], um stattdessen die erste Beobachtung für jede Gruppe zu erhalten.

Gruppe Optima

Angenommen, wir wollten das beste Jahr für jedes Team zurückgeben, gemessen an der Gesamtzahl der erzielten Läufe ( Rwir könnten dies natürlich leicht anpassen, um auf andere Metriken zu verweisen). Anstatt aus jedem Unterelement ein festes Element zu nehmen data.table, definieren wir den gewünschten Index jetzt dynamisch wie folgt:

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

Beachten Sie, dass dieser Ansatz natürlich kombiniert werden kann .SDcols, um nur Teile von data.tablejedem zurückzugeben .SD(mit der Einschränkung, .SDcolsdie über die verschiedenen Teilmengen hinweg festgelegt werden sollte).

NB : .SD[1L]wird derzeit optimiert GForce( siehe auch ), data.tableEinbauten , die massiv die häufigsten gruppierten Operationen wie beschleunigen sumoder mean- siehe ?GForcefür weitere Einzelheiten und halten ein Auge auf / Sprachunterstützung für Anfragen Feature Verbesserung für Updates an dieser Front: 1 , 2 , 3 , 4 , 5 , 6

Gruppierte Regression

Zurück zur obigen Anfrage bezüglich der Beziehung zwischen ERAund W, nehmen wir an, dass diese Beziehung von Team zu Team unterschiedlich ist (dh es gibt für jedes Team eine andere Steigung). Wir können diese Regression leicht wiederholen, um die Heterogenität in dieser Beziehung wie folgt zu untersuchen (wobei zu beachten ist, dass die Standardfehler dieses Ansatzes im Allgemeinen falsch sind - die Spezifikation ERA ~ W*teamIDwird besser sein - dieser Ansatz ist leichter zu lesen und die Koeffizienten sind in Ordnung). ::

# 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')]

Verteilung der angepassten Koeffizienten

Während es ein gutes Maß an Heterogenität gibt, gibt es eine deutliche Konzentration um den beobachteten Gesamtwert

Hoffentlich hat dies die Fähigkeit aufgeklärt, .SDschönen, effizienten Code zu ermöglichen data.table!

MichaelChirico
quelle
5
Gute Erklärung. Nur ein Kommentar: Anstelle von Teams [, .SD [which.max (R)], by = teamID] können Sie die schnelle Funktion data.table order nutzen: über Teams [order (teamID, -R), .SD [ 1L], keyby = teamID], dies sollte schneller sein.
Bartleby
@bartleby danke, das ist zwar richtig, aber der allgemeine Ansatz ist immer noch nützlich zu wissen - zB wenn wir nach einer gruppenabhängigen Menge bestellen müssen. Vielleicht ändere ich das Beispiel.
MichaelChirico