Manipuler les données avec dplyr
La version originale de ce chapitre a été écrite par Julien Barnier dans le cadre de son Introduction à R et au tidyverse.
dplyr
est une extension facilitant le traitement et la manipulation de données contenues dans une ou plusieurs tables (qu’il s’agisse de data frame ou de tibble). Elle propose une syntaxe claire et cohérente, sous formes de verbes, pour la plupart des opérations de ce type.
Par ailleurs, les fonctions de dplyr
sont en général plus rapides que leur équivalent sous R de base, elles permettent donc de traiter des données de grande dimension1.
dplyr
part du principe que les données sont tidy (voir la section consacrée aux tidy data). Les fonctions de l’extension peuvent s’appliquer à des tableaux de type data.frame
ou tibble
, et elles retournent systématiquement un tibble
(voir la section dédiée).
Préparation
dplyr
fait partie du coeur du tidyverse, elle est donc chargée automatiquement avec :
On peut également la charger individuellement avec :
Dans ce qui suit on va utiliser les données du jeu de données nycflights13
, contenu dans l’extension du même nom (qu’il faut donc avoir installé). Celui-ci correspond aux données de tous les vols au départ d’un des trois aéroports de New-York en 2013. Il a la particularité d’être réparti en trois tables :
flights
contient des informations sur les vols : date, départ, destination, horaires, retard…airports
contient des informations sur les aéroportsairlines
contient des données sur les compagnies aériennes
On va charger les trois tables du jeu de données :
library(nycflights13)
## Chargement des trois tables du jeu de données
data(flights)
data(airports)
data(airlines)
Normalement trois objets correspondant aux trois tables ont dû apparaître dans votre environnement.
Ces trois tableaux sont au format tibble
. Il s’agit d’une extension des tableaux de données utilisé par le tidyverse
. Les tibble
{data-pkg="tibble} s’utilisent comme des data.frame
, avec justes quelques différentes :
- leur classe est
c("tbl_df", "tbf", "data.frame")
; - leur présentation dans la console est amélioriée ;
df[, j]
renvoie toujours untibble
avec une seule colonne (et non le contenu de cette colonne que l’on obtient avecdf[[j]]
) ;- les colonnes d’un
tibble
peuvent être des listes ; - à la différence d’un tableau de données classique où il est possible d’utiliser un nom partiel (par exemple écrire
df$ab
pour obtenirdf$abc
), il est obligatoire d’utiliser les noms complets avec untibble
.
Pour convertir un tableau de données en tibble
, on utilisera la fonction as_tibble
.
Les verbes de dplyr
La manipulation de données avec dplyr
se fait en utilisant un nombre réduit de verbes, qui correspondent chacun à une action différente appliquée à un tableau de données.
slice
Le verbe slice
sélectionne des lignes du tableau selon leur position. On lui passe un chiffre ou un vecteur de chiffres.
Si on souhaite sélectionner la 345e ligne du tableau airports
:
Si on veut sélectionner les 5 premières lignes :
filter
filter
sélectionne des lignes d’un tableau de données selon une condition. On lui passe en paramètre un test, et seules les lignes pour lesquelles ce test renvoit TRUE
(vrai) sont conservées.
Par exemple, si on veut sélectionner les vols du mois de janvier, on peut filtrer sur la variable month de la manière suivante :
Si on veut uniquement les vols avec un retard au départ (variable dep_delay) compris entre 10 et 15 minutes :
Si on passe plusieurs arguments à filter
, celui-ci rajoute automatiquement une condition et entre les conditions. La ligne ci-dessus peut donc également être écrite de la manière suivante, avec le même résultat :
Enfin, on peut également placer des fonctions dans les tests, qui nous permettent par exemple de sélectionner les vols avec la plus grande distance :
select et rename
select
permet de sélectionner des colonnes d’un tableau de données. Ainsi, si on veut extraire les colonnes lat
et lon
du tableau airports :
Si on fait précéder le nom d’un -
, la colonne est éliminée plutôt que sélectionnée :
select
comprend toute une série de fonctions facilitant la sélection de multiples colonnes. Par exemple, starts_with
, ends_width
, contains
ou matches
permettent d’exprimer des conditions sur les noms de variables :
La syntaxe colonne1:colonne2
permet de sélectionner toutes les colonnes situées entre colonne1 et colonne2 incluses2 :
select
peut être utilisée pour réordonner les colonnes d’une table en utilisant la fonction everything()
, qui sélectionne l’ensemble des colonnes non encore sélectionnées. Ainsi, si on souhaite faire passer la colonne name en première position de la table airports
, on peut faire :
Une variante de select
est rename
3, qui permet de renommer facilement des colonnes. On l’utilise en lui passant des paramètres de la forme nouveau_nom = ancien_nom
. Ainsi, si on veut renommer les colonnes lon et lat de airports
en longitude et latitude :
Si les noms de colonnes comportent des espaces ou des caractères spéciaux, on peut les entourer de guillemets ("
) ou de quotes inverses (`
) :
tmp <- rename(flights,
"retard départ" = dep_delay,
"retard arrivée" = arr_delay)
select(tmp, `retard départ`, `retard arrivée`)
arrange
arrange
réordonne les lignes d’un tableau selon une ou plusieurs colonnes.
Ainsi, si on veut trier le tableau flights
selon le retard au départ croissant :
On peut trier selon plusieurs colonnes. Par exemple selon le mois, puis selon le retard au départ :
Si on veut trier selon une colonne par ordre décroissant, on lui applique la fonction desc()
:
Combiné avec slice
, arrange
permet par exemple de sélectionner les trois vols ayant eu le plus de retard :
mutate
mutate
permet de créer de nouvelles colonnes dans le tableau de données, en général à partir de variables existantes.
Par exemple, la table airports
contient l’altitude de l’aéroport en pieds. Si on veut créer une nouvelle variable alt_m avec l’altitude en mètres, on peut faire :
On peut créer plusieurs nouvelles colonnes en une seule fois, et les expressions successives peuvent prendre en compte les résultats des calculs précédents. L’exemple suivant convertit d’abord la distance en kilomètres dans une variable distance_km, puis utilise cette nouvelle colonne pour calculer la vitesse en km/h.
flights <- mutate(flights,
distance_km = distance / 0.62137,
vitesse = distance_km / air_time * 60)
select(flights, distance, distance_km, vitesse)
À noter que mutate
est évidemment parfaitement compatible avec les fonctions vues dans le chapitre sur les recodages : fonctions de forcats
, if_else
, case_when
…
L’avantage d’utiliser mutate
est double. D’abord il permet d’éviter d’avoir à saisir le nom du tableau de données dans les conditions d’un if_else
ou d’un case_when
:
flights <- mutate(flights,
type_retard = case_when(
dep_delay > 0 & arr_delay > 0 ~ "Retard départ et arrivée",
dep_delay > 0 & arr_delay <= 0 ~ "Retard départ",
dep_delay <= 0 & arr_delay > 0 ~ "Retard arrivée",
TRUE ~ "Aucun retard"))
Utiliser mutate
pour les recodages permet aussi de les intégrer dans un pipeline de traitement de données, concept présenté dans la section suivante.
Citons également les fonctions recode
et recode_factor
.
Enchaîner les opérations avec le pipe
Quand on manipule un tableau de données, il est très fréquent d’enchaîner plusieurs opérations. On va par exemple filtrer pour extraire une sous-population, sélectionner des colonnes puis trier selon une variable.
Dans ce cas on peut le faire de deux manières différentes. La première est d’effectuer toutes les opérations en une fois en les emboîtant
:
Cette notation a plusieurs inconvénients :
- elle est peu lisible
- les opérations apparaissent dans l’ordre inverse de leur réalisation. Ici on effectue d’abord le
filter
, puis leselect
, puis learrange
, alors qu’à la lecture du code c’est learrange
qui apparaît en premier. - Il est difficile de voir quel paramètre se rapporte à quelle fonction
Une autre manière de faire est d’effectuer les opérations les unes après les autres, en stockant les résultats intermédiaires dans un objet temporaire :
tmp <- filter(flights, dest == "LAX")
tmp <- select(tmp, dep_delay, arr_delay)
arrange(tmp, dep_delay)
C’est nettement plus lisible, l’ordre des opérations est le bon, et les paramètres sont bien rattachés à leur fonction. Par contre, ça reste un peu “verbeux”, et on crée un objet temporaire tmp
dont on n’a pas réellement besoin.
Pour simplifier et améliorer encore la lisibilité du code, on va utiliser un nouvel opérateur, baptisé pipe4. Le pipe se note %>%
, et son fonctionnement est le suivant : si j’exécute expr %>% f
, alors le résultat de l’expression expr
, à gauche du pipe, sera passé comme premier argument à la fonction f
, à droite du pipe, ce qui revient à exécuter f(expr)
.
Ainsi les deux expressions suivantes sont rigoureusement équivalentes :
Ce qui est intéressant dans cette histoire, c’est qu’on va pouvoir enchaîner les pipes. Plutôt que d’écrire :
On va pouvoir faire :
À chaque fois, le résultat de ce qui se trouve à gauche du pipe est passé comme premier argument à ce qui se trouve à droite : on part de l’objet flights
, qu’on passe comme premier argument à la fonction filter
, puis on passe le résultat de ce filter
comme premier argument du select
.
Le résultat final est le même avec les deux syntaxes, mais avec le pipe l’ordre des opérations correspond à l’ordre naturel de leur exécution, et on n’a pas eu besoin de créer d’objet intermédiaire.
Si la liste des fonctions enchaînées est longue, on peut les répartir sur plusieurs lignes à condition que l’opérateur %>%
soit en fin de ligne :
On appelle une suite d’instructions de ce type un pipeline.
Évidemment, il est naturel de vouloir récupérer le résultat final d’un pipeline pour le stocker dans un objet. Par exemple, on peut stocker le résultat du pipeline ci-dessus dans un nouveau tableau delay_la
de la manière suivante :
delay_la <- flights %>%
filter(dest == "LAX") %>%
select(dep_delay, arr_delay) %>%
arrange(dep_delay)
Dans ce cas, delay_la
contiendra le tableau final, obtenu après application des trois instructions filter
, select
et arrange
.
Cette notation n’est pas forcément très intuitive au départ. Il faut bien comprendre que c’est le résultat final, une fois application de toutes les opérations du pipeline, qui est renvoyé et stocké dans l’objet en début de ligne.
Une manière de le comprendre peut être de voir que la notation suivante :
est équivalente à :
L’utilisation du pipe n’est pas obligatoire, mais elle rend les scripts plus lisibles et plus rapides à saisir. On l’utilisera donc dans ce qui suit.
Opérations groupées
group_by
Un élément très important de dplyr
est la fonction group_by
. Elle permet de définir des groupes de lignes à partir des valeurs d’une ou plusieurs colonnes. Par exemple, on peut grouper les vols selon leur mois :
Par défaut ceci ne fait rien de visible, à part l’apparition d’une mention Groups dans l’affichage du résultat. Mais à partir du moment où des groupes ont été définis, les verbes comme slice
, mutate
ou summarise
vont en tenir compte lors de leurs opérations.
Par exemple, si on applique slice
à un tableau préalablement groupé, il va sélectionner les lignes aux positions indiquées pour chaque groupe. Ainsi la commande suivante affiche le premier vol de chaque mois, selon leur ordre d’apparition dans le tableau :
Idem pour mutate
: les opérations appliquées lors du calcul des valeurs des nouvelles colonnes sont aplliquée groupe de lignes par groupe de lignes. Dans l’exemple suivant, on ajoute une nouvelle colonne qui contient le retard moyen du mois correspondant :
flights %>%
group_by(month) %>%
mutate(mean_delay_month = mean(dep_delay, na.rm = TRUE)) %>%
select(dep_delay, month, mean_delay_month)
Ceci peut permettre, par exemple, de déterminer si un retard donné est supérieur ou inférieur au retard moyen du mois en cours.
group_by
peut aussi être utile avec filter
, par exemple pour sélectionner les vols avec le retard au départ le plus important pour chaque mois :
Attention : la clause group_by
marche pour les verbes déjà vus précédemment, sauf pour arrange
, qui par défaut trie la table sans tenir compte des groupes. Pour obtenir un tri par groupe, il faut lui ajouter l’argument .by_group = TRUE
.
On peut voir la différence en comparant les deux résultats suivants :
summarise et count
summarise
permet d’agréger les lignes du tableau en effectuant une opération “résumée” sur une ou plusieurs colonnes. Par exemple, si on souhaite connaître les retards moyens au départ et à l’arrivée pour l’ensemble des vols du tableau flights
:
flights %>%
summarise(retard_dep = mean(dep_delay, na.rm=TRUE),
retard_arr = mean(arr_delay, na.rm=TRUE))
Cette fonction est en général utilisée avec group_by
, puisqu’elle permet du coup d’agréger et résumer les lignes du tableau groupe par groupe. Si on souhaite calculer le délai maximum, le délai minimum et le délai moyen au départ pour chaque mois, on pourra faire :
flights %>%
group_by(month) %>%
summarise(max_delay = max(dep_delay, na.rm=TRUE),
min_delay = min(dep_delay, na.rm=TRUE),
mean_delay = mean(dep_delay, na.rm=TRUE))
summarise
dispose d’un opérateur spécial, n()
, qui retourne le nombre de lignes du groupe. Ainsi si on veut le nombre de vols par destination, on peut utiliser :
n()
peut aussi être utilisée avec filter
et mutate
.
À noter que quand on veut compter le nombre de lignes par groupe, on peut utiliser directement la fonction count
. Ainsi le code suivant est identique au précédent :
Grouper selon plusieurs variables
On peut grouper selon plusieurs variables à la fois, il suffit de les indiquer dans la clause du group_by
:
On peut également compter selon plusieurs variables :
On peut utiliser plusieurs opérations de groupage dans le même pipeline. Ainsi, si on souhaite déterminer le couple origine/destination ayant le plus grand nombre de vols selon le mois de l’année, on devra procéder en deux étapes :
- d’abord grouper selon mois, origine et destination pour calculer le nombre de vols
- puis grouper uniquement selon le mois pour sélectionner la ligne avec la valeur maximale.
Au final, on obtient le code suivant :
flights %>%
group_by(month, origin, dest) %>%
summarise(nb = n()) %>%
group_by(month) %>%
filter(nb == max(nb))
Lorsqu’on effectue un group_by
suivi d’un summarise
, le tableau résultat est automatiquement dégroupé de la dernière variable de regroupement. Ainsi le tableau généré par le code suivant est groupé par month et origin :
Cela peut permettre “d’enchaîner” les opérations groupées. Dans l’exemple suivant on calcule le pourcentage des trajets pour chaque destination par rapport à tous les trajets du mois :
flights %>%
group_by(month, dest) %>%
summarise(nb = n()) %>%
mutate(pourcentage = nb / sum(nb) * 100)
On peut à tout moment “dégrouper” un tableau à l’aide de ungroup
. Ce serait par exemple nécessaire, dans l’exemple précédent, si on voulait calculer le pourcentage sur le nombre total de vols plutôt que sur le nombre de vols par mois :
flights %>%
group_by(month, dest) %>%
summarise(nb = n()) %>%
ungroup() %>%
mutate(pourcentage = nb / sum(nb) * 100)
À noter que count
, par contre, renvoit un tableau non groupé :
Autres fonctions utiles
dplyr
contient beaucoup d’autres fonctions utiles pour la manipulation de données.
sample_n et sample_frac
sample_n
et sample_frac
permettent de sélectionner un nombre de lignes ou une fraction des lignes d’un tableau aléatoirement. Ainsi si on veut choisir 5 lignes au hasard dans le tableau airports
:
Si on veut tirer au hasard 10% des lignes de flights
:
Ces fonctions sont utiles notamment pour faire de “l’échantillonnage” en tirant au hasard un certain nombre d’observations du tableau.
lead et lag
lead
et lag
permettent de décaler les observations d’une variable d’un cran vers l’arrière (pour lead
) ou vers l’avant (pour lag
).
[1] 2 3 4 5 NA
[1] NA 1 2 3 4
Ceci peut être utile pour des données de type “séries temporelles”. Par exemple, on peut facilement calculer l’écart entre le retard au départ de chaque vol et celui du vol précédent :
flights %>%
mutate(dep_delay_prev = lead(dep_delay),
dep_delay_diff = dep_delay - dep_delay_prev) %>%
select(dep_delay_prev, dep_delay, dep_delay_diff)
tally
tally
est une fonction qui permet de compter le nombre d’observations d’un groupe :
Lors de son premier appel, elle sera équivalente à un summarise(n = n())
ou à un count()
. Là où la fonction est intelligente, c’est que si on l’appelle plusieurs fois successivement, elle prendra en compte l’existence d’un n
déjà calculé et effectuera automatiquement un summarise(n = sum(n))
:
distinct
distinct
filtre les lignes du tableau pour ne conserver que les lignes distinctes, en supprimant toutes les lignes en double.
On peut lui spécifier une liste de variables : dans ce cas, pour toutes les observations ayant des valeurs identiques pour les variables en question, distinct
ne conservera que la première d’entre elles.
L’option .keep_all
permet, dans l’opération précédente, de conserver l’ensemble des colonnes du tableau :
Ressources
Toutes les ressources ci-dessous sont en anglais…
Le livre R for data science, librement accessible en ligne, contient plusieurs chapitres très complets sur la manipulation des données, notamment :
- Data transformation pour les manipulations
- Relational data pour les tables multiples
Le site de l’extension comprend une liste des fonctions et les pages d’aide associées, mais aussi une introduction au package et plusieurs articles dont un spécifiquement sur les jointures.
Une “antisèche” très synthétique est également accessible depuis RStudio, en allant dans le menu Help puis Cheatsheets et Data Transformation with dplyr.
Enfin, on trouvera des exercices dans l’Introduction à R et au tidyverse de Julien Barnier.
dplyr et data.table
Pour ceux travaillant également avec l’extension data.table
, il est possible de concilier tibble et data.table avec l’extension dtplyr
et sa fonction tbl_dt
.
[1] "tbl_dt" "tbl" "data.table" "data.frame"
Le tableau de données est à la fois compatible avec data.table
(et notamment sa syntaxe particulière des crochets) et les verbes de dplyr
.
Pour décrouvrir data.table
, voir le chapitre dédié.
dplyr et survey
L’extension srvyr
vise à permettre d’utiliser les verbes de dplyr
avec les plans d’échantillonnage complexe définis avec survey
. Le fonctionnement de cette extension est expliqué dans une vignette dédiée : https://cran.r-project.org/web/packages/srvyr/vignettes/srvyr-vs-survey.html.
Elles sont cependant moins rapides que les fonctions de
data.table
, voir le chapitre dédié↩À noter que cette opération est un peu plus “fragile” que les autres, car si l’ordre des colonnes change elle peut renvoyer un résultat différent.↩
Il est également possible de renommer des colonnes directement avec
select
, avec la même syntaxe que pourrename
.↩Le pipe a été introduit à l’origine par l’extension
magrittr
, et repris pardplyr
↩