10  Manipulation de données avec dplyr

dplyr, un package du tidyverse simplifie grandement la manipulation de tableaux de données. Il permet de renommer, ordonner, sélectionner et crééer de nouvelles colonnes, filtrer des lignes, faire facilement des résumés groupés et des sous-jeux de données.

Celles et ceux familiers avec SQL, et plus largement les bases de données, seront en terrain connu.

10.1 tibble

dplyr comme le reste du tidyverse s’appuie massivement sur les data.frame, dont la méthode d’affichage n’était pas optimale. Le package tibble introduit une classe légèrement modifiée de data.frames dont la conséquence la plus visibles est la méthode d’affichage : à la fois plus compacte et plus informative.


Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union

Comparez l’affichage par défaut de ça : (je ne l’affiche pas ici par empathie envers vos molettes de souris) :

iris # berk

à ça :

iris %>% as_tibble() # <3
# A tibble: 150 × 5
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
          <dbl>       <dbl>        <dbl>       <dbl> <fct>  
 1          5.1         3.5          1.4         0.2 setosa 
 2          4.9         3            1.4         0.2 setosa 
 3          4.7         3.2          1.3         0.2 setosa 
 4          4.6         3.1          1.5         0.2 setosa 
 5          5           3.6          1.4         0.2 setosa 
 6          5.4         3.9          1.7         0.4 setosa 
 7          4.6         3.4          1.4         0.3 setosa 
 8          5           3.4          1.5         0.2 setosa 
 9          4.4         2.9          1.4         0.2 setosa 
10          4.9         3.1          1.5         0.1 setosa 
# ℹ 140 more rows

Les fonctions de dplyr sont toutes des verbes, qui traduisent bien l’action à mener.

Le premier argument est toujours un tibble et la sortie est aussi un tibble. Ce qui se marie à merveille avec le pipe de maggritr %>% ou le pipe désormais embarqué dans R |>.

La plupart des commandes présentées ci-dessous sont tellement explicites qu’elles se passent de commentaires.

10.2 select pour sélectionner des colonnes

Indexer par les noms de colonnes. Ici on ne retient que celles passées à select :

starwars %>% 
  select(name, height, sex, species)
# A tibble: 87 × 4
   name               height sex    species
   <chr>               <int> <chr>  <chr>  
 1 Luke Skywalker        172 male   Human  
 2 C-3PO                 167 none   Droid  
 3 R2-D2                  96 none   Droid  
 4 Darth Vader           202 male   Human  
 5 Leia Organa           150 female Human  
 6 Owen Lars             178 male   Human  
 7 Beru Whitesun Lars    165 female Human  
 8 R5-D4                  97 none   Droid  
 9 Biggs Darklighter     183 male   Human  
10 Obi-Wan Kenobi        182 male   Human  
# ℹ 77 more rows

Mais on peut aussi utiliser une indexation positionnelle

starwars %>% 
  select(1:5)
# A tibble: 87 × 5
   name               height  mass hair_color    skin_color 
   <chr>               <int> <dbl> <chr>         <chr>      
 1 Luke Skywalker        172    77 blond         fair       
 2 C-3PO                 167    75 <NA>          gold       
 3 R2-D2                  96    32 <NA>          white, blue
 4 Darth Vader           202   136 none          white      
 5 Leia Organa           150    49 brown         light      
 6 Owen Lars             178   120 brown, grey   light      
 7 Beru Whitesun Lars    165    75 brown         light      
 8 R5-D4                  97    32 <NA>          white, red 
 9 Biggs Darklighter     183    84 black         light      
10 Obi-Wan Kenobi        182    77 auburn, white fair       
# ℹ 77 more rows

Et négative :

starwars %>% 
  select(-name, -height, -mass, -hair_color)
