Que signifie .SD dans data.table dans R

173

.SDsemble utile mais je ne sais pas vraiment ce que je fais avec. Qu'est ce que cela signifie? Pourquoi y a-t-il une période précédente (point). Que se passe-t-il lorsque je l'utilise?

J'ai lu: .SDest un data.tablecontenant le sous-ensemble de xdonnées de pour chaque groupe, à l'exclusion des colonnes de groupe. Il peut être utilisé lors du regroupement par i, lors du regroupement par by, par clé byet _ad hoc_by

Cela signifie-t-il que la fille data.tableest gardée en mémoire pour la prochaine opération?

Farrel
la source
30
?data.tablea été amélioré dans la v1.7.10, grâce à cette question. Il explique maintenant le nom .SDselon la réponse acceptée.
Matt Dowle

Réponses:

205

.SDsignifie quelque chose comme " Subset of Data.table". Il n'y a pas de signification pour l'initiale ".", sauf que cela rend 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 .SDc'est:

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

Fondamentalement, l' by=yinstruction divise la table de données d'origine 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 sur eux à leur tour.

Pendant qu'il fonctionne sur l'un ou l'autre, il vous permet de faire référence au sous-marin actuel data.tableen utilisant le pseudo / poignée / symbole .SD. C'est très pratique, car vous pouvez accéder aux colonnes et les utiliser comme si vous étiez assis à la ligne de commande en travaillant avec une seule table data.table appelée .SD... sauf qu'ici, data.tableeffectuera ces opérations sur chaque sous- data.tabledéfini par combinaisons de touches, les "coller" ensemble et renvoyer les résultats en un seul data.table!

Josh O'Brien
la source
12
Oui, exactement. Une autre façon de voir .SDest DT[,print(.SD),by=y].
Matt Dowle
6
@MatthewDowle - Pendant que vous êtes ici, une question pour vous. Faire DT[,print(.SD[,y]),by=y], indique que j'ai accès à la valeur de y, même si cela ne fait pas partie de .SD. D'où est la valeur de la yportée? Est-il disponible b / c c'est la valeur actuelle de by?
Josh O'Brien
8
@Josh Nice. Oui. Le .SD[,y]est un data.tablesous-ensemble régulier, donc depuis yn'est pas une colonne de .SDcelui - ci regarde dans l'environnement qui l'a appelé, qui dans ce cas est l' jenvironnement (de la DTrequête) où les byvariables sont disponibles. S'il ne s'y trouve pas, il regarde dans le parent, et son parent et ainsi de suite de la manière R habituelle. (Eh bien, via la portée héritée de la jointure, également, qui n'est pas utilisée dans ces exemples car il n'y en a pas i).
Matt Dowle
1
@Josh FAQ 2.8 a plus à ce sujet, pourrait avoir besoin d'être amélioré (suggestions bienvenues).
Matt Dowle
3
@Josh Parce que les variables de groupe sont disponibles dans j, aussi, et sont la longueur 1. by=list(x,y,z)signifierait x, yet zsont à la disposition j. Pour un accès générique, ils sont également intégrés .BY. La FAQ 2.10 a un peu d'histoire à ce sujet, mais une certaine clarté pourrait être ajoutée ?data.table. Super, l'aide docu serait la bienvenue. Encore mieux si vous souhaitez rejoindre un projet et changer directement.
Matt Dowle
98

É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" - .SDest dans sa forme la plus élémentaire un référence réflexive à data.tablelui-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 .SDc'est luidata.table - même un (avec la mise en garde qu'il ne permet pas l'affectation avec :=).

L'utilisation plus simple de .SDest pour le sous-ensemble de colonnes (c'est-à-dire quand .SDcolsest 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 .SDdans 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 Pitchingou Pitching[]:

identical(Pitching, Pitching[ , .SD])
# [1] TRUE

En termes de sous-ensemble, .SDest 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 .SDest est de limiter les colonnes contenues dans l' .SDutilisation de l' .SDcolsargument à [:

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, fwritene peut pas lire automatiquement Dateou POSIXctcolonnes , et les conversions entre character/ factor/ numericsont courantes. Nous pouvons utiliser .SDet .SDcolsconvertir par lots des groupes de telles colonnes.

Nous remarquons que les colonnes suivantes sont stockées comme characterdans l' Teamsensemble 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 sapplyici, 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 listoù chaque élément est une colonne - ainsi, sapply/ lapplys'applique FUNà chaque colonne et renvoie le résultat comme sapply/ le lapplyferait habituellement (ici, FUN == is.characterrenvoie un logicalde longueur 1, sapplyrenvoie donc un vecteur).

La syntaxe pour convertir ces colonnes factorest très similaire - ajoutez simplement l' :=opérateur d'affectation

Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]

Notez que nous devons mettre fktentre parenthèses ()pour forcer R à interpréter cela comme des noms de colonnes, au lieu d'essayer d'attribuer le nom fktau RHS.

La flexibilité de .SDcols(et :=) pour accepter un charactervecteur ou un integervecteur 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 factorcolonnes 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 teamnouveau 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 Pitchingtableau. Comment la relation (linéaire) entre W(gagne) et ERAvarie-t-elle en fonction des autres covariables incluses dans la spécification?

Voici un court script exploitant la puissance .SDdont 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)

ajuster le coefficient OLS sur W, diverses spécifications

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.tablela syntaxe est belle pour sa simplicité et sa robustesse. La syntaxe x[i]gère avec souplesse deux approches courantes du sous-ensemble - quand iest un logicalvecteur, x[i]retournera ces lignes de xcorrespondant à où iest TRUE; quand en iest un autredata.table , a joinest exécuté (sous forme simple, en utilisant le keys de xet 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_performanceau Pitchingtableau 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 .SDtrouve à droite dans Teams[.SD](puisque le RHS de :=dans ce cas nécessite des nrow(Pitching[rank_in_team == 1])valeurs.

.SDOpé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.tableprocessus jdoivent penser que vous data.tableêtes divisé en plusieurs sous-composants data.table, chacun correspondant à une seule valeur de votre ou vos byvariables:

regroupement illustré

Dans ce cas, .SDest de nature multiple - il se réfère à chacun de ces sous- data.tables, un à la fois (un peu plus précisément, la portée de .SDest 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 .SDc'est lui-même un data.table, et qui .Nfait 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 .SDcolspour ne renvoyer que des parties de data.tablepour chacun .SD(avec la mise en garde qui .SDcolsdoit être corrigée dans les différents sous-ensembles)

NB : .SD[1L]est actuellement optimisé par GForce( voir aussi ), des data.tableinternes qui accélèrent massivement les opérations groupées les plus courantes comme sumou mean- voir ?GForcepour 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 ERAet 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*teamIDsera 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')]

distribution des coefficients ajustés

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 .SDfaciliter un code beau et efficace data.table!

MichaelChirico
la source
5
Bonne explication. Juste un commentaire: au lieu de Teams [, .SD [which.max (R)], by = teamID], vous pouvez exploiter la fonction de commande rapide data.table: via Teams [order (teamID, -R), .SD [ 1L], keyby = teamID], cela devrait être plus rapide.
bartleby
@bartleby merci, en effet c'est correct, mais l'approche générale est toujours utile à savoir - par exemple si nous devons commander par une quantité dépendante du groupe. je vais peut-être changer l'exemple.
MichaelChirico