4.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 document27. Nous vous présentons ici l’un des opérateurs de chaînage du package {svFlow}28 : %>.%. 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.
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 = ~fmean(bmi),
median = ~fmedian(bmi),
number = ~fn(bmi)),
rows = NULL, digits = 1,
col.names = c("Genre", "Moyenne", "Médiane", "Observations"),
caption = "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_25etbiometry_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 = ~fmean(bmi),
median = ~fmedian(bmi),
number = ~fn(bmi))
knitr::kable(biometry_tab, rows = NULL, digits = 1,
col.names = c("Genre", "Moyenne", "Médiane", "Observations"),
caption = "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 |
- La version ci-dessous avec chaînage des opérations est plus lisible29.
biometry %>.%
mutate_(., bmi = ~weight / (height/100)^2) %>.%
filter_(., ~age <= 25) %>.%
group_by_(., ~gender) %>.%
summarise_(.,
mean = ~fmean(bmi),
median = ~fmedian(bmi),
number = ~fn(bmi)) ->
biometry_tab
knitr::kable(biometry_tab, rows = NULL, digits = 1,
col.names = c("Genre", "Moyenne", "Médiane", "Observations"),
caption = "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 !
Nous vous proposons une quatrième façon de présenter votre code à l’aide de la forme “liste à puces” (bullet point en anglais). Le principe consiste à enchaîner les étapes en les précédant d’un pseudo-opérateur liste à puce .=. Cela donne ceci :
biometry_tab <- {
.= biometry
.= mutate_(., bmi = ~weight / (height/100)^2)
.= filter_(., ~age <= 25)
.= group_by_(., ~gender)
.= summarise_(.,
mean = ~fmean(bmi),
median = ~fmedian(bmi),
number = ~fn(bmi))
}
knitr::kable(biometry_tab, rows = NULL, digits = 1,
col.names = c("Genre", "Moyenne", "Médiane", "Observations"),
caption = "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 |
Cette présentation du code donne l’impression d’une liste à puces (les puces étant les .=), encadrées par des accolades { }. Chaque item de la liste est indépendante des autres, contrairement à l’opérateur pipe qui lie les instructions les unes aux autres. Cela facilite le débogage du code.
Le code le plus clair à la lecture est celui avec chaînage des opérations, ou avec liste à puce (selon que vous soyez habitué à l’un ou à l’autre). 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 %->% ou le résultat de la liste à puce avec l’opérateur %<-%. Ces opérateurs d’assignation alternatifs à -> ou <- vont dégrouper les données (s’il y avait un group_by_() dans le pipeline ou la liste à puce). 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.trame ou tbl_bf). Si vous n’avez pas modifié les options de SciViews::R, cette classe sera un data.trame. 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 ou première ligne de la liste à puce.
4.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 pour 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. Ceci est également vrai à l’intérieur d’une liste à puce.
À vous de jouer !
Effectuez maintenant les exercices du tutoriel A04La_wrangling (Traitement des données).
BioDataScience1::run("A04La_wrangling")
Réalisez le travail A04Ia_transformation.
Travail individuel pour les étudiants inscrits au cours de Science des Données Biologiques I : visualisation à l’UMONS à terminer avant le 2025-12-01 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 5.
Réalisez en groupe le travail A04Ga_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 2025-12-16 23:59:59.
Initiez votre projet GitHub Classroom
Voyez les explications dans le fichier README.md, partie I.
Effectuez également des remaniements de données dans votre projet de groupe récurrent.
Réalisez en groupe le travail A02Ga_analysis, partie III.
Travail en groupe de 4 pour les étudiants inscrits au cours de Science des Données Biologiques I : visualisation à l’UMONS à terminer avant le 2025-12-16 23:59:59.
Initiez votre projet GitHub Classroom
Voyez les explications dans le fichier README.md, partie III.
Pour en savoir plus
Présentation en détail du “dot-pipe” assez proche fonctionnellement de
%>.%du package {svFlow}.Section sur le pipe dans “R for Data Science second edition” expliquant l’utilisation du pipe de R de base et de {magrittr}.
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.↩︎Le pakage {svFlow} est préinstallé dans la SciViews Box. Si vous travaillez avec un R installé autrement, vous pouvez facilement ajouter le package {svFlow} avec l’instruction suivante :
install.packages('svFlow', repos = c('https://sciviews.r-universe.dev', 'https://cloud.r-project.org')). Ensuite, vous faiteslibrary(svFlow)pour rendre ses fonctions disponibles.↩︎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.↩︎