Chapitre 8 Manipuler les données avec dplyr

Certaines parties de ce chapitre sont extraites du cours de Julien Barnier “Introduction à R et au tidyverse” (https://juba.github.io/tidyverse)

Le package dplyr est une extension facilitant le traitement et la manipulation de données contenues dans une ou plusieurs tables. Elle propose une syntaxe claire et cohérente. Ses fonctions sont en général plus rapides que leur équivalent sous R de base, elles permettent donc de traiter efficacement des données de grande dimension.

Les fonctions de ce package peuvent s’appliquer à des tableaux de type data.frame ou tibble (package tibble de tidyverse), et elles retournent systématiquement un `tibble.

Dans ce qui suit on va utiliser le 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…
year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum origin dest air_time distance hour minute time_hour
2013 1 1 517 515 2 830 819 11 UA 1545 N14228 EWR IAH 227 1400 5 15 2013-01-01T10:00:00Z
2013 1 1 533 529 4 850 830 20 UA 1714 N24211 LGA IAH 227 1416 5 29 2013-01-01T10:00:00Z
2013 1 1 542 540 2 923 850 33 AA 1141 N619AA JFK MIA 160 1089 5 40 2013-01-01T10:00:00Z
2013 1 1 544 545 -1 1004 1022 -18 B6 725 N804JB JFK BQN 183 1576 5 45 2013-01-01T10:00:00Z
2013 1 1 554 600 -6 812 837 -25 DL 461 N668DN LGA ATL 116 762 6 0 2013-01-01T11:00:00Z
2013 1 1 554 558 -4 740 728 12 UA 1696 N39463 EWR ORD 150 719 5 58 2013-01-01T10:00:00Z
2013 1 1 555 600 -5 913 854 19 B6 507 N516JB EWR FLL 158 1065 6 0 2013-01-01T11:00:00Z
2013 1 1 557 600 -3 709 723 -14 EV 5708 N829AS LGA IAD 53 229 6 0 2013-01-01T11:00:00Z
2013 1 1 557 600 -3 838 846 -8 B6 79 N593JB JFK MCO 140 944 6 0 2013-01-01T11:00:00Z
2013 1 1 558 600 -2 753 745 8 AA 301 N3ALAA LGA ORD 138 733 6 0 2013-01-01T11:00:00Z
2013 1 1 558 600 -2 849 851 -2 B6 49 N793JB JFK PBI 149 1028 6 0 2013-01-01T11:00:00Z
2013 1 1 558 600 -2 853 856 -3 B6 71 N657JB JFK TPA 158 1005 6 0 2013-01-01T11:00:00Z
2013 1 1 558 600 -2 924 917 7 UA 194 N29129 JFK LAX 345 2475 6 0 2013-01-01T11:00:00Z
2013 1 1 558 600 -2 923 937 -14 UA 1124 N53441 EWR SFO 361 2565 6 0 2013-01-01T11:00:00Z
2013 1 1 559 600 -1 941 910 31 AA 707 N3DUAA LGA DFW 257 1389 6 0 2013-01-01T11:00:00Z
2013 1 1 559 559 0 702 706 -4 B6 1806 N708JB JFK BOS 44 187 5 59 2013-01-01T10:00:00Z
2013 1 1 559 600 -1 854 902 -8 UA 1187 N76515 EWR LAS 337 2227 6 0 2013-01-01T11:00:00Z
2013 1 1 600 600 0 851 858 -7 B6 371 N595JB LGA FLL 152 1076 6 0 2013-01-01T11:00:00Z
2013 1 1 600 600 0 837 825 12 MQ 4650 N542MQ LGA ATL 134 762 6 0 2013-01-01T11:00:00Z
2013 1 1 601 600 1 844 850 -6 B6 343 N644JB EWR PBI 147 1023 6 0 2013-01-01T11:00:00Z
  • airports contient des informations sur les aéroports
faa name lat lon alt tz dst tzone
04G Lansdowne Airport 41.13047 -80.61958 1044 -5 A America/New_York
06A Moton Field Municipal Airport 32.46057 -85.68003 264 -6 A America/Chicago
06C Schaumburg Regional 41.98934 -88.10124 801 -6 A America/Chicago
06N Randall Airport 41.43191 -74.39156 523 -5 A America/New_York
09J Jekyll Island Airport 31.07447 -81.42778 11 -5 A America/New_York
0A9 Elizabethton Municipal Airport 36.37122 -82.17342 1593 -5 A America/New_York
0G6 Williams County Airport 41.46731 -84.50678 730 -5 A America/New_York
0G7 Finger Lakes Regional Airport 42.88356 -76.78123 492 -5 A America/New_York
0P2 Shoestring Aviation Airfield 39.79482 -76.64719 1000 -5 U America/New_York
0S9 Jefferson County Intl 48.05381 -122.81064 108 -8 A America/Los_Angeles
0W3 Harford County Airport 39.56684 -76.20240 409 -5 A America/New_York
10C Galt Field Airport 42.40289 -88.37511 875 -6 U America/Chicago
17G Port Bucyrus-Crawford County Airport 40.78156 -82.97481 1003 -5 A America/New_York
19A Jackson County Airport 34.17586 -83.56160 951 -5 U America/New_York
1A3 Martin Campbell Field Airport 35.01581 -84.34683 1789 -5 A America/New_York
1B9 Mansfield Municipal 42.00013 -71.19677 122 -5 A America/New_York
1C9 Frazier Lake Airpark 54.01333 -124.76833 152 -8 A America/Vancouver
1CS Clow International Airport 41.69597 -88.12923 670 -6 U America/Chicago
1G3 Kent State Airport 41.15139 -81.41511 1134 -5 A America/New_York
1G4 Grand Canyon West Airport 35.89990 -113.81567 4813 -7 A America/Phoenix
  • airlines contient des données sur les compagnies aériennes
carrier name
9E Endeavor Air Inc. 
AA American Airlines Inc. 
AS Alaska Airlines Inc. 
B6 JetBlue Airways
DL Delta Air Lines Inc. 
EV ExpressJet Airlines Inc. 
F9 Frontier Airlines Inc. 
FL AirTran Airways Corporation
HA Hawaiian Airlines Inc. 
MQ Envoy Air
OO SkyWest Airlines Inc. 
UA United Air Lines Inc. 
US US Airways Inc. 
VX Virgin America
WN Southwest Airlines Co. 
YV Mesa Airlines Inc. 

On va charger les trois tables du jeu de données :

library(nycflights13)

Trois objets correspondant aux trois tables ont dû apparaître dans votre environnement.

On charge le package dplyr :

library(dplyr)

8.1 La fonction slice

La fonction 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)

8.2 La fonction filter

La fonction filter sélectionne des lignes d’une table selon une condition. On lui passe en paramètre un test, et seules les lignes pour lesquelles ce test renvoie TRUE 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)

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

8.3 La fonction select

La fonction 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)

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

select(flights, year:day)

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

8.4 La fonction arrange

La fonction 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 :

arrange(flights, dep_delay)

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

arrange(flights, month, dep_delay)

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

arrange(flights, desc(dep_delay))

8.5 Enchaîner les fonctions 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 extraire une sous-population avec filter, sélectionner des colonnes avec select puis trier selon une variable avec arrange, etc.

Quand on veut enchaîner des opérations, on peut le faire de différentes manières. 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é pipe8. 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)

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.

À 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. É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 dans un nouvel objet. L’utilisation du pipe n’est pas obligatoire, mais elle rend les scripts plus lisibles et plus rapides à saisir.

8.6 La fonction 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 fonctions 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)

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

flights %>%
  group_by(month, dest)

8.7 La fonction summarise

La fonction 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),
            nb = n())

summarise dispose d’un opérateur spécial, n(), qui retourne le nombre de lignes du groupe.

help(summarise)

8.8 Autres fonctions utiles de dplyr

8.8.1 La fonction rename

La fonction rename permet de renommer 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 :

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

Même si cela est déconseillé, 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`)