# A tibble: 87 × 10
   skin_color eye_color birth_year sex   gender homeworld species films vehicles
   <chr>      <chr>          <dbl> <chr> <chr>  <chr>     <chr>   <lis> <list>  
 1 fair       blue            19   male  mascu… Tatooine  Human   <chr> <chr>   
 2 gold       yellow         112   none  mascu… Tatooine  Droid   <chr> <chr>   
 3 white, bl… red             33   none  mascu… Naboo     Droid   <chr> <chr>   
 4 white      yellow          41.9 male  mascu… Tatooine  Human   <chr> <chr>   
 5 light      brown           19   fema… femin… Alderaan  Human   <chr> <chr>   
 6 light      blue            52   male  mascu… Tatooine  Human   <chr> <chr>   
 7 light      blue            47   fema… femin… Tatooine  Human   <chr> <chr>   
 8 white, red red             NA   none  mascu… Tatooine  Droid   <chr> <chr>   
 9 light      brown           24   male  mascu… Tatooine  Human   <chr> <chr>   
10 fair       blue-gray       57   male  mascu… Stewjon   Human   <chr> <chr>   
# ℹ 77 more rows
# ℹ 1 more variable: starships <list>

Ou encore des helpers fournis par tidyselect (allez donc jeter un oeil à ?tidyselect::language) :

starwars %>% 
  select(name, species, ends_with("color"))
# A tibble: 87 × 5
   name               species hair_color    skin_color  eye_color
   <chr>              <chr>   <chr>         <chr>       <chr>    
 1 Luke Skywalker     Human   blond         fair        blue     
 2 C-3PO              Droid   <NA>          gold        yellow   
 3 R2-D2              Droid   <NA>          white, blue red      
 4 Darth Vader        Human   none          white       yellow   
 5 Leia Organa        Human   brown         light       brown    
 6 Owen Lars          Human   brown, grey   light       blue     
 7 Beru Whitesun Lars Human   brown         light       blue     
 8 R5-D4              Droid   <NA>          white, red  red      
 9 Biggs Darklighter  Human   black         light       brown    
10 Obi-Wan Kenobi     Human   auburn, white fair        blue-gray
# ℹ 77 more rows

10.3 rename : pour renommer des colonnes

rename est bien pratique pour renommer des colonnes

starwars %>% 
  rename(id=name, sp=species) %>% 
  select(id, sp)
# A tibble: 87 × 2
   id                 sp   
   <chr>              <chr>
 1 Luke Skywalker     Human
 2 C-3PO              Droid
 3 R2-D2              Droid
 4 Darth Vader        Human
 5 Leia Organa        Human
 6 Owen Lars          Human
 7 Beru Whitesun Lars Human
 8 R5-D4              Droid
 9 Biggs Darklighter  Human
10 Obi-Wan Kenobi     Human
# ℹ 77 more rows

Sachez que vous pouvez aussi combiner select et rename comme suit :

starwars %>% 
  select(id=name, sp=species)
# A tibble: 87 × 2
   id                 sp   
   <chr>              <chr>
 1 Luke Skywalker     Human
 2 C-3PO              Droid
 3 R2-D2              Droid
 4 Darth Vader        Human
 5 Leia Organa        Human
 6 Owen Lars          Human
 7 Beru Whitesun Lars Human
 8 R5-D4              Droid
 9 Biggs Darklighter  Human
10 Obi-Wan Kenobi     Human
# ℹ 77 more rows

10.4 slice : selection positionnelle des lignes

Vous pouvez passer un ou plusieurs ids de colonnes :

keep_these <- c(1, 5, 8)
starwars %>% slice(keep_these)
# A tibble: 3 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Luke Sky…    172    77 blond      fair       blue              19 male  mascu…
2 Leia Org…    150    49 brown      light      brown             19 fema… femin…
3 R5-D4         97    32 <NA>       white, red red               NA none  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

C’est plus lisible que starwars[keep_these] et ça reste pipable : que demande le peuple ?

Naturellement vous pouvez slicer négativement.

10.5 filter : selection conditionnelle des lignes

Parfait pour inspecter, analyser des sous-jeux de données :

starwars %>% 
  filter(species=="Human")
# A tibble: 35 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 3 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 4 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 5 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 6 Biggs D…    183    84 black      light      brown           24   male  mascu…
 7 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
 8 Anakin …    188    84 blond      fair       blue            41.9 male  mascu…
 9 Wilhuff…    180    NA auburn, g… fair       blue            64   male  mascu…
10 Han Solo    180    80 brown      fair       brown           29   male  mascu…
# ℹ 25 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

On peut combiner les conditions, y compris sur les numeric. Ici, on ne retient que les petites brunes (humaines) aux yeux non bleus :

