web-dev-qa-db-fra.com

Que signifie .SD dans Data.table dans R

.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.tables est conservé en mémoire pour la prochaine opération?

157
Farrel

.SD Signifie quelque chose comme "Subset de Data.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!

188
Josh O'Brien

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:

Chargement des données Lahman

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

Nue .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).

Sous-colonne: .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:

Conversion de type de colonne

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.

Contrôler le RHS d'un modèle

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)

fit OLS coefficient on W, various specifications

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.

Jointures conditionnelles

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.

Opérations groupées .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:

grouping illustrated

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:

Souscription de groupe

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.

Groupe Optima

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

Régression groupée

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

distribution of fitted coefficients

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!

75
MichaelChirico

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

1
Sharon