La richesse de R, alimentée par une communauté de développeurs très active, rend le choix d’une méthode adaptée à une problématique donnée difficile, et c’est tant mieux. Vous trouverez ici une modeste participation au débat qui oppose les deux packages d’analyse des données les plus en vue dans la communauté R : data.table
et dplyr
. L’article se présente en deux parties :
- Un rappel sur les syntaxes de dplyr et data.table, que vous pouvez passer si vous connaissez déjà les packages.
- Une comparaison de l’efficacité des deux packages sur une étude de cas à partir des données du package
nycflights13
Rappels sur dplyr et data.table
Si vous connaissez déjà la syntaxe de ces packages, vous pouvez directement aller à la partie Comparaisons sur une étude de cas simple. On rappelle ici les principales caractéristiques de ces packages mais pour se former à leur utilisation on peut se référer au cours de perfectionnement de Martin Chevalier. Pour une exploration de ce qu’englobe le tidyverse
et notamment une présentation des commandes de dplyr
, vous pouvez jeter un oeil à l’introduction à R et au tidyverse de J. Barnier. Enfin pour data.table, on trouve des informations utiles sur le cours Manipulations avancée avec data.table de J. Larmarange.
dplyr et le tidyverse
Le tidyverse
(contraction de “tidy” et “universe”) est un concept initié par Hadley Wickham, chef statisticien de RStudio. Il regroupe un ensemble de packages utiles au traitement statistique et au nettoyage de bases de données. On va s’intéresser ici presque seulement au package dplyr
(dont les instructions seront appliquées aux tibbles
, un format de data.frame issu du tidyverse
), mais vous pouvez parcourir les packages proposés dans le tidyverse sur le site officiel.
dplyr
propose un ensemble d’opérations de traitement de données sous une syntaxe différente de celle utilisée dans les fonctions de base de R. Ce langage présente le double avantage d’être à la fois lisible pour quelqu’un habitué aux langages tels que SAS ou SQL et de proposer des fonctions optimisées qui présentent de bonnes performances en termes de temps d’exécution. La grammaire dplyr
s’appuie en effet sur des fonctions au nom explicite :
mutate(data, newvar1=fonction(var1,var2...))
ettransmute(data, newvar1=fonction(var1,var2...))
créent de nouvelles variablesfilter(data, condition)
sélectionne au sein d’une table certaines observations, à la manière dewhere
dans SAS.arrange(data, var1, descending var2,...)
trie une base selon une ou plusieurs variables (l’équivalent d’uneproc sort
).select(data, var1 : varX)
sélectionne certaines variables dans une base, à la manière dekeep
dans SAS.summarise(data, newvar1=mean(var1), newvar2=sum(var2))
réalise toute sorte d’opérations statistiques sur une table.group_by(data, var)
regroupe une table par une variable- et bien d’autres…
Un aspect pratique de ce langage est que toutes ces opérations peuvent être chaînées à l’aide de l’opérateur %>%
(“pipe” en anglais) dont la syntaxe est la suivante : data %>% fonction(...)
est équivalent à fonction(data, ...)
. Cette syntaxe permet de chaîner un grand nombre d’opérations sur une base commune, en limitant le nombre de fois où l’on écrit des tables intermédiaires tout en conservant une grande lisibilité du code. Ce petit exemple vous en convaincra peut-être :
library(tidyverse) # On aurait aussi pu charger seulement le package dplyr
# on crée un data frame avec 100 lignes, chaque individu appartenant à un des 50 groupes
df <- data.frame(id1 = c(1:100), idgpe = sample(50), replace = TRUE)
# on y applique les instructions de dplyr
df %>% as_tibble() %>% mutate(var = rnorm(100)) %>%
group_by(idgpe) %>% summarise(var_mean = mean(var)) -> output_tibble
Un regard peu habitué contesterait peut-être l’aspect très lisible de l’instruction, mais ça l’est réellement. Le déroulé est le suivant :
- on transforme notre data.frame en tibble (pour rappel : format optimisé de data.frame pour dplyr) avec
as_tibble
- on crée une variable
var
avecmutate
- on agrège par
idgpe
avecgroup_by
- on calcule la moyenne de
var
avecsummarise
, que l’on stocke dansvar_mean
. Comme cette instruction suit un group_by, elle est réalisée à l’intérieur de chaque groupe (défini paridgpe
), sinon elle aurait été réalisé sur l’ensemble de la table.
Tout cela est stocké dans une table output_tibble, qui est ainsi un tibble agrégé par idgpe
et qui a donc 50 lignes. L’intérêt de ce chaînage est qu’il permet une économie de code et d’écriture d’éventuelles tables intermédiaires.
Data.table
Le package data.table
ne prétend pas, contrairement au tidyverse
, proposer une syntaxe concurrente à base R mais enrichir celle-ci. Il est axé autour d’un nouveau format d’objet, le data.table, qui est un type de data.frame qui permet une utilisation optimisée de l’opérateur [
.
Tout data.frame peut être converti en data.table grâce à la fonction as.data.table
, ou, de manière plus optimale pour l’utilisation de la mémoire, grâce à la fonction setDT
qui permet de directement transformer la nature de l’objet sans avoir à en écrire un autre. Il est important d’avoir en tête qu’un data.frame converti en data.table conserve les caractéristiques d’un data.frame. Cependant, l’opérateur [
appliqué au data.table change de signification et devient :
DT[i,j,by]
Avec i
qui permet de sélectionner des observations (sans avoir besoin de répéter le nom de la base dans laquelle on se trouve), j
qui permet de créer ou sélectionner des variables et by
de regrouper les traitement selon les modalités d’une variable définie. Comme dans dplyr
, il est possible de chaîner les opérations réalisées comme le montre l’exemple suivant, qui reprend le même cas de figure que celui illustrant le package dplyr
:
library(data.table)
# on convertit notre data frame précédemment créé en data.table
dt <- as.data.table(df)
# on y applique les même instructions
dt[, list(var_mean = mean(rnorm(100))), by = list(idgpe = idgpe)] -> output_dt
Le fait de renseigner mes variables au sein de list()
me permet d’avoir une table en sortie au niveau de idgpe
(donc 50 observations), sans cela ma variable est bien moyennée par groupe mais la table en sortie est toujours au niveau id1
(100 observations).
Vitesses d’exécution
Voilà donc pour les présentations! Allez, on montre le résultat d’un petit microbenchmark
des deux juste pour voir :
## Unit: milliseconds
## expr min lq mean median uq max neval
## dplyr 6.6939 9.39175 12.218420 12.0326 14.41980 21.2397 100
## data.table 1.9088 2.50940 2.939709 2.9378 3.28745 6.3173 100
Sur cet exemple, on voit un avantage clair à data.table! Mais on est sur une toute petite table en entrée. On va essayer de se rapprocher de cas plus concrets en s’intéressant à un exemple sur des bases plus importantes.
Comparaisons sur une étude de cas simple
Les avantages et inconvénients de ces deux packages sont à l’origine de nombreux débats. Vous pouvez vous en convaincre en suivant cette discussion sur stackoverflow. On peut quand même dégager deux compromis :
- Le choix de l’un ou l’autre des packages dépend beaucoup de ce que l’on va en faire (types d’analyses, taille des données, profils des utilisateurs du code…).
- Les deux packages sont plus intéressants que base R pour l’analyse de données, que ce soit en termes de facilité d’écriture ou de performances.
Pour ce deuxième point, on va essayer de s’en convaincre ensemble avec ce petit exemple.
Notre étude de cas
Pour cet exemple, on utilise les données du package de Hadley Wickham que l’on trouve dans nycflights13
. En particulier, la base flights
donne toutes les heures de départ et d’arrivée selon les aéroports de départ et d’arrivée ainsi que les retards au départ et à l’arrivée. La base weather
donne elle des indications météo, heure par heure, dans chaque aéroport. Tout bon statisticien qui se respecte devrait commencer à se dire qu’il y a quelque chose à faire pour tenter d’expliquer les retards des avions (spoiler alert : on ne va pas le faire).
Commençons par charger nos packages (n’oubliez pas de faire install.packages("nom_pck")
avant si vous ne l’avez jamais fait) et nos données :
# Les packages nécessaires
library(tidyverse) # Regroupe différents packages, voir https://www.tidyverse.org/
library(data.table)
library(microbenchmark) # Pour les calculs de vitesse d'exécution
library(nycflights13) # Pour les données
# data.table pour tests avec data.table
flightsdt <- as.data.table(flights)
weatherdt <- as.data.table(weather)
Notez que l’on n’est pas obligés de faire du dplyr sur des tibbles plutôt que des data frame, mais on suit ici les recommandations d’Hadley Wickham.
Moyenne des retards et fusion des tables
Un rapide examen des bases vous montre que la première étape avant toute analyse est comme souvent de regrouper les éléments de flights par heure et aéroport de départ pour pouvoir les fusionner avec la table weather, qui donnent les indications météo minute par minute. On écrit cette instruction de 3 manières différentes :
En base R
flights_time_hour <- aggregate.data.frame(list(arr_delay = flights$arr_delay,
dep_delay = flights$dep_delay),
list(time_hour = flights$time_hour, origin = flights$origin),
mean)
output_base <- merge(weather, flights_time_hour, by = c("time_hour", "origin"), sort = FALSE)
(J’ai utilisé aggregate.data.frame
et pas tapply
pour avoir directement un data.frame en sortie)
En dplyr
flights %>% group_by(time_hour, origin) %>%
summarise(arr_delay = mean(arr_delay),
dep_delay = mean(dep_delay)) %>%
inner_join(weather, by = c("time_hour", "origin")) -> output_dplyr
## `summarise()` regrouping output by 'time_hour' (override with `.groups` argument)
En data.table
output_DT <- merge(flightsdt[, list(arr_perc_delay = mean(arr_delay),
dep_perc_delay = mean(dep_delay)), by = c("time_hour", "origin")],
weatherdt, by = c("time_hour", "origin"))
J’ai utilisé la fonction merge
plutôt que DT1[DT2, on = c("time_hour", "origin"), nomatch = 0]
car j’ai constaté qu’elle était plus rapide, conformément à ce que montre bien cet article du Jozef’s Rblog.
Comparaisons des vitesses d’exécution
Je vous laisse juger de la lisibilité de chacune de ces instructions, qui font toutes la même chose, car c’est finalement assez subjectif. On donne ici les résultats d’un microbenchmark
de ces instructions :
## Unit: milliseconds
## expr min lq mean median uq max neval
## Base 2984.1129 3194.0318 3367.71999 3290.3493 3593.8568 3810.2907 10
## DplyR 343.0911 361.0002 487.68594 468.3438 578.0777 768.4978 10
## DT 45.8798 47.9191 57.85761 54.5310 68.4981 80.8654 10
Les résultats sont très nettement en faveur des packages dplyr
et data.table
, ce dernier ayant l’avantage. Sans doute existe-t-il des moyens de plus optimiser l’instruction en base R, mais là n’est pas vraiment la question. On voit qu’avec une syntaxe simple et lisible, dplyr
et data.table
font beaucoup mieux que l’instruction qui viendrait à l’esprit d’un statisticien qui n’utiliserait que les premières fonctions venues de base R.