starwars %>% 
  filter(species=="Human", sex=="female", height <= 170, eye_color != "blue")
# A tibble: 3 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Leia Org…    150    49 brown      light      brown             19 fema… femin…
2 Shmi Sky…    163    NA black      fair       brown             72 fema… femin…
3 Dormé        165    NA brown      light      brown             NA fema… femin…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

C’est peut-être le moment de réviser les opérateurs logiques que nous avons vu à l’apéritif.

Si vous optez, positivement ou négativement, pour plus d’un critère concernant une colonne, vous pouvez utiliser %in%:

starwars %>% 
  filter(species %in% c("Human", "Droid"), # humans and droids
         !(hair_color %in% c("brown", "blond"))) # NO brown or blond hair
# A tibble: 25 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 C-3PO       167  75   <NA>       gold       yellow         112   none  mascu…
 2 R2-D2        96  32   <NA>       white, bl… red             33   none  mascu…
 3 Darth V…    202 136   none       white      yellow          41.9 male  mascu…
 4 Owen La…    178 120   brown, gr… light      blue            52   male  mascu…
 5 R5-D4        97  32   <NA>       white, red red             NA   none  mascu…
 6 Biggs D…    183  84   black      light      brown           24   male  mascu…
 7 Obi-Wan…    182  77   auburn, w… fair       blue-gray       57   male  mascu…
 8 Wilhuff…    180  NA   auburn, g… fair       blue            64   male  mascu…
 9 Palpati…    170  75   grey       pale       yellow          82   male  mascu…
10 Boba Fe…    183  78.2 black      fair       brown           31.5 male  mascu…
# ℹ 15 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

10.6 mutate : pour créer de nouvelles colonnes

Bien utile pour calculer l’indice de masse corporelle de ce beau monde par exemple :

starwars %>% 
  select(name, height, mass, species) %>% 
  mutate(bmi=mass/((height/100)^2))
# A tibble: 87 × 5
   name               height  mass species   bmi
   <chr>               <int> <dbl> <chr>   <dbl>
 1 Luke Skywalker        172    77 Human    26.0
 2 C-3PO                 167    75 Droid    26.9
 3 R2-D2                  96    32 Droid    34.7
 4 Darth Vader           202   136 Human    33.3
 5 Leia Organa           150    49 Human    21.8
 6 Owen Lars             178   120 Human    37.9
 7 Beru Whitesun Lars    165    75 Human    27.5
 8 R5-D4                  97    32 Droid    34.0
 9 Biggs Darklighter     183    84 Human    25.1
10 Obi-Wan Kenobi        182    77 Human    23.2
# ℹ 77 more rows

Vous pouvez utiliser “immédiatement” une colonne créé par mutate. C’est tellement beau :

sw <- starwars %>% 
  select(name, height, mass, species) %>% 
  mutate(height=height/100, bmi=mass/height^2) 
sw
# A tibble: 87 × 5
   name               height  mass species   bmi
   <chr>               <dbl> <dbl> <chr>   <dbl>
 1 Luke Skywalker       1.72    77 Human    26.0
 2 C-3PO                1.67    75 Droid    26.9
 3 R2-D2                0.96    32 Droid    34.7
 4 Darth Vader          2.02   136 Human    33.3
 5 Leia Organa          1.5     49 Human    21.8
 6 Owen Lars            1.78   120 Human    37.9
 7 Beru Whitesun Lars   1.65    75 Human    27.5
 8 R5-D4                0.97    32 Droid    34.0
 9 Biggs Darklighter    1.83    84 Human    25.1
10 Obi-Wan Kenobi       1.82    77 Human    23.2
# ℹ 77 more rows

mutate est un outil très puissant pour mettre de l’ordre et nettoyer vos jeux de données. Pour les facteurs et les chaînes de caractères, il se marie à merveille avec les fonctions de forcats et stringr respectivement. Minute papillon, nous les verrons un peu plus tard.

Avant de quitter mutate, sa variante transmute crée de nouvelles colonnes et omet toutes les autres :

starwars %>% 
  transmute(name, sex, height=height/100)
