require(data.table)
knitr::opts_chunk$set(
  comment = "#",
    error = FALSE,
     tidy = FALSE,
    cache = FALSE,
 collapse = TRUE
)
.old.th = setDTthreads(1)

Cette vignette présente la syntaxe de data.table , sa forme générale, comment extraire les lignes, sélectionner et faire des opérations sur les colonnes, et réaliser des agrégations par groupe. Il est avantageux d'être familiarisé avec la structure de données data.frame de base du R, mais cela n'est pas essentiel pour suivre cette vignette.


Analyser des données en utilisant data.table

Les opérations concernant le traitement des données telles que subset, group, update, join, etc. sont toutes intimement liées. En regroupant ces opérations apparentées cela nous permet :

En résumé, si vous souhaitez réduire drastiquement le temps de programmation et de compilation, alors ce package est fait pour vous. C'est la philosophie suivie par data.table pour rendre cela possible. Notre but est d'illustrer ceci au travers de cette série de vignettes.

Données {#data}

Dans cette vignette, nous utiliseront les données NYC-flights14 obtenues du package flights (disponible sur GitHub seulement). Il contient les horaires des vols d'avions du Bureau of Transportation Statistics à propos de tous les vols partant des aéroports de New York City en 2014 (inspiré de nycflights13). Les données ne concernent que les mois de janvier à octobre 2014.

Vous pouvez utiliser le lecteur de fichiers rapide et convivial 'fread' de 'data.table' pour charger 'flights' ditectement ainsi :

options(width = 100L)
input <- if (file.exists("../flights14.csv")) {
   "../flights14.csv"
} else {
  "https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv"
}
flights <- fread(input)
flights
dim(flights)

A noter : 'fread' accepte directement les URLS 'http' et 'https', ainsi que les commandes système opérationnelles telles que les sorties de 'sed' et 'awk'. Voir '?fread' pour les exemples.

Introduction

Dans cette vignette, nous allons

  1. Commencez par les bases - qu'est-ce qu'un data.table, sa forme générale, comment réaliser un sous-ensemble des lignes, comment sélectionner et effectuer des calculs sur les colonnes;

  2. Nous verrons ensuite comment effectuer des agrégations de données par groupe

1. Les bases {#basics-1}

a) 'data.table' c'est quoi ? {#what-is-datatable-1a}

'data.table' est un package R qui fournit une version étendue d'un 'data.frame', qui est la structure de données standard pour stocker des données dans la 'base' R. Dans la Data section ci-dessus, nous avons vu comment créer une 'data.table' avec 'fread()', mais on peut aussi en créer une en utilisant la fonction 'data.table()' . Voici un exemple :

DT = data.table(
  ID = c("b","b","b","a","a","c"),
  a = 1:6,
  b = 7:12,
  c = 13:18
)
DT
class(DT$ID)

Vous pouvez aussi convertir des objets existants en une data.table en utilisant setDT() (pour les structures data.frame et list) ou as.data.table() (pour les autres structures). Pour les autres détails concernant les différences (ce qui est hors du champ de cette vignette), voir ?setDT et ?as.data.table.

Notez que :

b) Forme générale - dans quel sens la 'data.table' est-elle étendue ? {#enhanced-1b}

Par rapport à un data.frame, vous pouvez faire beaucoup plus de choses qu'extraire des lignes et sélectionner des colonnes dans la structure d'une data.table, par exemple, avec [ ... ] (Notez bien : nous pourrions aussi faire référence à écrire quelque chose dans DT[...] comme "interroger DT", par analogie ou similairement à SQL). Pour le comprendre il faut d'abord que nous regardions la forme générale de la syntaxe data.table, comme indiqué ci-dessous :

DT[i, j, by]

##   R:                 i                 j        by
## SQL:  where | order by   select | update  group by

Les utilisateurs ayant des connaissances SQL feront peut être directement le lien avec cette syntaxe.

La manière de le lire (à haute voix) est :

Utiliser DT, extraire ou trier les lignes en utilisant i, puis calculer j, grouper avec by.

Commençons par voir 'i' et 'j' d'abord - en indiçant les lignes et en travaillant sur les colonnes.

c) Regrouper les lignes en 'i' {#subset-i-1c}

-- Obtenir tous les vols qui ont "JFK" comme aéroport de départ pendant le mois de juin.

ans <- flights[origin == "JFK" & month == 6L]
head(ans)

-- Récupérer les deux premières lignes de flights. {#subset-rows-integer}

ans <- flights[1:2]
ans

-- Trier flights d'abord sur la colonne origin dans l'ordre ascending, puis par dest dans l'ordre descendant :

Nous pouvons utiliser la fonction R 'order()' pour faire cela.

ans <- flights[order(origin, -dest)]
head(ans)

order() est optimisé en interne

Nous discuterons de l'ordonnancement rapide de la data.table plus en détails dans la vignette fonctionnement interne de data.table internals.

d) Sélection de colonne(s) dans j {#select-j-1d}

-- Sélectionner la colonne arr_delay, mais la renvoyer en tant que vector.

ans <- flights[, arr_delay]
head(ans)

-- Sélectionner la colonne arr_delay, mais la renvoyer en tant que data.table.

ans <- flights[, list(arr_delay)]
head(ans)

Un data.table (et également un data.frame) est aussi en interne une list , avec la caractéristique que chaque élément a la même longueur et que la list possède un attribut class. En permettant à j de renvoyer une list cela permet de convertir et de renvoyer des data.table très efficacement.

Conseil : {#tip-1}

Tant que j-expression renvoie une list, chaque élément de la liste sera converti en colonne dans la data.table résultante. Ce qui fait que j est très puissant, comme nous le verrons bientôt. Il est aussi très important de comprendre cela dans le cas où vous auriez à faire des requêtes plus compliquées !!

-- Sélectionner à la fois les colonnes arr_delay et dep_delay.

ans <- flights[, .(arr_delay, dep_delay)]
head(ans)

## forme alternative
# ans <- flights[, list(arr_delay, dep_delay)]

-- Sélectionner à la fois les colonnes arr_delay et dep_delay et les renommer en delay_arr et delay_dep.

Comme .() est juste un alias pour list(), nous pouvons donner un nom quelconque aux colonnes comme si on créait une list.

ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)]
head(ans)

e) Calcul ou do dans 'j'

