5.5 Chaînage des instructions

Le chaînage (ou “pipe” en anglais) permet de combiner une suite d’instructions R. Il permet une représentation facilement lisible et compréhensible d’un traitement décomposé en plusieurs étapes simples de remaniement des données.

Différents opérateurs de chaînage existent dans R. Le Tidyverse et RStudio sont en faveur de l’adoption d’un opérateur de chaînage %>% issu du package {magrittr}. Si nous sommes sensibles au clin d’œil fait ici à un artiste belge bien connu (“ceci n’est pas un pipe”), nous n’adhérons pas à ce choix pour des raisons multiples et plutôt techniques qui n’ont pas leur place dans ce document23. Nous vous présentons ici l’un des opérateurs de chaînage du package {flow} : %>.%. Le jeu de données sur la biométrie humaine est employé pour cette démonstration qui va comparer le remaniement d’un tableau de données avec et sans l’utilisation du chaînage.

biometry <- read("biometry", package = "BioDataScience", lang = "fr")

Vous vous intéressez à l’indice de masse corporelle ou IMC (BMI en anglais) des individus de moins de 25 ans. Vous souhaitez représenter la moyenne, la médiane et le nombre d’observations de manière séparée pour les hommes et les femmes. Pour obtenir ces résultats vous devez :

  • calculer le BMI,
  • filtrer le tableau pour ne retenir que les individus de moins de 25 ans,
  • résumer les données afin d’obtenir la moyenne et la médiane par genre,
  • afficher un tableau de données avec ces résultats.

Il est très clair ici que le traitement peut être décomposé en étapes plus simples. Cela apparaît naturellement rien que dans la description de ce qui doit être fait. Sans l’utilisation de l’opérateur de chaînage, deux approches sont possibles :

  • Imbriquer les instructions les unes dans les autres (très difficile à lire et à déboguer) :
knitr::kable(
  summarise(
    group_by(
      filter(
        mutate(biometry, bmi = weight / (height/100)^2),
        age <= 25),
      gender),
    mean   = mean(bmi), 
    median = median(bmi),
    number = n()),
  rows = NULL,  digits = 1,
  col = c("Genre", "Moyenne", "Médiane", "Observations"),
  caption = "IMC d'hommes (M) et femmes (W) de 25 ans maximum."
)
Tableau 5.10: IMC d’hommes (M) et femmes (W) de 25 ans maximum.
Genre Moyenne Médiane Observations
M 22.3 22.1 97
W 21.8 21.0 94
  • Passer par des variables intermédiaires (biometry_25 et biometry_tab). Les instructions sont plus lisibles, mais les variables intermédiaires “polluent” inutilement l’environnement de travail (en tout cas, si elles ne servent plus par après) :
biometry <- mutate(biometry, bmi = weight / (height/100)^2)
biometry_25 <- filter(biometry, age <= 25)
biometry_25 <- group_by(biometry_25, gender)
biometry_tab <- summarise(biometry_25,
  mean   = mean(bmi), 
  median = median(bmi),
  number = n())
knitr::kable(biometry_tab, rows = NULL, digits = 1,
  col = c("Genre", "Moyenne", "Médiane", "Observations"),
  caption = "IMC d'hommes (M) et femmes (W) de 25 ans maximum.")
Tableau 5.11: IMC d’hommes (M) et femmes (W) de 25 ans maximum.
Genre Moyenne Médiane Observations
M 22.3 22.1 97
W 21.8 21.0 94
  • Des trois approches, la version ci-dessous avec chaînage des opérations est la plus lisible et la plus pratique24.
biometry %>.%
  mutate(., bmi = weight / (height/100)^2) %>.%
  filter(., age <= 25) %>.%
  group_by(., gender) %>.%
  summarise(.,
    mean   = mean(bmi),
    median = median(bmi),
    number = n()) %>.%
  knitr::kable(., rows = NULL, digits = 1, 
    col = c("Genre", "Moyenne", "Médiane", "Observations"),
    caption = "IMC d'hommes (M) et femmes (W) de 25 ans maximum.")
Tableau 5.12: IMC d’hommes (M) et femmes (W) de 25 ans maximum.
Genre Moyenne Médiane Observations
M 22.3 22.1 97
W 21.8 21.0 94

Le pipe %>.% injecte le résultat précédent dans l’instruction suivante à travers l’objet . Ainsi, en seconde ligne mutate(.), . se réfère à biometry. A la ligne suivante, filter(.), le . se réfère au résultat issu de l’opération mutate(), et ainsi de suite. La logique d’enchaînement des opérations sur le résultat, à chaque fois, du calcul précédent est donc le fondement de cet opérateur “pipe”.

Le pipe permet d’éviter de répéter le nom des objets (version avec variables intermédiaires), ce qui alourdit inutilement le code et le rend moins agréable à la lecture. L’imbrication des fonctions dans la première version est catastrophique pour la compréhension du code car les arguments des fonctions de plus haut niveau sont repoussés loin. Par exemple, l’argument de l’appel à group_by() (gender) se retrouve quatre lignes plus loin. Et encore, nous avons pris soin d’indenter le code pour repérer sur un plan vertical qui appartient à qui, mais imaginez ce que cela donne si l’instruction est mise à plat sur une seule ligne ! Le code le plus clair à la lecture est définitivement celui avec chaînage des opérations. Or, un code plus lisible est plus compréhensible… et donc, moins bogué.

À vous de jouer !

Effectuez maintenant les exercices du tutoriel A05La_traitement (Traitement des données I).

BioDataScience1::run("A05La_traitement")

Réalisez l’assignation A05Ga_transformation.

Si vous êtes un utilisateur non enregistré ou que vous travaillez en dehors d’un cours, faites un “fork” de ce dépôt.

Voyez les explications dans le fichier README.md.

Pour en savoir plus

  1. Le lecteur intéressé pourra lire les différents articles suivants : more pipes in R, y compris les liens qui s’y trouvent, permet de se faire une idée de la diversité des opérateurs de chaînage dans R et de leur historique. Dot pipe présente l’opérateur %.>% du package {wrapr} très proche du nôtre et in praise of syntactic sugar explique ses avantages. Nous partageons l’idée que le “pipe de base” ne devrait pas modifier l’instruction de droite contrairement à ce que fait %>% de {magrittr}, et notre opérateur %>.% va en outre plus loin encore que %.>% dans la facilité de débogage du code chaîne.↩︎

  2. Le chaînage n’est cependant pas forcément plus facile à déboguer que la version avec variables intermédiaires. Le package {flow} propose la fonction debug_flow() à appeler directement après un plantage pour inspecter la dernière instruction qui a causé l’erreur, voir ?debug_flow.↩︎