8.8.2 La fonction mutate

La fonction 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 flights contient la durée du vol en minutes. Si on veut créer une nouvelle variable duree_h avec cette durée en heures, on peut faire :

flights <- mutate(flights, duree_h = air_time / 60)
select(flights, air_time, duree_h)

On peut créer plusieurs nouvelles colonnes en une seule commande, et les expressions successives peuvent prendre en compte les résultats des calculs précédents. L’exemple suivant convertit d’abord la durée en heures dans une variable duree_h et la distance en kilomètres dans une variable distance_km, puis utilise ces nouvelles colonnes pour calculer la vitesse en km/h.

flights <- mutate(flights, 
                  duree_h = air_time / 60,
                  distance_km = distance / 0.62137,
                  vitesse = distance_km / duree_h)
select(flights, air_time, duree_h, distance, distance_km, vitesse)

À noter que mutate est évidemment parfaitement compatible avec les fonctions de recodages comme 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"))

8.8.3 La fonction distinct

La fonction 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)

8.8.4 La fonction case_when

La fonction case_when est une généralisation de la fonction if_else(du package dplyr) ou ifelse (du package de base) qui permet d’indiquer plusieurs tests et leurs valeurs associées.

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

select(flights, dep_delay, arr_delay , type_retard)

  1. Le pipe a été introduit à l’origine par le package magrittr, et repris par dplyr↩︎