-- Combien de voyages on eu un retard total < 0 ?

ans <- flights[, sum( (arr_delay + dep_delay) < 0 )]
ans

Que se passe-t-il dans ce cas ?

f) Sous-ensemble de i et do dans j

-- Calculer le nombre moyen de retards des arrivées et des départs pour tous les vols au départ de l'aéroport "JFK" pendant le mois de juin.

ans <- flights[origin == "JFK" & month == 6L,
               .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
ans

Parce que les trois composants principaux de la requête (i, j et by) figurent ensemble dans [...], data.table peut les voir tous trois et optimiser la requête dans sa totalité avant l'évaluation, plutôt que d'optimiser chacun séparément. Par conséquent nous pouvons éviter le sous-ensemble complet (par exemple trier les colonnes annexes arr_delay et dep_delay), pour la rapidité et l'efficacité de la mémoire.

-- Combien de voyages ont été réalisés en 2014 au départ de l'aéroport "JFK" au mois de juin ?

ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans

La fonction length() nécessite un argument d'entrée. Il suffit juste de calculer le nombre de lignes du sous-ensemble. On aurait pu utiliser n'importe quelle colonne comme argument d'entrée de length(). Cette approche est une réminiscence de SELECT COUNT(dest) FROM flights WHERE origin = 'JFK' AND month = 6 en SQL.

Ce type d'opération arrive assez fréquement, particulièrement lors des regroupements (comme nous le verrons dans la section suivante), au point que data.table fournit un symbole spécial .N pour cela.

g) Gérer les éléments absents dans i

-- Que se passe-t-il quand on interroge des éléments non-existants ?

Lorsque vous interrogez une data.table pour des éléments qui n'existent pas, le comportement dépend de la méthode utilisée.

setkeyv(flights, "origin")

Ceci réalise une jointure parfaite sur la colonne clé x, fournissant une rangée avec d et NA pour les colonnes absentes. En utilisant setkeyv, la table est triée en fonction des clés fournies et un index interne est créé, permettant une recherche binaire et des performances optimisées.

flights["XYZ"]
# Retourne:
#    origin year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum ...
# 1:    XYZ   NA    NA  NA       NA             NA        NA       NA             NA        NA      NA     NA      NA ...

