Une traduction de ce document est disponible en : en | es | fr | ru
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.
data.tableLes 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 :
syntaxe concise et cohérente quel que soit l’ensemble des opérations que vous souhaitez effectuer pour atteindre votre objectif final.
effectuer une analyse fluide sans la charge cognitive de devoir faire correspondre chaque opération à une fonction particulière à partir d’un ensemble potentiellement énorme de fonctions disponibles avant d’effectuer l’analyse.
automatiquement optimiser les opérations en interne et de manière très efficace en connaissant précisément les données requises pour chaque opération, ce qui permet d’obtenir un code très rapide et efficace sur le plan de la mémoire.
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.
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 :
input <- if (file.exists("../flights14.csv")) {
"../flights14.csv"
} else {
"https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv"
}
flights <- fread(input)
flights
| year | month | day | dep_delay | arr_delay | carrier | origin | dest | air_time | distance | hour |
|---|---|---|---|---|---|---|---|---|---|---|
| 2014 | 1 | 1 | 14 | 13 | AA | JFK | LAX | 359 | 2475 | 9 |
| 2014 | 1 | 1 | -3 | 13 | AA | JFK | LAX | 363 | 2475 | 11 |
| 2014 | 1 | 1 | 2 | 9 | AA | JFK | LAX | 351 | 2475 | 19 |
| 2014 | 1 | 1 | -8 | -26 | AA | LGA | PBI | 157 | 1035 | 7 |
| 2014 | 1 | 1 | 2 | 1 | AA | JFK | LAX | 350 | 2475 | 13 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 2014 | 10 | 31 | 1 | -30 | UA | LGA | IAH | 201 | 1416 | 14 |
| 2014 | 10 | 31 | -5 | -14 | UA | EWR | IAH | 189 | 1400 | 8 |
| 2014 | 10 | 31 | -8 | 16 | MQ | LGA | RDU | 83 | 431 | 11 |
| 2014 | 10 | 31 | -4 | 15 | MQ | LGA | DTW | 75 | 502 | 11 |
| 2014 | 10 | 31 | -5 | 1 | MQ | LGA | SDF | 110 | 659 | 8 |
dim(flights)
# [1] 253316 11
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.
Dans cette vignette, nous allons
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;
Nous verrons ensuite comment effectuer des agrégations de données par groupe
‘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
| ID | a | b | c |
|---|---|---|---|
| b | 1 | 7 | 13 |
| b | 2 | 8 | 14 |
| b | 3 | 9 | 15 |
| a | 4 | 10 | 16 |
| a | 5 | 11 | 17 |
| c | 6 | 12 | 18 |
class(DT$ID)
# [1] "character"
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.
Les numéros de ligne sont imprimés avec un : afin de séparer visuellement le numéro de ligne de la première colonne.
Lorsque le nombre de lignes à imprimer dépasse l’option globale datatable.print.nrows (défaut = r getOption("datatable.print.nrows")), il n’imprime automatiquement que les 5 premières et les 5 dernières lignes (comme on peut le voir dans la section Data). Pour un grand data.frame, vous avez pu vous retrouver à attendre que des tables plus grandes s’impriment et se mettent en page, parfois sans fin. Cette restriction permet d’y remédier, et vous pouvez demander le nombre par défaut de la façon suivante :
getOption("datatable.print.nrows")
data.table ne définit ni n’utilise jamais de nom de ligne. Nous verrons pourquoi dans la vignette("datatable-keys-fast-subset", package="data.table").
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.
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.
ans <- flights[origin == "JFK" & month == 6L]
head(ans)
| year | month | day | dep_delay | arr_delay | carrier | origin | dest | air_time | distance | hour |
|---|---|---|---|---|---|---|---|---|---|---|
| 2014 | 6 | 1 | -9 | -5 | AA | JFK | LAX | 324 | 2475 | 8 |
| 2014 | 6 | 1 | -10 | -13 | AA | JFK | LAX | 329 | 2475 | 12 |
| 2014 | 6 | 1 | 18 | -1 | AA | JFK | LAX | 326 | 2475 | 7 |
| 2014 | 6 | 1 | -6 | -16 | AA | JFK | LAX | 320 | 2475 | 10 |
| 2014 | 6 | 1 | -4 | -45 | AA | JFK | LAX | 326 | 2475 | 18 |
| 2014 | 6 | 1 | -6 | -23 | AA | JFK | LAX | 329 | 2475 | 14 |
Dans le cadre d’un data.table, on peut se référer aux colonnes comme s’il s’agissait de variables, un peu comme dans SQL ou Stata. Par conséquent, nous nous référons simplement à origin et month comme s’il s’agissait de variables. Nous n’avons pas besoin d’ajouter le préfixe vol$ à chaque fois. Néanmoins, l’utilisation de flights$origin et flights$month fonctionnerait parfaitement.
Les indices de ligne qui satisfont la condition origin == "JFK" & month == 6L sont calculés, et puisqu’il n’y a rien d’autre à faire, toutes les colonnes de flights aux lignes correspondant à ces indices de ligne sont simplement renvoyées sous forme d’un data.table.
Une virgule après la condition dans i n’est pas nécessaire. Mais flights[origin == "JFK" & month == 6L, ] fonctionnerait parfaitement. Avec un data.frame, cependant, la virgule est indispensable.
flights.ans <- flights[1:2]
ans
| year | month | day | dep_delay | arr_delay | carrier | origin | dest | air_time | distance | hour |
|---|---|---|---|---|---|---|---|---|---|---|
| 2014 | 1 | 1 | 14 | 13 | AA | JFK | LAX | 359 | 2475 | 9 |
| 2014 | 1 | 1 | -3 | 13 | AA | JFK | LAX | 363 | 2475 | 11 |
i. Nous retournons donc un data.table avec toutes les colonnes de flights aux lignes pour ces index de ligne.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)
| year | month | day | dep_delay | arr_delay | carrier | origin | dest | air_time | distance | hour |
|---|---|---|---|---|---|---|---|---|---|---|
| 2014 | 1 | 5 | 6 | 49 | EV | EWR | XNA | 195 | 1131 | 8 |
| 2014 | 1 | 6 | 7 | 13 | EV | EWR | XNA | 190 | 1131 | 8 |
| 2014 | 1 | 7 | -6 | -13 | EV | EWR | XNA | 179 | 1131 | 8 |
| 2014 | 1 | 8 | -7 | -12 | EV | EWR | XNA | 184 | 1131 | 8 |
| 2014 | 1 | 9 | 16 | 7 | EV | EWR | XNA | 181 | 1131 | 8 |
| 2014 | 1 | 13 | 66 | 66 | EV | EWR | XNA | 188 | 1131 | 9 |
order() est optimisé en interneNous pouvons utiliser “-” sur les colonnes character dans le cadre d’un data.table pour trier par ordre décroissant.
De plus, order(...) dans le cadre d’un data.table utilise l’ordre radix rapide interne de data.table forder(). Ce tri a apporté une telle amélioration par rapport à base::order de R que le projet R a adopté l’algorithme data.table comme tri par défaut en 2016 pour R 3.3.0 (pour référence, voir ?sort et les R Release NEWS).
Nous discuterons de l’ordonnancement rapide de la data.table plus en détails dans la vignette fonctionnement interne de data.table internals.
jarr_delay, mais la renvoyer en tant que vector.ans <- flights[, arr_delay]
head(ans)
# [1] 13 13 9 -26 1 0
Puisque les colonnes peuvent être appelées comme si elles étaient des variables dans le cadre d’un data.table, nous nous référons directement à la variable dont nous voulons créer un sous-ensemble. Puisque nous voulons toutes les lignes, nous sautons simplement i.
Il renvoie toutes les lignes de la colonne arr_delay.
arr_delay, mais la renvoyer en tant que data.table.ans <- flights[, list(arr_delay)]
head(ans)
| arr_delay |
|---|
| 13 |
| 13 |
| 9 |
| -26 |
| 1 |
| 0 |
Nous enveloppons les variables (noms de colonnes) dans list(), ce qui assure qu’un data.table est retourné. Dans le cas d’un seul nom de colonne, le fait de ne pas utiliser list() renvoie un vecteur, comme on peut le voir dans l’exemple précédent](#select-j-1d).
data.table permet aussi d’envelopper les colonnes avec .() au lieu de list(). C’est un alias de list() ; les deux signifient la même chose. N’hésitez pas à utiliser ce que vous préférez ; nous avons remarqué que la plupart des utilisateurs semblent préférer .() pour la concision, donc nous continuerons à utiliser .() par la suite.
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.
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 !!
arr_delay et dep_delay.ans <- flights[, .(arr_delay, dep_delay)]
head(ans)
| arr_delay | dep_delay |
|---|---|
| 13 | 14 |
| 13 | -3 |
| 9 | 2 |
| -26 | -8 |
| 1 | 2 |
| 0 | 4 |
## forme alternative
# ans <- flights[, list(arr_delay, dep_delay)]
.(), ou list(). C’est tout.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)
| delay_arr | delay_dep |
|---|---|
| 13 | 14 |
| 13 | -3 |
| 9 | 2 |
| -26 | -8 |
| 1 | 2 |
| 0 | 4 |
ans <- flights[, sum( (arr_delay + dep_delay) < 0 )]
ans
# [1] 141814
j de data.table peut gérer plus que la sélection de colonnes - il peut gérer des expressions, c’est-à-dire calculer sur des colonnes. Cela ne devrait pas être surprenant, car on peut se référer aux colonnes comme si elles étaient des variables. Nous devrions donc pouvoir calculer en appelant des fonctions sur ces variables. Et c’est précisément ce qui se passe ici.i et do dans jans <- flights[origin == "JFK" & month == 6L,
.(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
ans
| m_arr | m_dep |
|---|---|
| 5.839 | 9.808 |
Nous commençons par effectuer un sous-ensemble dans i pour trouver les indices de ligne correspondants à origin égal à l’aéroport "JFK", et où le mois est égal à 6L. Nous n’effectuons pas encore le sous-ensemble de toutes les data.table correspondant à ces lignes.
Maintenant, nous regardons j et nous constatons qu’il n’utilise que deux colonnes. Et ce que nous devons faire, c’est calculer leur moyenne avec mean(). Par conséquent, nous regroupons uniquement les colonnes d’intérêt aux lignes correspondantes, et nous calculons leurs moyennes.
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.
ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans
# [1] 8422
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.
iLorsque 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")
dt["d"]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 ...
dt[x == "d"]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,...
nomatch=NULLPour 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.
.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
# [1] 8422
Une fois de plus, nous introduisons i pour obtenir les indices de lignes pour lesquels l’aéroport origin est “JFK”, et le mois est 6.
Nous voyons que j n’utilise que .N et aucune autre colonne. Par conséquent, le sous-ensemble complet n’est pas matérialisé. Nous renvoyons simplement le nombre de lignes dans le sous-ensemble (qui est juste la longueur des indices de ligne).
Notez que nous n’avons pas enveloppé .N avec list() ou .(). Par conséquent, un vecteur est retourné.
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.
j (comme avec un data.frame) ?Si vous imprimez le nom des colonnes explicitement, il n’y a pas de différence avec un data.frame (depuis v1.9.8).
arr_delay et dep_delay à la manière d’un data.frame.ans <- flights[, c("arr_delay", "dep_delay")]
head(ans)
| arr_delay | dep_delay |
|---|---|
| 13 | 14 |
| 13 | -3 |
| 9 | 2 |
| -26 | -8 |
| 1 | 2 |
| 0 | 4 |
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.
..select_cols = c("arr_delay", "dep_delay")
flights[ , ..select_cols]
| arr_delay | dep_delay |
|---|---|
| 13 | 14 |
| 13 | -3 |
| 9 | 2 |
| -26 | -8 |
| 1 | 2 |
| ⋮ | ⋮ |
| -30 | 1 |
| -14 | -5 |
| 16 | -8 |
| 15 | -4 |
| 1 | -5 |
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.
with = FALSEflights[ , select_cols, with = FALSE]
| arr_delay | dep_delay |
|---|---|
| 13 | 14 |
| 13 | -3 |
| 9 | 2 |
| -26 | -8 |
| 1 | 2 |
| ⋮ | ⋮ |
| -30 | 1 |
| -14 | -5 |
| 16 | -8 |
| 15 | -4 |
| 1 | -5 |
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
| x | y |
|---|---|
| 2 | 4 |
| 2 | 5 |
| 3 | 6 |
| 3 | 7 |
| 3 | 8 |
## (2) en utilisant with
DF[with(DF, x > 1), ]
| x | y |
|---|---|
| 2 | 4 |
| 2 | 5 |
| 3 | 6 |
| 3 | 7 |
| 3 | 8 |
L’utilisation de with() dans (2) permet d’utiliser la colonne x de DF comme s’il s’agissait d’une variable.
D’où le nom de l’argument with dans data.table. Mettre with = FALSE désactive la possibilité de se référer aux colonnes comme si elles étaient des variables, restaurant ainsi le « mode data.frame ».
Nous pouvons également désélectionner des colonnes en utilisant - ou !. Par exemple :
## pas d'exécution
# renvoie toutes les colonnes sauf arr_delay et dep_delay
ans <- flights[, !c("arr_delay", "dep_delay")]
# ou
ans <- flights[, -c("arr_delay", "dep_delay")]
A partir de la v1.9.5+, on peut aussi sélectionner en spécifiant les noms des colonnes de début et de fin, par exemple, year:day pour sélectionner les trois premières colonnes.
## pas d'exécution
# renvoie year,month et day
ans <- flights[, year:day]
# renvoie day, month et year
ans <- flights[, day:year]
# renvoie toutes les colonnes sauf year, month et day
ans <- flights[, -(year:day)]
ans <- flights[, !(year:day)]
Ceci est particulièrement pratique lorsque l’on travaille de manière interactive.
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.
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.
byans <- flights[, .(.N), by = .(origin)]
ans
| origin | N |
|---|---|
| JFK | 81483 |
| LGA | 84433 |
| EWR | 87400 |
## ou résultat identique en utilisant un vecteur de chaînes de caractères dans 'by'
# ans <- flights[, .(.N), by = "origin"]
Nous savons que .N est une variable spéciale qui contient le nombre de lignes dans le groupe courant. En groupant par origine, on obtient le nombre de lignes, .N, pour chaque groupe.
En faisant head(flights) vous pouvez voir que les aéroports d’origine sont dans l’ordre “JFK”, “LGA”, et “EWR”. L’ordre original de regroupement des variables est préservé dans le résultat. Il est important de garder cela à l’esprit!
Comme nous n’avons pas fourni de nom pour la colonne retournée dans j, elle a été nommée N automatiquement en reconnaissant le symbole spécial .N.
by accepte également un vecteur de caractères de noms de colonnes. Ceci est particulièrement utile pour le codage par programmation, par exemple pour concevoir une fonction avec les colonnes de regroupement (sous la forme d’un vecteur character) comme argument de la fonction.
Lorsqu’il n’y a qu’une seule colonne ou expression à laquelle se référer dans j et by, nous pouvons abandonner la notation .(). Ceci est purement pratique. Nous pourrions plutôt faire :
ans <- flights[, .N, by = origin]
ans
| origin | N |
|---|---|
| JFK | 81483 |
| LGA | 84433 |
| EWR | 87400 |
Nous utiliserons cette forme pratique chaque fois que cela sera possible.
"AA"?Le code unique de transporteur "AA" correspond à American Airlines Inc.
ans <- flights[carrier == "AA", .N, by = origin]
ans
| origin | N |
|---|---|
| JFK | 11923 |
| LGA | 11730 |
| EWR | 2649 |
Nous obtenons d’abord les indices de ligne pour l’expression carrier == "AA" à partir de i.
En utilisant ces index de ligne, nous obtenons le nombre de lignes groupées par origine. Une fois de plus, aucune colonne n’est matérialisée ici, car l’expression `j’ ne nécessite aucune colonne pour définir le sous-ensemble et le calcul est donc rapide et peu gourmand en mémoire.
origin, dest du transporteur ayant pour code "AA"?ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
head(ans)
| origin | dest | N |
|---|---|---|
| JFK | LAX | 3387 |
| LGA | PBI | 245 |
| EWR | LAX | 62 |
| JFK | MIA | 1876 |
| JFK | SEA | 298 |
| EWR | MIA | 848 |
## ou résultat identique en utilisant une chaîne de caractères dans 'by'
# ans <- flights[carrier == "AA", .N, by = c("origin", "dest")]
by accepte plusieurs colonnes. Nous fournissons simplement toutes les colonnes par lesquelles il faut grouper. Notez l’utilisation de .() dans by – encore une fois, c’est juste un raccourci pour list(), et list() peut être utilisé ici aussi. Nous nous en tiendrons à nouveau à .() dans cette vignette.orig,dest pour le transporteur ayant le code "AA"?ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
by = .(origin, dest, month)]
ans
| origin | dest | month | V1 | V2 |
|---|---|---|---|---|
| JFK | LAX | 1 | 6.590 | 14.229 |
| LGA | PBI | 1 | -7.759 | 0.310 |
| EWR | LAX | 1 | 1.367 | 7.500 |
| JFK | MIA | 1 | 15.721 | 18.743 |
| JFK | SEA | 1 | 14.357 | 30.750 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| LGA | MIA | 10 | -6.252 | -1.421 |
| JFK | MIA | 10 | -1.880 | 6.677 |
| EWR | PHX | 10 | -3.032 | -4.290 |
| JFK | MCO | 10 | -10.048 | -1.613 |
| JFK | DCA | 10 | 16.484 | 15.516 |
Comme nous n’avons pas fourni de noms de colonnes pour les expressions dans j, elles ont été automatiquement générées en tant que V1 et V2.
Une fois de plus, notez que l’ordre d’entrée des colonnes de regroupement est préservé dans le résultat.
Maintenant qu’adviendrait-il si nous voulions trier les résultats en groupant les colonnes origin, dest et month ?
by : keybydata.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.
ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
keyby = .(origin, dest, month)]
ans
| origin | dest | month | V1 | V2 |
|---|---|---|---|---|
| EWR | DFW | 1 | 6.428 | 10.013 |
| EWR | DFW | 2 | 10.537 | 11.346 |
| EWR | DFW | 3 | 12.865 | 8.080 |
| EWR | DFW | 4 | 17.793 | 12.921 |
| EWR | DFW | 5 | 18.488 | 18.683 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| LGA | PBI | 1 | -7.759 | 0.310 |
| LGA | PBI | 2 | -7.865 | 2.404 |
| LGA | PBI | 3 | -5.754 | 3.033 |
| LGA | PBI | 4 | -13.967 | -4.733 |
| LGA | PBI | 5 | -10.357 | -6.857 |
by par keyby. Cela ordonne automatiquement le résultat par ordre croissant des variables de regroupement. En fait, à cause de l’implémentation interne de by qui nécessite d’abord un tri avant de récupérer l’ordre de la table originale, keyby est typiquement plus rapide que by parce qu’il ne nécessite pas cette seconde étape.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("datatable-keys-fast-subset", package="data.table"); 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.
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)]
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)
| origin | dest | N |
|---|---|---|
| EWR | PHX | 121 |
| EWR | MIA | 848 |
| EWR | LAX | 62 |
| EWR | DFW | 1618 |
| JFK | STT | 229 |
| JFK | SJU | 690 |
Rappelons que nous pouvons utiliser - sur une colonne character dans order() dans le cadre d’un data.table. Ceci est possible grâce à l’optimisation interne des requêtes de data.table.
Rappelez-vous aussi que order(...) dans le contexte d’un data.table est automatiquement optimisé pour utiliser l’algorithme de tri radix rapide interne de data.table forder() pour plus de rapidité.
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)
| origin | dest | N |
|---|---|---|
| EWR | PHX | 121 |
| EWR | MIA | 848 |
| EWR | LAX | 62 |
| EWR | DFW | 1618 |
| JFK | STT | 229 |
| JFK | SJU | 690 |
| JFK | SFO | 1312 |
| JFK | SEA | 298 |
| JFK | SAN | 299 |
| JFK | ORD | 432 |
Nous pouvons ajouter des expressions l’une après l’autre, formant une chaîne d’opérations, c’est-à-dire DT[ ... ][ ... ][ ... ].
Vous pouvez également les enchaîner verticalement :
DT[ ...
][ ...
][ ...
]
byby 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
| dep_delay | arr_delay | N |
|---|---|---|
| TRUE | TRUE | 72836 |
| FALSE | TRUE | 34583 |
| FALSE | FALSE | 119304 |
| TRUE | FALSE | 26593 |
La dernière ligne correspond à dep_delay > 0 = TRUE et arr_delay > 0 = FALSE. Nous pouvons voir que les vols r flights[!is.na(arr_delay) & !is.na(dep_delay), .N, .(dep_delay>0, arr_delay>0)][, N[4L]] ont commencé en retard mais sont arrivés en avance (ou à l’heure).
Notez que nous n’avons pas fourni de noms à by-expression. Par conséquent, les noms ont été automatiquement assignés dans le résultat. Comme pour j, vous pouvez nommer ces expressions comme vous le feriez pour des éléments de n’importe quelle liste, comme par exemple DT[, .N, .(dep_delayed = dep_delay>0, arr_delayed = arr_delay>0)].
Vous pouvez fournir d’autres colonnes avec des expressions, par exemple : DT[, .N, by = .(a, b>0)].
j - .SDmean() 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.
.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
| ID | a | b | c |
|---|---|---|---|
| b | 1 | 7 | 13 |
| b | 2 | 8 | 14 |
| b | 3 | 9 | 15 |
| a | 4 | 10 | 16 |
| a | 5 | 11 | 17 |
| c | 6 | 12 | 18 |
DT[, print(.SD), by = ID]
# a b c
# <int> <int> <int>
# 1: 1 7 13
# 2: 2 8 14
# 3: 3 9 15
# a b c
# <int> <int> <int>
# 1: 4 10 16
# 2: 5 11 17
# a b c
# <int> <int> <int>
# 1: 6 12 18
| ID |
|---|
.SD contient toutes les colonnes à l’exception des colonnes de regroupement par défaut.
Il est également généré en conservant l’ordre original - les données correspondant à ID = "b", puis ID = "a", et enfin ID = "c".
Pour calculer sur uneou plusieurs colonnes vous pouvez utiliser simplement la fonction de base R lapply().
DT[, lapply(.SD, mean), by = ID]
| ID | a | b | c |
|---|---|---|---|
| b | 2.0 | 8.0 | 14.0 |
| a | 4.5 | 10.5 | 16.5 |
| c | 6.0 | 12.0 | 18.0 |
.SD contient les lignes correspondant aux colonnes a, b et c pour ce groupe. Nous calculons la moyenne avec mean() sur chacune de ces colonnes en utilisant la fonction de base déjà familière lapply().
Chaque groupe renvoie une liste de trois éléments contenant la valeur moyenne qui deviendra les colonnes du data.table résultant.
Puisque lapply() renvoie une liste, il n’est pas nécessaire de l’entourer d’un .() supplémentaire (si nécessaire, référez-vous à cette astuce).
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.
mean() ?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
| origin | dest | month | arr_delay | dep_delay |
|---|---|---|---|---|
| JFK | LAX | 1 | 6.590 | 14.229 |
| LGA | PBI | 1 | -7.759 | 0.310 |
| EWR | LAX | 1 | 1.367 | 7.500 |
| JFK | MIA | 1 | 15.721 | 18.743 |
| JFK | SEA | 1 | 14.357 | 30.750 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| LGA | MIA | 10 | -6.252 | -1.421 |
| JFK | MIA | 10 | -1.880 | 6.677 |
| EWR | PHX | 10 | -3.032 | -4.290 |
| JFK | MCO | 10 | -10.048 | -1.613 |
| JFK | DCA | 10 | 16.484 | 15.516 |
.SD pour chaque groupe :ans <- flights[, head(.SD, 2), by = month]
head(ans)
| month | year | day | dep_delay | arr_delay | carrier | origin | dest | air_time | distance | hour |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2014 | 1 | 14 | 13 | AA | JFK | LAX | 359 | 2475 | 9 |
| 1 | 2014 | 1 | -3 | 13 | AA | JFK | LAX | 363 | 2475 | 11 |
| 2 | 2014 | 1 | -1 | 1 | AA | JFK | LAX | 358 | 2475 | 8 |
| 2 | 2014 | 1 | -5 | 3 | AA | JFK | LAX | 358 | 2475 | 11 |
| 3 | 2014 | 1 | -11 | 36 | AA | JFK | LAX | 375 | 2475 | 8 |
| 3 | 2014 | 1 | -3 | 14 | AA | JFK | LAX | 368 | 2475 | 11 |
.SD est un data.table qui contient toutes les lignes de ce groupe. Nous allons simplement subdiviser les deux premières lignes comme nous l’avons déjà vu ici.
Pour chaque groupe, head(.SD, 2) renvoie les deux premières lignes sous forme de data.table, qui est également une liste, ce qui nous évite de l’entourer de .().
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 ?.
a et b pour chaque groupe de ID ?DT[, .(val = c(a,b)), by = ID]
| ID | val |
|---|---|
| b | 1 |
| b | 2 |
| b | 3 |
| b | 7 |
| b | 8 |
| ⋮ | ⋮ |
| a | 5 |
| a | 10 |
| a | 11 |
| c | 6 |
| c | 12 |
c() qui concatène des vecteurs, ainsi que l’astuce de tout à l’heure.a et b concaténées, mais renvoyées en tant que colonne de liste ?DT[, .(val = list(c(a,b))), by = ID]
| ID | val |
|---|---|
| b | 1,2,3,7,8,9 |
| a | 4, 5,10,11 |
| c | 6,12 |
Ici, nous concaténons d’abord les valeurs avec c(a,b) pour chaque groupe, et nous les enveloppons avec list(). Ainsi, pour chaque groupe, nous renvoyons une liste de toutes les valeurs concaténées.
Notez que ces virgules ne servent qu’à l’affichage. Une colonne de liste peut contenir n’importe quel objet dans chaque cellule et, dans cet exemple, chaque cellule est elle-même un vecteur et certaines cellules contiennent des vecteurs plus longs que d’autres.
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)
# [1] 1 2 3 7 8 9
# [1] 4 5 10 11
# [1] 6 12
| ID |
|---|
## et
DT[, print(list(c(a,b))), by = ID] # (2)
# [[1]]
# [1] 1 2 3 7 8 9
#
# [[1]]
# [1] 4 5 10 11
#
# [[1]]
# [1] 6 12
#
| ID |
|---|
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.
La flexibilité de j nous permet de ranger toute liste d’objets comme un élément de data.table. Par exemple lorsque des modèles statistiques sont adaptés aux groupes, ils peuvent être placés dans un data.table. Le code est concis et facile à comprendre.
## les vols long courrier couvrent-ils les retards au départ davantage que les vols à courte distance ?
## la couverture varie-t-elle selon les mois ?
flights[, `:=`(makeup = dep_delay - arr_delay)]
makeup.models <- flights[, .(fit = list(lm(makeup ~ distance))), by = .(month)]
makeup.models[, .(coefdist = coef(fit[[1]])[2], rsq = summary(fit[[1]])$r.squared), by = .(month)]
| month | coefdist | rsq |
|---|---|---|
| 1 | 0.004 | 0.027 |
| 2 | -0.004 | 0.022 |
| 3 | 0.001 | 0.004 |
| 4 | 0.002 | 0.006 |
| 5 | 0.002 | 0.008 |
| 6 | 0.000 | 0.000 |
| 7 | 0.003 | 0.012 |
| 8 | 0.003 | 0.019 |
| 9 | 0.001 | 0.005 |
| 10 | 0.002 | 0.011 |
Avec les data.frames il nous faut un code plus complexe pour obtenir le même résultat.
setDF(flights)
flights.split <- split(flights, f = flights$month)
makeup.models.list <- lapply(flights.split, function(df) c(month = df$month[1], fit = list(lm(makeup ~ distance, data = df))))
makeup.models.df <- do.call(rbind, makeup.models.list)
data.frame(t(sapply(
makeup.models.df[, "fit"],
function(model) c(coefdist = coef(model)[2L], rsq = summary(model)$r.squared)
)))
| coefdist.distance | rsq |
|---|---|
| 0.004 | 0.027 |
| -0.004 | 0.022 |
| 0.001 | 0.004 |
| 0.002 | 0.006 |
| 0.002 | 0.008 |
| 0.000 | 0.000 |
| 0.003 | 0.012 |
| 0.003 | 0.019 |
| 0.001 | 0.005 |
| 0.002 | 0.011 |
setDT(flights)
La forme générale de la syntaxe de data.table est :
DT[i, j, by]
Jusqu’ici nous avons vu que,
i :Nous pouvons subdiviser les lignes comme dans un data.frame - sauf que vous n’avez pas besoin d’utiliser DT$ de façon répétitive puisque les colonnes dans le contexte d’un data.table sont vues comme si elles étaient des variables.
Nous pouvons également trier un data.table en utilisant order(), qui utilise en interne l’algorithme de tri rapide de data.table pour de meilleures performances.
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 vignette("datatable-keys-fast-subset", package="data.table") et vignette("datatable-joins", package="data.table").
j :Sélectionner les colonnes à la manière de data.table : DT[, .(colA, colB)].
Sélectionner les colonnes à la manière de data.frame : DT[, c("colA", "colB")].
Effectuer des calculs sur les colonnes : DT[, .(sum(colA), mean(colB))].
Indiquer les noms si nécessaire : DT[, .(sA =sum(colA), mB = mean(colB))].
Combiner avec i : DT[colA > valeur, sum(colB)].
by :En utilisant by, nous pouvons grouper par colonnes en spécifiant une liste de colonnes ou un vecteur de caractères de noms de colonnes ou même des expressions. La flexibilité de j, combinée à by et i, en fait une syntaxe très puissante.
by peut gérer plusieurs colonnes ainsi que des expressions.
Nous pouvons regrouper les colonnes par ‘keyby’ pour trier automatiquement les résultats groupés.
Nous pouvons utiliser .SD et .SDcols dans j pour opérer sur plusieurs colonnes en utilisant des fonctions de base déjà connues. Voici quelques exemples:
DT[, lapply(.SD, fun), by = ..., .SDcols = ...] - applique fun à toutes les colonnes spécifiées dans .SDcols tout en groupant par les colonnes spécifiées dans by.
DT[, head(.SD, 2), by = ...] - renvoie les deux premières lignes pour chaque groupe.
DT[col > val, head(.SD, 1), by = ...] - combine i avec j et by.
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 prochaine (vignette("datatable-reference-semantics", package="data.table")) comment ajouter / mettre à jour / supprimer des colonnes par référence et comment les combiner avec i et by .