Une version actualisée de ce chapitre est disponible sur guide-R : dplyr & Le pipe

La version originale de ce chapitre a été écrite par Julien Barnier dans le cadre de son Introduction à R et au tidyverse.

Ce chapitre est évoqué dans le webin-R #04 (manipuler les données avec dplyr) sur YouTube.

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 :

library(tidyverse)

On peut également la charger individuellement avec :

library(dplyr)

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éroports
  • airlines 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 un tibble avec une seule colonne (et non le contenu de cette colonne que l’on obtient avec df[[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 obtenir df$abc), il est obligatoire d’utiliser les noms complets avec un tibble.

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 :

slice(airports, 345)

Si on veut sélectionner les 5 premières lignes :

slice(airports, 1:5)

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 :

filter(flights, month == 1)

Si on veut uniquement les vols avec un retard au départ (variable dep_delay) compris entre 10 et 15 minutes :

filter(flights, dep_delay >= 10 & dep_delay <= 15)

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 :

filter(flights, dep_delay >= 10, dep_delay <= 15)

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 :

filter(flights, distance == max(distance))

select, rename et relocate

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 :

select(airports, lat, lon)

Si on fait précéder le nom d’un -, la colonne est éliminée plutôt que sélectionnée :

select(airports, -lat, -lon)

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 :

select(flights, starts_with("dep_"))

La syntaxe colonne1:colonne2 permet de sélectionner toutes les colonnes situées entre colonne1 et colonne2 incluses2 :

select(flights, year:day)

all_of et any_of permettent de fournir une liste de variables à extraire sous forme de vecteur textuel. Alors que all_of renverra une erreur si une variable n’est pas trouvée dans le tableau de départ, any_of sera moins stricte.

select(flights, all_of(c("year", "month", "day")))
select(flights, all_of(c("century", "year", "month", "day")))
Erreur : Can't subset columns that don't exist. 
x Column `century` doesn't exist.
select(flights, any_of(c("century", "year", "month", "day")))

where permets de sélectionner des variables à partir d’une fonction qui renvoie une valeur logique. Par exemple, pour sélectionner seulement les variables textuelles.

select(flights, where(is.character))

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 :

select(airports, name, everything())

Pour réordonner des colonnes, on pourra aussi avoir recours à relocate en indiquant les premières variables. IL n’est pas nécessaire d’ajouter everything() car avec relocate toutes les variables sont conservées.

relocate(airports, lon, lat, name)

Une variante de select est rename3, 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 :

dplyr::rename(airports, longitude = lon, latitude = lat)

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 <- dplyr::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 :

dplyr::arrange(flights, dep_delay)

On peut trier selon plusieurs colonnes. Par exemple selon le mois, puis selon le retard au départ :

dplyr::arrange(flights, month, dep_delay)

Si on veut trier selon une colonne par ordre décroissant, on lui applique la fonction desc() :

dplyr::arrange(flights, desc(dep_delay))

Combiné avec slice, arrange permet par exemple de sélectionner les trois vols ayant eu le plus de retard :

tmp <- dplyr::arrange(flights, desc(dep_delay)) 
slice(tmp, 1:3)

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 :

airports <- mutate(airports, alt_m = alt / 3.2808)
select(airports, name, alt, alt_m)

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.

flights$month_name <- recode_factor(flights$month,
  "1" = "Jan",
  "2" = "Feb",
  "3" = "Mar",
  "4" = "Apr",
  "5" = "May",
  "6" = "Jun",
  "7" = "Jul",
  "8" = "Aug",
  "9" = "Sep",
  "10" = "Oct",
  "11" = "Nov",
  "12" = "Dec"
)

Enchaîner les opérations avec le pipe

Une version actualisée de cette section est disponible sur guide-R : 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 :

arrange(select(filter(flights, dest == "LAX"), dep_delay, arr_delay), dep_delay)

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 le select, puis le arrange, alors qu’à la lecture du code c’est le arrange 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 :

filter(flights, dest == "LAX")
flights %>% filter(dest == "LAX")

Ce qui est intéressant dans cette histoire, c’est qu’on va pouvoir enchaîner les pipes. Plutôt que d’écrire :

select(filter(flights, dest == "LAX"), dep_delay, arr_delay)

On va pouvoir faire :

flights %>% filter(dest == "LAX") %>% select(dep_delay, arr_delay)

À 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 :

flights %>% 
  filter(dest == "LAX") %>% 
  select(dep_delay, arr_delay) %>% 
  arrange(dep_delay)

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 :

delay_la <- flights %>% 
  filter(dest == "LAX") %>% 
  select(dep_delay, arr_delay)

est équivalente à :

delay_la <- (flights %>% filter(dest == "LAX") %>% select(dep_delay, arr_delay))

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 :

flights %>% group_by(month)

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 :

flights %>% group_by(month) %>% slice(1)

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 :

flights %>% 
  group_by(month) %>% 
  filter(dep_delay == max(dep_delay, na.rm = TRUE))

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 :

flights %>% 
  group_by(month) %>% 
  dplyr::arrange(desc(dep_delay))
flights %>% 
  group_by(month) %>% 
  dplyr::arrange(desc(dep_delay), .by_group = TRUE)

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 %>% 
  dplyr::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) %>%
  dplyr::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 :

flights %>%
  group_by(dest) %>%
  dplyr::summarise(nb = n())

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 :

flights %>%
  dplyr::count(dest)

Grouper selon plusieurs variables

On peut grouper selon plusieurs variables à la fois, il suffit de les indiquer dans la clause du group_by :

flights %>%
  group_by(month, dest) %>%
  dplyr::summarise(nb = n()) %>%
  dplyr::arrange(desc(nb))
`summarise()` has grouped output by 'month'. You can
override using the `.groups` argument.

On peut également compter selon plusieurs variables :

flights %>% 
  dplyr::count(origin, dest) %>% 
  dplyr::arrange(desc(n))

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) %>%
  dplyr::summarise(nb = n()) %>%
  group_by(month) %>%
  filter(nb == max(nb))
