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 :
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.
Données
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
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19
# 4: 2014 1 1 -8 -26 AA LGA PBI 157 1035 7
# 5: 2014 1 1 2 1 AA JFK LAX 350 2475 13
# ---
# 253312: 2014 10 31 1 -30 UA LGA IAH 201 1416 14
# 253313: 2014 10 31 -5 -14 UA EWR IAH 189 1400 8
# 253314: 2014 10 31 -8 16 MQ LGA RDU 83 431 11
# 253315: 2014 10 31 -4 15 MQ LGA DTW 75 502 11
# 253316: 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.
Introduction
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
1. Les bases
a) ‘data.table’ c’est quoi ?
‘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
# <char> <int> <int> <int>
# 1: b 1 7 13
# 2: b 2 8 14
# 3: b 3 9 15
# 4: a 4 10 16
# 5: a 5 11 17
# 6: 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
.
Notez que :
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 = 100), 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 granddata.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 “Sous-ensemble basé sur des clés et recherche binaire rapide”.
b) Forme générale - dans quel sens la ‘data.table’ est-elle étendue ?
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.
c) Regrouper les lignes en ‘i’
– 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)
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 6 1 -9 -5 AA JFK LAX 324 2475 8
# 2: 2014 6 1 -10 -13 AA JFK LAX 329 2475 12
# 3: 2014 6 1 18 -1 AA JFK LAX 326 2475 7
# 4: 2014 6 1 -6 -16 AA JFK LAX 320 2475 10
# 5: 2014 6 1 -4 -45 AA JFK LAX 326 2475 18
# 6: 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
etmonth
comme s’il s’agissait de variables. Nous n’avons pas besoin d’ajouter le préfixevol$
à chaque fois. Néanmoins, l’utilisation deflights$origin
etflights$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 deflights
aux lignes correspondant à ces indices de ligne sont simplement renvoyées sous forme d’undata.table
.Une virgule après la condition dans
i
n’est pas nécessaire. Maisflights[origin == "JFK" & month == 6L, ]
fonctionnerait parfaitement. Avec undata.frame
, cependant, la virgule est indispensable.
– Récupérer les deux premières lignes de flights
.
ans <- flights[1:2]
ans
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
- Dans ce cas, il n’y a pas de condition. Les indices des lignes sont
déjà fournis dans
i
. Nous retournons donc undata.table
avec toutes les colonnes deflights
aux lignes pour ces index de ligne.
– 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)
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 5 6 49 EV EWR XNA 195 1131 8
# 2: 2014 1 6 7 13 EV EWR XNA 190 1131 8
# 3: 2014 1 7 -6 -13 EV EWR XNA 179 1131 8
# 4: 2014 1 8 -7 -12 EV EWR XNA 184 1131 8
# 5: 2014 1 9 16 7 EV EWR XNA 181 1131 8
# 6: 2014 1 13 66 66 EV EWR XNA 188 1131 9
order()
est optimisé en interne
Nous pouvons utiliser “-” sur les colonnes
character
dans le cadre d’undata.table
pour trier par ordre décroissant.De plus,
order(...)
dans le cadre d’undata.table
utilise l’ordre radix rapide interne dedata.table
forder()
. Ce tri a apporté une telle amélioration par rapport àbase::order
de R que le projet R a adopté l’algorithmedata.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.
d) Sélection de colonne(s) dans j
– Sélectionner la colonne arr_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 simplementi
.Il renvoie toutes les lignes de la colonne
arr_delay
.
– Sélectionner la colonne arr_delay
, mais la renvoyer
en tant que data.table
.
ans <- flights[, list(arr_delay)]
head(ans)
# arr_delay
# <int>
# 1: 13
# 2: 13
# 3: 9
# 4: -26
# 5: 1
# 6: 0
Nous enveloppons les variables (noms de colonnes) dans
list()
, ce qui assure qu’undata.table
est retourné. Dans le cas d’un seul nom de colonne, le fait de ne pas utiliserlist()
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 delist()
. C’est un alias delist()
; 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.
Conseil :
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)
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# 6: 0 4
## forme alternative
# ans <- flights[, list(arr_delay, dep_delay)]
e) Calcul ou do dans ‘j’
– Combien de voyages on eu un retard total < 0 ?
ans <- flights[, sum( (arr_delay + dep_delay) < 0 )]
ans
# [1] 141814
Que se passe-t-il dans ce cas ?
- Le
j
dedata.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.
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
# m_arr m_dep
# <num> <num>
# 1: 5.839349 9.807884
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ù lemois
est égal à6L
. Nous n’effectuons pas encore le sous-ensemble de toutes lesdata.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 avecmean()
. 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.
– 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
# [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.
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")
- Sous-ensemble basé sur les clés :
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 ...
- Sous-ensemble logique :
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,...
- Correspondance exacte en utilisant
nomatch=NULL
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
:
.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éroportorigin
est “JFK”, et lemois
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
aveclist()
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
.
h) Super ! Mais comment référencer les colonnes par nom dans
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).
– 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)
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# 6: 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
.
– Sélectionnez les colonnes nommées dans une variable en utilisant
le préfixe ..
select_cols = c("arr_delay", "dep_delay")
flights[ , ..select_cols]
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# ---
# 253312: -30 1
# 253313: -14 -5
# 253314: 16 -8
# 253315: 15 -4
# 253316: 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.
– Sélectionner les colonnes nommées dans une variable en utilisant
with = FALSE
flights[ , select_cols, with = FALSE]
# arr_delay dep_delay
# <int> <int>
# 1: 13 14
# 2: 13 -3
# 3: 9 2
# 4: -26 -8
# 5: 1 2
# ---
# 253312: -30 1
# 253313: -14 -5
# 253314: 16 -8
# 253315: 15 -4
# 253316: 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
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
## (2) en utilisant with
DF[with(DF, x > 1), ]
# x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
-
L’utilisation de
with()
dans (2) permet d’utiliser la colonnex
deDF
comme s’il s’agissait d’une variable.D’où le nom de l’argument
with
dansdata.table
. Mettrewith = FALSE
désactive la possibilité de se référer aux colonnes comme si elles étaient des variables, restaurant ainsi le « modedata.frame
». -
Nous pouvons également désélectionner des colonnes en utilisant
-
ou!
. Par exemple : -
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.
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
# origin N
# <char> <int>
# 1: JFK 81483
# 2: LGA 84433
# 3: 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 parorigine
, 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éeN
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 vecteurcharacter
) comme argument de la fonction.-
Lorsqu’il n’y a qu’une seule colonne ou expression à laquelle se référer dans
j
etby
, nous pouvons abandonner la notation.()
. Ceci est purement pratique. Nous pourrions plutôt faire :ans <- flights[, .N, by = origin] ans # origin N # <char> <int> # 1: JFK 81483 # 2: LGA 84433 # 3: EWR 87400
Nous utiliserons cette forme pratique chaque fois que cela sera possible.
– Comment calculer le nombre de voyages au départ de chaque aéroport
pour le transporteur ayant le code "AA"
?
Le code unique de transporteur "AA"
correspond à
American Airlines Inc.
ans <- flights[carrier == "AA", .N, by = origin]
ans
# origin N
# <char> <int>
# 1: JFK 11923
# 2: LGA 11730
# 3: EWR 2649
Nous obtenons d’abord les indices de ligne pour l’expression
carrier == "AA"
à partir dei
.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.
– Comment obtenir le nombre total de voyages pour chaque paire
origin, dest
du transporteur ayant pour code
"AA"
?
ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
head(ans)
# origin dest N
# <char> <char> <int>
# 1: JFK LAX 3387
# 2: LGA PBI 245
# 3: EWR LAX 62
# 4: JFK MIA 1876
# 5: JFK SEA 298
# 6: 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.()
dansby
– encore une fois, c’est juste un raccourci pourlist()
, etlist()
peut être utilisé ici aussi. Nous nous en tiendrons à nouveau à.()
dans cette vignette.
– 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"
?
ans <- flights[carrier == "AA",
.(mean(arr_delay), mean(dep_delay)),
by = .(origin, dest, month)]
ans
# origin dest month V1 V2
# <char> <char> <int> <num> <num>
# 1: JFK LAX 1 6.590361 14.2289157
# 2: LGA PBI 1 -7.758621 0.3103448
# 3: EWR LAX 1 1.366667 7.5000000
# 4: JFK MIA 1 15.720670 18.7430168
# 5: JFK SEA 1 14.357143 30.7500000
# ---
# 196: LGA MIA 10 -6.251799 -1.4208633
# 197: JFK MIA 10 -1.880184 6.6774194
# 198: EWR PHX 10 -3.032258 -4.2903226
# 199: JFK MCO 10 -10.048387 -1.6129032
# 200: JFK DCA 10 16.483871 15.5161290
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 queV1
etV2
.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
?
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
# Key: <origin, dest, month>
# origin dest month V1 V2
# <char> <char> <int> <num> <num>
# 1: EWR DFW 1 6.427673 10.0125786
# 2: EWR DFW 2 10.536765 11.3455882
# 3: EWR DFW 3 12.865031 8.0797546
# 4: EWR DFW 4 17.792683 12.9207317
# 5: EWR DFW 5 18.487805 18.6829268
# ---
# 196: LGA PBI 1 -7.758621 0.3103448
# 197: LGA PBI 2 -7.865385 2.4038462
# 198: LGA PBI 3 -5.754098 3.0327869
# 199: LGA PBI 4 -13.966667 -4.7333333
# 200: LGA PBI 5 -10.357143 -6.8571429
- Tout ce que nous avons fait, c’est remplacer
by
parkeyby
. Cela ordonne automatiquement le résultat par ordre croissant des variables de regroupement. En fait, à cause de l’implémentation interne deby
qui nécessite d’abord un tri avant de récupérer l’ordre de la table originale,keyby
est typiquement plus rapide queby
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 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)
# origin dest N
# <char> <char> <int>
# 1: EWR PHX 121
# 2: EWR MIA 848
# 3: EWR LAX 62
# 4: EWR DFW 1618
# 5: JFK STT 229
# 6: JFK SJU 690
Rappelons que nous pouvons utiliser
-
sur une colonnecharacter
dansorder()
dans le cadre d’undata.table
. Ceci est possible grâce à l’optimisation interne des requêtes dedata.table
.Rappelez-vous aussi que
order(...)
dans le contexte d’undata.table
est automatiquement optimisé pour utiliser l’algorithme de tri radix rapide interne dedata.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
# <char> <char> <int>
# 1: EWR PHX 121
# 2: EWR MIA 848
# 3: EWR LAX 62
# 4: EWR DFW 1618
# 5: JFK STT 229
# 6: JFK SJU 690
# 7: JFK SFO 1312
# 8: JFK SEA 298
# 9: JFK SAN 299
# 10: 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[ ... ][ ... ][ ... ]
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
# dep_delay arr_delay N
# <lgcl> <lgcl> <int>
# 1: TRUE TRUE 72836
# 2: FALSE TRUE 34583
# 3: FALSE FALSE 119304
# 4: TRUE FALSE 26593
La dernière ligne correspond à
dep_delay > 0 = TRUE
etarr_delay > 0 = FALSE
. Nous pouvons voir que les vols 26593 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 pourj
, vous pouvez nommer ces expressions comme vous le feriez pour des éléments de n’importe quelle liste, comme par exempleDT[, .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)]
.
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
:
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
# <char> <int> <int> <int>
# 1: b 1 7 13
# 2: b 2 8 14
# 3: b 3 9 15
# 4: a 4 10 16
# 5: a 5 11 17
# 6: 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
# Empty data.table (0 rows and 1 cols): 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"
, puisID = "a"
, et enfinID = "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
# <char> <num> <num> <num>
# 1: b 2.0 8.0 14.0
# 2: a 4.5 10.5 16.5
# 3: c 6.0 12.0 18.0
.SD
contient les lignes correspondant aux colonnesa
,b
etc
pour ce groupe. Nous calculons la moyenne avecmean()
sur chacune de ces colonnes en utilisant la fonction de base déjà familièrelapply()
.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.
.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
# origin dest month arr_delay dep_delay
# <char> <char> <int> <num> <num>
# 1: JFK LAX 1 6.590361 14.2289157
# 2: LGA PBI 1 -7.758621 0.3103448
# 3: EWR LAX 1 1.366667 7.5000000
# 4: JFK MIA 1 15.720670 18.7430168
# 5: JFK SEA 1 14.357143 30.7500000
# ---
# 196: LGA MIA 10 -6.251799 -1.4208633
# 197: JFK MIA 10 -1.880184 6.6774194
# 198: EWR PHX 10 -3.032258 -4.2903226
# 199: JFK MCO 10 -10.048387 -1.6129032
# 200: JFK DCA 10 16.483871 15.5161290
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)
# month year day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 1 2014 1 14 13 AA JFK LAX 359 2475 9
# 2: 1 2014 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2 2014 1 -1 1 AA JFK LAX 358 2475 8
# 4: 2 2014 1 -5 3 AA JFK LAX 358 2475 11
# 5: 3 2014 1 -11 36 AA JFK LAX 375 2475 8
# 6: 3 2014 1 -3 14 AA JFK LAX 368 2475 11
.SD
est undata.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 dedata.table
, qui est également une liste, ce qui nous évite de l’entourer de.()
.
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]
# ID val
# <char> <int>
# 1: b 1
# 2: b 2
# 3: b 3
# 4: b 7
# 5: b 8
# 6: b 9
# 7: a 4
# 8: a 5
# 9: a 10
# 10: a 11
# 11: c 6
# 12: c 12
- C’est tout. Aucune syntaxe particulière n’est requise. Tout ce que
nous avons besoin de connaître est la fonction de base
c()
qui concatène des vecteurs, ainsi que l’astuce de tout à l’heure.
– 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]
# ID val
# <char> <list>
# 1: b 1,2,3,7,8,9
# 2: a 4, 5,10,11
# 3: c 6,12
Ici, nous concaténons d’abord les valeurs avec
c(a,b)
pour chaque groupe, et nous les enveloppons aveclist()
. 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
# Empty data.table (0 rows and 1 cols): 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
# Empty data.table (0 rows and 1 cols): 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 =12
, alors que (2) renvoie 1+1+1=3
.
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 subdiviser les lignes comme dans un
data.frame
- sauf que vous n’avez pas besoin d’utiliserDT$
de façon répétitive puisque les colonnes dans le contexte d’undata.table
sont vues comme si elles étaient des variables.Nous pouvons également trier un
data.table
en utilisantorder()
, 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
vignettes “Clés et sous-ensembles basés sur la recherche binaire
rapide” et “Jointures et jointures liées au temps”.
En utilisant 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)]
.
En utilisant 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é dej
, combinée àby
eti
, 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
dansj
pour opérer sur plusieurs colonnes en utilisant des fonctions de base déjà connues. Voici quelques exemples:DT[, lapply(.SD, fun), by = ..., .SDcols = ...]
- appliquefun
à toutes les colonnes spécifiées dans.SDcols
tout en groupant par les colonnes spécifiées dansby
.DT[, head(.SD, 2), by = ...]
- renvoie les deux premières lignes pour chaque groupe.DT[col > val, head(.SD, 1), by = ...]
- combinei
avecj
etby
.