Cosa significa .SD in data.table in R


173

.SDsembra utile ma non so davvero cosa ci sto facendo. Cosa significa? Perché c'è un periodo precedente (punto)? Cosa succede quando lo uso?

Ho letto: contiene .SDun data.tablesottoinsieme dei xdati di ciascun gruppo, escluse le colonne del gruppo. Può essere utilizzato quando si raggruppano per i, quando si raggruppano per by, con chiave bye _ad hoc_by

Ciò significa che la figlia data.tableè tenuta in memoria per l'operazione successiva?


30
?data.tableè stato migliorato nella versione 1.7.10, grazie a questa domanda. Ora spiega il nome .SDsecondo la risposta accettata.
Matt Dowle,

Risposte:


205

.SDsta per " Subset di Data.table". Non c'è alcun significato per l'iniziale ".", tranne per il fatto che rende ancora più improbabile che ci sia uno scontro con un nome di colonna definito dall'utente.

Se questo è il tuo 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

Ciò potrebbe aiutarti a vedere cosa .SDè:

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

Fondamentalmente, l' by=yistruzione suddivide il data.table originale in questi due sotto-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

e opera su di loro a loro volta.

Mentre funziona su uno dei due, ti consente di fare riferimento al sottotitolo corrente data.tableusando il nick-name / handle / simbolo .SD. È molto utile, poiché puoi accedere e operare sulle colonne proprio come se fossi seduto alla riga di comando lavorando con un singolo data.table chiamato .SD... tranne che qui, data.tableeseguirà tali operazioni su ogni singolo sub- data.tabledefinito da combinazioni della chiave, "incollandole" insieme e restituendo i risultati in un unico data.table!


12
Si, esattamente. Un altro modo di vedere .SDè DT[,print(.SD),by=y].
Matt Dowle,

6
@MatthewDowle - Mentre sei qui, una domanda per te. Fare DT[,print(.SD[,y]),by=y], indica che ho accesso al valore di y, anche se non fa parte di .SD. Da dove viene il valore ydell'ambito? È disponibile b / c è il valore corrente di by?
Josh O'Brien,

8
@Josh Nice. Sì. Si .SD[,y]tratta di un data.tablesottoinsieme regolare, quindi poiché ynon è una colonna di .SDesso viene visualizzato nell'ambiente che lo ha chiamato, che in questo caso è l' jambiente (della DTquery) in cui bysono disponibili le variabili. Se non trovato lì, appare nel genitore, e nel suo genitore e così via nel solito modo R. (Bene, anche tramite l'ambito ereditato da join, che non viene utilizzato in questi esempi perché non ci sono messaggi i).
Matt Dowle,

1
@Josh FAQ 2.8 ha di più su questo, potrebbe essere necessario migliorare (suggerimenti ben accetti).
Matt Dowle,

3
@Josh Perché le variabili di gruppo sono disponibili in j, troppo, e sono di lunghezza 1. by=list(x,y,z)significherebbe x, ye zsono a disposizione per j. Anche per l'accesso generico sono racchiusi .BY. La FAQ 2.10 ha un po 'di storia, ma è possibile aggiungere un po' di chiarezza ?data.table. Un grande aiuto docu sarebbe molto gradito. Ancora meglio se desideri unirti al progetto e cambiare direttamente.
Matt Dowle,

98

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)

misura il coefficiente OLS su W, varie specifiche

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:

raggruppamento illustrato

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

distribuzione dei coefficienti adattati

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!


5
Buona spiegazione Un solo commento: Invece di Teams [, .SD [which.max (R)], di = teamID], puoi sfruttare la funzione veloce data.table order: tramite Teams [order (teamID, -R), .SD [ 1L], keyby = teamID], questo dovrebbe essere più veloce.
Bartolby il

@bartleby grazie, in effetti è corretto, ma l'approccio generale è ancora utile da sapere, ad esempio se è necessario ordinare in base a una quantità dipendente dal gruppo. forse cambierò l'esempio.
MichaelChirico,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.