-
Notifications
You must be signed in to change notification settings - Fork 19
/
18-purrr.qmd
1376 lines (990 loc) · 42.6 KB
/
18-purrr.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
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Itérer avec `purrr` {#sec-purrr}
{{< include _setup.qmd >}}
`purrr` est une extension du *tidyverse* qui fournit des outils pour travailler avec les vecteurs et les fonctions, et notamment pour itérer sur les éléments de vecteurs ou de listes en leur appliquant une fonction.
Dans cette section on aura besoin des extensions du *tidyverse* (dont `purrr` fait partie), que nous chargeons donc immédiatement, de même que les jeux de données `rp2018` et `hdv2003` de `questionr`.
```{r message=FALSE}
library(tidyverse)
library(questionr)
data(hdv2003)
data(rp2018)
```
## Exemple d'application
Pour mieux appréhender de quoi il s'agit, on part du vecteur suivant, qui contient des extraits (fictifs ?) de discours politiques.
```{r}
discours <- c(
"nous privilégierons une intergouvernementalisation sans agir anticonstitutionnellement",
"le souffle de la nation est le vent qui agite les drapeaux de nos libertés",
"nous devons faire preuve de plus de pédagogie pour cette réforme",
"mon compte twitter a été piraté"
)
```
On souhaite calculer la longueur de chaque extrait, en nombre de mots.
On commence par découper grossièrement chaque extrait en mots en utilisant la fonction `str_split()` de `stringr`^[Pour une application sérieuse on utilisera des fonctions spécifiques de "tokenization" d'extensions dédiées à l'analyse textuelle, comme `quanteda`.].
```{r}
mots <- str_split(discours, " ")
str(mots)
```
L'objet `mots` est une liste de vecteurs de chaînes de caractères qui contiennent les mots des différents extraits.
Calculer le nombre de mots de chaque extrait revient à calculer la longueur de chaque élément de `mots`. Pour cela on pourrait vouloir utiliser la fonction `length()` directement :
```{r}
length(mots)
```
Ceci ne fonctionne pas, car `length()` nous retourne le nombre d'éléments de `mots`, pas celui de chacun de ses éléments : ce qu'on veut, ça n'est pas `length(mots)` mais `length(mots[[1]])`, `length(mots[[2]])`, etc.
On a vu @sec-no-boucles qu'on peut pour cela utiliser une boucle `for`, par exemple de la manière suivante.
```{r}
resultat <- list()
for (item in mots) {
resultat <- c(resultat, length(item))
}
resultat
```
Ça fonctionne, mais la syntaxe est un peu "lourde".
La fonction `map()` de `purrr` propose exactement cette fonctionnalité. Elle prend deux arguments principaux :
1. un vecteur ou une liste
2. une fonction
et elle retourne une liste contenant le résultat de la fonction appliquée à chaque élément du vecteur ou de la liste.
En utilisant `map()` on peut remplacer notre boucle `for` par un simple :
```{r}
map(mots, length)
```
`map` va itérer sur les éléments de `mots`, leur appliquer tour à tour la fonction `length` passée en argument, et regrouper les résultats dans une liste.
À noter qu'on peut évidemment utiliser le *pipe*.
```{r}
mots %>% map(length)
```
Ici notre résultat est une liste. Or il pourrait être simplifié sous forme de vecteur atomique, puisque tous ses éléments sont des nombres.
Si on souhaitait obtenir un vecteur numérique avec une boucle `for`, il faut soit convertir le résultat de la boucle précédente en vecteur atomique (avec `unlist()` ou `purrr::flatten_int()`), soit modifier cette boucle pour qu'elle génère plutôt un vecteur numérique :
```{r}
resultat <- numeric(length(discours))
for (i in seq_along(mots)) {
resultat[i] <- length(mots[[i]])
}
resultat
```
Mais `purrr` propose des variantes de la fonction `map` qui permettent justement de s'assurer du type de résultat obtenu. Ainsi, `map_dbl()` renverra toujours un vecteur de nombres flottants, et `map_int()` un vecteur de nombres entiers. En remplacement de la boucle `for` ci-dessus, on peut donc utiliser :
```{r}
mots %>% map_int(length)
```
## `map` et ses variantes
### Modes d'appel de `map`
L'objectif de `map` est donc d'appliquer une fonction à l'ensemble des éléments d'un vecteur ou d'une liste.
On a vu qu'on pouvait l'utiliser pour appliquer la fonction `length` à chacun des vecteurs contenus par la liste `mots`. En utilisant `map_int` on s'assure de récupérer un simple vecteur numérique, plus facile à utiliser par la suite si on souhaite par exemple calculer une moyenne.
```{r}
mots %>% map_int(length)
```
Si on souhaitait plutôt extraire le dernier mot de chaque vecteur, on pourrait créer une fonction spécifique et l'appliquer avec `map()`.
```{r}
dernier_mot <- function(v) {
tail(v, 1)
}
mots %>% map(dernier_mot)
```
Comme notre résultat est une liste de chaînes de caractères simples, on peut forcer le résultat à être plutôt un vecteur de type *character* en utilisant `map_chr()` :
```{r}
mots %>% map_chr(dernier_mot)
```
Comme notre fonction est très courte, on peut aussi préférer utiliser une *fonction anonyme*, introduites @sec-fonctions-anonymes.
```{r}
mots %>% map_chr(function(v) {
tail(v, 1)
})
```
On peut aussi utiliser la notation abrégée sous forme de formule, propre aux fonctions du *tidyverse*, présentée @sec-syntaxes-abregees.
```{r}
mots %>% map_chr(~ tail(.x, 1))
```
On peut également utiliser la notation compacte pour les fonctions anonymes disponible sous R à partir de la version 4.1.
```{r}
mots %>% map_chr(\(v) tail(v, 1))
```
Enfin, si on fournit des arguments supplémentaires à `map`, ils sont passés comme argument à la fonction qu'il applique, on peut donc également utiliser la notation suivante :
```{r}
mots %>% map_chr(tail, 1)
```
Dans ce qui suit on utilisera de préférence la notation "formule", mais toutes les versions ci-dessus sont équivalentes et donnent le même résultat.
::: {.callout-note}
Petite astuce à noter, si on transmet à `map()` autre chose qu'une fonction, elle utilisera cette information pour extraire des éléments. Ainsi, `v %>% map(1)` extraiera le premier élément de chaque élément du vecteur `v`, `v %>% map("foo")` extraiera les éléments nommés "foo", etc.
:::
### Variantes de `map`
On a vu que `map` propose plusieurs variantes qui permettent de contrôler le type de résultat qu'elle retourne :
- `map()` retourne une liste
- `map_int()` retourne un vecteur atomique d'entiers
- `map_dbl()` retourne un vecteur atomique de nombres flottants
- `map_chr()` retourne un vecteur atomique de chaînes de caractères
- `map_lgl()` retourne un vecteur atomique de `TRUE` / `FALSE`
Attention, ces variantes sont très strictes : si la fonction appelée retourne un résultat qui n'est pas compatible avec le résultat attendu, elle génère une erreur. C'est le cas si dans le code précédent on essaie de récupérer chaque dernier mot sous forme d'un vecteur de nombres :
```{r error=TRUE}
mots %>% map_dbl(tail, 1)
```
Pour pouvoir utiliser ces variantes et obtenir un vecteur atomique, chaque résultat retourné par la fonction appliquée doit être de longueur 1. Ainsi, si on souhaitait extraire plutôt la liste des mots contenant un "f", certains résultats ne contiennent aucun élément, et d'autres en contiennent deux :
```{r}
mots %>% map(~ str_subset(.x, "f"))
```
Dans ce cas on ne peut pas utiliser `map_chr()` : si on essaie on obtient un message d'erreur nous indiquant que certains résultats de `str_subset()` ne sont pas au bon format.
```{r error=TRUE}
mots %>% map_chr(~ str_subset(.x, "f"))
```
Dans ce cas, on doit donc utiliser `map()` et conserver le résultat sous forme de liste, qui elle peut contenir des éléments de longueurs différentes.
```{r}
mots %>%
map(~ str_subset(.x, "f"))
```
On notera qu'on peut tout à fait enchaîner les `map()` si on veut effectuer des opérations supplémentaires.
```{r}
mots %>%
map(~ str_subset(.x, "f")) %>%
map_int(length)
```
### `map` et tableaux de données
La page suivante contient les données du jeu de données `rp2018` sous la forme de fichiers CSV, avec un fichier par département :
<https://github.com/juba/tidyverse/tree/main/resources/data/rp2018>
À partir de cette page, on peut télécharger les fichiers CSV en utilisant des adresses de la forme :
<https://raw.githubusercontent.com/juba/tidyverse/main/resources/data/rp2018/rp2018_01.csv>
En remplaçant "01" par le code du département souhaité.
On peut créer une fonction `genere_url()` qui, à partir d'une liste de codes de départements, retourne les adresses des fichiers correspondant.
```{r}
genere_url <- function(codes) {
paste0(
"https://raw.githubusercontent.com/juba/tidyverse/main/resources/data/rp2018/rp2018_",
codes,
".csv"
)
}
genere_url(c("42", "69"))
```
Grâce à la fonction `read_csv()`, on peut charger directement dans notre session R un fichier en indiquant son URL.
```{r eval=FALSE}
data69 <- read_csv(genere_url("69"))
```
Comment faire si l'on souhaite charger les fichiers de plusieurs départements ? La fonction `read_csv()` n'accepte qu'une seule URL à la fois, elle n'est pas vectorisée.
Dans ce cas on peut utiliser `map()` pour l'appliquer tour à tour à plusieurs URL.
```{r message=FALSE}
departements <- c("38", "42", "69")
urls <- genere_url(departements)
dfs <- urls %>% map(read_csv)
```
Le résultat `dfs` est une liste de trois tableaux de données. Chacun de ces éléments est un *tibble* : d'abord celui du fichier CSV des données de l'Isère, puis celui de la Loire, et enfin celui du Rhône.
On peut itérer sur cette liste `dfs` pour appliquer une fonction à chacun de ces tableaux.
```{r}
# Affichage des dimensions de chaque tableau
dfs %>% map(dim)
```
```{r}
# Calcul de la moyenne de la variable dipl_aucun
dfs %>% map_dbl(~ mean(.x$dipl_aucun))
```
```{r}
# Calcul du coefficient associé à la variable dipl_sup dans
# la régression linéaire de cadres en fonction de dipl_sup
dfs %>% map_dbl(~ {
reg <- lm(cadres ~ dipl_sup, data = .x)
reg$coefficients["dipl_sup"]
})
```
Si on souhaite réunir ces trois *tibbles* en un seul, on peut utiliser la fonction `bind_rows()` de `dplyr`.
```{r message=FALSE, eval=FALSE}
departements <- c("38", "42", "69")
urls <- genere_url(departements)
df <- urls %>%
map(read_csv) %>%
bind_rows()
df
```
Les quelques lignes de code ci-dessus partent donc d'une liste d'identifiants de départements, génèrent les URL des fichiers CSV correspondant, les importent dans R et assemblent le résultat en un seul tableau. Plutôt efficace !
::: {.callout-note}
`bind_rows()` est une fonction de `dplyr`. Si on souhaite utiliser uniquement des fonctions de `purrr`, on peut utiliser `list_rbind()`, qui donne le même résultat.
:::
Une fonction utile en complément de `map` est la fonction `list.files()`, qui peut lister les fichiers ayant une certaine extension dans un dossier spécifique. Par exemple, l'instruction suivante liste tous les fichiers se terminant par `.csv` du sous-dossier `data`.
```{r eval=FALSE}
fichiers <- list.files("data", "*.csv", full.names = TRUE)
```
On peut dès lors utiliser `map()`, `read_csv()` et `bind_rows()`, pour lire tous ces fichiers en une seule fois et les concaténer en un seul tableau de données.
```{r eval=FALSE}
d <- fichiers %>%
map(read_csv) %>%
bind_rows()
```
::: {.callout-note}
On peut aussi utiliser `bind_cols()` ou `list_cbind()` quand les résultats d'un `map` sont des colonnes d'un tableau de données qu'on souhaite rassembler en les concaténant.
:::
## Itérer sur les colonnes d'un tableau de données
On a vu @sec-data-frame-tibbles que les tableaux de données (*data frame* ou *tibble*) sont en fait des listes dont les éléments sont les colonnes du tableau. Si on applique `map()` à un tableau, celle-ci itérera donc sur ses colonnes.
Par exemple, on peut appliquer `n_distinct` au jeu de données `starwars` et obtenir le nombre de valeurs distinctes de chacune de ses colonnes.
```{r}
starwars %>% map_int(n_distinct)
```
Le résultat est équivalent à celui qu'on obtient en faisant un `summarise()` sur l'ensemble des colonnes, comme vu @sec-across, sauf que `map_int()` retourne un vecteur numérique tandis que `summarise()` renvoie un *tibble* à une ligne.
```{r}
starwars %>%
summarise(
across(everything(), n_distinct)
)
```
De la même manière, si on veut connaître le nombre de valeurs manquantes pour chaque variable :
```{r}
starwars %>% map_int(~ sum(is.na(.x)))
```
Contrairement à `across()`, on ne peut pas spécifier directement une sélection de colonnes à `map()`. On peut par contre utiliser des fonctions comme `keep()` ou `discard()` qui "filtrent" les éléments d'une liste via une fonction qui renvoie `TRUE` ou `FALSE`.
On peut par exemple utiliser `discard()` après `map()` pour ne conserver que les colonnes ayant au moins une valeur `NA`.
```{r}
starwars %>%
map_int(~ sum(is.na(.x))) %>%
discard(~ .x == 0)
```
Ou bien utiliser `keep()` pour n'appliquer `mean()` qu'aux variables numériques.
```{r}
starwars %>%
keep(is.numeric) %>%
map_dbl(mean, na.rm = TRUE)
```
## `modify`
`modify()` est une variante de `map()` qui a pour particularité de renvoyer un résultat du même type que la liste ou le vecteur donné en entrée.
Ainsi, si on l'applique à un vecteur de chaînes de caractères, le résultat sera aussi un vecteur de chaînes de caractères.
```{r}
v <- c("brouette", "moto", "igloo")
v %>% modify(~ paste("incroyable", .x))
```
Si on l'applique à une liste, le résultat sera aussi une liste.
```{r}
v <- list("brouette", "moto", "igloo")
v %>% modify(~ paste("incroyable ", .x))
```
L'objectif de `modify()` est de permettre de "modifier" une liste ou un vecteur en lui appliquant une fonction tout en étant sûr qu'on ne va pas modifier son type.
L'intérêt principal de `modify()` est qu'elle propose deux variantes, `modify_if()` et `modify_at()`, qui sélectionnent les éléments respectivement via une fonction et via leur nom ou leur position, et qui n'appliquent la fonction de transformation qu'aux éléments sélectionnés.
Cela peut être particulièrement utile quand on l'applique à un tableau de données. Par exemple le code suivant utilise `modify_if()` pour transformer uniquement les colonnes de type `factor` de `hdv2003` en `character`, et laisser les autres inchangées.
```{r include=FALSE}
hdv2003 <- as_tibble(hdv2003)
```
```{r results="hide"}
hdv2003 %>% modify_if(is.factor, as.character)
```
On notera qu'on obtient le même résultat avec le code suivant qui utilise `across()` de `dplyr`.
```{r results="hide"}
hdv2003 %>%
mutate(
across(
where(is.factor),
as.character
)
)
```
`modify_at` permet d'appliquer une fonction à certaines variables à partir de leurs noms.
```{r results="hide"}
hdv2003 %>%
modify_at(c("sexe", "qualif"), as.character)
```
En utilisant `vars()`, on peut sélectionner les variables avec toutes les possibilités offertes par la *tidy selection*.
```{r results="hide"}
hdv2003 %>%
modify_at(vars(hard.rock:sport), as.character)
```
Là aussi, on peut obtenir le même résultat en utilisant `across()`.
```{r results="hide"}
hdv2003 %>%
mutate(
across(
hard.rock:sport,
as.character
)
)
```
## `imap`
Imaginons que nous avons récupéré les données suivantes, qui représentent des évaluations obtenues par quatre restaurants, sous la forme d'une liste.
```{r}
restos <- list(
"La bonne fourchette" = c(3, 3, 2, 5, 2, 3, 2, 4, 1, 3),
"La choucroute de l'amer" = c(4, 1, 2, 4, 2, 5, 2),
"L'Hair de rien" = c(1, 5, 5, 1, 5, 3, 1, 5, 2),
"La blanquette de Vaulx" = c(4, 1, 3, 1, 3, 3, 1, 4, 2, 5)
)
```
À partir de cette liste, on souhaite créer un tableau de données comportant la moyenne et l'écart-type des notes de chaque restaurant. Comme on l'a vu précédemment, cela peut se faire avec l'aide de `map()` et `bind_rows()`.
```{r}
restos %>%
map(~ tibble(moyenne = mean(.x), ecart_type = sd(.x))) %>%
bind_rows()
```
On obtient le tableau souhaité, mais il manque une information : le nom du restaurant correspondant à chaque ligne. Cette information est incluse dans les noms des éléments de la liste `restos`, or la fonction passée à `map` n'y a pas accès, elle n'a accès qu'à leurs valeurs.
C'est pour ce type de cas de figure que `purrr` propose la famille de fonctions `imap()`. Celle-ci fonctionne de la même manière que `map()`, sauf que la fonction appliquée prend deux arguments : d'abord la valeur de l'élément courant, puis son nom.
Dans l'exemple suivant, on applique `imap()` à une liste simple et on affiche un message avec le nom et la valeur de chaque élément.
```{r}
l <- list(nom1 = 1, nom2 = 3)
l2 <- l %>% imap(function(valeur, nom) {
message("La valeur de ", nom, " est ", valeur)
})
```
On peut évidemment utiliser la notation "formule" de `purrr`, il faut juste se souvenir que dans ce cas `.x` correspond à la valeur, et `.y` au nom.
```{r}
l2 <- l %>% imap(~ {
message("La valeur de ", .y, " est ", .x)
})
```
Tout comme `map()` proposait les variantes `map_int()`, `map_chr()` ou `map_lgl()`, on peut également utiliser `imap_dbl()` ou `imap_chr()` pour forcer le type de résultat retourné.
Pour reprendre notre exemple de départ, on peut donc, en utilisant `imap`, récupérer le nom de l'élément courant de la liste `restos` et l'utiliser pour rajouter le nom du restaurant dans notre *tibble* de résultats.
On peut le faire avec une fonction anonyme "classique" :
```{r}
restos %>%
imap(function(notes, nom) {
tibble(resto = nom, moyenne = mean(notes), ecart_type = sd(notes))
}) %>%
bind_rows()
```
On peut aussi utiliser la notation compacte de type formule, en se souvenant à nouveau que `.x` correspond à la valeur de l'élément courant, et `.y` à son nom.
```{r}
restos %>%
imap(~ {
tibble(resto = .y, moyenne = mean(.x), ecart_type = sd(.x))
}) %>%
bind_rows()
```
::: {.callout-note}
Si on utilise `imap()` sur une liste ou un vecteur qui n'a pas de noms, le deuxième argument passé à la fonction appliquée sera l'indice de l'élément courant : 1 pour le premier, 2 pour le deuxième, etc.
:::
## `walk`
`walk()` est une variante de `map()` qui a pour particularité de ne pas retourner de résultat. On l'utilise lorsqu'on souhaite parcourir un vecteur ou une liste et appliquer à ses éléments une fonction dont on ne souhaite conserver que les "effets de bord" : afficher un message, générer un graphique, enregistrer un fichier...
Par exemple, le code suivant génère quatre diagrammes en barre indiquant la répartition des notes des différents restaurants vus dans la section précédente.
```{r eval=FALSE}
walk(restos, ~ barplot(table(.x)))
```
```{r echo=FALSE}
par(mfrow = c(2, 2))
walk(restos, ~ barplot(table(.x)))
```
Comme pour `map()`, la variante `iwalk()` permet d'itérer à la fois sur les valeurs et sur les noms des éléments du vecteur ou de la liste. Ceci permet par exemple d'afficher le nom du restaurant comme titre de chaque graphique.
```{r eval=FALSE}
iwalk(restos, ~ barplot(table(.x), main = .y))
```
```{r echo=FALSE}
par(mfrow = c(2, 2))
iwalk(restos, ~ barplot(table(.x), main = .y))
```
Au final, on notera que l'utilisation de `walk()`, comme elle ne retourne pas de résultats, est très proche de celle d'une boucle `for`.
## `map2` et `pmap` : itérer sur plusieurs vecteurs en parallèle
Supposons qu'un.e collègue, qui travaille avec nous sur le jeu de données `rp2018`, nous a envoyé une liste de variables dont elle voudrait connaître les corrélations. Cette liste a été saisie dans un tableur sur deux colonnes, chaque ligne indiquant deux variables pour lesquelles elle souhaite qu'on effectue ce calcul.
Après importation dans R on obtient le tableau de données suivant.
```{r}
correlations <- tribble(
~var1, ~var2,
"dipl_sup", "dipl_aucun",
"dipl_sup", "cadres",
"hlm", "cadres",
"hlm", "ouvr",
"proprio", "hlm"
)
correlations
```
Pour pouvoir calculer les corrélations souhaitées, on doit itérer sur les deux vecteurs `var1` et `var2` *en parallèle*, et calculer la corrélation entre la colonne de `rp2018` correspondant à la valeur courante de `var1` et celle correspondant à la valeur courante de `var2`.
C'est précisément ce que fait la fonction `map2()`. Celle-ci prend trois arguments en entrée :
1. deux listes ou vecteurs qui seront itérés en parallèle
2. une fonction qui accepte deux arguments : ceux-ci prendront tour à tour les deux valeurs courantes des deux listes ou vecteurs itérés
On peut donc utiliser `map2` pour itérer parallèlement sur les deux colonnes `var1` et `var2` de notre tableau `correlations`, et calculer la corrélation des deux colonnes correspondantes de `rp2018`.
```{r}
map2(
correlations$var1,
correlations$var2,
~ cor(rp2018[[.x]], rp2018[[.y]])
)
```
`map2()` propose les mêmes variantes `map2_int()`, `mapr2_chr()`, etc. que `map()`. On peut donc utiliser `map2_dbl()` pour récupérer un vecteur numérique plutôt qu'une liste, et l'utiliser par exemple pour rajouter une colonne à notre tableau de départ.
```{r}
correlations$corr <- map2_dbl(
correlations$var1,
correlations$var2,
~ cor(rp2018[[.x]], rp2018[[.y]])
)
correlations
```
Si on souhaite uniquement capturer les effets de bord sans récupérer les résultats de la fonctions appliquée, on peut aussi utiliser la variante `walk2()`.
Supposons maintenant que notre collègue nous a envoyé, toujours sous la même forme, une liste de variables dont elle souhaite obtenir un nuage de points, mais en fournissant également un titre à ajouter au graphique. On obtient le tableau suivant :
```{r}
nuages <- tribble(
~var1, ~var2, ~titre,
"dipl_sup", "dipl_aucun", "Diplômés du supérieur x sans diplôme",
"dipl_sup", "cadres", "Pourcentage de cadres x diplômés du supérieur",
"hlm", "cadres", "Pas facile de trouver un titre",
"proprio", "cadres", "Oui non vraiment c'est pas simple"
)
nuages
```
On est dans une situation similaire à la précédente, sauf que cette fois on doit itérer sur trois vecteurs en parallèle. On va donc utiliser la fonction `pmap()` qui permet d'itérer sur autant de listes ou vecteurs que l'on souhaite. Plus précisément, comme on souhaite générer des graphiques on va utiliser la variante `pwalk()` qui ne retourne pas de résultat.
`pmap()` et `pwalk()` prennent deux arguments principaux :
- les vecteurs et listes sur lesquels itérer, eux-mêmes regroupés dans une liste
- une fonction acceptant autant d'arguments que de vecteur ou listes sur lesquels on itère
Dans notre exemple on aurait donc un appel de la forme suivante.
```{r include=FALSE}
data(rp2018)
rp2018_orig <- rp2018
rp2018 <- sample_n(rp2018, size = 500)
```
```{r eval=FALSE}
pwalk(
list(nuages$var1, nuages$var2, nuages$titre),
function(var1, var2, titre) {
plot(
rp2018[[var1]], rp2018[[var2]],
xlab = var1, ylab = var2, main = titre
)
}
)
```
```{r echo=FALSE}
par(mfrow = c(2, 2))
pwalk(
list(nuages$var1, nuages$var2, nuages$titre),
function(var1, var2, titre) {
plot(
rp2018[[var1]], rp2018[[var2]],
xlab = var1, ylab = var2, main = titre
)
}
)
```
Petite précision, si la liste est nommée, il faut que les noms des arguments de la fonction correspondent aux noms de la liste.
```{r eval=FALSE}
pwalk(
list(v1 = nuages$var1, v2 = nuages$var2, titre = nuages$titre),
function(v1, v2, titre) {
plot(
rp2018[[v1]], rp2018[[v2]],
xlab = v1, ylab = v2, main = titre
)
}
)
```
À noter que comme `nuages` est un tableau de données, donc une liste dont les éléments sont ses colonnes, on obtient le même résultat avec :
```{r eval=FALSE}
pwalk(
nuages,
function(var1, var2, titre) {
plot(
rp2018[[var1]], rp2018[[var2]],
xlab = var1, ylab = var2, main = titre
)
}
)
```
On peut utiliser la syntaxe "formule" pour la fonction anonyme, dans ce cas les arguments sont accessibles avec la notation `..1`, `..2`, etc. On notera que dans ce cas la syntaxe "formule" est sans doute moins lisible que la syntaxe classique avec `function()` qui permet de nommer les paramètres.
```{r eval=FALSE}
pwalk(
nuages,
~ {
plot(
rp2018[[..1]], rp2018[[..2]],
xlab = ..1, ylab = ..2, main = ..3
)
}
)
```
Comme pour `map()` et `map2()`, `pmap()` propose aussi les variantes `pmap_int()`, `pmap_chr()`, etc.
```{r include=FALSE}
rp2018 <- rp2018_orig
```
## Répéter une opération
Les fonctions de `purrr` peuvent être utilisées quand on souhaite juste répéter une opération un certain nombre de fois, à la place d'une boucle `for`.
Par exemple si on souhaite générer 10 vecteurs de 100 nombre aléatoires, on pourra remplacer la boucle suivante :
```{r}
res <- list()
for (i in 1:10) {
res <- c(res, rnorm(100))
}
```
Par un appel à `map()` :
```{r}
res <- map(1:10, ~ rnorm(100))
```
Ce qui donne un code un peu plus compact et plus lisible.
::: {.callout-note}
De la même manière, si on s'intéresse juste aux effets de bord, on pourra éventuellement remplacer une boucle `for` par un appel à `walk()`.
:::
## Quand (ne pas) utiliser `map`
Une fois qu'on a compris la logique de `map()` et de ses variantes, on peut être tenté.es de l'appliquer un peu systématiquement. Il faut cependant garder en tête que son usage n'est pas toujours conseillé notamment dans les cas où il existe déjà une fonction vectorisée qui permet d'obtenir le même résultat.
Ainsi cela n'aurait pas de sens de faire :
```{r}
v <- 1:5
map_dbl(v, ~ .x + 10)
```
Quand on peut simplement faire :
```{r}
v + 10
```
Pour prendre un exemple un peu moins caricatural, de nombreuses fonctions de `stringr` sont vectorisées, il n'est donc pas utile de faire :
```{r}
textes <- c("fantastique", "effectivement", "igloo")
map_int(textes, ~ str_count(.x, "f"))
```
Quand on peut faire simplement :
```{r}
str_count(textes, "f")
```
Par contre `map()` est utile quand on souhaite appliquer une fonction qui n'est pas vectorisée à plusieurs valeurs, comme c'est le cas par exemple avec `read_csv()`, qui ne permet pas de charger plusieurs fichiers d'un coup :
```{r eval=FALSE}
fichiers <- c("fichier1.csv", "fichier2.csv")
l <- fichiers %>% map(read_csv)
```
Ou quand on veut itérer sur un argument non vectorisé, par exemple ici sur l'argument `pattern` de `str_count()` :
```{r}
voyelle <- c(a = "a", e = "e", i = "i")
textes <- c("brouette", "moto", "igloo")
voyelle %>% map(~ str_count(textes, pattern = .x))
```
On l'utilise également quand on veut appliquer une fonction non pas à une liste, mais aux éléments qu'elle contient :
```{r}
l <- list(1:3, c(2, 5))
l %>% map_int(length)
```
À noter qu'en termes de performance, `map()` n'est pas forcément plus rapide qu'une boucle `for`, puisque dans les deux cas on itère sur un ensemble de valeurs. Par contre une fonction vectorisée existante sera toujours (beaucoup) plus rapide.
## `purrr` vs `*apply`
Les fonctions de `purrr` ont des équivalents dans R "de base", ce sont notamment les fonctions de la famille `apply` : `lapply`, `sapply`, `mapply`...
L'avantage de `map()` et des autres fonctions fournies par `purrr` et qu'elles sont plus explicites : on a des fonctions différentes selon qu'on veut seulement appliquer une fonction (`map()`), générer des effets de bord (`walk`), modifier une liste sans changer son type (`modify()`), etc. `purrr` propose également de nombreuses fonctions utiles qui facilite le travail avec les vecteurs et listes.
Mais un des avantages principaux des fonctions de la famille `map()` est qu'elles sont consistantes et cohérentes dans le type de résultat qu'elles retournent : on est certain que `map()` ou `imap()` retourneront une liste, que `map_chr()` ou `map2_chr()` retourneront un vecteur de chaînes de caractères, etc.
Là encore, il n'est pas question de dire qu'il ne faut pas utiliser les fonctions `*apply`. Si vous en avez l'habitude et qu'elles fonctionnent pour vous, il n'y a pas spécialement de raison de changer. Mais si vous n'avez pas l'habitude de ce type d'opérations ou si vous préférez une syntaxe plus cohérente et plus facile à retenir, les fonctions de `purrr` peuvent être intéressantes.
Si vous souhaitez en savoir plus, l'ouvrage en ligne *R for data science* contient [une comparaison plus détaillée](https://r4ds.had.co.nz/iteration.html#base-r) des deux familles de fonctions.
## Ressources
Au-delà de celles présentées ici, `purrr` propose de nombreuses autres fonctions facilitant la manipulation et les itérations sur les listes et les vecteurs. On peut en trouver la [liste complète](https://purrr.tidyverse.org/reference/index.html) et la documentation associée (en anglais) sur [le site de l'extension](https://purrr.tidyverse.org).
La section *Iteration* de l'ouvrage en ligne *R for data science* (en anglais) propose [une présentation de plusieurs fonctions de purrr](https://r4ds.had.co.nz/iteration.html#the-map-functions).
RStudio propose une [antisèche](https://github.com/rstudio/cheatsheets/blob/master/purrr.pdf) (en anglais, format PDF) qui résume les différentes fonctions de `purrr`.
Sur le blog en français de Lise Vaudor, on trouvera un billet [Itérer des fonctions avec purrr](https://perso.ens-lyon.fr/lise.vaudor/iterer-des-fonctions-avec-purrr/) et une suite [practice makes purrr-fect](https://perso.ens-lyon.fr/lise.vaudor/practice-makes-purrr-fect/).
## Exercices
### `map` et ses variantes
**Exercice 1.1**
La liste suivante rassemble les notes obtenues par un élève dans différentes matières.
```{r}
notes <- list(
maths = c(12, 15, 8, 10),
anglais = c(18, 11, 9),
sport = c(5, 13),
musique = 14
)
```
En utilisant `map()`, calculer une liste indiquant la moyenne dans chaque matière.
::: {.solution-exo}
```{r eval=FALSE}
notes %>% map(mean)
```
:::
En utilisant une variante de `map()`, simplifier le résultat pour obtenir un vecteur numérique.
::: {.solution-exo}
```{r eval=FALSE}
notes %>% map_dbl(mean)
```
:::
On a rajouté à la liste les notes obtenues en technologie, parmi lesquelles une note est manquante.
```{r}
notes <- list(
maths = c(12, 15, 8, 10),
anglais = c(18, 11, 9),
sport = c(5, 13),
musique = 14,
techno = c(12, NA)
)
```
Calculer à nouveau un vecteur numérique des moyennes par matière, mais sans tenir compte de la valeur manquante.
::: {.solution-exo}
```{r eval=FALSE}
notes %>% map_dbl(~ mean(.x, na.rm = TRUE))
# Ou bien
notes %>% map_dbl(mean, na.rm = TRUE)
```
:::
Calculer une liste qui contient pour chaque matière la moyenne, la note minimale et la note maximale.
::: {.solution-exo}
```{r eval=FALSE}
# On peut aussi utiliser list() plutôt que c()
notes %>%
map(~ c(
moyenne = mean(.x, na.rm = TRUE),
min = min(.x, na.rm = TRUE),
max = max(.x, na.rm = TRUE)
))
```
:::
**Exercice 1.2**
La liste suivante comporte les parcours biographiques de 5 personnes sous la forme de vecteurs indiquant leurs communes de résidence successives.
```{r}
parcours <- list(
c("Lyon", "F1ixevi11e", "Saint-Dié-en-Poui11y"),
c("Sainte-Gabelle-sur-Sarthe"),
c("Décines", "Meyzieu", "Demptezieu"),
c("Meyzieu", "Lyon", "Paris", "F1ixevi11e", "Lyon"),
c("La Bâtie-Divisin", "Versai11es")
)
```
À l'aide de `map()`, calculer une nouvelle liste comportant le nombre de villes de résidence pour chaque parcours.
::: {.solution-exo}
```{r eval=FALSE}
parcours %>% map(length)
```
:::
Utiliser une variante de `map()` pour simplifier le résultat et obtenir un vecteur numérique plutôt qu'une liste.
::: {.solution-exo}
```{r eval=FALSE}
parcours %>% map_int(length)
```
:::
Déterminer pour chaque parcours le nombre de fois où la personne a résidé à Lyon.
::: {.solution-exo}
```{r eval=FALSE}
parcours %>% map_int(~ sum(.x == "Lyon"))
```
:::
On vient de repérer un problème dans les données : des caractères "l" ont été remplacés par des "1". Utiliser `map()` pour corriger l'objet `parcours` en remplaçant tous les "1" par des "l".
::: {.solution-exo}
```{r eval=FALSE}
parcours <- parcours %>%
map(~ str_replace_all(.x, "1", "l"))
```
:::
**Exercice 1.3**
Le vecteur suivant contient les adresses de deux fichiers CSV contenant les données de `rp2018` pour les départements de l'Ain et du Rhône :
```{r}
urls <- c(
"https://raw.githubusercontent.com/juba/tidyverse/main/resources/data/rp2018/rp2018_01.csv",
"https://raw.githubusercontent.com/juba/tidyverse/main/resources/data/rp2018/rp2018_69.csv"
)
```
Utiliser `map()` pour charger ces deux tableaux de données dans une liste nommée `dfs`.
::: {.solution-exo}
```{r eval=FALSE}
dfs <- urls %>% map(read_csv)
```
:::
Utiliser `map()` et `bind_rows()` pour charger ces deux tableaux de données et les regrouper en une seule table `d`. Que constatez-vous ?
::: {.solution-exo}
```{r eval=FALSE}
# Génère une erreur !
d <- urls %>%
map(read_csv) %>%
bind_rows()
```
:::
À l'aide de `map()`, afficher la variable `code_insee` des deux tableaux de `dfs`. D'où vient le problème ?
::: {.solution-exo}
```{r eval=FALSE}
# Une variable a été importée en character, l'autre en numérique
dfs %>% map(~ .x$code_insee)
```
:::
Trouver une solution pour corriger le problème.
::: {.solution-exo}
```{r eval=FALSE}
# En convertissant manuellement code_insee en character
d <- urls %>%
map(~ {
tab <- read_csv(.x)
tab$code_insee <- as.character(tab$code_insee)
tab
}) %>%
bind_rows()
# Ou bien en utilisant l'option col_types de read_csv
d <- urls %>%
map(read_csv, col_types = c("code_insee" = "character")) %>%
bind_rows()
```
:::
### `modify` et itération sur les colonnes d'un tableau
**Exercice 2.1**
Soit le tableau de données `d` suivant :
```{r}
d <- tribble(
~prenom, ~nom, ~age, ~taille,
"pierre-edmond", "multinivo", 19, 151,
"YVONNE-HENRI", "QUIDEU", 73, 182,
"jean-adélaïde", "hacépé", 27, NA
)
```
Utliser `typeof()` et `map()` pour afficher le type de toutes les colonnes de `d`.
::: {.solution-exo}
```{r eval=FALSE}
d %>% map(typeof)
```
:::
En utilisant `keep()` et `map()`, calculer la moyenne de toutes les variables numériques de `d`.
::: {.solution-exo}
```{r eval=FALSE}
d %>%
keep(is.numeric) %>%
map(mean, na.rm = TRUE)
```