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.table
se 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 .SD
suo 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 Pitching
o 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 .SD
nell'uso .SDcols
dell'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, fwrite
non è possibile leggere automaticamente colonne Date
o POSIXct
colonne e le conversioni avanti e indietro tra character
/ factor
/ numeric
sono comuni. Possiamo usare .SD
e .SDcols
convertire in batch gruppi di tali colonne.
Notiamo che le seguenti colonne sono memorizzate come character
nel Teams
set 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 sapply
qui, 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 list
cui ogni elemento è una colonna - quindi sapply
/ si lapply
applica FUN
a ciascuna colonna e restituisce il risultato come sapply
/ di lapply
solito (qui, FUN == is.character
restituisce un logical
di lunghezza 1, quindi sapply
restituisce 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 fkt
parentesi ()
per forzare R a interpretarlo come nomi di colonna, anziché cercare di assegnare il nome fkt
a RHS.
La flessibilità di .SDcols
(e :=
) accettare un character
vettore o un integer
vettore di posizioni di colonna può anche rivelarsi utile per la conversione basata su schemi di nomi di colonne *. Potremmo convertire tutte le factor
colonne 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 team
nuovo 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 Pitching
tabella. In che modo la relazione (lineare) tra W
(vince) e ERA
varia a seconda di quali altre covariate sono incluse nella specifica?
Ecco un breve script che sfrutta il potere di .SD
cui 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.table
la sintassi è bella per la sua semplicità e robustezza. La sintassi x[i]
gestisce in modo flessibile due approcci comuni al subsetting: quando i
è un logical
vettore, x[i]
restituirà quelle righe x
corrispondenti a dove si i
trova TRUE
; quando i
è un altrodata.table
, join
viene eseguita una (in forma semplice, usando la key
s di x
e 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_performance
al Pitching
tavolo 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 .SD
trova sulla destra Teams[.SD]
(poiché l'RHS :=
in questo caso richiede nrow(Pitching[rank_in_team == 1])
valori.
.SD
Operazioni 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.table
processi j
devono pensare al tuo data.table
come diviso in molti componenti secondari data.table
, ognuno dei quali corrisponde a un singolo valore delle tue by
variabili:
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 .N
riferisce 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 .SDcols
per restituire solo porzioni di data.table
ciascuno .SD
(con l'avvertenza che .SDcols
dovrebbe essere riparata attraverso i vari sottoinsiemi)
NB : .SD[1L]
è attualmente ottimizzato da GForce
( vedi anche ), data.table
interni che accelerano notevolmente le operazioni raggruppate più comuni come sum
o mean
- vedi ?GForce
per 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 ERA
e 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*teamID
saranno 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 .SD
facilitare un codice bello ed efficiente data.table
!
?data.table
è stato migliorato nella versione 1.7.10, grazie a questa domanda. Ora spiega il nome.SD
secondo la risposta accettata.