5.5 Chaînage des instructions

Le chaînage (ou “pipe” en anglais, prononcez “païpe”) 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 a introduit 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 {svFlow} : %>.%. 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(
  ssummarise(
    sgroup_by(
      sfilter(
        smutate(biometry, bmi = weight / (height/100)^2),
        age <= 25),
      gender),
    mean   = fmean(bmi), 
    median = fmedian(bmi),
    number = fn(bmi)),
  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.8: 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 <- smutate(biometry, bmi = weight / (height/100)^2)
biometry_25 <- sfilter(biometry, age <= 25)
biometry_25 <- sgroup_by(biometry_25, gender)
biometry_tab <- ssummarise(biometry_25,
  mean   = fmean(bmi), 
  median = fmedian(bmi),
  number = fn(bmi))
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.9: 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 %>.%
  smutate(., bmi = weight / (height/100)^2) %>.%
  sfilter(., age <= 25) %>.%
  sgroup_by(., gender) %>.%
  ssummarise(.,
    mean   = fmean(bmi),
    median = fmedian(bmi),
    number = fn(bmi)) ->
  biometry_tab
  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.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

Le pipe %>.% injecte le résultat précédent dans l’instruction suivante à travers l’objet . Ainsi, en seconde ligne smutate(.), . se réfère à biometry. A la ligne suivante, sfilter(.), le . se réfère au résultat issu de l’opération smutate(), 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 à sgroup_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 pouvez aussi assigner le résultat de la dernière étape de votre pipeline avec l’opérateur alternatif pointant à droite %->% qui va dégrouper les données (s’il y avait un (s)group_by() dans le pipeline). Cela évite des problèmes plus tard, si on oublie que le regroupement est là ! Il va aussi collecter les résultats du pipeline, c’est-à-dire effectuer l’équivalent de collect_dtx(), en un tableau dans sa forme par défaut (classe data.frame, data.table ou tbl_bf). Si vous n’avez pas modifié les options de SciViews::R, cette classe sera un data.table. Enfin, indiquez bien le nom de l’objet final comme ici biometry_tab à la ligne pour le mettre en évidence et le placer comme dernière étape du pipeline.

5.5.1 Opérateur pipe de base ou léger |>

L’opérateur de base de R à partir de sa version 4.1 est |>. Il est plus limité que %>.% et se contente d’injecter l’expression de gauche comme premier argument dans l’expression de droite. Donc, x |> log(base = 3) est strictement équivalent à log(x, base = 3). Cet opérateur est donc cosmétique afin de présenter du code plus complexe de manière plus lisible. Notez bien que contrairement à %>.%, vous ne devez pas et même ne pouvez pas préciser où l’expression de gauche est injectée à l’aide du point .. Ainsi, x |> log(., base = 3) sera une erreur.

En SciViews::R nous utilisons %>.% pour relier les différentes étapes d’un pipeline complexe et bien indiquer qu’il s’agit d’une étape suivante. Le %>.% peut se lire à haute voix “… et ensuite…”. L”opérateur pipe léger |> s’utilise éventuellement au sein d’un même étape pour réarranger le code de manière plus lisible.

À vous de jouer !

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

BioDataScience1::run("A05La_wrangling")

Réalisez le travail A05Ia_transformation.

Travail individuel pour les étudiants inscrits au cours de Science des Données Biologiques I : visualisation à l’UMONS à terminer avant le 2023-12-04 23:59:59.

Initiez votre projet GitHub Classroom

Voyez les explications dans le fichier README.md.

Vous allez maintenant planifier la récolte et collecter des données relatives à l’étude de l’obésité afin de préparer le travail du module 6.

Réalisez en groupe le travail A05Ga_biometry, partie I.

Travail en groupe de 4 pour les étudiants inscrits au cours de Science des Données Biologiques I : visualisation à l’UMONS à terminer avant le 2023-12-19 23:59:59.

Initiez votre projet GitHub Classroom

Voyez les explications dans le fichier README.md, partie I.

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 {svFlow} 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.↩︎