Éditer:
Compte tenu de la satisfaction de cette réponse, je l'ai convertie en une vignette de package maintenant disponible ici
Compte tenu de la fréquence à laquelle cela se produit, je pense que cela mérite un peu plus d'exposé, au-delà de la réponse utile donnée par Josh O'Brien ci-dessus.
En plus du S ubset de l' acronyme D ata habituellement cité / créé par Josh, je pense qu'il est également utile de considérer le "S" pour signifier "Selfsame" ou "Self-reference" - .SD
est dans sa forme la plus élémentaire un référence réflexive à data.table
lui-même - comme nous le verrons dans les exemples ci-dessous, cela est particulièrement utile pour enchaîner des "requêtes" (extractions / sous-ensembles / etc en utilisant [
). En particulier, cela signifie également que .SD
c'est luidata.table
- même un (avec la mise en garde qu'il ne permet pas l'affectation avec :=
).
L'utilisation plus simple de .SD
est pour le sous-ensemble de colonnes (c'est-à-dire quand .SDcols
est spécifié); Je pense que cette version est beaucoup plus simple à comprendre, nous allons donc en parler en premier ci-dessous. L'interprétation de .SD
dans sa deuxième utilisation, les scénarios de regroupement (c'est-à-dire, quand by =
ou keyby =
est spécifié), est légèrement différente, conceptuellement (même si au fond c'est la même chose, car, après tout, une opération non groupée est un cas limite de groupement avec juste un groupe).
Voici quelques exemples illustratifs et quelques autres exemples d'usages que j'implémente souvent moi-même:
Chargement des données Lahman
Pour donner à cela une sensation plus réelle, plutôt que de créer des données, chargeons des ensembles de données sur le baseball à partir de 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
Nu .SD
Pour illustrer ce que je veux dire sur la nature réflexive de .SD
, considérons son utilisation la plus 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
Autrement dit, nous venons de revenir Pitching
, c'est-à-dire que c'était une façon d'écrire trop verbeuse Pitching
ou Pitching[]
:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
En termes de sous-ensemble, .SD
est toujours un sous-ensemble des données, c'est juste un élément trivial (l'ensemble lui-même).
Sous-ensemble de colonnes: .SDcols
La première façon d'avoir un impact sur ce qui .SD
est est de limiter les colonnes contenues dans l' .SD
utilisation de l' .SDcols
argument à [
:
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
C'est juste à titre d'illustration et c'était assez ennuyeux. Mais même cette utilisation simple se prête à une grande variété d'opérations de manipulation de données hautement bénéfiques / omniprésentes:
Conversion de type de colonne
La conversion de type de colonne est une réalité pour le munging de données - à partir de cette écriture, fwrite
ne peut pas lire automatiquement Date
ou POSIXct
colonnes , et les conversions entre character
/ factor
/ numeric
sont courantes. Nous pouvons utiliser .SD
et .SDcols
convertir par lots des groupes de telles colonnes.
Nous remarquons que les colonnes suivantes sont stockées comme character
dans l' Teams
ensemble de données:
# 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
Si vous ne savez pas comment utiliser sapply
ici, notez que c'est la même chose que pour la 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 clé pour comprendre cette syntaxe est de rappeler que a data.table
(ainsi que a data.frame
) peut être considéré comme un list
où chaque élément est une colonne - ainsi, sapply
/ lapply
s'applique FUN
à chaque colonne et renvoie le résultat comme sapply
/ le lapply
ferait habituellement (ici, FUN == is.character
renvoie un logical
de longueur 1, sapply
renvoie donc un vecteur).
La syntaxe pour convertir ces colonnes factor
est très similaire - ajoutez simplement l' :=
opérateur d'affectation
Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]
Notez que nous devons mettre fkt
entre parenthèses ()
pour forcer R à interpréter cela comme des noms de colonnes, au lieu d'essayer d'attribuer le nom fkt
au RHS.
La flexibilité de .SDcols
(et :=
) pour accepter un character
vecteur ou un integer
vecteur de positions de colonne peut également être utile pour la conversion basée sur des modèles de noms de colonnes *. Nous pourrions convertir toutes les factor
colonnes en character
:
fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]
Et puis convertissez toutes les colonnes qui contiennent de team
nouveau en factor
:
team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
** L' utilisation explicite des numéros de colonne (comme DT[ , (1) := rnorm(.N)]
) est une mauvaise pratique et peut entraîner une corruption silencieuse du code au fil du temps si les positions des colonnes changent. Même l'utilisation implicite de nombres peut être dangereuse si nous ne gardons pas un contrôle intelligent / strict sur l'ordre du moment où nous créons l'index numéroté et quand nous l'utilisons.
Contrôle du RHS d'un modèle
La spécification variable des modèles est une caractéristique essentielle d'une analyse statistique robuste. Essayons de prédire l'ERA (Earned Runs Average, une mesure de la performance) d'un lanceur en utilisant le petit ensemble de covariables disponibles dans le Pitching
tableau. Comment la relation (linéaire) entre W
(gagne) et ERA
varie-t-elle en fonction des autres covariables incluses dans la spécification?
Voici un court script exploitant la puissance .SD
dont explore cette question:
# 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)
Le coefficient a toujours le signe attendu (les meilleurs lanceurs ont tendance à avoir plus de victoires et moins de courses autorisées), mais la magnitude peut varier considérablement en fonction de ce que nous contrôlons d'autre.
Jointures conditionnelles
data.table
la syntaxe est belle pour sa simplicité et sa robustesse. La syntaxe x[i]
gère avec souplesse deux approches courantes du sous-ensemble - quand i
est un logical
vecteur, x[i]
retournera ces lignes de x
correspondant à où i
est TRUE
; quand en i
est un autredata.table
, a join
est exécuté (sous forme simple, en utilisant le key
s de x
et i
, sinon, quand on =
est spécifié, en utilisant les correspondances de ces colonnes).
C'est génial en général, mais cela échoue lorsque nous souhaitons effectuer une jointure conditionnelle , dans laquelle la nature exacte de la relation entre les tables dépend de certaines caractéristiques des lignes dans une ou plusieurs colonnes.
Cet exemple est un peu artificiel, mais illustre l'idée; voir ici ( 1 , 2 ) pour plus.
Le but est d'ajouter une colonne team_performance
au Pitching
tableau qui enregistre la performance de l'équipe (rang) du meilleur lanceur de chaque équipe (telle que mesurée par l'ERA la plus basse, parmi les lanceurs avec au moins 6 matchs enregistrés).
# 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)]]
Notez que la x[y]
syntaxe renvoie des nrow(y)
valeurs, c'est pourquoi se .SD
trouve à droite dans Teams[.SD]
(puisque le RHS de :=
dans ce cas nécessite des nrow(Pitching[rank_in_team == 1])
valeurs.
.SD
Opérations groupées
Souvent, nous souhaitons effectuer une opération sur nos données au niveau du groupe . Lorsque nous spécifions by =
(ou keyby =
), le modèle mental de ce qui se passe lorsque les data.table
processus j
doivent penser que vous data.table
êtes divisé en plusieurs sous-composants data.table
, chacun correspondant à une seule valeur de votre ou vos by
variables:
Dans ce cas, .SD
est de nature multiple - il se réfère à chacun de ces sous- data.table
s, un à la fois (un peu plus précisément, la portée de .SD
est un seul sous- data.table
). Cela nous permet d'exprimer de manière concise une opération que nous aimerions effectuer sur chaque sous-ensembledata.table
avant que le résultat réassemblé ne nous soit renvoyé.
Ceci est utile dans une variété de paramètres, dont les plus courants sont présentés ici:
Sous-ensemble de groupe
Obtenons la saison la plus récente de données pour chaque équipe dans les données de Lahman. Cela peut être fait tout simplement avec:
# 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]
Rappelez-vous que .SD
c'est lui-même un data.table
, et qui .N
fait référence au nombre total de lignes dans un groupe (il est égal à nrow(.SD)
dans chaque groupe), donc .SD[.N]
renvoie l' intégralité de.SD
pour la dernière ligne associée à chacun teamID
.
Une autre version courante de ceci est d'utiliser à la .SD[1L]
place pour obtenir la première observation pour chaque groupe.
Groupe Optima
Supposons que nous voulions renvoyer la meilleure année pour chaque équipe, mesurée par le nombre total de points marqués ( R
; nous pourrions facilement l'ajuster pour faire référence à d'autres mesures, bien sûr). Au lieu de prendre un élément fixe de chaque sous- data.table
, nous définissons maintenant l'index souhaité dynamiquement comme suit:
Teams[ , .SD[which.max(R)], by = teamID]
Notez que cette approche peut bien sûr être combinée avec .SDcols
pour ne renvoyer que des parties de data.table
pour chacun .SD
(avec la mise en garde qui .SDcols
doit être corrigée dans les différents sous-ensembles)
NB : .SD[1L]
est actuellement optimisé par GForce
( voir aussi ), des data.table
internes qui accélèrent massivement les opérations groupées les plus courantes comme sum
ou mean
- voir ?GForce
pour plus de détails et garder un œil sur / support vocal pour les demandes d'amélioration des fonctionnalités pour les mises à jour sur ce front: 1 , 2 , 3 , 4 , 5 , 6
Régression groupée
Pour revenir à l'enquête ci-dessus concernant la relation entre ERA
et W
, supposons que nous nous attendions à ce que cette relation diffère d'une équipe à l'autre (c'est-à-dire qu'il existe une pente différente pour chaque équipe). Nous pouvons facilement réexécuter cette régression pour explorer l'hétérogénéité de cette relation comme suit (en notant que les erreurs standard de cette approche sont généralement incorrectes - la spécification ERA ~ W*teamID
sera meilleure - cette approche est plus facile à lire et les coefficients sont corrects) :
# 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')]
Bien qu'il y ait une assez grande hétérogénéité, il y a une concentration distincte autour de la valeur globale observée
Espérons que cela a élucidé le pouvoir de .SD
faciliter un code beau et efficace data.table
!
?data.table
a été amélioré dans la v1.7.10, grâce à cette question. Il explique maintenant le nom.SD
selon la réponse acceptée.