-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy path10-dplyr.qmd
1488 lines (965 loc) · 46.2 KB
/
10-dplyr.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
# Manipuler les données avec `dplyr` {#sec-dplyr}
{{< include _setup.qmd >}}
`dplyr` est une extension facilitant le traitement et la manipulation de données contenues dans une ou plusieurs tables. Elle propose une syntaxe claire et cohérente, sous formes de verbes, pour la plupart des opérations de ce type.
`dplyr` part du principe que les données sont organisées selon le modèle des *tidy data* (voir @sec-tidydata). Les fonctions de l'extension peuvent s'appliquer à des tableaux de type `data.frame` ou `tibble`, et elles retournent systématiquement un `tibble` (voir @sec-tibbles).
Le code présent dans ce document nécessite d'avoir installé la version 1.0 de `dplyr` (ou plus récente).
## Préparation
`dplyr` fait partie du coeur du *tidyverse*, elle est donc chargée automatiquement avec :
```{r message=FALSE, warning=FALSE, cache = FALSE}
library(tidyverse)
```
On peut également la charger individuellement.
```{r}
library(dplyr)
```
Dans ce qui suit on va utiliser le jeu de données `nycflights13`, contenu dans l'extension du même nom (qu'il faut donc avoir installé). Celui-ci correspond aux données de tous les vols au départ d'un des trois aéroports de New-York en 2013. Il a la particularité d'être réparti en trois tables :
- `flights` contient des informations sur les vols : date, départ, destination, horaires, retard...
- `airports` contient des informations sur les aéroports
- `airlines` contient des données sur les compagnies aériennes
On va charger les trois tables du jeu de données :
```{r cache = FALSE}
library(nycflights13)
## Chargement des trois tables
data(flights)
data(airports)
data(airlines)
```
Trois objets correspondant aux trois tables ont dû apparaître dans votre environnement.
## Les verbes de `dplyr`
La manipulation de données avec `dplyr` se fait en utilisant un nombre réduit de verbes, qui correspondent chacun à une action différente appliquée à un tableau de données.
### `slice`
Le verbe `slice` sélectionne des lignes du tableau selon leur position. On lui passe un chiffre ou un vecteur de chiffres.
Si on souhaite sélectionner la 345e ligne du tableau `airports` :
```{r}
slice(airports, 345)
```
Si on veut sélectionner les 5 premières lignes :
```{r}
slice(airports, 1:5)
```
`slice` propose plusieurs variantes utiles, dont `slice_head` et `slice_tail`, qui permettent de sélectionner les premières ou les dernières lignes du tableau (on peut spécifier le nombre de lignes souhaitées avec `n`, ou la proportion avec `prop`).
```{r}
slice_tail(airports, n = 3)
```
```{r}
slice_head(airlines, prop = 0.2)
```
Autres variantes utiles, `slice_min` et `slice_max` permettent de sélectionner les lignes avec les valeurs les plus grandes ou les plus petite d'une variable donnée. Ainsi, la commande suivante sélectionne le vol ayant le retard au départ le plus faible.
```{r}
slice_min(flights, dep_delay)
```
On peut aussi spécifier le nombre de lignes souhaitées, par exemple la commande suivante retourne les 5 aéroports avec l'altitude la plus élevée (en cas de valeurs ex-aequo, il se peut que le nombre de lignes retournées soit plus élevé que celui demandé).
```{r}
slice_max(airports, alt, n = 5)
```
### `filter` {#sec-filter}
`filter` sélectionne des lignes d'une table selon une condition. On lui passe en paramètre un test, et seules les lignes pour lesquelles ce test renvoie `TRUE` (vrai) sont conservées. Pour plus d'informations sur les tests et leur syntaxe, voir @sec-tests.
Par exemple, si on veut sélectionner les vols du mois de janvier, on peut filtrer sur la variable `month` de la manière suivante :
```{r}
filter(flights, month == 1)
```
Si on veut uniquement les vols avec un retard au départ (variable `dep_delay`) compris entre 10 et 15 minutes :
```{r}
filter(flights, dep_delay >= 10 & dep_delay <= 15)
```
Si on passe plusieurs arguments à `filter`, celui-ci rajoute automatiquement une condition *et* entre les conditions. La commande précédente peut donc être écrite de la manière suivante, avec le même résultat :
```{r eval = FALSE}
filter(flights, dep_delay >= 10, dep_delay <= 15)
```
On peut également placer des fonctions dans les tests, qui nous permettent par exemple de sélectionner les vols ayant une distance supérieure à la distance médiane :
```{r}
filter(flights, distance > median(distance))
```
### `select` et `rename` {#sec-select-rename}
`select` permet de sélectionner des colonnes d'un tableau de données. Ainsi, si on veut extraire les colonnes `lat` et `lon` du tableau airports :
```{r}
select(airports, lat, lon)
```
Si on fait précéder le nom d'un `-`, la colonne est éliminée plutôt que sélectionnée :
```{r}
select(airports, -lat, -lon)
```
`select` comprend toute une série de fonctions facilitant la sélection de colonnes multiples. Par exemple, `starts_with`, `ends_width`, `contains` ou `matches` permettent d'exprimer des conditions sur les noms de variables.
```{r}
select(flights, starts_with("dep_"))
```
La syntaxe `colonne1:colonne2` permet de sélectionner toutes les colonnes situées entre `colonne1` et `colonne2` incluses^[À noter que cette opération est un peu plus "fragile" que les autres, car si l'ordre des colonnes change elle peut renvoyer un résultat différent.].
```{r}
select(flights, year:day)
```
`select` propose de nombreuses autres possibilités de sélection qui sont décrites dans [la documentation de l'extension `tidyselect`](https://tidyselect.r-lib.org/reference/language.html).
Une variante de `select` est `rename`^[Il est également possible de renommer des colonnes directement avec `select`, avec la même syntaxe que pour `rename`.], qui permet de renommer des colonnes. On l'utilise en lui passant des paramètres de la forme `nouveau_nom = ancien_nom`. Ainsi, si on veut renommer les colonnes `lon` et `lat` de `airports` en `longitude` et `latitude` :
```{r}
rename(airports, longitude = lon, latitude = lat)
```
Si les noms de colonnes comportent des espaces ou des caractères spéciaux, on peut les entourer de guillemets (`"`) ou de quotes inverses (`` ` ``) :
```{r}
tmp <- rename(
flights,
"retard départ" = dep_delay,
"retard arrivée" = arr_delay
)
select(tmp, `retard départ`, `retard arrivée`)
```
### `arrange`
`arrange` réordonne les lignes d'un tableau selon une ou plusieurs colonnes.
Ainsi, si on veut trier le tableau `flights` selon le retard au départ croissant :
```{r}
arrange(flights, dep_delay)
```
On peut trier selon plusieurs colonnes. Par exemple selon le mois, puis selon le retard au départ :
```{r}
arrange(flights, month, dep_delay)
```
Si on veut trier selon une colonne par ordre décroissant, on lui applique la fonction `desc()` :
```{r}
arrange(flights, desc(dep_delay))
```
Combiné avec `slice`, `arrange` permet par exemple de sélectionner les trois vols ayant eu le plus de retard :
```{r}
tmp <- arrange(flights, desc(dep_delay))
slice(tmp, 1:3)
```
### `mutate`
`mutate` permet de créer de nouvelles colonnes dans le tableau de données, en général à partir de variables existantes.
Par exemple, la table `flights` contient la durée du vol en minutes.. Si on veut créer une nouvelle variable `duree_h` avec cette durée en heures, on peut faire :
```{r}
flights <- mutate(flights, duree_h = air_time / 60)
select(flights, air_time, duree_h)
```
On peut créer plusieurs nouvelles colonnes en une seule commande, et les expressions successives peuvent prendre en compte les résultats des calculs précédents. L'exemple suivant convertit d'abord la durée en heures dans une variable `duree_h` et la distance en kilomètres dans une variable `distance_km`, puis utilise ces nouvelles colonnes pour calculer la vitesse en km/h.
```{r}
flights <- mutate(
flights,
duree_h = air_time / 60,
distance_km = distance / 0.62137,
vitesse = distance_km / duree_h
)
select(flights, air_time, duree_h, distance, distance_km, vitesse)
```
À noter que `mutate` est évidemment parfaitement compatible avec les fonctions vues @sec-vectorfactor sur les recodages : `fct_recode`, `ifelse`, `case_when`...
L'avantage d'utiliser `mutate` est double. D'abord il permet d'éviter d'avoir à saisir le nom du tableau de données dans les conditions d'un `ifelse` ou d'un `case_when` :
```{r, eval=FALSE}
flights <- mutate(
flights,
type_retard = case_when(
dep_delay > 0 & arr_delay > 0 ~ "Retard départ et arrivée",
dep_delay > 0 & arr_delay <= 0 ~ "Retard départ",
dep_delay <= 0 & arr_delay > 0 ~ "Retard arrivée",
TRUE ~ "Aucun retard"
)
)
```
Ensuite, il permet aussi d'intégrer ces recodages dans un *pipeline* de traitement de données, concept présenté dans la section suivante.
## Enchaîner les opérations avec le *pipe* {#sec-pipe}
Quand on manipule un tableau de données, il est très fréquent d'enchaîner plusieurs opérations. On va par exemple extraire une sous-population avec `filter`, sélectionner des colonnes avec `select` puis trier selon une variable avec `arrange`, etc.
Quand on veut enchaîner des opérations, on peut le faire de différentes manières. La première est d'effectuer toutes les opérations en une fois en les "emboîtant" :
```{r eval=FALSE}
arrange(select(filter(flights, dest == "LAX"), dep_delay, arr_delay), dep_delay)
```
Cette notation a plusieurs inconvénients :
- elle est peu lisible
- les opérations apparaissent dans l'ordre inverse de leur réalisation. Ici on effectue d'abord le `filter`, puis le `select`, puis le `arrange`, alors qu'à la lecture du code c'est le `arrange` qui apparaît en premier.
- Il est difficile de voir quel paramètre se rapporte à quelle fonction
Une autre manière de faire est d'effectuer les opérations les unes après les autres, en stockant les résultats intermédiaires dans un objet temporaire :
```{r eval=FALSE}
tmp <- filter(flights, dest == "LAX")
tmp <- select(tmp, dep_delay, arr_delay)
arrange(tmp, dep_delay)
```
C'est nettement plus lisible, l'ordre des opérations est le bon, et les paramètres sont bien rattachés à leur fonction. Par contre, ça reste un peu "verbeux", et on crée un objet temporaire `tmp` dont on n'a pas réellement besoin.
Pour simplifier et améliorer encore la lisibilité du code, on va utiliser un nouvel opérateur, baptisé *pipe*^[Le *pipe* a été introduit à l'origine par l'extension `magrittr`, et repris par `dplyr`]. Le *pipe* se note `%>%`, et son fonctionnement est le suivant : si j'exécute `expr %>% f`, alors le résultat de l'expression `expr`, à gauche du *pipe*, sera passé comme premier argument à la fonction `f`, à droite du *pipe*, ce qui revient à exécuter `f(expr)`.
Ainsi les deux expressions suivantes sont rigoureusement équivalentes :
```{r eval=FALSE}
filter(flights, dest == "LAX")
```
```{r eval=FALSE}
flights %>% filter(dest == "LAX")
```
Ce qui est particulièrement intéressant, c'est qu'on va pouvoir enchaîner les *pipes*. Plutôt que d'écrire :
```{r eval=FALSE}
select(filter(flights, dest == "LAX"), dep_delay, arr_delay)
```
On va pouvoir faire :
```{r eval=FALSE}
flights %>% filter(dest == "LAX") %>% select(dep_delay, arr_delay)
```
À chaque fois, le résultat de ce qui se trouve à gauche du *pipe* est passé comme premier argument à ce qui se trouve à droite : on part de l'objet `flights`, qu'on passe comme premier argument à la fonction `filter`, puis on passe le résultat de ce `filter` comme premier argument du `select`.
Le résultat final est le même avec les deux syntaxes, mais avec le *pipe* l'ordre des opérations correspond à l'ordre naturel de leur exécution, et on n'a pas eu besoin de créer d'objet intermédiaire.
Si la liste des fonctions enchaînées est longue, on peut les répartir sur plusieurs lignes à condition que l'opérateur `%>%` soit en fin de ligne :
```{r, eval=FALSE}
flights %>%
filter(dest == "LAX") %>%
select(dep_delay, arr_delay) %>%
arrange(dep_delay)
```
::: {.callout-note}
On appelle une suite d'instructions de ce type un *pipeline*.
:::
Évidemment, il est naturel de vouloir récupérer le résultat final d'un *pipeline* pour le stocker dans un objet. On peut stocker le résultat du *pipeline* ci-dessus dans un nouveau tableau `delay_la` de la manière suivante :
```{r}
delay_la <- flights %>%
filter(dest == "LAX") %>%
select(dep_delay, arr_delay) %>%
arrange(dep_delay)
```
Dans ce cas, `delay_la` contiendra le tableau final, obtenu après application des trois instructions `filter`, `select` et `arrange`.
Cette notation n'est pas forcément très intuitive au départ : il faut bien comprendre que c'est le résultat final, une fois application de toutes les opérations du *pipeline*, qui est renvoyé et stocké dans l'objet en début de ligne.
Une manière de le comprendre peut être de voir que la notation suivante :
```{r}
delay_la <- flights %>%
filter(dest == "LAX") %>%
select(dep_delay, arr_delay)
```
est équivalente à :
```{r}
delay_la <- (flights %>% filter(dest == "LAX") %>% select(dep_delay, arr_delay))
```
::: {.callout-note}
L'utilisation du *pipe* n'est pas obligatoire, mais elle rend les scripts plus lisibles et plus rapides à saisir. On l'utilisera donc dans ce qui suit.
:::
::: {.callout-warning}
Depuis la version 4.1, R propose un *pipe* "natif", qui fonctionne partout, même si on n'utilise pas les extensions du *tidyverse*. Celui-ci est noté `|>`.
Il s'utilise de la même manière que `%>%` :
`flights |> filter(dest == "LAX")`
Ce *pipe* natif est à la fois un peu plus rapide et un peu moins souple. Par exemple, il est possible avec `%>%` d'appeler une fonction sans mettre de parenthèses :
`df %>% View`
Ce n'est pas possible d'omettre les parenthèses avec `|>`, on doit obligatoirement faire :
`df |> View()`
Dans la suite de ce document on privilégiera (pour l'instant) le *pipe* du *tidyverse* `%>%`, pour des raisons de compatibilité avec des versions de R moins récentes.
:::
## Opérations groupées
### `group_by`
Un élément très important de `dplyr` est la fonction `group_by`. Elle permet de définir des groupes de lignes à partir des valeurs d'une ou plusieurs colonnes. Par exemple, on peut grouper les vols selon leur mois :
```{r}
flights %>% group_by(month)
```
Par défaut ceci ne fait rien de visible, à part l'apparition d'une mention `Groups` dans l'affichage du résultat. Mais à partir du moment où des groupes ont été définis, les verbes comme `slice`, `mutate` ou `summarise` vont en tenir compte lors de leurs opérations.
Par exemple, si on applique `slice` à un tableau préalablement groupé, il va sélectionner les lignes aux positions indiquées *pour chaque groupe*. Ainsi la commande suivante affiche le premier vol de chaque mois, selon leur ordre d'apparition dans le tableau :
```{r}
flights %>% group_by(month) %>% slice(1)
```
Plus utile, en utilisant une variante comme `slice_min` ou `slice_max`, on peut sélectionner les lignes ayant les valeurs les plus grandes ou les plus petites *pour chaque groupe*. Par exemple la commande suivant sélectionne, pour chaque mois de l'année, le vol ayant eu le retard le plus important.
```{r}
flights %>% group_by(month) %>% slice_max(dep_delay)
```
Idem pour `mutate` : les opérations appliquées lors du calcul des valeurs des nouvelles colonnes sont appliquées groupe de lignes par groupe de lignes. Dans l'exemple suivant, on ajoute une nouvelle colonne qui contient le retard moyen *pour chaque compagnie aérienne*. Cette valeur est donc différente d'une compagnie à une autre, mais identique pour tous les vols d'une même compagnie :
```{r}
flights %>%
group_by(carrier) %>%
mutate(mean_delay_carrier = mean(dep_delay, na.rm = TRUE)) %>%
select(dep_delay, mean_delay_carrier)
```
Ceci peut permettre, par exemple, de déterminer si un retard donné est supérieur ou inférieur au retard médian de la compagnie :
```{r}
flights %>%
group_by(carrier) %>%
mutate(
median_delay = median(dep_delay, na.rm = TRUE),
delay_carrier = ifelse(
dep_delay > median_delay,
"Supérieur",
"Inférieur ou égal"
)
) %>%
select(dep_delay, median_delay, delay_carrier)
```
`group_by` peut aussi être utile avec `filter`, par exemple pour sélectionner *pour chaque mois* les vols avec un retard au départ plus élevé que le retard moyen ce mois-ci.
```{r}
flights %>%
group_by(month) %>%
filter(dep_delay >= mean(dep_delay, na.rm = TRUE))
```
::: {.callout-warning}
**Attention :** la clause `group_by` marche pour les verbes déjà vus précédemment, *sauf* pour `arrange`, qui par défaut trie la table sans tenir compte des groupes. Pour obtenir un tri par groupe, il faut lui ajouter l'argument `.by_group = TRUE`.
:::
On peut voir la différence en comparant les deux résultats suivants :
```{r}
flights %>%
group_by(month) %>%
arrange(desc(dep_delay))
```
```{r}
flights %>%
group_by(month) %>%
arrange(desc(dep_delay), .by_group = TRUE)
```
### `summarise` et `count`
`summarise` permet d'agréger les lignes du tableau en effectuant une opération "résumée" sur une ou plusieurs colonnes. Par exemple, si on souhaite connaître les retards moyens au départ et à l'arrivée pour l'ensemble des vols du tableau `flights` :
```{r}
flights %>%
summarise(
retard_dep = mean(dep_delay, na.rm = TRUE),
retard_arr = mean(arr_delay, na.rm = TRUE)
)
```
Cette fonction est en général utilisée avec `group_by`, puisqu'elle permet du coup d'agréger et résumer les lignes du tableau groupe par groupe. Si on souhaite calculer le délai maximum, le délai minimum et le délai moyen au départ pour chaque mois, on pourra faire :
```{r}
flights %>%
group_by(month) %>%
summarise(
max_delay = max(dep_delay, na.rm = TRUE),
min_delay = min(dep_delay, na.rm = TRUE),
mean_delay = mean(dep_delay, na.rm = TRUE)
)
```
`summarise` dispose d'un opérateur spécial, `n()`, qui retourne le nombre de lignes du groupe. Ainsi si on veut le nombre de vols par destination, on peut utiliser :
```{r}
flights %>%
group_by(dest) %>%
summarise(nb = n())
```
`n()` peut aussi être utilisée avec `filter` et `mutate`.
À noter que quand on veut compter le nombre de lignes par groupe, il est plus simple d'utiliser directement la fonction `count`. Ainsi le code suivant est identique au précédent :
```{r}
flights %>%
count(dest)
```
### Grouper selon plusieurs variables
On peut grouper selon plusieurs variables à la fois, il suffit de les indiquer dans la clause du `group_by`. Le *pipeline* suivant calcule le retard moyen au départ pour chaque mois et pour chaque destination, et trie le résultat par retard décroissant :
```{r}
flights %>%
group_by(month, dest) %>%
summarise(retard_moyen = mean(dep_delay, na.rm = TRUE)) %>%
arrange(desc(retard_moyen))
```
On peut également utiliser `count` sur plusieurs variables. Les commandes suivantes comptent le nombre de vols pour chaque couple aéroport de départ / aéroport d'arrivée, et trie le résultat par nombre de vols décroissant. Ici la colonne qui contient le nombre de vols, créée par `count`, s'appelle `n` par défaut :
```{r}
flights %>%
count(origin, dest) %>%
arrange(desc(n))
```
On peut utiliser plusieurs opérations de groupage dans le même *pipeline*. Ainsi, si on souhaite déterminer le couple aéroport de départ / aéroport d'arrivée ayant le retard moyen au départ le plus élevé pour chaque mois de l'année, on devra procéder en deux étapes :
- d'abord grouper selon mois, aéroports d'origine et d'arrivée pour calculer le retard moyen
- puis grouper uniquement selon le mois pour sélectionner le mois avec le retard moyen maximal.
Au final, on obtient le code suivant :
```{r}
flights %>%
group_by(month, origin, dest) %>%
summarise(retard_moyen = mean(dep_delay, na.rm = TRUE)) %>%
group_by(month) %>%
slice_max(retard_moyen)
```
### Dégroupage
Lorsqu'on effectue un `group_by` suivi d'un `summarise`, le tableau résultat est automatiquement dégroupé *de la dernière variable de regroupement*. Ainsi le tableau généré par le code suivant est seulement groupé par `month` et `origin` :
```{r}
flights %>%
group_by(month, origin, dest) %>%
summarise(retard_moyen = mean(dep_delay, na.rm = TRUE))
```
`dplyr` nous le signale d'ailleurs via un message d'avertissement : `summarise() has grouped output by 'month', 'origin'`.
Ce dégroupage progressif peut permettre "d'enchaîner" les opérations groupées. Dans l'exemple suivant on calcule le retard moyen au départ par destination et on conserve les trois retards les plus importants *pour chaque mois*.
```{r}
flights %>%
group_by(month, dest) %>%
summarise(retard_moyen = mean(dep_delay, na.rm = TRUE)) %>%
slice_max(retard_moyen, n = 3)
```
On peut à tout moment "dégrouper" un tableau à l'aide de `ungroup`. C'est nécessaire, dans l'exemple précédent, si on veut seulement récupérer les trois retards les plus importants pour l'ensemble des couples mois / destination.
```{r}
flights %>%
group_by(month, dest) %>%
summarise(retard_moyen = mean(dep_delay, na.rm = TRUE)) %>%
ungroup() %>%
slice_max(retard_moyen, n = 3)
```
On peut aussi spécifier précisément le comportement de dégroupage de `summarise` en lui fournissant un argument supplémentaire `.groups` qui peut prendre notamment les valeurs suivantes :
- `"drop_last"` : dégroupe seulement de la dernière variable de groupage
- `"drop"` : dégroupe totalement le tableau résultat (équivaut à l'application d'un `ungroup`)
- `"keep"` : conserve toutes les variables de groupage
Ce concept de dégroupage successif peut être un peu déroutant de prime abord. Il est donc utile de faire attention aux avertissements affichés par ces opérations, et il ne faut pas hésiter à ajouter un `ungroup` en fin de pipeline si on sait qu'on ne souhaite pas utiliser les groupes encore existants par la suite.
À noter que la fonction `count`, de son côté, renvoie un tableau non groupé.
```{r}
flights %>%
count(month, dest)
```
## Autres fonctions utiles
`dplyr` contient beaucoup d'autres fonctions utiles pour la manipulation de données.
### `slice_sample`
Ce verbe permet de sélectionner aléatoirement un nombre de lignes (avec l'argument `n`) ou une fraction des lignes (avec l'argument `prop`) d'un tableau.
Ainsi si on veut choisir 5 lignes au hasard dans le tableau `airports` :
```{r}
airports %>% slice_sample(n = 5)
```
Si on veut tirer au hasard 10% des lignes de `flights` :
```{r}
flights %>% slice_sample(prop = 0.1)
```
Ces fonctions sont utiles notamment pour faire de "l'échantillonnage" en tirant au hasard un certain nombre d'observations du tableau.
### `lead` et `lag`
`lead` et `lag` permettent de décaler les observations d'une variable d'un cran vers l'arrière (pour `lead`) ou vers l'avant (pour `lag`).
```{r}
lead(1:5)
lag(1:5)
```
Ceci peut être utile pour des données de type "séries temporelles". Par exemple, on peut facilement calculer l'écart entre le retard au départ de chaque vol et celui du vol précédent :
```{r}
flights %>%
mutate(
dep_delay_prev = lag(dep_delay),
dep_delay_diff = dep_delay - dep_delay_prev
) %>%
select(dep_delay_prev, dep_delay, dep_delay_diff)
```
### `distinct` et `n_distinct`
`distinct` filtre les lignes du tableau pour ne conserver que les lignes distinctes, en supprimant toutes les lignes en double.
```{r}
flights %>%
select(day, month) %>%
distinct()
```
On peut lui spécifier une liste de variables : dans ce cas, pour toutes les observations ayant des valeurs identiques pour les variables en question, `distinct` ne conservera que la première d'entre elles.
```{r}
flights %>%
distinct(month, day)
```
L'option `.keep_all` permet, dans l'opération précédente, de conserver l'ensemble des colonnes du tableau :
```{r}
flights %>%
distinct(month, day, .keep_all = TRUE)
```
La fonction `n_distinct`, elle, renvoie le nombre de valeurs distinctes d'un vecteur. On peut notamment l'utiliser dans un `summarise`.
Dans l'exemple qui suit on calcule, pour les trois aéroports de départ de la table `flights` le nombre de valeurs distinctes de l'aéroport d'arrivée :
```{r}
flights %>%
group_by(origin) %>%
summarise(n_dest = n_distinct(dest))
```
### `relocate`
`relocate` peut être utilisé pour réordonner les colonnes d'une table. Par défaut, si on lui passe un ou plusieurs noms de colonnes, `relocate` les place en début de tableau.
```{r}
airports %>% relocate(lat, lon)
```
Les arguments supplémentaires `.before` et `.after` permettent de préciser à quel endroit déplacer la ou les colonnes indiquées.
```{r}
airports %>% relocate(starts_with('tz'), .after = name)
```
## Tables multiples
Le jeu de données `nycflights13` est un exemple de données réparties en plusieurs tables. Ici on en a trois : les informations sur les vols dans `flights`, celles sur les aéroports dans `airports` et celles sur les compagnies aériennes dans `airlines`.
`dplyr` propose différentes fonctions permettant de travailler avec des données structurées de cette manière.
### Concaténation : `bind_rows` et `bind_cols`
Les fonctions `bind_rows` et `bind_cols` permettent d'ajouter des lignes (respectivement des colonnes) à une table à partir d'une ou plusieurs autres tables.
L'exemple suivant (certes très artificiel) montre l'utilisation de `bind_rows`. On commence par créer trois tableaux `t1`, `t2` et `t3` :
```{r}
t1 <- airports %>%
select(faa, name, lat, lon) %>%
slice(1:2)
t1
```
```{r}
t2 <- airports %>%
select(faa, name, lat, lon) %>%
slice(5:6)
t2
```
```{r}
t3 <- airports %>%
select(faa, name) %>%
slice(100:101)
t3
```
On concaténe ensuite les trois tables avec `bind_rows` :
```{r}
bind_rows(t1, t2, t3)
```
On remarquera que si des colonnes sont manquantes pour certaines tables, comme les colonnes `lat` et `lon` de `t3`, des `NA` sont automatiquement insérées.
Il peut être utile, quand on concatène des lignes, de garder une trace du tableau d'origine de chacune des lignes dans le tableau final. C'est possible grâce à l'argument `.id` de `bind_rows`. On passe à cet argument le nom d'une colonne qui contiendra l'indicateur d'origine des lignes :
```{r}
bind_rows(t1, t2, t3, .id = "source")
```
Par défaut la colonne `.id` ne contient qu'un nombre, différent pour chaque tableau. On peut lui spécifier des valeurs plus explicites en "nommant" les tables dans `bind_rows` de la manière suivante :
```{r}
bind_rows(table1 = t1, table2 = t2, table3 = t3, .id = "source")
```
`bind_cols` permet de concaténer des colonnes et fonctionne de manière similaire :
```{r}
t1 <- flights %>% slice(1:5) %>% select(dep_delay, dep_time)
t2 <- flights %>% slice(1:5) %>% select(origin, dest)
t3 <- flights %>% slice(1:5) %>% select(arr_delay, arr_time)
bind_cols(t1, t2, t3)
```
À noter que `bind_cols` associe les lignes uniquement *par position*. Les lignes des différents tableaux associés doivent donc correspondre (et leur nombre doit être identique). Pour associer des tables *par valeur*, on doit utiliser des jointures.
### Jointures
#### Clés implicites
Très souvent, les données relatives à une analyse sont réparties dans plusieurs tables différentes. Dans notre exemple, on peut voir que la table `flights` contient le code de la compagnie aérienne du vol dans la variable `carrier` :
```{r}
flights %>% select(carrier)
```
Et que par ailleurs la table `airlines` contient une information supplémentaire relative à ces compagnies, à savoir le nom complet.
```{r}
airlines
```
Il est donc naturel de vouloir associer les deux, ici pour ajouter les noms complets des compagnies à la table `flights`. Pour cela on va effectuer une *jointure* : les lignes d'une table seront associées à une autre en se basant non pas sur leur position, mais sur les valeurs d'une ou plusieurs colonnes. Ces colonnes sont appelées des *clés*.
Pour faire une jointure de ce type, on va utiliser la fonction `left_join` :
```{r eval = FALSE}
left_join(flights, airlines)
```
Pour faciliter la lecture, on va afficher seulement certaines colonnes du résultat :
```{r}
left_join(flights, airlines) %>%
select(month, day, carrier, name)
```
On voit que la table résultat est bien la fusion des deux tables d'origine selon les valeurs des deux colonnes clés `carrier`. On est parti de la table `flights`, et pour chaque ligne de celle-ci on a ajouté les colonnes de `airlines` pour lesquelles la valeur de `carrier` est la même. On a donc bien une nouvelle colonne `name` dans notre table résultat, avec le nom complet de la compagnie aérienne.
::: {.callout-note}
À noter qu'on peut tout à fait utiliser le *pipe* avec les fonctions de jointure :
`flights %>% left_join(airlines)`.
:::
Nous sommes ici dans le cas le plus simple concernant les clés de jointure : les deux clés sont uniques et portent le même nom dans les deux tables. Par défaut, si on ne lui spécifie pas explicitement les clés, `dplyr` fusionne en utilisant l'ensemble des colonnes communes aux deux tables. On peut d'ailleurs voir dans cet exemple qu'un message a été affiché précisant que la jointure s'est bien faite sur la variable `carrier`.
#### Clés explicites
La table `airports`, contient des informations supplémentaires sur les aéroports : nom complet, altitude, position géographique, etc. Chaque aéroport est identifié par un code contenu dans la colonne `faa`.
Si on regarde la table `flights`, on voit que le code d'identification des aéroports apparaît à deux endroits différents : pour l'aéroport de départ dans la colonne `origin`, et pour celui d'arrivée dans la colonne `dest`. On a donc deux clés de jointure possibles, et qui portent un nom différent de la clé de `airports`.
On va commencer par fusionner les données concernant l'aéroport de départ. Pour simplifier l'affichage des résultats, on va se contenter d'un sous-ensemble des deux tables :
```{r}
flights_ex <- flights %>% select(month, day, origin, dest)
airports_ex <- airports %>% select(faa, alt, name)
```
Si on se contente d'un `left_join` comme à l'étape précédente, on obtient un message d'erreur car aucune colonne commune ne peut être identifiée comme clé de jointure :
```{r error=TRUE}
flights_ex %>% left_join(airports_ex)
```
On doit donc spécifier explicitement les clés avec l'argument `by` de `left_join`. Ici la clé est nommée `origin` dans la première table, et `faa` dans la seconde. La syntaxe est donc la suivante :
```{r}
flights_ex %>%
left_join(airports_ex, by = c("origin" = "faa"))
```
On constate que les deux nouvelles colonnes `name` et `alt` contiennent bien les données correspondant à l'aéroport de départ.
On va stocker le résultat de cette jointure dans la table `flights_ex` :
```{r}
flights_ex <- flights_ex %>%
left_join(airports_ex, by = c("origin" = "faa"))
```
Supposons qu'on souhaite maintenant fusionner à nouveau les informations de la table `airports`, mais cette fois pour les aéroports d'arrivée de notre nouvelle table `flights_ex`. Les deux clés sont donc désormais `dest` dans la première table, et `faa` dans la deuxième. La syntaxe est donc la suivante :
```{r}
flights_ex %>%
left_join(airports_ex, by = c("dest" = "faa"))
```
Cela fonctionne, les informations de l'aéroport d'arrivée ont bien été ajoutées, mais on constate que les colonnes ont été renommées. En effet, ici les deux tables fusionnées contenaient toutes les deux des colonnes `name` et `alt`. Comme on ne peut pas avoir deux colonnes avec le même nom dans un tableau, `dplyr` a renommé les colonnes de la première table en `name.x` et `alt.x`, et celles de la deuxième en `name.y` et `alt.y`.
C'est pratique, mais pas forcément très parlant. On pourrait renommer manuellement les colonnes avec `rename` avant de faire la jointure pour avoir des intitulés plus explicites, mais on peut aussi utiliser l'argument `suffix` de `left_join`, qui permet d'indiquer les suffixes à ajouter aux colonnes.
```{r}
flights_ex %>%
left_join(
airports_ex,
by = c("dest" = "faa"),
suffix = c("_depart", "_arrivee")
)
```
On obtient ainsi directement des noms de colonnes nettement plus clairs.
### Types de jointures
Jusqu'à présent nous avons utilisé la fonction `left_join`, mais il existe plusieurs types de jointures.
Partons de deux tables d'exemple, `personnes` et `voitures` :
```{r}
personnes <- tibble(
nom = c("Sylvie", "Sylvie", "Monique", "Gunter", "Rayan", "Rayan"),
voiture = c("Twingo", "Ferrari", "Scenic", "Lada", "Twingo", "Clio")
)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes)
```
```{r}
voitures <- tibble(
voiture = c("Twingo", "Ferrari", "Clio", "Lada", "208"),
vitesse = c("140", "280", "160", "85", "160")
)
```
```{r, echo=FALSE, eval=TRUE}
kable(voitures)
```
#### left_join
Si on fait un `left_join` de `voitures` sur `personnes` :
```{r, echo=TRUE, eval=FALSE}
personnes %>% left_join(voitures)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes %>% left_join(voitures))
```
On voit que chaque ligne de `personnes` est bien présente, et qu'on lui a ajouté une ligne de `voitures` correspondante si elle existe. Dans le cas du `Scenic`, il n'y a avait pas de ligne dans `voitures`, donc `vitesse` a été mise à `NA`. Dans le cas de `208`, présente dans `voitures` mais pas dans `personnes`, la ligne n'apparaît pas.
Si on fait un `left_join` cette fois de `personnes` sur `voitures`, c'est l'inverse :
```{r, echo=TRUE, eval=FALSE}
voitures %>% left_join(personnes)
```
```{r, echo=FALSE, eval=TRUE}
kable(voitures %>% left_join(personnes))
```
La ligne `208` est là, mais `nom` est à `NA`. Par contre `Monique` est absente. Et on remarquera que la ligne `Twingo`, présente deux fois dans `personnes`, a été dupliquée pour être associée aux deux lignes de données de `Sylvie` et `Rayan`.
En résumé, quand on fait un `left_join(x, y)`, toutes les lignes de `x` sont présentes, et dupliquées si nécessaire quand elles apparaissent plusieurs fois dans `y`. Les lignes de `y` non présentes dans `x` disparaissent. Les lignes de `x` non présentes dans `y` se voient attribuer des `NA` pour les nouvelles colonnes.
Intuitivement, on pourrait considérer que `left_join(x, y)` signifie "ramener l'information de la table `y` sur la table `x`".
En général, `left_join` sera le type de jointures le plus fréquemment utilisé.
#### right_join
La jointure `right_join` est l'exacte symétrique de `left_join`, c'est-à dire que `right_join(x, y)` est équivalent à `left_join(y, x)` :
```{r, echo=TRUE, eval=FALSE}
personnes %>% right_join(voitures)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes %>% right_join(voitures))
```
#### inner_join
Dans le cas de `inner_join(x, y)`, seules les lignes présentes à la fois dans `x` et `y` sont conservées (et si nécessaire dupliquées) dans la table résultat :
```{r, echo=TRUE, eval=FALSE}
personnes %>% inner_join(voitures)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes %>% inner_join(voitures))
```
Ici la ligne `208` est absente, ainsi que la ligne `Monique`, qui dans le cas d'un `left_join` avait été conservée et s'était vue attribuer une `vitesse` à `NA`.
#### full_join
Dans le cas de `full_join(x, y)`, toutes les lignes de `x` et toutes les lignes de `y` sont conservées (avec des `NA` ajoutés si nécessaire) même si elles sont absentes de l'autre table :
```{r, echo=TRUE, eval=FALSE}
personnes %>% full_join(voitures)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes %>% full_join(voitures))
```
#### semi_join et anti_join
`semi_join` et `anti_join` sont des jointures *filtrantes*, c'est-à-dire qu'elles sélectionnent les lignes de `x` sans ajouter les colonnes de `y`.
Ainsi, `semi_join` ne conservera que les lignes de `x` pour lesquelles une ligne de `y` existe également, et supprimera les autres. Dans notre exemple, la ligne `Monique` est donc supprimée :
```{r, echo=TRUE, eval=FALSE}
personnes %>% semi_join(voitures)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes %>% semi_join(voitures))
```
Un `anti_join` fait l'inverse, il ne conserve que les lignes de `x` absentes de `y`. Dans notre exemple, on ne garde donc que la ligne `Monique` :
```{r, echo=TRUE, eval=FALSE}
personnes %>% anti_join(voitures)
```
```{r, echo=FALSE, eval=TRUE}
kable(personnes %>% anti_join(voitures))
```
## Ressources
Toutes les ressources ci-dessous sont en anglais...
Le livre *R for data science*, librement accessible en ligne, contient plusieurs chapitres très complets sur la manipulation des données, notamment :
- [Data transformation](http://r4ds.had.co.nz/transform.html) pour les manipulations
- [Relational data](http://r4ds.had.co.nz/relational-data.html) pour les tables multiples
Le [site de l'extension](http://dplyr.tidyverse.org/) comprend une [liste des fonctions](http://dplyr.tidyverse.org/reference/index.html) et les pages d'aide associées, mais aussi une [introduction](http://dplyr.tidyverse.org/articles/dplyr.html) au package et plusieurs articles dont un spécifiquement sur les [jointures](http://dplyr.tidyverse.org/articles/two-table.html).
Enfin, une "antisèche" très synthétique est également accessible depuis RStudio, en allant dans le menu *Help* puis *Cheatsheets* et *Data Transformation with dplyr*.
## Exercices
On commence par charger les extensions et les données nécessaires.
```{r echo=TRUE, message=FALSE, warning=FALSE}
library(tidyverse)
library(nycflights13)
data(flights)
data(airports)
data(airlines)
```
### Les verbes de base de `dplyr`
**Exercice 1.1**
Sélectionner la dixième ligne du tableau des aéroports (`airports`).
```{r echo=FALSE, ref.label='dp11'}
```
::: {.solution-exo}
```{r dp11, eval=FALSE}
slice(airports, 10)
```
:::
Sélectionner les 5 premières lignes de la table `airlines`.
```{r echo=FALSE, ref.label='dp11b'}
```
::: {.solution-exo}
```{r dp11b, eval=FALSE}
slice_head(airlines, n = 5)
```
:::