Ceci réalise une opération standard de sous-ensemble qui ne trouve aucune correspondance de lignes et donc renvoie une data.table vide.

  flights[origin == "XYZ"]
# Retourne:
# Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,...

Pour une correspondance stricte sans NA pour les éléments absents, utiliser nomatch=NULL :

flights["XYZ", nomatch=NULL]
# Retourne:
# Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,...

En assimilant ces comportements, cela vous ôtera toute confusion lorsque vous trouverez des éléments absents parmi vos données.

Symbol spécial .N: {#special-N}

.N est une variable interne spéciale qui contient le nombre d'observations dans le groupe actuel. Elle est particulièrement utile combinée avec by comme nous le verrons dans la prochaine section. S'il n'y a pas de groupe pour les opérations, le nombre de lignes dans le sous-ensemble sera simplement renvoyé.

Maintenant que nous savons, nous pouvons accomplir la même tâche en utilisant .N ainsi :

ans <- flights[origin == "JFK" & month == 6L, .N]
ans

On aurait pu faire la même opération en écrivant nrow(flights[origin == "JFK" & month == 6L]). Néanmoins il aurait fallu d'abord dissocier la data.table entière en fonction des indices de lignes dans i puis renvoyer les lignes en utilisant nrow(), ce qui est inutile et pas efficace. Nous aborderons en détails ce sujet et d'autres aspects de l'optimisation dans la vignette architecture de data.table.

h) Super ! Mais comment référencer les colonnes par nom dans j (comme avec un data.frame) ? {#refer_j}

Si vous imprimez le nom des colonnes explicitement, il n'y a pas de différence avec un data.frame (depuis v1.9.8).

-- Sélectionner simultanément les colonnes arr_delay et dep_delay à la manière d'un data.frame.

ans <- flights[, c("arr_delay", "dep_delay")]
head(ans)

Si vous avez stocké les colonnes souhaitées dans un vecteur de caractères, il y a deux options : utiliser le préfixe .. , ou utiliser l'argument with.

-- Sélectionnez les colonnes nommées dans une variable en utilisant le préfixe ..

select_cols = c("arr_delay", "dep_delay")
flights[ , ..select_cols]

Pour les habitués du terminal Unix, le préfixe .. devrait rappeler la commande de "remontée d'un niveau", qui est analogue à ce qui se passe ici -- le .. demande à data.table de chercher la variable select_cols "un nivau au-dessus", c'est à dire dans ce cas, dans l'envronnement global.

-- Sélectionner les colonnes nommées dans une variable en utilisant with = FALSE

flights[ , select_cols, with = FALSE]

L'argument s'appelle with d'après la fonction R with() à cause de la fonctionnalité similaire. Supposez que vous ayiez une data.frame DF et que vous vouliez dissocier toutes les lignes où x > 1. Dans la base R vous pouvez écrire :

DF = data.frame(x = c(1,1,1,2,2,3,3,3), y = 1:8)

## (1) méthode classique
DF[DF$x > 1, ] # data.frame needs that ',' as well

## (2) en utilisant with
DF[with(DF, x > 1), ]

with = TRUE est la valeur par défaut dans data.table car nous pouvons faire plus en permettant à j de gérer des expressions - particulièrement en combinant avec by, comme nous le verrons dans un instant.

2. Aggrégations

Nous avons déjà vu i et j dans la forme générale d'une data.table dans la secton précédente. Dans cette section, nous allons voir comment ils peuvent être combinés ensemble avec by pour réaliser des opérations par groupe. Voyons quelques exemples.

a) Regrouper avec by

-- Comment obtenir le nombre de voyages au départ de chaque aéroport ?

ans <- flights[, .(.N), by = .(origin)]
ans

## ou résultat identique en utilisant un vecteur de chaînes de caractères dans 'by'
# ans <- flights[, .(.N), by = "origin"]

