.SD
semble utile mais je ne sais pas vraiment ce que je vais en faire. Qu'est ce que cela signifie? Pourquoi y a-t-il une période précédente (point final). Qu'est-ce qui se passe quand je l'utilise?
Je lis: .SD
est un data.table
contenant le sous-ensemble des données de x
pour chaque groupe, à l'exclusion de la ou des colonnes du groupe. Il peut être utilisé lors du regroupement par i
, lors du regroupement par by
, à clé by
et à _ad hoc_ by
Est-ce que cela signifie que la fille data.table
s est conservé en mémoire pour la prochaine opération?
.SD
Signifie quelque chose comme "S
ubset de D
ata.table". Le "."
Initial n'a aucune signification, sauf qu'il est encore plus improbable qu'il y ait un conflit avec un nom de colonne défini par l'utilisateur.
S'il s'agit de votre 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
Cela peut vous aider voir ce que .SD
Est:
DT[ , .SD[ , paste(x, v, sep="", collapse="_")], by=y]
# y V1
# 1: 1 a1_b3_c5
# 2: 3 a2_b4_c6
En gros, l’instruction by=y
Divise le fichier data.table original en ces deux sous - 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
et opère à leur tour.
Pendant qu’il fonctionne sur l’un ou l’autre, il vous permet de vous référer au sous-répertoire actuel - data.table
En utilisant le pseudonyme/handle/symbole .SD
. C'est très pratique, car vous pouvez accéder aux colonnes et les utiliser comme si vous étiez assis sur la ligne de commande et ne travailliez qu'avec un seul fichier data.table appelé .SD
... sauf qu'ici, data.table
effectuera ces opérations sur chaque sous-utilisateur - data.table
défini par des combinaisons de la clé, en les "collant" ensemble et en renvoyant les résultats dans un seul data.table
!
Compte tenu de la fréquence à laquelle cela se présente, je pense que cela mérite un peu plus de précision, au-delà de la réponse utile donnée par Josh O'Brien ci-dessus.
En plus de la [~ # ~] s [~ # ~] de l'bset de [~ # ~] d [~ # ~] acronyme habituellement cité/créé par Josh, je pense qu'il est également utile de considérer le "S" comme un sigle "Selfsame" ou "Self-reference" - .SD
Est sous sa forme la plus élémentaire a référence réflexive au data.table
Lui-même - comme nous le verrons dans les exemples ci-dessous, cela est particulièrement utile pour chaîner "requêtes" (extractions/sous-ensembles/etc utilisant [
). En particulier, cela signifie également que .SD
Est lui-même un data.table
(à condition de ne pas permettre l'affectation avec :=
).
L’utilisation plus simple de .SD
S’applique aux sous-ensembles de colonnes (c.-à-d. Lorsque .SDcols
Est spécifié); Je pense que cette version est beaucoup plus simple à comprendre, nous allons donc en parler en premier. L’interprétation de .SD
Dans sa deuxième utilisation, regroupant des scénarios (c’est-à-dire, lorsque by =
Ou keyby =
Est spécifié), est légèrement différente sur le plan conceptuel (bien qu’il car, après tout, une opération non groupée est un cas Edge de regroupement avec un seul groupe).
Voici quelques exemples illustratifs et d'autres exemples d'usages que je mets souvent en œuvre:
Pour donner à cela une impression plus réaliste, plutôt que de créer des données, chargeons quelques 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
.SD
Pour illustrer ce que je veux dire à propos de 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
C’est-à-dire que nous venons de renvoyer Pitching
, c’est-à-dire qu’il s’agissait d’une manière trop verbeuse d’écrire Pitching
ou Pitching[]
:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
En termes de sous-ensemble, .SD
Est toujours un sous-ensemble des données, il s'agit simplement d'un sous-ensemble (l'ensemble lui-même).
.SDcols
La première façon d'influer sur ce que .SD
Est de limiter les colonnes contenues dans .SD
À l'aide de l'argument .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
Ceci est juste pour l'illustration et était assez ennuyeux. Mais même cette simple utilisation se prête à une grande variété d'opérations de manipulation de données hautement bénéfiques/omniprésentes:
La conversion du type de colonne est une réalité de la vie pour la collecte de données - à la date de rédaction de cet article, fwrite
ne peut pas lire automatiquement les colonnes Date
ou POSIXct
, et les conversions entre character
/factor
/numeric
sont courantes. Nous pouvons utiliser .SD
Et .SDcols
Pour convertir par lots des groupes de telles colonnes.
Nous remarquons que les colonnes suivantes sont stockées sous la forme character
dans le fichier Teams
:
# 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 êtes dérouté par l'utilisation de sapply
ici, notez qu'il en va de même 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 qu'un data.table
(Ainsi qu'un data.frame
) Peut être considéré comme un list
où chaque élément est une colonne - ainsi, sapply
/lapply
applique FUN
à chaque colonne et renvoie le résultat sous la forme sapply
/lapply
habituellement (ici, FUN == is.character
renvoie un logical
de longueur 1, donc sapply
renvoie un vecteur).
La syntaxe pour convertir ces colonnes en factor
est très similaire - ajoutez simplement l'opérateur d'affectation :=
Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]
Notez que nous devons envelopper fkt
entre parenthèses ()
Pour forcer R à interpréter cela en tant que noms de colonnes, au lieu d'essayer d'attribuer le nom fkt
au RHS.
La flexibilité de .SDcols
(Et de :=
) Pour accepter un vecteur character
ou un vecteur integer
de positions de colonne peut également utile pour la conversion basée sur des modèles de noms de colonnes *. Nous pourrions convertir toutes les colonnes factor
en character
:
fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]
Puis convertissez toutes les colonnes contenant team
en factor
:
team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
** Explicitement utiliser des numéros de colonne (comme DT[ , (1) := rnorm(.N)]
) est une mauvaise pratique et peut conduire à un code silencieusement corrompu dans le temps si les positions des colonnes changent. Même implicitement, utiliser des nombres peut être dangereux si nous ne gardons pas un contrôle intelligent/strict sur le classement lorsque nous créons l'index numéroté et quand nous l'utilisons.
La spécification de modèle variable est une caractéristique essentielle d'une analyse statistique robuste. Essayons de prédire l'ERA (moyenne des gains obtenus, mesure de la performance) d'un lanceur en utilisant le petit ensemble de covariables disponible dans la table Pitching
. Comment la relation (linéaire) entre W
(victoires) et ERA
varie-t-elle en fonction des autres covariables incluses dans la spécification?
Voici un court script utilisant la puissance de .SD
Qui 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 l'ampleur peut varier considérablement en fonction de ce que nous contrôlons.
La syntaxe data.table
Est belle pour sa simplicité et sa robustesse. La syntaxe x[i]
Gère avec souplesse deux approches courantes de sous-paramétrage: lorsque i
est un vecteur logical
, x[i]
Renverra les lignes de x
correspondant à où i
est TRUE
; lorsque i
est un autre data.table
, un join
est exécuté (en clair, en utilisant les key
de x
et i
, sinon, lorsque on =
Est spécifié, en utilisant les correspondances de ces colonnes).
C’est génial en général, mais n’est pas valable 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 d’une ou de plusieurs colonnes.
Cet exemple est un peu artificiel, mais illustre l’idée; voir ici ( 1 , 2 ) pour plus d'informations.
L’objectif est d’ajouter une colonne team_performance
À la table Pitching
qui enregistre la performance de l’équipe (rang) du meilleur lanceur de chaque équipe (mesurée par le plus bas ERA, parmi les lanceurs ayant au moins 6 parties enregistrées).
# 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 syntaxe x[y]
Renvoie nrow(y)
valeurs, raison pour laquelle .SD
Est à droite dans Teams[.SD]
(Puisque le RHS de :=
nécessite dans ce cas nrow(Pitching[rank_in_team == 1])
valeurs.
.SD
Nous aimerions souvent effectuer des opérations sur nos données au niveau du groupe. Lorsque nous spécifions by =
(Ou keyby =
), Le modèle mental de ce qui se produit lorsque data.table
Traite j
consiste à penser à votre data.table
divisé en plusieurs sous-composants - data.table
, chacun correspondant à une valeur unique de votre (vos) variable (s) by
:
Dans ce cas, .SD
Est de nature multiple - il fait référence à chacun de ces sous - - data.table
S, un à la fois (légèrement plus précisément, la portée de .SD
correspond à un seul sous - data.table
). Cela nous permet d’exprimer de manière concise une opération que nous aimerions effectuer sur chaque sous-fichier - data.table
avant que le résultat réassemblé ne nous soit renvoyé.
Ceci est utile dans une variété de paramètres, les plus courants étant présentés ici:
Voyons la saison de données la plus récente pour chaque équipe dans les données de Lahman. Cela peut être fait très 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
Est lui-même un data.table
, Et que .N
Fait référence au nombre total de lignes d'un groupe (égal à nrow(.SD)
dans chaque groupe). ), donc .SD[.N]
renvoie le intégralité de .SD
pour la dernière ligne associée à chaque teamID
.
Une autre version courante consiste à utiliser .SD[1L]
À la place pour obtenir l'observation première de chaque groupe.
Supposons que nous voulions renvoyer l’année meilleure 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, de cours). Au lieu de prendre un élément fixed de chaque sous - - data.table
, 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 renvoyer uniquement des parties du data.table
Pour chaque .SD
(Avec l’avertissement que .SDcols
Doit être corrigé à travers les différents sous-ensembles)
[~ # ~] nb [~ # ~]: .SD[1L]
est actuellement optimisé par GForce
( voir aussi ), data.table
internes accélèrent massivement les opérations groupées les plus courantes telles que sum
ou mean
- voir ?GForce
Pour plus de détails et gardez un œil sur/l'assistance vocale pour les demandes d'amélioration de fonctionnalités pour les mises à jour sur ce sujet: 1 , 2 , =, 4 , 5 , 6
Pour revenir à la question ci-dessus concernant la relation entre ERA
et W
, supposons 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 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')]
Bien qu’il y ait beaucoup d’hétérogénéité, il existe une concentration distincte autour de la valeur globale observée
Espérons que cela a élucidé le pouvoir de .SD
Pour faciliter un code beau et efficace dans data.table
!
J'ai fait une vidéo à ce sujet après avoir parlé de .SD à Matt Dowle. Vous pouvez la voir sur YouTube: https://www.youtube.com/watch?v=DwEzQuYfMsI