# A tibble: 87 × 3
   name               sex    height
   <chr>              <chr>   <dbl>
 1 Luke Skywalker     male     1.72
 2 C-3PO              none     1.67
 3 R2-D2              none     0.96
 4 Darth Vader        male     2.02
 5 Leia Organa        female   1.5 
 6 Owen Lars          male     1.78
 7 Beru Whitesun Lars female   1.65
 8 R5-D4              none     0.97
 9 Biggs Darklighter  male     1.83
10 Obi-Wan Kenobi     male     1.82
# ℹ 77 more rows

10.7 arrange : trier les lignes

Un verbe très pratique pour trier les données :

sw %>% 
  arrange(bmi)
# A tibble: 87 × 5
   name          height  mass species      bmi
   <chr>          <dbl> <dbl> <chr>      <dbl>
 1 Wat Tambor      1.93    48 Skakoan     12.9
 2 Padmé Amidala   1.85    45 Human       13.1
 3 Adi Gallia      1.84    50 Tholothian  14.8
 4 Sly Moore       1.78    48 <NA>        15.1
 5 Roos Tarpals    2.24    82 Gungan      16.3
 6 Lama Su         2.29    88 Kaminoan    16.8
 7 Jar Jar Binks   1.96    66 Gungan      17.2
 8 Ayla Secura     1.78    55 Twi'lek     17.4
 9 Shaak Ti        1.78    57 Togruta     18.0
10 Barriss Offee   1.66    50 Mirialan    18.1
# ℹ 77 more rows

Ici, par d’ex aequo sur la colonne bmi mais on aurait pu ajouter une autre colonne dans arrange en cas d’égalité.

Pour trier par ordre descendant, il nous faut ajouter desc pour que les plus voluptueux passe en premier. Notez Yoda en troisième position. Ça c’est de la science, pas de la fiction. En vrai, cela questionne aussi (sans doute) le domaine de validité du BMI.

sw %>% arrange(desc(bmi))
# A tibble: 87 × 5
   name                  height  mass species          bmi
   <chr>                  <dbl> <dbl> <chr>          <dbl>
 1 Jabba Desilijic Tiure   1.75  1358 Hutt           443. 
 2 Dud Bolt                0.94    45 Vulptereen      50.9
 3 Yoda                    0.66    17 Yoda's species  39.0
 4 Owen Lars               1.78   120 Human           37.9
 5 IG-88                   2      140 Droid           35  
 6 R2-D2                   0.96    32 Droid           34.7
 7 Grievous                2.16   159 Kaleesh         34.1
 8 R5-D4                   0.97    32 Droid           34.0
 9 Jek Tono Porkins        1.8    110 <NA>            34.0
10 Darth Vader             2.02   136 Human           33.3
# ℹ 77 more rows

Vous pourriez être tentée de combiner arrange puis slice pour ne retenir que les n plus (ou moins) quelque chose :

sw %>% 
  arrange(desc(bmi)) %>% 
  slice(1:5)
# A tibble: 5 × 5
  name                  height  mass species          bmi
  <chr>                  <dbl> <dbl> <chr>          <dbl>
1 Jabba Desilijic Tiure   1.75  1358 Hutt           443. 
2 Dud Bolt                0.94    45 Vulptereen      50.9
3 Yoda                    0.66    17 Yoda's species  39.0
4 Owen Lars               1.78   120 Human           37.9
5 IG-88                   2      140 Droid           35  

Mais peut-être préférerez vous l’alternative compacte de slice_* et pièces rattachées :

sw %>% slice_max(bmi, n=5)
# A tibble: 5 × 5
  name                  height  mass species          bmi
  <chr>                  <dbl> <dbl> <chr>          <dbl>
1 Jabba Desilijic Tiure   1.75  1358 Hutt           443. 
2 Dud Bolt                0.94    45 Vulptereen      50.9
3 Yoda                    0.66    17 Yoda's species  39.0
4 Owen Lars               1.78   120 Human           37.9
5 IG-88                   2      140 Droid           35  

10.8 count : compter des lignes sur des critères en colonnes

Pour obtenir des résumés, vous pouvez utiliser count qui rajouter une colonne qui peut se révéler fort utile :

starwars %>% 
  count(species, name="N") %>% # if you omit `name`, count create `n` by default
  arrange(desc(N)) %>% 
  slice_head(n=5)