-- Comment calculer le nombre de voyages au départ de chaque aéroport pour le transporteur ayant le code "AA"? {#origin-.N}

Le code unique de transporteur "AA" correspond à American Airlines Inc.

ans <- flights[carrier == "AA", .N, by = origin]
ans

-- Comment obtenir le nombre total de voyages pour chaque paire origin, dest du transporteur ayant pour code "AA"? {#origin-dest-.N}

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
head(ans)

## ou résultat identique en utilisant une chaîne de caractères dans 'by'
# ans <- flights[carrier == "AA", .N, by = c("origin", "dest")]

-- Comment obtenir les valeurs moyennes menselles du retard des arrivées et des départs pour chaque paire orig,dest pour le transporteur ayant le code "AA"? {#origin-dest-month}

ans <- flights[carrier == "AA",
        .(mean(arr_delay), mean(dep_delay)),
        by = .(origin, dest, month)]
ans

Maintenant qu'adviendrait-il si nous voulions trier les résultats en groupant les colonnes origin, dest et month ?

b) Tri by : keyby

data.table conserve l'ordre original des groupes; c'est intentionnel et défini à la conception. Il existe des cas où conserver l'ordre original est essentiel. Mais à certains moments, nous aimerions trier automatiquement par variables dans notre regroupement.

-- Donc comment pourrions-nous trier directement sur toutes les variables de regroupement ?

ans <- flights[carrier == "AA",
        .(mean(arr_delay), mean(dep_delay)),
        keyby = .(origin, dest, month)]
ans

Clés : actuellement keyby en fait un peu plus que simplement trier. Il définit une clé également après le tri en initialisant un attribute appelé sorted.

Nous en apprendrons plus au sujet des clés dans la vignette Clés et sous-ensembles basés sur la recherche binaire rapide; pour l'instant, tout ce que vous devez savoir est que vous pouvez utiliser keyby pour trier automatiquement le résultat selon les colonnes spécifiées dans by.

c) Chaînage

Considérons la tâche consistant à récupérer le nombre total de voyages pour chaque couple origin, dest du transporteur "AA".

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]

-- Comment trier ans en utilisant la colonne origin en mode croissant, et la colonne dest en mode décroissant ?

On peut stocker le résultat intermédiaire dans une variable, puis passer order(origin, -dest) sur cette variable. Cela semble plus direct.

ans <- ans[order(origin, -dest)]
head(ans)

Mais ceci nécessite d'avoir assigné le résultat intermédiaire et de réécrire ce résultat. On peut faire mieux et éviter cette assignation intermédiaire à une variable temporaire en chaînant les expressions ensemble.

ans <- flights[carrier == "AA", .N, by = .(origin, dest)][order(origin, -dest)]
head(ans, 10)

d) Expressions de by

-- by accepte-t-il également expressions, ou simplement des colonnes ?

Oui, il le fait. Par exemple, si nous avions voulu chercher combien de vols sont partis en retard mais sont arrivés plus tôt (ou à l'heure), ou parts à l'heure mais arrivés en retard, etc...

ans <- flights[, .N, .(dep_delay>0, arr_delay>0)]
ans

e) Colonnes multiples dans j - .SD

-- Faut-il calculer mean() pour chaque colonne individuellement ?

Bien sûr il n'est pas pratique de devoir entrer mean(myCol) pour chaque colonne, une par une. Et s'il fallait faire la moyenne mean() sur 100 colonnes ?

Comment faire cela de manière efficace et concise ? Pour y arriver, relisons ce conseil - "Tant que la j-expression renvoie une list, chaque élément de cette list sera converti en une colonne de la data.table résultat". Si nous pouvons adresser le sous-ensemble de données de chaque groupe comme une variable de regroupement, nous pourrons ensuite boucler sur toutes les colonnes de cette variables en utilisant la fonction de base familière (ou en passe de le devenir) lapply(). Il n'y a pas de nouveaux noms à apprendre particuliers pour data.table.