`summarise()` has grouped output by 'month', 'origin'. You
can override using the `.groups` argument.

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 :

flights %>%
  group_by(month, origin, dest) %>%
  dplyr::summarise(nb = n())
`summarise()` has grouped output by 'month', 'origin'. You
can override using the `.groups` argument.

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) %>%
  dplyr::summarise(nb = n()) %>% 
  mutate(pourcentage = nb / sum(nb) * 100)
`summarise()` has grouped output by 'month'. You can
override using the `.groups` argument.

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) %>%
  dplyr::summarise(nb = n()) %>% 
  ungroup() %>% 
  mutate(pourcentage = nb / sum(nb) * 100)
`summarise()` has grouped output by 'month'. You can
override using the `.groups` argument.

À noter que count, par contre, renvoit un tableau non groupé :

flights %>% 
  dplyr::count(month, dest)

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 :

airports %>% sample_n(5)

Si on veut tirer au hasard 10% des lignes de flights :

flights %>% sample_frac(0.1)

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).

lead(1:5)
[1]  2  3  4  5 NA
lag(1:5)
[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 :

flights %>% 
  group_by(month, origin, dest) %>% 
  tally

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)) :

flights %>% 
  group_by(month, origin, dest) %>% 
  tally %>%
  tally

distinct

distinct filtre les lignes du tableau pour ne conserver que les lignes distinctes, en supprimant toutes les lignes en double.

flights %>%
  select(day, month) %>%
  distinct

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.

flights %>%
  distinct(month, day)

L’option .keep_all permet, dans l’opération précédente, de conserver l’ensemble des colonnes du tableau :

flights %>%
  distinct(month, day, .keep_all = TRUE) 

Fonctions avancées

On pourra consulter le chapitre dplyr avancé de l’excellente Introduction à R et au tidyverse de Julien Barnier (https://juba.github.io/tidyverse).

Julien Barnier aborde notamment les fonctions across pour appliquer une transformation à plusieurs colonnes, rowwise et c_across pour appliquer une transformation ligne à ligne, rename_with pour renommer les colonnes à l’aide d’une fonction, ou encore la syntaxe abrégée permettant de définir des fonctions anonymes.

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 :

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. Cette extension a connu une profonde évolution en 2019. Pour plus d’informations, voir https://dtplyr.tidyverse.org/. 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.


  1. Elles sont cependant moins rapides que les fonctions de data.table, voir le chapitre dédié↩︎

  2. À 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.↩︎

  3. Il est également possible de renommer des colonnes directement avec select, avec la même syntaxe que pour rename.↩︎

  4. Le pipe a été introduit à l’origine par l’extension magrittr, et repris par dplyr↩︎