Modificare:
Data la benvenuta risposta, l'ho convertita in una vignetta del pacchetto ora disponibile qui
Data la frequenza con cui si presenta, penso che ciò meriti un po 'più di esposizione, oltre alla risposta utile data da Josh O'Brien sopra.
Oltre alla S ubset del D ata acronimo di solito riferita / creato da Josh, penso che sia anche utile considerare la "S" a riposo per "selfsame" o "Self-di riferimento" - .SDè nella sua più elementare veste un riferimento riflessivo a data.tablese stesso - come vedremo negli esempi seguenti, questo è particolarmente utile per concatenare "query" (estrazioni / sottoinsiemi / ecc [.). In particolare, ciò significa anche che .SDè esso stesso undata.table (con l'avvertenza con cui non consente l'assegnazione :=).
L'uso più semplice di .SDè per il sottoinsieme di colonne (cioè, quando .SDcolsè specificato); Penso che questa versione sia molto più semplice da capire, quindi la tratteremo per prima. Nel .SDsuo secondo utilizzo, l'interpretazione degli scenari di raggruppamento (cioè quando by =o quando keyby =è specificato) è leggermente diversa, concettualmente (sebbene alla base sia la stessa, poiché, dopo tutto, un'operazione non raggruppata è un caso limite di raggruppamento con solo un gruppo).
Ecco alcuni esempi illustrativi e alcuni altri esempi di usi che io stesso implemento spesso:
Caricamento dati Lahman
Per dare a questo un aspetto più reale, piuttosto che inventare dati, cariciamo alcuni set di dati sul baseball da 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
Nudo .SD
Per illustrare cosa intendo per la natura riflessiva di .SD, considera il suo uso più banale:
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
Cioè, siamo appena tornati Pitching, cioè questo era un modo eccessivamente dettagliato di scrivere Pitchingo Pitching[]:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
In termini di subsetting, .SDè ancora un sottoinsieme dei dati, è solo banale (il set stesso).
Sottoimpostazione colonna: .SDcols
Il primo modo per influire su ciò che .SDè è limitare le colonne contenute .SDnell'uso .SDcolsdell'argomento a [:
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
Questo è solo a scopo illustrativo ed è stato piuttosto noioso. Ma anche questo semplice utilizzo si presta a un'ampia varietà di operazioni di manipolazione dei dati altamente utili / onnipresenti:
Conversione del tipo di colonna
La conversione del tipo di colonna è un dato di fatto per il munging dei dati: al momento della stesura di questo documento, fwritenon è possibile leggere automaticamente colonne Dateo POSIXctcolonne e le conversioni avanti e indietro tra character/ factor/ numericsono comuni. Possiamo usare .SDe .SDcolsconvertire in batch gruppi di tali colonne.
Notiamo che le seguenti colonne sono memorizzate come characternel Teamsset di dati:
# 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
Se sei confuso dall'uso di sapplyqui, nota che è lo stesso della base 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
La chiave per comprendere questa sintassi è ricordare che a data.table(e a data.frame) possono essere considerati come un punto in listcui ogni elemento è una colonna - quindi sapply/ si lapplyapplica FUNa ciascuna colonna e restituisce il risultato come sapply/ di lapplysolito (qui, FUN == is.characterrestituisce un logicaldi lunghezza 1, quindi sapplyrestituisce un vettore).
La sintassi in cui convertire queste colonne factorè molto simile: è sufficiente aggiungere l' :=operatore di assegnazione
Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]
Si noti che dobbiamo racchiudere le fktparentesi ()per forzare R a interpretarlo come nomi di colonna, anziché cercare di assegnare il nome fkta RHS.
La flessibilità di .SDcols(e :=) accettare un charactervettore o un integervettore di posizioni di colonna può anche rivelarsi utile per la conversione basata su schemi di nomi di colonne *. Potremmo convertire tutte le factorcolonne in character:
fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]
E quindi converti tutte le colonne che contengono di teamnuovo in factor:
team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
** L' uso esplicito dei numeri di colonna (come DT[ , (1) := rnorm(.N)]) è una cattiva pratica e può portare a codice silenziosamente danneggiato nel tempo se le posizioni delle colonne cambiano. Anche l'uso implicito dei numeri può essere pericoloso se non manteniamo un controllo intelligente / rigoroso sull'ordinamento di quando creiamo l'indice numerato e quando lo utilizziamo.
Controllo dell'RHS di un modello
Le specifiche del modello variabile sono una caratteristica fondamentale di una solida analisi statistica. Proviamo a prevedere l'ERA di un lanciatore (Earned Runs Average, una misura della prestazione) usando il piccolo set di covariate disponibili nella Pitchingtabella. In che modo la relazione (lineare) tra W(vince) e ERAvaria a seconda di quali altre covariate sono incluse nella specifica?
Ecco un breve script che sfrutta il potere di .SDcui esplora questa domanda:
# 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)

Il coefficiente ha sempre il segno atteso (i migliori lanciatori tendono ad avere più vittorie e meno corse consentite), ma la grandezza può variare sostanzialmente a seconda di cos'altro controlliamo.
Join condizionali
data.tablela sintassi è bella per la sua semplicità e robustezza. La sintassi x[i]gestisce in modo flessibile due approcci comuni al subsetting: quando iè un logicalvettore, x[i]restituirà quelle righe xcorrispondenti a dove si itrova TRUE; quando iè un altrodata.table , joinviene eseguita una (in forma semplice, usando la keys di xe i, altrimenti, quando on =è specificato, usando le corrispondenze di quelle colonne).
Questo è ottimo in generale, ma non è all'altezza quando desideriamo eseguire un join condizionale , in cui la natura esatta della relazione tra le tabelle dipende da alcune caratteristiche delle righe in una o più colonne.
Questo esempio è un po 'inventato, ma illustra l'idea; vedere qui ( 1 , 2 ) per ulteriori informazioni.
L'obiettivo è quello di aggiungere una colonna team_performanceal Pitchingtavolo che registri le prestazioni della squadra (rango) del miglior lanciatore su ogni squadra (come misurato dall'ERA più bassa, tra i lanciatori con almeno 6 partite registrate).
# 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)]]
Si noti che la x[y]sintassi restituisce nrow(y)valori, motivo per cui si .SDtrova sulla destra Teams[.SD](poiché l'RHS :=in questo caso richiede nrow(Pitching[rank_in_team == 1])valori.
.SDOperazioni raggruppate
Spesso vorremmo eseguire alcune operazioni sui nostri dati a livello di gruppo . Quando specifichiamo by =(o keyby =), il modello mentale per ciò che accade quando i data.tableprocessi jdevono pensare al tuo data.tablecome diviso in molti componenti secondari data.table, ognuno dei quali corrisponde a un singolo valore delle tue byvariabili:

In questo caso, .SDè di natura multipla - si riferisce a ciascuna di queste sottosezioni data.table, una alla volta (leggermente più accuratamente, l'ambito di .SDè una singola sotto- data.table). Questo ci consente di esprimere in modo conciso un'operazione che vorremmo eseguire su ciascun sotto-componentedata.table prima che il risultato riassemblato ci venga restituito.
Ciò è utile in una varietà di impostazioni, le più comuni delle quali sono presentate qui:
Sottoinsieme di gruppi
Otteniamo la stagione più recente di dati per ogni squadra nei dati di Lahman. Questo può essere fatto semplicemente con:
# 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]
Ricorda che .SDè esso stesso a data.table, e che si .Nriferisce al numero totale di righe in un gruppo (è uguale nrow(.SD)all'interno di ciascun gruppo), quindi .SD[.N]restituisce l' intero valore.SD dell'ultima riga associata a ciascuno teamID.
Un'altra versione comune di questo è utilizzare .SD[1L]invece per ottenere la prima osservazione per ciascun gruppo.
Gruppo Optima
Supponiamo di voler restituire l' anno migliore per ogni squadra, misurato in base al numero totale di corse segnate ( R; potremmo facilmente regolare questo per fare riferimento ad altre metriche, ovviamente). Invece di prendere un elemento fisso da ogni sottotitolo data.table, ora definiamo dinamicamente l' indice desiderato come segue:
Teams[ , .SD[which.max(R)], by = teamID]
Si noti che questo approccio può ovviamente essere combinato con .SDcolsper restituire solo porzioni di data.tableciascuno .SD(con l'avvertenza che .SDcolsdovrebbe essere riparata attraverso i vari sottoinsiemi)
NB : .SD[1L]è attualmente ottimizzato da GForce( vedi anche ), data.tableinterni che accelerano notevolmente le operazioni raggruppate più comuni come sumo mean- vedi ?GForceper maggiori dettagli e tieni d'occhio / supporto vocale per richieste di miglioramento delle funzionalità per gli aggiornamenti su questo fronte: 1 , 2 , 3 , 4 , 5 , 6
Regressione raggruppata
Ritornando alla precedente domanda sulla relazione tra ERAe W, supponiamo che ci aspettiamo che questa relazione differisca per squadra (cioè, c'è una pendenza diversa per ogni squadra). Possiamo facilmente rieseguire questa regressione per esplorare l'eterogeneità in questa relazione come segue (osservando che gli errori standard di questo approccio sono generalmente errati - le specifiche ERA ~ W*teamIDsaranno migliori - questo approccio è più facile da leggere e i coefficienti sono OK) :
# 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')]

Sebbene vi sia una discreta eterogeneità, esiste una netta concentrazione attorno al valore complessivo osservato
Spero che questo abbia chiarito il potere di .SDfacilitare un codice bello ed efficiente data.table!
?data.tableè stato migliorato nella versione 1.7.10, grazie a questa domanda. Ora spiega il nome.SDsecondo la risposta accettata.