Symbole spécial .SD: {#special-SD}

data.table fournit le symbole spécial .SD. Il tire son nom de Sous-ensemble de Données. C'est une data.table qui contient les données du groupe actuel tel qu'il a été défini avec by.

Souvenez-vous qu'une data.table est représentée en interne comme une list dont toutes les colonnes ont la même longueur.

Utilisons la data.table DT précédente pour avoir un aperçu de ce à quoi ressemble .SD .

DT

DT[, print(.SD), by = ID]

Pour calculer sur uneou plusieurs colonnes vous pouvez utiliser simplement la fonction de base R lapply().

DT[, lapply(.SD, mean), by = ID]

Nous y sommes presque. Il reste encore une petite chose à régler. Dans notre data.table flights , nous avons voulu calculer seulement la mean() des deux colonnes arr_delay et dep_delay. Mais .SD contiendrait par défaut toutes les colonnes autres que les variables de groupement.

-- Comment spécifier uniquement les colonnes sur lesquelles nous voulons appliquer mean() ?

.SDcols

En utilisant l'argument .SDcols. Il accepte soit des noms soit des indices de colonnes. Par exemple, .SDcols = c("arr_delay", "dep_delay") permet que .SD ne comporte que ces deux colonnes pour chaque groupe.

De la même manière que part g), vous pouvez également spécifier les colonnes à supprimer au lieu des colonnes à garder en utilisant le - ou !. De plus, vous pouvez sélectionner des colonnes consécutives avec colA:colB et les désélectionner avec !(colA:colB) ou -(colA:colB).

Maintenant essayons d'utiliser .SD avec .SDcols pour obtenir la moyenne mean() des colonnes arr_delay et dep_delay groupées par origin, dest et month.

flights[carrier == "AA",                       ## Seulement les vols sur porteurs "AA"
        lapply(.SD, mean),                     ## calcule la moyenne
        by = .(origin, dest, month),           ## pour chaque 'origin,dest,month'
        .SDcols = c("arr_delay", "dep_delay")] ## pour seulement ceux spécifiés dans .SDcols

f) Extraire .SD pour chaque groupe :

-- Comment renvoyer les deux premières lignes de chque 'month`?

ans <- flights[, head(.SD, 2), by = month]
head(ans)

g) Pourquoi garder j si flexible ?

Ainsi nous avons une syntaxe cohérente et continuons l'utilisation de fonctions de base déja existantes (et familières) au lieu d'apprendre de nouvelles fonctions. Pour illustrer cela utilisons la data.table DT que nous avons créée tout au début dans la section Qu'est-ce qu'une data.table ?.

-- Comment concaténer les colonnes a et b pour chaque groupe de ID ?

DT[, .(val = c(a,b)), by = ID]

-- Que se passerait-il si nous voulions avoir toutes les valeurs des colonnes a et b concaténées, mais renvoyées en tant que colonne de liste ?

DT[, .(val = list(c(a,b))), by = ID]

Une fois que vous commencerez à utiliser j, vous découvrirez la puissance de sa syntaxe. Une manière pratique de l'aborder est de la tester en utilisant print().

Par exemple :

## inspectez la différence entre
DT[, print(c(a,b)), by = ID] # (1)

## et
DT[, print(list(c(a,b))), by = ID] # (2)

Dans (1), pour chaque groupe, un vecteur est renvoyé, de longueur = 6,4,2 ici. Néanmoins, (2) renvoie une liste de longueur 1 pour chaque groupe, dont chaque premier élément contient des vecteurs de longueur 6,4,2. C'est pourquoi, (1) a pour longueur totale 6+4+2 =r 6+4+2, alors que (2) renvoie `1+1+1=`r 1+1+1.

Résumé

La forme générale de la syntaxe de data.table est :

DT[i, j, by]

Jusqu'ici nous avons vu que,

En utilisant i :

Nous pouvons faire beaucoup plus dans i en créant une data.table avec clés, ce qui permet de réaliser rapidement les sous-ensembles et les jointures. Nous verrons cela dans les vignettes "Clés et sous-ensembles basés sur la recherche binaire rapide" et "Jointures et jointures liées au temps".

En utilisant j :

  1. Sélectionner les colonnes à la manière de data.table : DT[, .(colA, colB)].

  2. Sélectionner les colonnes à la manière de data.frame : DT[, c("colA", "colB")].

  3. Effectuer des calculs sur les colonnes : DT[, .(sum(colA), mean(colB))].

  4. Indiquer les noms si nécessaire : DT[, .(sA =sum(colA), mB = mean(colB))].

  5. Combiner avec i : DT[colA > valeur, sum(colB)].

En utilisant by :

Et souvenez-vous du conseil :

Tant que j renvoie un objet list, chaque élément de la liste va devenir une colonne du data.table résultant.

Nous verrons dans la vignette suivante comment ajouter / mettre à jour / supprimer des colonnes par référence et comment les combiner avec i et by .


setDTthreads(.old.th)


Rdatatable/data.table documentation built on Nov. 20, 2024, 6:44 p.m.