-
Notifications
You must be signed in to change notification settings - Fork 19
/
21-targets.qmd
451 lines (307 loc) · 19.2 KB
/
21-targets.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# Organiser un projet avec `targets` {#sec-targets}
{{< include _setup.qmd >}}
[targets](https://docs.ropensci.org/targets/) est une extension développée par [Will Landau](https://twitter.com/wmlandau) qui permet d'organiser un projet sous la forme d'un *pipeline* de traitements, composé de différentes étapes, et gérant automatiquement les dépendances entre celles-ci^[Pour les personnes habituées au développement, il s'agit d'un équivalent à [GNU Make](https://www.gnu.org/software/make/) pour R.]. Cette organisation a plusieurs avantages :
- elle permet une description de toutes les étapes du pipeline dans un fichier dédié, et force à séparer ces différentes étapes dans des fonctions à part, ce qui facilite la lisibilité et la maintenance du projet
- elle facilite la reproductibilité des traitements, car elle garantit que toutes les étapes ont bien été effectuées dans le bon ordre et dans un nouvel environnement
- elle optimise les temps de calcul, car en cas de modification seules les étapes qui le nécessitent sont relancées
L'utilisation de `targets` dans des petits projets peut être vue comme une complexité supplémentaire pas toujours très utile, mais elle peut être très bénéfique pour des projets plus complexes ou comportant des temps de calculs importants à certaines étapes.
`targets` fait partie de l'initiative [rOpenSci](https://ropensci.org/).
## Définition du pipeline
```{r, message=FALSE, warning=FALSE, echo=FALSE, cache=FALSE, eval=TRUE}
library(targets)
# On se place à la racine du projet d'exemple
knitr::opts_knit$set(root.dir = "resources/targets_sample_project/")
options(crayon.enabled = FALSE)
```
```{r cache=FALSE, echo=FALSE, include=FALSE}
# On supprime le cache
targets::tar_destroy("local")
```
### Projet d'exemple
```{r cache=FALSE, echo=FALSE, include=FALSE}
targets_content <- readLines("_targets.R", encoding = "UTF-8")
file.rename("_targets.R", "_targets.R.orig")
targets_content <- targets_content %>%
discard(~str_detect(.x, "# Génération rapport")) %>%
discard(~str_detect(.x, "tar_render")) %>%
str_replace_all(fixed("calcule_evo(min_n = 1000),"), "calcule_evo(min_n = 1000)")
writeLines(targets_content, "_targets.R")
calculs_content <- readLines("R/fonctions_calculs.R", encoding = "UTF-8")
file.rename("R/fonctions_calculs.R", "R/fonctions_calculs.R.orig")
calculs_content <- calculs_content %>%
str_replace_all(fixed("round(evo / `2019` * 100, 1)"), "round(evo / `2019` * 100, 2)")
writeLines(calculs_content, "R/fonctions_calculs.R")
```
On part d'un projet très simple : à partir du [fichier national des prénoms donnés à la naissance](https://www.insee.fr/fr/statistiques/2540004), diffusé par l'INSEE, on souhaite produire un document indiquant les prénoms ayant les évolutions les plus fortes (à la hausse ou à la baisse) entre 2019 et 2020.
Le dossier de notre projet s'organise de la manière suivante :
```
data/
└── nat2020.csv
R/
├── fonctions_recode.R
└── fonctions_calculs.R
_targets.R
```
::: {.callout-note}
À noter que `targets` n'impose aucune structure de projet particulière en-dehors de la présence du fichier `_targets.R`. On aurait donc pu avoir une organisation tout à fait différente.
:::
Le fichier `data/nat2020.csv` contient les données brutes [téléchargées depuis le site de l'INSEE](https://www.insee.fr/fr/statistiques/2540004).
Le fichier `R/fonctions_recode.R` contient deux fonctions de traitement et de remise en forme des données.
```{r eval=FALSE, code=readLines('R/fonctions_recode.R')}
```
Le fichier `R/fonctions_calculs.R` contient une seule fonction qui calcule les variables d'évolution 2019-2020.
```{r eval=FALSE, cache=FALSE, code=readLines('R/fonctions_calculs.R')}
```
### `_targets.R`
C'est dans le fichier `_targets.R`, situé à la racine du dossier, qu'on va définir le pipeline constitué de toutes les étapes de notre traitement : chargement et manipulation des données, calculs, génération de rapports, etc. Ces étapes sont également appelées *cibles* (*targets*).
::: {.callout-note}
La syntaxe présentée ici est celle proposée par l'extension `tarchetypes`, qui est un peu plus facile à prendre en main et plus lisible que la syntaxe native de `targets`.
:::
Le fichier `_targets.R` commence par charger à la fois `targets` et `tarchetypes`.
```{r eval=FALSE}
# Packages nécessaires pour ce script
library(targets)
library(tarchetypes)
```
On va ensuite utiliser `source()` pour charger le contenu des deux fichiers `R/fonctions_recode.R` et `R/fonctions_calculs.R`, et pouvoir utiliser par la suite les fonctions qu'ils définissent.
```{r eval=FALSE}
# Chargement des fonctions
source("R/fonctions_recode.R")
source("R/fonctions_calculs.R")
```
On définit ensuite des options globales pour le pipeline. L'option `packages` de `tar_option_set()`, permet de spécifier une liste d'extensions à charger systématiquement avant le lancement de chaque étape. Ici on s'assure que l'extension `tidyverse` est bien chargée et disponible, et on positionne l'option `tidyverse.quiet` à `TRUE` pour supprimer le message qu'elle affiche systématiquement au chargement.
```{r eval=FALSE}
# Options pour les différentes étapes
options(tidyverse.quiet = TRUE)
tar_option_set(packages = "tidyverse")
```
Vient enfin la définition du pipeline proprement dit. Celle-ci se fait via la fonction `tar_plan()` de `tarchetypes`.
```{r eval=FALSE}
tar_plan(
)
```
La première opération que l'on souhaite effectuer est de charger les données contenues dans `data/nat2020.csv`. Pour cela on va d'abord créer une première étape qui consiste à *référencer* notre fichier CSV à l'aide de la fonction `tar_file()`.
```{r eval=FALSE}
tar_plan(
# Chargement du fichier CSV
tar_file(csv_file, "data/nat2020.csv")
)
```
Cette première étape définit une *cible* (*target*), nommée `csv_file`, qui pointe vers notre fichier CSV.
On ajoute une seconde étape qui charge les données à l'aide de `read_csv2()`.
```{r eval=FALSE}
tar_plan(
# Chargement du fichier CSV
tar_file(csv_file, "data/nat2020.csv"),
donnees_brutes = read_csv2(csv_file),
)
```
Cette nouvelle étape définit une deuxième cible nommée `donnees_brutes`. Cette cible correspond au nom d'une étape, mais aussi à un objet : dans ce qui suit, `donnees_brutes` correspond au tableau de données résultat du `read_csv2()`.
On va utiliser cet objet `donnees_brutes` dans une troisième étape nommée `donnees` qui lui applique les deux fonctions de filtrage et transformation définies dans `R/fonctions_recode.R`.
```{r eval=FALSE}
tar_plan(
# Chargement du fichier CSV
tar_file(csv_file, "data/nat2020.csv"),
donnees_brutes = read_csv2(csv_file),
# Mise en forme des données
donnees = donnees_brutes %>%
filter_data() %>%
pivot_2019_2020()
)
```
Ici aussi, `donnees` est à la fois le nom d'une cible et un objet contenant nos données retravaillées. On utilise cet objet dans une étape supplémentaire qui utilise la fonction de `R/fonctions_calculs.R` pour calculer les variables d'évolution.
```{r eval=FALSE}
tar_plan(
# Chargement du fichier CSV
tar_file(csv_file, "data/nat2020.csv"),
donnees_brutes = read_csv2(csv_file),
# Mise en forme des données
donnees = donnees_brutes %>%
filter_data() %>%
pivot_2019_2020(),
# Calcul indicateurs
donnees_evo = donnees %>%
calcule_evo(min_n = 1000)
)
```
::: {.callout-warning}
On notera que les cibles doivent toutes avoir des noms différents. Si on exécute plusieurs étapes de transformation ou de calcul sur un tableau de données, on devra donner un nom distinct à ces cibles et aux objets qui correspondent.
:::
Au final, notre fichier `_targets.R` est donc le suivant :
```{r eval=FALSE, code=readLines('_targets.R'), cache=FALSE}
```
::: {.callout-note}
`targets` offre aussi la possibilité de définir notre pipeline directement dans un fichier RMarkdown, ce qui peut permettre notamment de mieux le documenter. Pour plus d'information on pourra se référer au chapitre [Target Markdown](https://books.ropensci.org/targets/markdown.html) du manuel en ligne.
:::
## Exécution du pipeline
Une fois notre pipeline défini, `targets` fournit des outils permettant de visualiser sa structure et son état, notamment la fonction `tar_visnetwork()`.
```{r}
#| eval: false
tar_visnetwork()
```
```{r}
#| cache: false
#| eval: true
#| echo: false
if (knitr::is_html_output(excludes=c("epub", "epub2"))) {
tar_visnetwork()
}
```
Les différentes cibles apparaissent sous forme de cercles, et les fonctions qui leur sont appliquées sous forme de triangles. Les flèches indiquent que `targets` a automatiquement créé un *réseau de dépendances* entre cibles et fonctions : ainsi la cible `donnees` dépend des fonctions `filter_data`, `pivot_2019_2020` et de la cible `donnees_brutes`, qui elle-même dépend de la cible `csv_file`.
La couleur des différents éléments montrent que ceux-ci sont à l'état *outdated* : ils ne sont pas à jour.
On va donc exécuter notre pipeline, en utilisant la fonction `tar_make()`.
```{r cache=FALSE}
tar_make()
```
Lorsqu'on utilise `tar_make()`, `targets` lance une nouvelle session R (pour éviter tout problème ou conflit lié à l'état de notre session actuelle), charge les extensions définies via `tar_option_set()`, et exécute les cibles définies avec `tar_plan()`.
On visualise le nouvel état de notre pipeline, et on voit que toutes les cibles sont passées à l'état *up to date*.
```{r}
#| eval: false
tar_visnetwork()
```
```{r}
#| cache: false
#| eval: true
#| echo: false
if (knitr::is_html_output(excludes=c("epub", "epub2"))) {
tar_visnetwork()
}
```
À chaque étape, `targets` crée et stocke dans un cache chacun des objets correspondants aux différentes cibles (`donnees_brutes`, `donnees`, etc.). On peut charger à tout moment ces objets dans notre session avec la fonction `tar_load()`^[Ces données sont stockées dans le répertoire `_targets` à la racine du projet.].
```{r}
tar_load(donnees_evo)
donnees_evo
```
On peut aussi utiliser `tar_read()`, qui lit et retourne les résultats d'une des cibles, permettant de les stocker dans un nouvel objet.
```{r}
evo <- tar_read(donnees_evo)
```
## Modification du pipeline
Essayons de lancer à nouveau notre pipeline :
```{r cache=FALSE}
tar_make()
```
On voit que toutes les cibles ont été "skippées" : quand on lance `tar_make()`, seules les cibles qui sont à l'état *outdated* sont recalculées. Les résultats des autres sont conservés tels quels.
On va maintenant modifier légèrement notre fichier `R/fonctions_calculs.R` : plutôt que d'arrondir les évolutions en pourcentages à deux décimale, on n'en conserve plus qu'une.
```{r echo=FALSE, include=FALSE, cache=FALSE}
file.remove("R/fonctions_calculs.R")
file.rename("R/fonctions_calculs.R.orig", "R/fonctions_calculs.R")
```
```{r eval=FALSE, cache=FALSE, code=readLines('R/fonctions_calculs.R')}
```
On visualise à nouveau l'état de notre pipeline :
```{r}
#| eval: false
tar_visnetwork()
```
```{r}
#| cache: false
#| eval: true
#| echo: false
if (knitr::is_html_output(excludes=c("epub", "epub2"))) {
tar_visnetwork()
}
```
Grâce à sa gestion interne des dépendances entre les cibles, `targets` a vu que la fonction `calcule_evo` a été modifiée (elle est passée en statut *outdated*), et comme la cible `donnees_evo` dépend de cette fonction, celle-ci a également été placée en *outdated*. On peut obtenir directement une liste des cibles qui ne sont plus à jour à l'aide de la fonction `tar_outdated()` :
```{r cache=FALSE}
tar_outdated()
```
On relance notre pipeline :
```{r cache=FALSE}
tar_make()
```
On voit que les cibles `csv_file`, `donnees_brutes` et `donnees` ont été "skippées" : `targets` est allé prendre directement leurs valeurs déjà stockées en cache. Par contre `donnees_evo` a bien été recalculée.
On peut vérifier que notre pipeline est désormais entièrement à jour :
```{r}
#| eval: false
tar_visnetwork()
```
```{r}
#| cache: false
#| eval: true
#| echo: false
if (knitr::is_html_output(excludes=c("epub", "epub2"))) {
tar_visnetwork()
}
```
::: {.callout-note}
À noter que `targets` gère aussi les modifications des fichiers externes. Ainsi, si on modifie le contenu de `nat2020.csv`, la cible `csv_file` passerait en *outdated*, tout comme l'ensemble des autres cibles puisqu'elles dépendent directement ou indirectement de celle-ci. Dans ce cas, un `tar_make()` aurait pour effet de recalculer l'intégralité du pipeline.
:::
## RMarkdown
```{r cache=FALSE, echo=FALSE, include=FALSE}
# Retour aux fichiers d'origine
file.remove("_targets.R")
file.rename("_targets.R.orig", "_targets.R")
```
Imaginons maintenant qu'on souhaite générer un rapport à partir d'un document RMarkdown en utilisant les données d'évolution calculées par notre pipeline. On crée donc un nouveau fichier `evolution.Rmd` dans un dossier `reports`.
```
data/
└── nat2020.csv
R/
├── fonctions_recode.R
└── fonctions_calculs.R
reports/
└── evolution.Rmd
_targets.R
```
Quand on utilise un document RMarkdown dans un pipeline, on doit accéder aux données en utilisant les fonctions `tar_read()` ou `tar_load()` : ceci permet de s'assurer qu'on récupère les données "à jour", et cela permet aussi à `targets` de déterminer un lien de dépendance entre le document et les données.
Comme on souhaite utiliser les données de `donnees_evo`, on devra donc utiliser quelque chose comme :
```{r eval=FALSE}
d <- tar_read(donnees_evo)
```
Au final, le contenu de notre fichier RMarkdown est le suivant :
```{r echo=FALSE, comment=""}
cat(htmltools::includeText("reports/evolution.Rmd"))
```
Pour ajouter ce rapport à notre pipeline, on crée une nouvelle cible dans le `tar_plan()` de `_targets.R`. Comme il s'agit d'un document RMarkdown, on utilise la fonction `tar_render()`.
```{r cache=FALSE, eval=FALSE, code=readLines("_targets.R")}
```
Visualisons notre pipeline modifié :
```{r}
#| eval: false
tar_visnetwork()
```
```{r}
#| cache: false
#| eval: true
#| echo: false
if (knitr::is_html_output(excludes=c("epub", "epub2"))) {
tar_visnetwork()
}
```
On voit que notre nouvelle cible `report_evo` a bien été prise en compte, qu'elle dépend bien de `donnees_evo` et qu'elle est à l'état *outdated*.
Si on exécute notre pipeline :
```{r cache=FALSE}
tar_make()
```
La cible `report_evo` a bien été calculée, et on devrait retrouver notre rapport compilé au format HTML dans le dossier `reports`.
## Gestion des données en cache
`targets` garde une copie des objets correspondant aux cibles du pipeline dans un cache, en fait sous forme de fichiers placés dans un sous-dossier `_targets`.
On a vu qu'on peut récupérer ces objets dans notre session via les fonctions `tar_read()` et `tar_load()`. `targets` propose également plusieurs fonctions pour gérer les données et métadonnées en cache :
- `tar_destroy()` supprime la totalite du répertoire `_targets`. Elle permet donc de "repartir de zéro", sans aucun cache et avec toutes les cibles à recalculer.
- `tar_delete(donnees)` supprime l'objet `donnees` du cache et place l'état de la cible correspondante à *outdated*. Elle permet de forcer le recalcul d'une cible et de celles qui en dépendent. À noter qu'on peut sélectionner plusieurs cibles en utilisant la syntaxe de la *tidy selection*.
- `tar_prune()` permet de supprimer les cibles qui ne sont plus présentes dans le pipeline. Elle permet donc de "faire le ménage" quand on a supprimé des étapes dans `_targets.R`.
## Avantages et limites
### Avantages
On peut voir dans cette introduction rapide que l'utilisation de `targets` présente de nombreux avantages :
1. le fichier `_targets.R` fournit une description détaillée des étapes du projet. Cela facilite les choses quand on revient dessus après un certain temps et qu'on n'a plus tous les détails en tête, ou si on le partage avec quelqu'un.
2. chaque cible du pipeline est définie via des fonctions, ce qui garantit une séparation et une encapsulation des différentes étapes.
3. l'utilisation de `tar_make()` garantit que toutes les cibles du pipeline sont recalculées dans le bon ordre : pas de risque de lancer un script sur des données qui ne seraient pas complètement à jour parce qu'on a oublié de relancer certains recodages par exemple.
4. `tar_make()` s'exécute toujours dans un environnement vide, ce qui élimine les problèmes liés à l'état de notre session en cours et garantit la reproductibilité des résultats.
5. comme `targets` conserve une copie des résultats des cibles en cache, pas besoin de tout recalculer quand on relance le projet, on peut récupérer directement les résultats et savoir si ils sont à jour.
6. `tar_make()` ne recalcule que les cibles qui le nécessitent, les temps de calcul et d'exécution sont optimisés.
Parmi les inconvénients liés à l'utilisation de `targets`, on notera que le débuggage est un peu plus complexe, même si l'extension [fournit plusieurs outils](https://books.ropensci.org/targets/debugging.html) pour faciliter le travail.
### Interactivité et développement du pipeline
Une des limitations de `targets` est que le pipeline ne permet pas l'utilisation de fonctions "interactives". Par exemple, on pourrait ajouter une étape affichant un graphique dans `tar_plan()` :
```{r eval=FALSE}
graphique = ggplot(donnees_evo) + geom_histogram(aes(x = evo))
```
Ceci fonctionne, mais ne provoque pas l'affichage du graphique. Il faut faire un `tar_read(graphique)` pour pouvoir le visualiser.
De la même manière, on ne peut pas utiliser d'interfaces interactives comme celles vues pour faciliter les recodages de variables (par exemple @sec-irec). Il est donc souvent pratique de commencer à développer des transformations, calculs ou analyses de façon "interactive", via un script classique dans lequel on importe les données nécessaires via `tar_read()`. Une fois qu'on obtient le résultat souhaité, on transforme ce code en une ou plusieurs fonctions et on les intègre au pipeline de `targets`.
::: {.callout-note}
On notera que les documents RMarkdown s'utilisent très bien avec `targets` : du moment qu'on charge les données avec `tar_read()` ou `tar_load()`, ils permettent à la fois une utilisation "interactive" pendant leur écriture, et une intégration directe dans un pipeline avec `tar_render()` sans avoir besoin de les modifier.
:::
## Ressources
Nous n'avons vu ici qu'un petit aperçu des fonctionnalités de `targets`, qui est une extension extrêmement riche et qui propose de nombreuses autres possibilités, comme la parallélisation des calculs, la gestion des versions de paquets via `renv`, la création programmatique de cibles...
Le *package* bénéficie d'une excellente documentation en anglais. On pourra donc se référer aux sites officiels de [targets](https://docs.ropensci.org/targets) et [tarchetypes](https://docs.ropensci.org/tarchetypes), mais surtout à l'ouvrage en ligne [The targets R Package User Manual](https://books.ropensci.org/targets/), très clair et très complet.
Le groupe des utilisateurs de R de Lille a accueilli une intervention (toujours en anglais) de Will Landau, l'auteur de `targets`. Celle-ci est disponible [en vidéo sur YouTube](https://www.youtube.com/watch?v=FODSavXGjYg).