# 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)
)
)
11 Construire une chaîne de traitement reproductible avec targets
11.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.
Le package targets
permet de construire simplement une chaîne de traitement reproductible.
Les deux éléments suivants sont à prendre en considération :
- ce package ne sera approprié que si la chaîne de traitement est exclusivement écrite en
R
; - il est fortement recommandé de savoir créer des fonctions, afin de modulariser le code.
11.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.
Le guide des bonnes pratiques utilitR
devrait prochainement s’enrichir d’éléments concernant la gestion de pipelines de données en R
et en Python
.
Les premiers éléments du débat sont disponibles sur l’issue #388 dans le dépôt Github
d’utilitR
.
11.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
11.4 Un projet minimal pour comprendre l’essentiel
11.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
└───── ...
Organiser ses fichiers de cette façon est très commun, mais pas indispensable pour l’utilisation de targets
. La seule obligation est que le fichier _targets.R
soit positionné dans le répertoire de travail.
Une manière commode pour un utilisateur souhaitant utiliser targets
est donc de créer un projet RStudio à la racine duquel il place ce fichier. En prévision des futures fonctions qu’il va écrire, il crée un dossier R/
. Le fichier _targets.R
détaille l’enchaînement des traitements. Il doit toujours contenir une instruction chargeant le package targets
.
11.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:
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()
▶ dispatched target csv_file
● completed target csv_file [0.068 seconds]
▶ dispatched 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.
● completed target raw_filosofi_epci [0.19 seconds]
▶ dispatched target grandes_villes
● completed target grandes_villes [0.085 seconds]
▶ dispatched target prop_sup_25k
● completed target prop_sup_25k [0.004 seconds]
▶ completed pipeline [0.644 seconds]
Lorsque la chaîne de traitement est de taille relativement modeste (comme ici), on peut la visualiser avec la fonction tar_visnetwork
:
On obtient bien un diagramme linéaire comme on en avait l’intuition.
Il est tout à fait possible de stocker l’ensemble des cibles intermédiaires dans un emplacement différent du projet. Il s’agit même d’une bonne pratique de séparer le lieu de stockage du code de celui des données.
Il sera nécessaire d’éditer les options de la chaîne dans le fichier _targets.R
. Par exemple avec cette ligne de commande, au début du fichier _targets.R
(mais après l’appel à library(targets)
:
tar_config_set(store = "mon_dossier_donnees/projet-toto")
11.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
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()
✔ skipped target csv_file
✔ skipped target raw_filosofi_epci
▶ dispatched target grandes_villes
● completed target grandes_villes [0.007 seconds]
▶ dispatched target prop_sup_25k
● completed target prop_sup_25k [0.003 seconds]
▶ completed pipeline [0.232 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/
.
11.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
# 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.
Par défaut, les cibles sont stockées au format rds
. Ce format présente deux inconvénients :
- il est spécifique à
R
et ne permet pas de lire les étapes intermédiaires dans un autre langage (par exemplePython
) ; - la sérialisation des objets
R
nécessaire pour écrire sous formatrds
ou lire un tel fichier est assez lente.
Il est conseillé d’utiliser un autre format de stockage des cibles.
En premier lieu, le format par défaut qui peut être utilisé est le format qs. À l’instar du format rds
, celui-ci est spécifique à R
mais présente l’avantage d’être beaucoup plus rapide en termes de temps en lecture/écriture. Pour cela, il convient d’ajouter la ligne suivante au début des options du fichier _targets.R
:
tar_option_set(format = "fst_dt")
Pour les dataframes, il est possible d’utiliser des formats plus universels ou plus appropriés. Les formats à privilégier sont les suivants:
-
parquet
: format qui tend à devenir un standard dans le monde de la science des données. Ce format présente plusieurs avantages, parmi lesquels le fait qu’il est très compressé, très rapide et qu’il conserve les métadonnées du fichier ce qui permet, à la différence des formats type CSV, de conserver l’intégrité des typages des colonnes (voir la fiche Importer des fichiers parquets pour plus de détails) ; -
fst_tbl
(utilisateurs du tidyverse) oufst_dt
(utilisateurs de data.table) : formats spécifiques àR
présentant des avantages proches de ceux d’un fichierparquet
. Ils préservent la nature d’un data.frame, ce qui permet de repartir d’un tibble ou d’un datatable sans avoir à faire de conversion à chaque étape du pipeline.
Le choix du format de stockage d’un objet se fait directement lors de la déclaration de la cible dans _targets.R
:
tar_target(
grandes_villes, garde_grandes_villes(raw_filosofi_epci),
format = "parquet"
)
Dans le dossier _targets/object
, le fichier sera ainsi stocké au format exigé.
Il n’est pas recommandé d’utiliser les formats parquet
, fst_dt
ou fst_tbl
par défaut car ils ne permettent de stocker que des dataframes. Or, un pipeline peut stocker des objets de nature beaucoup plus diverses (listes, objets ggplot, etc.)
L’utilisation du garbage collector peut parfois s’avérer utile pour nettoyer la mémoire de la session R
dans laquelle tourne le pipeline. Ceci est particulièrement utile lorsque les objets manipulés sont volumineux (voir la fiche Superviser sa session R).
Dans targets
, cette opération est possible en ajoutant l’argument garbage_collection = TRUE
à la définition de la cible :
tar_target(
grandes_villes, garde_grandes_villes(raw_filosofi_epci),
garbage_collection = TRUE
)
11.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.
11.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')
)
11.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
11.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.
Les branches statiques, qui nécessitent l’usage du package tarchetypes
, ne sont pas abordées ici.
11.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.
11.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
11.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…
11.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