# A tibble: 5 × 2
  species      N
  <chr>    <int>
1 Human       35
2 Droid        6
3 <NA>         4
4 Gungan       3
5 Kaminoan     2

Et si vous voulez garder les autres colonnes, add_count est votre nouveau copain :

starwars %>% 
  select(name, sex, species) %>% 
  add_count(species) %>% 
  filter(n >= 3)
# A tibble: 48 × 4
   name               sex    species     n
   <chr>              <chr>  <chr>   <int>
 1 Luke Skywalker     male   Human      35
 2 C-3PO              none   Droid       6
 3 R2-D2              none   Droid       6
 4 Darth Vader        male   Human      35
 5 Leia Organa        female Human      35
 6 Owen Lars          male   Human      35
 7 Beru Whitesun Lars female Human      35
 8 R5-D4              none   Droid       6
 9 Biggs Darklighter  male   Human      35
10 Obi-Wan Kenobi     male   Human      35
# ℹ 38 more rows

10.9 summarise : résumés et résumés groupés

Créeons d’abord un mini starwars, sur lequel nous allons supprimer les lignes avec des données manquantes pour la colonne ehight puis illustrons les vertus de summary :

sw <- starwars %>% 
  select(species, sex, height) %>% 
  filter(species %in% c("Human", "Droid"), !is.na(height))
sw %>% summarise(height=mean(height))
# A tibble: 1 × 1
  height
   <dbl>
1   171.

Pas dingue hein. Peut être qu’un résumé par espèce et par sexe serait plus intéressant ? group_by est votre allié. On peut rajouter d’autres fonctions de résumé au passage :

sw %>% 
  group_by(species, sex) %>% 
  summarise(mean_height=mean(height),
            sd_height=sd(height))
`summarise()` has grouped output by 'species'. You can override using the
`.groups` argument.
# A tibble: 3 × 4
# Groups:   species [2]
  species sex    mean_height sd_height
  <chr>   <chr>        <dbl>     <dbl>
1 Droid   none          131.     49.1 
2 Human   female        164.     11.9 
3 Human   male          182.      8.16

accross est très utile ici pour résumer plusieurs colonnes, éventuellement avec plusieurs fonctions de résumé :

starwars %>% 
  select(species, height, mass) %>% 
  na.omit() %>% 
  filter(species %in% c("Human", "Droid")) %>% 
  group_by(species) %>%
  summarise(across(c(height, mass), list(mean=mean, sd=sd), .names = "{.col}_{fn}"))
# A tibble: 2 × 5
  species height_mean height_sd mass_mean mass_sd
  <chr>         <dbl>     <dbl>     <dbl>   <dbl>
1 Droid          140       52.0      69.8    51.0
2 Human          180.      11.5      81.3    19.3

10.10 join : combiner des tables

En analyse de données, nous avons souvent des tables à réunir via une colonne commune (par exemple un id). La famille de join est précieuse.

Nous allons utiliser deux petits jeux de données disponibles avec dplyr:

band_members
# A tibble: 3 × 2
  name  band   
  <chr> <chr>  
1 Mick  Stones 
2 John  Beatles
3 Paul  Beatles
band_instruments
# A tibble: 3 × 2
  name  plays 
  <chr> <chr> 
1 John  guitar
2 Paul  bass  
3 Keith guitar

Nous pouvons combiner ces tables avec left_join et ses variantes.

left_join(band_members, band_instruments, by="name")
# A tibble: 3 × 3
  name  band    plays 
  <chr> <chr>   <chr> 
1 Mick  Stones  <NA>  
2 John  Beatles guitar
3 Paul  Beatles bass  
right_join(band_members, band_instruments, by="name")
# A tibble: 3 × 3
  name  band    plays 
  <chr> <chr>   <chr> 
1 John  Beatles guitar
2 Paul  Beatles bass  
3 Keith <NA>    guitar
full_join(band_members, band_instruments, by="name")
# A tibble: 4 × 3
  name  band    plays 
  <chr> <chr>   <chr> 
1 Mick  Stones  <NA>  
2 John  Beatles guitar
3 Paul  Beatles bass  
4 Keith <NA>    guitar

❤ Placé dans le domaine public par Vincent Bonhomme