9 Construire une chaîne de traitement reproductible avec targets
9.1 Tâches concernées et recommandations
L’utilisateur souhaite automatiser une chaîne de traitement complexe afin de la rendre reproductible et rapide à exécuter en cas de modification.
9.2 Pourquoi utiliser targets
?
Le package targets
peut être particulièrement intéressant :
- dans le cadre du développement d’un prototype ayant vocation à devenir une chaîne de production pérenne écrite avec
R
; - dans le cas d’un projet d’étude qui vise à une forte reproductibilité.
Plus précisément, utiliser targets
pour un projet permet de :
- Viser la reproductibilité de l’ensemble des étapes de traitement, tout en réduisant au strict nécessaire la répétition de ces étapes, parfois longues ;
- Adopter des bonnes pratiques de développement en
R
par l’usage (modulariser le code, décomposer ses traitements par étapes, assurer la lisibilité des étapes successives du traitement…) - Représenter sous forme de pipeline les étapes de sa chaîne de traitement et leurs dépendances à partir d’une technique de graphiques directionnels asynchrones (appelés DAG pour l’acronyme anglais dans la sphère informatique)
- Faciliter la prise en main par une autre personne grâce à une organisation standardisée des codes et à une description complète de l’enchaînement des étapes intégrée dans le code lui-même.
9.3 Quelles sont les tâches automatisées par targets
?
targets
permet de définir et d’exécuter une chaîne de traitement avec :
- Sauvegarde automatique de résultats intermédiaires, ce qu’on appelle les “targets” (cibles)
- Traçabilité de ces résultats intermédiaires par
targets
: lors de la répétition d’une exécution de la chaîne de traitement, ils ne sont mobilisés que si ils sont reproductibles. - Si une fonction ou un input nécessaire au calcul d’une “target” est modifié,
targets
repère automatiquement les étapes à reconduire, et seulement celles-ci.
Ainsi, le lancement du traitement et la vérification de la reproductibilité sont effectués ensemble au cours du développement du projet par l’appel de tar_make()
.
Vérifier la reproductibilité revient ainsi à ne pas ‘tout relancer’ de 0 ! Ceci représenterait un coût trop élevé. targets
automatise le travail d’aller-retour dans les étapes d’une étude ou de prototypage (j’ai modifié l’étape 1, il faut donc que je relance l’étape 2 qui en dépend…), en construisant un graphe des dépendances des différentes étapes du traitement.
Pour la suite de la fiche, prenons l’exemple d’une étude qui se structurerait suivant les étapes suivantes :
- Charger les données
- Traiter les données
- Produire des résultats
- Représenter des résultats
9.4 Un projet minimal pour comprendre l’essentiel
9.4.1 Structure du projet
Un projet targets
est un projet R
en règle générale structuré de la sorte :
- un fichier
_targets.R
décrivant les éléments de configuration (par exemple packages utilisés) et l’enchaînement des traitements - un dossier
R
comprenant les scripts définissant les fonctions utilisées par le projet - un dossier
data
pour les données externes (non générées au cours du projet)
L’architecture des dossiers du projet ressemble par conséquent à ceci :
├── _targets.R
├── R/
├───── mesfonctions_pour_faire_ceci.R
├───── mesfonctions_pour_faire_cela.R
├──── ...
├── data/
├───── donnees_entrees.csv
└───── ...
9.4.2 Premier exemple
Partons d’un exemple simple :
- on lit les données de population depuis un fichier CSV ;
- on a créé une fonction pour ne garder que les communes de plus de 200 000 habitants ;
- sur ces communes, on désire connaître la proportion dont le revenu médian est supérieur à 25 000 euros.
La chaîne de traitement est donc ici linéaire. Chaque étape dépend de la précédente et uniquement de celle-ci. Le fichier d’instruction _targets
prendra alors la forme suivante:
# fichier _targets.R
library(targets)
tar_option_set(packages = c("dplyr", "readr"))
source("mesfonctions_pour_faire_ceci.R", encoding = "utf-8")
# on crée un fichier à partir d'un des jeux d'exemples
raw_file_path <- "data/donnes_entrees.csv"
dir.create("data")
readr::write_csv(doremifasolData::filosofi_com_2016, raw_file_path)
list(
tar_target(csv_file, raw_file_path, format = "file"),
tar_target(
raw_filosofi_epci, readr::read_csv(csv_file),
),
tar_target(
grandes_villes, garde_grandes_villes(raw_filosofi_epci)
),
tar_target(
prop_sup_25k, grandes_villes %>% dplyr::summarise(mean(MED16 > 25000)*100)
)
)
Les fonctions écrites par l’analyste et utilisées dans la chaîne de traitement (en l’occurrence garde_grandes_villes
) sont contenues dans les fichiers que l’on “source” au départ, ici depuis un script "mesfonctions_pour_faire_ceci.R
.
Les packages utilisés dans les traitements sont définis via la fonction tar_option_set
du package targets
. Ici, on a besoin des packages dplyr
et readr
dans notre chaîne de traitement.
La chaîne de traitement est représentée par une liste de tar_target
, soit les objets R
qui sont les cibles intermédiaires de l’analyse. Ils sont le résultat de l’application à une cible précédente d’une fonction pour obtenir la cible suivante :
- Ici la première cible est particulière (
format = file
) : on spécifie où sont les données d’entrée afin de surveiller si elles changent. - La seconde prend en entrée la première cible
data_file
et la transforme en appliquant la fonctionreadr::read_csv
en un nouvel objet R,raw_filosofi_epci
. Il s’agit ainsi des données brutes après l’import dansR
, avant toute modification - La troisième applique cette fois une fonction écrite par l’utilisateur à
raw_filosofi_epci
pour obtenirgrandes_villes
, et ainsi de suite…
Ainsi, le fichier _targets.R
contient la description de l’ensemble des étapes du traitement. La complexité des traitements est résumée de façon concise par un ensemble minimal de fonctions résumant les grandes étapes. Afin de faire tourner l’analyse, l’utilisateur fait appel au sein du projet à la fonction tar_make()
. Il s’agit de la fonction qu’un utilisateur du package targets
utilisera le plus fréquemment. L’utilisateur est informé de l’évolution des calculs.
tar_make()
• start target csv_file
• built target csv_file [0.093 seconds]
• start target raw_filosofi_epci
Rows: 34932 Columns: 29
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): CODGEO, LIBGEO
dbl (27): NBMENFISC16, NBPERSMENFISC16, MED16, PIMP16, TP6016, TP60AGE116, T...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
• built target raw_filosofi_epci [0.215 seconds]
• start target grandes_villes
• built target grandes_villes [0.01 seconds]
• start target prop_sup_25k
• built target prop_sup_25k [0.004 seconds]
• end pipeline [0.665 seconds]
Lorsque la chaîne de traitement est de taille relativement modeste (comme ici), on peut la visualiser avec la fonction tar_visnetwork
:
Warning message:
In dir.create("data") : 'data' already exists
On obtient bien un diagramme linéaire comme on en avait l’intuition.
9.4.3 Modification d’une étape intermédiaire
L’utilisateur décide ensuite de modifier la définition des grandes villes considérées. Supposons qu’il ajoute un argument à la fonction garde_grandes_villes
pour ne garder que celles dont la population est supérieure à seuil
. Dans le fichier _targets.R
, il est nécessaire de changer la définition de l’étape de définition de grandes_villes
. Cela amènera à une chaîne ayant la structure suivante
# fichier _targets.R
library(targets)
tar_option_set(packages = c("dplyr", "readr"))
source("mesfonctions_pour_faire_ceci.R", encoding = "utf-8")
# on crée un fichier à partir d'un des jeux d'exemples
raw_file_path <- "data/donnes_entrees.csv"
dir.create("data")
readr::write_csv(doremifasolData::filosofi_com_2016, raw_file_path)
list(
tar_target(csv_file, raw_file_path, format = "file"),
tar_target(
raw_filosofi_epci, readr::read_csv(csv_file),
),
tar_target(
grandes_villes, garde_grandes_villes(raw_filosofi_epci, seuil = 10000)
),
tar_target(
prop_sup_25k, grandes_villes %>% dplyr::summarise(mean(MED16 > 25000)*100)
)
)
Ici, le pipeline est de taille relativement modeste et il est facile d’identifier la source de modification. Néanmoins, la représentation sous forme de diagramme peut aider à mieux s’en rendre compte
Warning message:
In dir.create("data") : 'data' already exists
La modification de la fonction garde_grandes_villes
entraîne la nécessaire mise à jour de grandes_villes
et toutes les cibles qui en dépendent, mais pas du début de la chaîne de traitement !
targets
va ainsi intelligemment utiliser ceci pour minimiser le temps nécessaire pour mettre à jour l’ensemble de la chaîne de traitement
tar_make()
✔ skip target csv_file
✔ skip target raw_filosofi_epci
• start target grandes_villes
• built target grandes_villes [0.009 seconds]
• start target prop_sup_25k
• built target prop_sup_25k [0.004 seconds]
• end pipeline [0.274 seconds]
Warning message:
In dir.create("data") : 'data' already exists
Les cibles définies sont calculées successivement, stockées et mises à jour automatiquement dans un dossier _targets/objects/
.
9.4.4 Accéder à des éléments du pipeline dans une session R
On peut facilement accéder à un objet cible, quel que soit son emplacement dans la chaîne de traitement, puisque chaque cible est stockée sous la forme d’un fichier temporaire.
La fonction tar_load
permet de charger dans l’environnement R
l’objet en question. Par exemple, si on désire tester des choses sur grandes_villes
, on pourra utiliser la commande suivante
tar_load(grandes_villes)
Warning in system("timedatectl", intern = TRUE): running command 'timedatectl'
had status 1
head(grandes_villes)
# A tibble: 6 × 29
CODGEO LIBGEO NBMENFISC16 NBPERSMENFISC16 MED16 PIMP16 TP6016 TP60AGE116
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 01004 Ambérieu-e… 6363 14228 19721 49 17 19
2 01033 Valserhône 6472 15255 21405. 45 16 18
3 01053 Bourg-en-B… 18601 38014. 18249. 46 22 27
4 01173 Gex 4894 11276. 32304. 60 11 NA
5 01283 Oyonnax 9248 22444. 16948. 40 25 31
6 02168 Château-Th… 6805 15070. 17643. 43 24 33
# ℹ 21 more variables: TP60AGE216 <dbl>, TP60AGE316 <dbl>, TP60AGE416 <dbl>,
# TP60AGE516 <dbl>, TP60AGE616 <dbl>, TP60TOL116 <dbl>, TP60TOL216 <dbl>,
# PACT16 <dbl>, PTSA16 <dbl>, PCHO16 <dbl>, PBEN16 <dbl>, PPEN16 <dbl>,
# PPAT16 <dbl>, PPSOC16 <dbl>, PPFAM16 <dbl>, PPMINI16 <dbl>, PPLOGT16 <dbl>,
# PIMPOT16 <dbl>, D116 <dbl>, D916 <dbl>, RD16 <dbl>
Cela permettra à l’utilisateur de targets
de prototyper une nouvelle étape de traitement dans sa session R
puis, une fois satisfait, la mettre en production en mettant les fonctions dans le fichier XXXXX.R
et en créant l’étape tar_target
adéquate.
9.5 Intégrer un rapport en Rmarkdown
L’un des principaux gains à utiliser targets
est dans la fiabilisation du processus de production de fichiers markdown à l’issue d’une chaîne de traitement.
Deux philosophies existent pour produire un fichier reproductible dans une chaîne de traitement :
- Intégrer directement le fichier à la chaîne comme une étape finale du processus de production. Cela revient à produire le
RMarkdown
via untar_target
particulier ; - Exécuter la chaîne de traitement, ou les parties nouvelles de la chaîne de traitement, directement depuis le fichier
RMarkdown
. Dans ce cas, le fichier.Rmd
n’est plus exécuté depuis le_targets.R
mais au contraire sert à l’exécuter.
9.5.1 Concevoir un rapport en sortie de chaîne de traitement
Le package tarchetypes
est un complément utile. Ce package permet d’intégrer simplement des rapports Rmarkdown
dans la pipeline avec tarchetypes::tar_render()
. L’essentiel des calculs doit être en amont du rapport markdown, qui doit être rapide à exécuter.
Par exemple, on peut écrire un Rmarkdown report.Rmd
considéré comme une des cibles de l’analyse (par exemple, c’est le compte-rendu de l’analyse), et qui dépend d’autres cibles. On souhaite également qu’il soit reproductible, et mis à jour automatiquement en fonction des modifications sur les cibles dont il dépend.
Il suffit d’intégrer ces cibles via tar_read(data)
ou tar_load(data)
appelé dans un chunk du .Rmd
, et de spécifier un _targets.R
sur le modèle suivant :
# Fichier _targets.R
# report.Rmd est présent dans le projet.
library(targets)
library(tarchetypes)
list(
tar_target(data, data.frame(a = seq(2,9), b = seq(2,9))),
tar_render(report, path = 'report.Rmd')
)
9.5.2 Utiliser des objets issus d’une chaîne de traitement dans un R Markdown
Cette méthode est particulièrement appropriée lorsqu’on désire prototyper un rapport en utilisant un ou plusieurs objets de la chaîne de traitement.
Plus d’éléments sont disponibles dans la documentation officielle
9.6 Les branches
Souvent, les cibles d’une analyse (étapes intermédiaires) sont nombreuses et ont un certain degré de redondance.
Comment créer des cibles automatiquement (sans écrire explicitement dans _targets.R
chacune d’entre elles) ? targets
propose de décliner les cibles en “branches”.
On distingue :
- les branches définies dynamiquement : avant l’exécution, le nombre de branches est inconnu ;
- les branches définies statiquement : le nombre de branche est défini précisément avant l’exécution.
Le premier cas correspond à la répétition d’un grand nombre de tâches homogènes, le second plutôt à un petit nombre de tâches hétérogènes.
9.6.1 Les branches dynamiques
Certaines cibles peuvent être le résultat de l’application d’une même fonction à des variantes d’arguments (par exemple, un graphique de restitution pour plusieurs populations d’intérêt).
Pour cela, targets
propose les branches dynamiques.
9.6.2 Un exemple
Voici un exemple minimal de pipeline qui va itérer sur N couples d’arguments une même “simulation”, en évitant de créer N cibles distinctes pour les N résultats, et plutôt créer une seule cible résultats qui donnera lieu à autant de branches que de “simulations” :
#_targets.R
library(targets)
simulation <- function(x, y) x * y
list(
tar_target(x, c(10, 20, 30)),
tar_target(y, c(1, 2, 3)),
tar_target(
resultat,
data.frame(argument_1 = x, argument_2 = y, res = simulation(x, y)),
pattern = map(x, y))
)
Ce qui distingue ici la cible resultat
de ce qui a été vu précédemment, c’est l’utilisation de l’argument pattern
, qui a vocation à itérer sur les vecteurs cibles x
et y
grâce à map
.
Dans la console R
, l’utilisateur qui fait appel à tar_make()
voit apparaître la déclinaison de resultat
en trois branches, exécutées en parallèle.
tar_make()
● run target x
● run target y
● run branch resultat_1851c9ee
● run branch resultat_445bc859
● run branch resultat_1a0263ff
● end pipeline
On obtient le résultat suivant:
tar_read(resultat)
argument_1 argument_2 res
1 10 1 10
2 20 2 40
3 30 3 90
9.6.3 Itérer, croiser les arguments pour créer des branches
Les patterns peuvent être de plusieurs types : map
(itérer sur les arguments ligne à ligne), cross
(produit cartésien des arguments), head
(pour récupérer les premiers arguments), select
(pour récupérer certains arguments) …
Par exemple, remplacer map
par cross
dans la pipeline précédente donne lieu après un tar_make()
à
✓ skip target x
✓ skip target y
✓ skip branch resultat_1851c9ee
● run branch resultat_cca1045b
● run branch resultat_3b73d14e
● run branch resultat_fe2f6b6a
✓ skip branch resultat_66951ce8
● run branch resultat_ff612dde
● run branch resultat_d0a65303
● run branch resultat_0a18e8b1
✓ skip branch resultat_7fd56d9a
● end pipeline
Plutôt que d’appliquer la fonction simulation itérativement aux couples d’x
et y
(3 branches), la fonction est appliquée au produit cartésien de x
et y
(3 x 3 branches). On remarque d’ailleurs que targets
a compris que cela ne changeait pas certains résultats précédents (3 branches strictement identiques, qui ne sont pas recalculées).
tar_read(resultat)
argument_1 argument_2 res
1 10 1 10
2 10 2 20
3 10 3 30
4 20 1 20
5 20 2 40
6 20 3 60
7 30 1 30
8 30 2 60
9 30 3 90
Les pattern peuvent être combinés, avec par exemple pattern = cross(x, map(y, z))
.
#_targets.R
library(targets)
simulation <- function(x, y, z) x * y + z
list(
tar_target(x, c(10, 20, 30)),
tar_target(y, c(1, 2, 3)),
tar_target(z, c(2, 4, 6)),
tar_target(
resultat,
data.frame(argument_1 = x, argument_2 = y, argument_3 = z, res = simulation(x, y, z)),
pattern = cross(x, map(y, z)))
)
qui donne le résultat :
argument_1 argument_2 argument_3 res
1 10 1 2 12
2 10 2 4 24
3 10 3 6 36
4 20 1 2 22
5 20 2 4 44
6 20 3 6 66
7 30 1 2 32
8 30 2 4 64
9 30 3 6 96
Si l’on souhaite itérer sur des listes, plutôt que sur des vecteurs, on peut spécifier à la création de la cible qui sert d’argument aux branches, par exemple une liste de data.frames
, que l’on veut itérer sur les éléments "list"
.
#_targets.R
library(targets)
#' Multiplie la colonne "a" de df par un facteur
#' @param: df: data.frame
#' @param: factor: int
multiply <- function(df, factor){
df$a <- df$a * factor
df
}
list(
tar_target(x, list(data.frame(name = c('Marie','Marwan'), a = c(1, 2)),
data.frame(name = c('Bill','Boule'), a = c(2, 4))), iteration = 'list'),
tar_target(y, c(2, 3)),
tar_target(
resultat,
multiply(x, y),
pattern = map(x, y))
)
Etc…
9.7 Pour en savoir plus
- Manuel d’utilisation de
targets
-
Organiser un projet avec
targets
, une chapitre de Introduction à R et au tidyverse de Julien Barnier - High Performance Computing avec
targets
- Landau, W. M., (2021). The targets R package: a dynamic Make-like function-oriented pipeline toolkit for reproducibility and high-performance computing. Journal of Open Source Software, 6(57), 2959, https://doi.org/10.21105/joss.02959
- Vidéo de présentation de
targets
par Will Landau au meetup R Lille de juin 2021 - https://cran.r-project.org/web/packages/targets/targets.pdf
- https://docs.ropensci.org/tarchetypes/
- Un exemple https://github.com/InseeFrLab/lockdown-maps-R/
- Les “target factories”: https://wlandau.github.io/targetopia/contributing.html
- Un tutoriel de Noam Ross présentant l’usage de
targets
avec un système de stockage de type AWS (similaire au principe duSSPCloud
): https://github.com/noamross/targets-minio-versioning