Hey ! Quoi de neuf {dplyr} ? Le point sur la v1 !

Les outils et les pinces du package dplyr
Auteur : Elena Salette
Tags : Actualités, Ressources
Date :

On l’attendait depuis l’annonce faite par Hadley Wickham début mars, et le moment est enfin arrivé ! Le tout nouveau tout beau {dplyr} version 1.0.0 est (presque) disponible sur le CRAN, et il a bien été étoffé ! Quelles sont les news ? On vous dit tout.

Les nouvelles fonctionnalités de {dplyr}

La fonction de résumé summarise()

Utiliser des fonctions résumé qui renvoient plusieurs valeurs

Première modification notable, la fonction summarise() (ou summarize(), c’est pareil). Elle permet de calculer les variables résumé d’un jeu de données. Par exemple, la moyenne et le nombre d’individus par groupe :

mtcars %>%
  group_by(cyl) %>%
  summarise(
    mean_disp = mean(disp),
    n = n()
  )
## # A tibble: 3 x 3
##     cyl mean_disp     n
## * <dbl>     <dbl> <int>
## 1     4      105.    11
## 2     6      183.     7
## 3     8      353.    14

Jusqu’à la version 0.8.5, on ne pouvait utiliser que des fonctions résumé qui retourne une unique valeur. Dans le cas contraire, on obtenait un message d’erreur du type :

Erreur : Column `range_disp` must be length 1 (a summary value), not 2

À partir de la version 1.0.0, la fonction summarise() permet l’utilisation de fonctions résumé qui renvoient plusieurs valeurs. C’est le cas de la fonction range() qui nous renvoie les valeurs minimum et maximum d’une variable. On obtient alors une ligne par valeur retournée par notre fonction. Bien entendu, on peut toujours coupler la fonction summarise() à group_by() pour effectuer des calculs par groupe. Ainsi, notez que la colonne de groupes cyl a des valeurs dupliquées. C’est aussi le cas de la colonne de moyennes mean_disp car la fonction mean() ne renvoit qu’une seule valeur résumée. En revanche, les colonnes range_disp et quantile, dont les fonctions résumé renvoient, ici, deux valeurs, affichent alternativement la première, puis la seconde valeur résumé dans la colonne. L’astuce consiste à créer une nouvelle colonne telle que valeur ici, qui renvoit autant de valeurs distinctes que les fonctions résumé utilisées, pour mieux les identifier, ligne par ligne.

mtcars %>%
  group_by(cyl) %>%
  summarise(
    # renvoie 1 valeur
    mean_disp = mean(disp),
    # renvoie 2 valeurs
    valeur = c("min", "max"),
    range_disp = range(disp),
    quantile = quantile(disp, c(0.25, 0.75)), prob = c(0.25, 0.75)
  )
## # A tibble: 6 x 6
## # Groups:   cyl [3]
##     cyl mean_disp valeur range_disp quantile  prob
##   <dbl>     <dbl> <chr>       <dbl>    <dbl> <dbl>
## 1     4      105. min          71.1     78.8  0.25
## 2     4      105. max         147.     121.   0.75
## 3     6      183. min         145      160    0.25
## 4     6      183. max         258      196.   0.75
## 5     8      353. min         276.     302.   0.25
## 6     8      353. max         472      390    0.75

Par contre attention, il n’est pas possible de créer des colonnes de taille différente. Si j’utilise une première fonction qui renvoie deux valeurs, comme la fonction range(), je ne peux pas créer une autre variable de résumé qui renvoie un nombre différent de valeurs :

# Fonctionne 
mtcars %>%
  group_by(cyl) %>%
  summarise(
    # renvoie 1 valeur
    mean_disp = mean(disp), 
    # renvoie 3 valeurs
    quantile = quantile(disp, c(0.25, 0.50, 0.75)), prob = c(0.25, 0.50, 0.75)
  )
# Ne fonctionne pas
mtcars %>%
  group_by(cyl) %>%
  summarise(
    # renvoie 1 valeur
    mean_disp = mean(disp), 
    # renvoie 2 valeurs
    range_disp = range(disp),
    valeur = c("min", "max"),
    # renvoie 3 valeurs
    quantile = quantile(disp, c(0.25, 0.50, 0.75)), prob = c(0.25, 0.50, 0.75)
  )

Gérer les groupes à la sortie d’un summarise()

Vous avez sûrement déjà eu des problèmes de gestion des groupes après avoir fait un group_by() + summarise().
Est-ce que vous savez ce qui reste comme groupe à la sortie de l’opération suivante ?

homeworld_species <- starwars %>% 
  group_by(homeworld, species) %>% 
  summarise(n = n())

Réponse : les données du nouveau tableau sont toujours groupées, mais en fonction de la variable homeworldseulement. Pourquoi ? Parce que summarise()réduit le regroupement de la droite vers la gauche, d’une variable à la fois. En effet, si on faisait de nouveau passer un summarise()ici, le jeu de données ne serait plus groupé du tout.
Pour éviter d’avoir à gérer de futurs problèmes, nous vous avons peut-être recommandé de toujours utiliser ungroup()à la fin de vos opérations groupées. C’est une très bonne astuce, cela dit, avec la nouvelle version de {dplyr}, vous avez maintenant la possibilité de choisir le niveau de regroupement que vous souhaitez en sortie du summarise(), en utilisant le paramètre .groups :

  • .groups = "drop_last" retire le dernier niveau de groupe (comportement par défaut, sans message)
  • .groups = "drop" retire tous les niveaux de groupe
  • .groups = "keep" conserve tous les niveaux de groupe
  • .groups = "rowwise" retourne chaque ligne comme un groupe indépendant

La fonction de sélection select()

Autre nouveauté de cette version, les modifications de la fonction select() qui permet de sélectionner et renommer des colonnes dans une table. Jusqu’à la version 0.8.5, on pouvait sélectionner nos colonnes grâce à des fonctions spécifiques, issues de {tidyselect}, qui ne s’appliquent que dans le cadre de la fonction select() :

  • starts_with(x), ends_with(x), contains(x) : nom des colonnes qui respectivement commencent par, terminent par ou contiennent la chaîne de caractères x
  • matches(x) : nom des colonnes qui matchent avec une expression régulière (pour en savoir plus sur les expressions régulières, c’est par là : https://thinkr.fr/r-les-expressions-regulieres/)
  • num_range() : nom des colonnes qui matchent avec un ordre numérique, par exemple select(df, num_range("V", 4:6)) sélectionnera les variables V4, V5 et V6
  • one_of(vec_col) : sélectionne les colonnes dont le nom est présent dans le vecteur vec_col, dans l’ordre donné par vec_col.
  • everything() : toutes les colonnes, ou toutes les colonnes restantes, si vous avez utilisé d’autres fonctions de sélection
  • group_cols() : les colonnes qui correspondent aux groupes, créées via group_by()

Depuis la nouvelle version du package {tidyselect}, on dispose de nouvelles fonctions de sélection à utiliser avec select(), et qui peuvent être maintenant, ainsi que les fonctions précédemment citées, combinées avec des opérateurs logiques (&, |, !) :

  • all_of(vec_col) : sélectionne les colonnes dont le nom est présent dans le vecteur vec_col, dans l’ordre donné par vec_col. Toutes les valeurs de vec_col doivent correspondre à des noms de colonne du jeu de données.
  • any_of(vec_col) : idem que all_of() mais permet que certaines valeurs de vec_col ne correspondent à aucun nom de colonne du jeu de données
  • last_col() : la derniere colonne des données d’entrée

Mais la grande nouveauté de la fonction select() réside dans le fait qu’on peut désormais utiliser des fonctions booléennes de tests d’appartenance à une classe et les combiner avec les fonctions précédemment citées. Cela va nous permettre de sélectionner par exemple les variables en fonction de leur classe (numérique, facteur, chaîne de caractères, …). Par ailleurs, attention, depuis la version 1.1.0 de {tidyselect}, vous allez devoir utiliser where() pour les tests de format.

Quelques exemples :

# Sélection des variables numériques
starwars %>% select(where(is.numeric))
## # A tibble: 87 x 3
##    height  mass birth_year
##     <int> <dbl>      <dbl>
##  1    172    77       19  
##  2    167    75      112  
##  3     96    32       33  
##  4    202   136       41.9
##  5    150    49       19  
##  6    178   120       52  
##  7    165    75       47  
##  8     97    32       NA  
##  9    183    84       24  
## 10    182    77       57  
## # … with 77 more rows
# Sélection des variables caractère commençant par "h"
starwars %>% select(where(is.character) & starts_with("h"))
## # A tibble: 87 x 2
##    hair_color    homeworld
##    <chr>         <chr>    
##  1 blond         Tatooine 
##  2 <NA>          Tatooine 
##  3 <NA>          Naboo    
##  4 none          Tatooine 
##  5 brown         Alderaan 
##  6 brown, grey   Tatooine 
##  7 brown         Tatooine 
##  8 <NA>          Tatooine 
##  9 black         Tatooine 
## 10 auburn, white Stewjon  
## # … with 77 more rows

La foncion rename() profite des mêmes évolutions du package {tidyselect} et admet également les fonctions booléennes de tests d’appartenance à une classe.

La fonction pour réordonner les colonnes relocate()

Alors voici une nouvelle venue parmi les fonction de {dplyr}. La fonction relocate() permet de modifier l’ordre des colonnes de notre jeu de données. Comment ?

Créons un jeu de données pour un exemple reproductible :

df <- tibble(
  V1 = LETTERS[1:4],
  V2 = runif(4),
  V3 = rnorm(4),
  V4 = letters[5:8]
)
df
## # A tibble: 4 x 4
##   V1       V2     V3 V4   
##   <chr> <dbl>  <dbl> <chr>
## 1 A     0.684  1.92  e    
## 2 B     0.152  0.665 f    
## 3 C     0.356 -0.156 g    
## 4 D     0.793 -1.89  h
  • si seule la variable concernée est spécifiée, celle-ci passera en premiere position :
df %>% relocate(V4)
## # A tibble: 4 x 4
##   V4    V1       V2     V3
##   <chr> <chr> <dbl>  <dbl>
## 1 e     A     0.684  1.92 
## 2 f     B     0.152  0.665
## 3 g     C     0.356 -0.156
## 4 h     D     0.793 -1.89
  • en spécifiant les paramètres .after ou .before, il est possible de choisir la nouvelle position de notre variable :
df %>% relocate(V4, .after = V2)
## # A tibble: 4 x 4
##   V1       V2 V4        V3
##   <chr> <dbl> <chr>  <dbl>
## 1 A     0.684 e      1.92 
## 2 B     0.152 f      0.665
## 3 C     0.356 g     -0.156
## 4 D     0.793 h     -1.89
df %>% relocate(V4, .before = V2)
## # A tibble: 4 x 4
##   V1    V4       V2     V3
##   <chr> <chr> <dbl>  <dbl>
## 1 A     e     0.684  1.92 
## 2 B     f     0.152  0.665
## 3 C     g     0.356 -0.156
## 4 D     h     0.793 -1.89
  • on peut également appliquer des règles logiques à cette fonction. Par exemple : placer en dernières positions les variables de type chaîne de caractères :
df %>% relocate(is.character, .after = last_col())
## # A tibble: 4 x 4
##      V2     V3 V4    V1   
##   <dbl>  <dbl> <chr> <chr>
## 1 0.684  1.92  e     A    
## 2 0.152  0.665 f     B    
## 3 0.356 -0.156 g     C    
## 4 0.793 -1.89  h     D

La fonction de calcul avec conditions sur les variables across()

Nouvelle fonction majeure du package, la fonction across(). Elle signe l’arrêt de mort à venir des fonctions en _if , _at , _all et permet d’appliquer la même transformation à plusieurs colonnes, en utilisant les fonctions de {tidyselect}. Et comme pour select(), depuis la version 1.1.0 de {tidyselect}, vous allez devoir utiliser where(). Par exemple, je souhaite modifier dans les données iris les variables de type facteur en caractère (La colonne originale Species est de type factor) :

# dplyr (<= 0.8.5)
mutate_avant <- iris %>%
  as_tibble() %>%
  mutate_if(is.factor, as.character)
# dplyr (>= 1.0.0)
iris %>%
  as_tibble() %>%
  mutate(across(where(is.factor), as.character))
## # A tibble: 150 x 5
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
##           <dbl>       <dbl>        <dbl>       <dbl> <chr>  
##  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 
## # … with 140 more rows

Autre exemple plus complexe : calcul de la moyenne et de l’écart-type des variables commençant par “Sepal” et groupées par Species :

# dplyr (<= 0.8.5)
summarise_avant <- iris %>%
  group_by(Species) %>%
  summarise_at(vars(starts_with("Sepal")), list(mean = mean, sd = sd))
# dplyr (>= 1.0.0)
iris %>%
  group_by(Species) %>%
  summarise(across(starts_with("Sepal"), list(mean = mean, sd = sd)))
## # A tibble: 3 x 5
##   Species  Sepal.Length_me… Sepal.Length_sd Sepal.Width_mean Sepal.Width_sd
## * <fct>               <dbl>           <dbl>            <dbl>          <dbl>
## 1 setosa               5.01           0.352             3.43          0.379
## 2 versico…             5.94           0.516             2.77          0.314
## 3 virgini…             6.59           0.636             2.97          0.322

Allons encore plus loin avec une autre table :

df2 <- tibble(
  V1 = LETTERS[1:4],
  V2 = runif(4),
  V3 = rnorm(4),
  V4 = letters[5:8],
  Z1 = runif(4),
  Z2 = letters[20:23]
)
df2
## # A tibble: 4 x 6
##   V1       V2     V3 V4       Z1 Z2   
##   <chr> <dbl>  <dbl> <chr> <dbl> <chr>
## 1 A     0.702 -1.19  e     0.865 t    
## 2 B     0.541 -1.69  f     0.882 u    
## 3 C     0.194  2.47  g     0.682 v    
## 4 D     0.476  0.546 h     0.863 w

Calculons la moyenne de toutes les colonnes qui commencent par V et qui sont numériques. En même temps, récupérons la première valeur des colonnes de type caractère qui commencent par Z. Cette opération n’était pas possible de manière simple avec la version précédente.

# dplyr (<= 0.8.5)
# _premiere table avec 2 conditions
table1 <- df2 %>% 
  select(starts_with("V")) %>% 
  summarise_if(is.numeric,  list(mean = mean))
# _seconde table avec 2 conditions
table2 <- df2 %>% 
  select(starts_with("Z")) %>% 
  summarise_if(is.character,  list(first = first))
# _Coller les colonnes
table_avant <- table1 %>% bind_cols(table2)
# dplyr (>= 1.0.0)
df2 %>%
  summarise(
    across(starts_with("V") & where(is.numeric), mean, .names = "mean_{col}"),
    across(starts_with("Z") & where(is.character), first, .names = "first_{col}")
  )
## # A tibble: 1 x 3
##   mean_V2 mean_V3 first_Z2
##     <dbl>   <dbl> <chr>   
## 1   0.478  0.0328 t

Notez que la fonction across() inclut le calcul à réaliser. De fait, à l’instar du pipe %>% que l’on lit ‘ensuite’, across pourraient être lu par ‘pour toutes les colonnes qui (…), calcule (…)’

Les fonctions faisant référence au sous-jeux de données cur_

Elles sont appelées dans les fonctions summarise() ou mutate(), lorsque les données sont préalablement groupées par un group_by(). Ces fonctions, directement inspirées du package {data.table}, sont les suivantes :

  • cur_data() : renvoie un tibble contenant les données relatives au groupe concerné (Équivalent de {data.table} .SD)
  • cur_group() : renvoie un tibble contenant les clés du groupe (Équivalent de {data.table} .BY)
  • cur_group_id() : renvoie l’identifiant numérique unique du groupe (Équivalent de {data.table} .GRP)
  • cur_group_rows() : renvoie les numéros de lignes associées au groupe (Équivalent de {data.table} .I)

Exemple : ajout d’une colonne contenant les numéros uniques de groupes ainsi que leurs clés

df3 <- tibble(
  col_char = sample(letters[1:3], 6, replace = TRUE), 
  col_char2 = sample(letters[4:5], 6, replace = TRUE),
  V1 = LETTERS[1:6],
  V2 = runif(6),
  V3 = rnorm(6),
  V4 = letters[5:10],
  Z1 = runif(6),
  Z2 = letters[20:25]
)
gf <- df3 %>% 
  group_by(col_char, col_char2) %>% 
  mutate(
    key = cur_group(), 
    id = cur_group_id()
  ) %>%
  select(group_cols(), key, id)
gf
## # A tibble: 6 x 4
## # Groups:   col_char, col_char2 [5]
##   col_char col_char2 key$col_char $col_char2    id
##   <chr>    <chr>     <chr>        <chr>      <int>
## 1 b        d         b            d              2
## 2 a        e         a            e              1
## 3 c        d         c            d              4
## 4 b        e         b            e              3
## 5 c        d         c            d              4
## 6 c        e         c            e              5
gf$key
## # A tibble: 6 x 2
##   col_char col_char2
##   <chr>    <chr>    
## 1 b        d        
## 2 a        e        
## 3 c        d        
## 4 b        e        
## 5 c        d        
## 6 c        e

Les fonctions de filtre des lignes sur position slice_

On connaissait déjà la fonction slice(), qui pour rappel, nous permet de filtrer des lignes en fonction de leur position, et pour les données groupées via group_by(), en fonction de leur position au sein du groupe en question. Attention, il n’est pas recommandé de filtrer un jeu de données sur la base de position dans la table. En cas de mise à jour de votre table, les positions peuvent avoir changées et votre analyse ne sera plus reproductible. Il vaut mieux privilégier la fonction filter() pour laquelle vous êtes obligés de définir les raisons pour lesquelles vous filter les données de cette manière. :

df3 <- tibble(
  col_char = sample(letters[1:3], 6, replace = TRUE), 
  col_char2 = sample(letters[4:5], 6, replace = TRUE),
  V1 = LETTERS[1:6],
  V2 = runif(6),
  V3 = rnorm(6),
  V4 = letters[5:10],
  Z1 = runif(6),
  Z2 = letters[20:25]
)
df3 %>% slice(1:2)
## # A tibble: 2 x 8
##   col_char col_char2 V1         V2     V3 V4       Z1 Z2   
##   <chr>    <chr>     <chr>   <dbl>  <dbl> <chr> <dbl> <chr>
## 1 c        d         A     0.747   -0.341 e     0.540 t    
## 2 c        e         B     0.00248 -0.600 f     0.589 u

À cette fonction viennent s’ajouter :

  • slice_head() et slice_tail() : sélection des premières et dernières lignes par groupe
  • slice_sample() : sélection aléatoire de lignes, par groupe. Elle vient remplacer sample_n() et sample_frac()
  • slice_min() et slice_max() : sélection des lignes pour lesquelle la valeur est minimale, respectivement maximale, pour une variable donnée et par groupe. Ces fonctions viennent remplacer top_n() et top_frac().

Par exemple :

df3 %>% 
  group_by(col_char) %>% 
  slice_min(V2)
## # A tibble: 2 x 8
## # Groups:   col_char [2]
##   col_char col_char2 V1         V2     V3 V4       Z1 Z2   
##   <chr>    <chr>     <chr>   <dbl>  <dbl> <chr> <dbl> <chr>
## 1 a        d         C     0.543   -0.661 g     0.294 v    
## 2 c        e         B     0.00248 -0.600 f     0.589 u

La fonction de renommage rename_with()

Elle vient compléter la liste des fonctions rename() (rename_all(), rename_if(), rename_at()). On peut maintenant modifier le nom des colonnes en y appliquant une fonction. Par exemple, mettons la première lettre de chaque nom de colonne de mtcars en majuscule :

mtcars %>% rename_with(stringr::str_to_title)
##                      Mpg Cyl  Disp  Hp Drat    Wt  Qsec Vs Am Gear Carb
## Mazda RX4           21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag       21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710          22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive      21.4   6 258.0 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout   18.7   8 360.0 175 3.15 3.440 17.02  0  0    3    2
## Valiant             18.1   6 225.0 105 2.76 3.460 20.22  1  0    3    1
## Duster 360          14.3   8 360.0 245 3.21 3.570 15.84  0  0    3    4
## Merc 240D           24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2
## Merc 230            22.8   4 140.8  95 3.92 3.150 22.90  1  0    4    2
## Merc 280            19.2   6 167.6 123 3.92 3.440 18.30  1  0    4    4
## Merc 280C           17.8   6 167.6 123 3.92 3.440 18.90  1  0    4    4
## Merc 450SE          16.4   8 275.8 180 3.07 4.070 17.40  0  0    3    3
## Merc 450SL          17.3   8 275.8 180 3.07 3.730 17.60  0  0    3    3
## Merc 450SLC         15.2   8 275.8 180 3.07 3.780 18.00  0  0    3    3
## Cadillac Fleetwood  10.4   8 472.0 205 2.93 5.250 17.98  0  0    3    4
## Lincoln Continental 10.4   8 460.0 215 3.00 5.424 17.82  0  0    3    4
## Chrysler Imperial   14.7   8 440.0 230 3.23 5.345 17.42  0  0    3    4
## Fiat 128            32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
## Honda Civic         30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
## Toyota Corolla      33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1
## Toyota Corona       21.5   4 120.1  97 3.70 2.465 20.01  1  0    3    1
## Dodge Challenger    15.5   8 318.0 150 2.76 3.520 16.87  0  0    3    2
## AMC Javelin         15.2   8 304.0 150 3.15 3.435 17.30  0  0    3    2
## Camaro Z28          13.3   8 350.0 245 3.73 3.840 15.41  0  0    3    4
## Pontiac Firebird    19.2   8 400.0 175 3.08 3.845 17.05  0  0    3    2
## Fiat X1-9           27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1
## Porsche 914-2       26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
## Lotus Europa        30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2
## Ford Pantera L      15.8   8 351.0 264 4.22 3.170 14.50  0  1    5    4
## Ferrari Dino        19.7   6 145.0 175 3.62 2.770 15.50  0  1    5    6
## Maserati Bora       15.0   8 301.0 335 3.54 3.570 14.60  0  1    5    8
## Volvo 142E          21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2

Les fonctions qui n’existent plus

Ces fonctions ont totalement disparu, alors partez vite à la recherche de ces intrus qui vont faire planter vos codes à la mise à jour de {dplyr} ! Bon, si vous utilisez {renv} https://rstudio.github.io/renv/, vous êtes probablement sauvés…

  • id() : création d’un identifiant numérique unique pour chaque ligne d’un data frame. Remplacée par la fonction vctrs::vec_group_id()
  • failwith() : remplacement d’erreurs par une valeur par défaut. Remplacée par la fonction purrr::possibly()
  • tbl_cube() et les données spatio-temporelle nasa ont été déplacée dans un package séparé : {cubelyr}
  • rbind_all() et rbind_list() : utilisez maintenant bind_rows()
  • dr_dplyr() : apparemment c’était une fonction permettant de checker certains problèmes d’installation liés à Rcpp et à la version de R. Personnellement jamais utilisée, et apparemment je ne suis pas la seule.
  • all.equal.tbl_df() : les data frames, les tibbles et les grouped data frames ne sont plus considérés comme équivalents. Attention donc aux erreurs que vous pourriez rencontrer dans vos tests unitaires si vous utiliser la fonction expect_equal() pour comparer ces éléments.

Les fonctions has-been

Avec cette nouvelle version, certaines fonctions et paramètres sont devenus obsolètes et malgé le fait qu’ils existent toujours, nous vous conseillons d’opérer les changements nécessaires, sinon vous risquez de faire face à un certain nombre de problèmes dans un futur plus ou moins proche. Attention, il y en a un paquet et elles risquent de vous embêter car ils seront la source de bon nombre de warnings intempestifs :

Les fonctionnalités expérimentales

Nouveaux arguments de position pour la fonction mutate()

mutate() est la fonction qui permet entre autres de créer de nouvelles variables. Par défaut, ces dernières sont ajoutées à la fin de notre jeu de données. Les nouveaux paramètres .before et .after nous donnent le choix de la position à laquelle on souhaite intégrer ces nouvelles variables, top ! Attention, on ne peut renseigner que l’un des deux. Testons tout ça :

df <- tibble(
  V1 = LETTERS[1:4],
  V2 = runif(4),
  V3 = rnorm(4),
  V4 = letters[5:8]
)
# Test de .before :
dg <- df %>% 
  mutate(evalue = paste(round(V2, 2), "+", round(V3, 2)), .before = V3)
dg
## # A tibble: 4 x 5
##   V1       V2 evalue           V3 V4   
##   <chr> <dbl> <chr>         <dbl> <chr>
## 1 A     0.339 0.34 + 1.38   1.38  e    
## 2 B     0.723 0.72 + -0.2  -0.197 f    
## 3 C     0.578 0.58 + 1.62   1.62  g    
## 4 D     0.922 0.92 + -0.91 -0.907 h
# Test de .after : 
dg %>% mutate(somme = V2 + V3, .after = evalue)
## # A tibble: 4 x 6
##   V1       V2 evalue        somme     V3 V4   
##   <chr> <dbl> <chr>         <dbl>  <dbl> <chr>
## 1 A     0.339 0.34 + 1.38  1.72    1.38  e    
## 2 B     0.723 0.72 + -0.2  0.526  -0.197 f    
## 3 C     0.578 0.58 + 1.62  2.20    1.62  g    
## 4 D     0.922 0.92 + -0.91 0.0153 -0.907 h

On remarque également le nouveau paramètre .keep qui ajoute une fonctionnalité de sélection de variables à la fonction mutate() :

  • .keep = "all" : toutes les variables sont conservées (si vous voulez mon avis, franchement aucun intérêt)
  • .keep = "used" : ne conserve que les variables ayant servi à la construction des nouvelles variables
  • .keep = "unused" : ne conserve que les variables n’ayant PAS servi à la construction des nouvelles variables
  • .keep = "none" : ne conserve que les variables ayant permi de construire des groupes éventuels (pas de groupe : seule la variable nouvellement créée est conservée)

La fonction de calcul par groupe with_groups()

On l’a vu, on l’a vécu, grouper nos observations peut parfois être contraignant, et souvent on dégaine le ungroup() rapidos après avoir fini nos opérations sur les groupes. C’est même recommandé de le faire ! On comprend donc bien toute l’utilité de cette nouvelle fonction qui va nous permettre de ne créer/modifier nos groupes que pour une seule opération et laisser nos données tranquilles par la suite.

Exemple : faire des opérations sur des groupes différents, en même temps !

df4 <- tibble(
  col_char = sample(letters[1:3], 6, replace = TRUE), 
  col_char2 = sample(letters[4:5], 6, replace = TRUE),
  V2 = runif(6)
)
df4 %>%
  # moyenne de V2 par groupe - variable de groupe col_char
  with_groups(col_char, mutate, mean_by1 = mean(V2)) %>%
  # moyenne de V2 par groupe - variable de groupe col_char2
  with_groups(col_char2, mutate, mean_by2 = mean(V2))
## # A tibble: 6 x 5
##   col_char col_char2     V2 mean_by1 mean_by2
##   <chr>    <chr>      <dbl>    <dbl>    <dbl>
## 1 a        e         0.374     0.374    0.242
## 2 b        e         0.285     0.422    0.242
## 3 c        d         0.617     0.342    0.533
## 4 b        d         0.543     0.422    0.533
## 5 b        d         0.438     0.422    0.533
## 6 c        e         0.0669    0.342    0.242

La fonction d’encapsulation par groupe nest_by()

C’est une fonction très similaire à la fonction group_by, à la différence qu’au lieu de stocker la structure de groupe dans les métadonnées, elle la rend explicite directement dans les données : chaque clé de groupe aura une seule ligne. La fonction renvoie un tibble ayant pour colonnes :

  • les clés de groupe
  • une list-colonne contenant les données de chacun des groupes
iris %>% nest_by(Species)
## # A tibble: 3 x 2
## # Rowwise:  Species
##   Species                  data
##   <fct>      <list<tbl_df[,4]>>
## 1 setosa               [50 × 4]
## 2 versicolor           [50 × 4]
## 3 virginica            [50 × 4]

Les fonctions de mise à jour des valeurs dans les tableaux rows_*()

L’ajout de dernière minute, c’est la possibilité de mettre à jour certaines lignes d’une table de données avec une autre table contenant les informations à mettre à jour. Ces insertions se font sur la base d’une clé d’identification commune mais unique pour chaque individu. Ces modifications de lignes sont possibles sur la base de règles strictes, d’où 5 nouvelles fonctions avec des effets différents :

  • rows_update(x, y) met à jour les valeurs des lignes de x avec les valeurs de y si tous les individus existent.
  • rows_patch(x, y) met à jour uniquement les valeurs manquantes (NA) des lignes de x avec les valeurs de y .
  • rows_insert(x, y) ajoute de nouvelles lignes à x sur la base de y à condition que les individus n’existent pas déjà.
  • rows_upsert(x, y) met à jour les valeurs de x avec y et ajoute les nouveaux individus de y.
  • rows_delete(x, y) supprimes les lignes de x dont les individus dont dans y.

Ces effets contrôlés permettent de gérer ces modifications sur des base de données en évitant de tout casser. D’ailleurs pour faire remonter ces modifications sur les bases de données originales, vous aurez probablement besoin de spécifier in_place = TRUE. Mais tout ça reste encore expérimental.

Le renouveau des vignettes du package {dplyr}

On notera également l’effort fourni au niveau des vignettes du package {dplyr}, qui nous permettent de bien mieux comprendre son fonctionnement et l’utilisation de certaines fonctions :

  • vignette améliorée : vignette("rowwise")
  • nouvelle vignette : vignette("colwise")
  • vignette entièrement réécrite : vignette("programming")

Note : La fonction rowwise() précédemment etiquetée comme expérimentale a fait ses preuves et a été approuvée par la communauté, elle perd donc son statut de “on sait pas trop, à voir ce que ça donne” et est maintenant partie intégrante du package !

Et en coulisses…

On a gardé le meilleur pour la fin, et là on comprend pourquoi {dplyr} passe à la v1.0.0. En effet, grosse révolution dans les coulisses du package : bye bye {Rcpp} ! {dplyr} dispose maintenant d’une toute nouvelle implémentation, basée sur {vctrs} (un package bas niveau pour la manipulation haute performance des vecteurs R, n’entrons pas plus dans le détail, mais si vous êtes intéressées : https://vctrs.r-lib.org/. Il s’aligne sur d’autres de ses copains du tidyverse comme {tidyr} (>=1.0.0).

Les implications à prévoir :

  • Vous risquez de voir des effets au niveau des jointures ou des créations de variables : si vous combinez des facteurs ayant différents niveaux (levels), {dplyr} crée maintenant un nouveau facteur dont les levels seront l’union de ceux des facteurs combinés.
  • {dplyr} ne dépend plus de {Rcpp} ni de {BH}.
  • Les noms de lignes sont désormais conservés lorsque l’on travaille avec des data frames.

What more ?

Plein de modifications mineures qui n’auront pas un énorme impact sur votre utilisation quotidienne de {dplyr}, mais qu’il peut être intéressant de connaître : https://github.com/tidyverse/dplyr/blob/master/NEWS.md#minor-improvements-and-bug-fixes

Tous ces changements méritaient bien le passage à une version considérée comme stable ! Il ne devrait plus y avoir d’énormes changements avant un bout de temps. Prenez maintenant le temps d’intégrer tout ça, votre code devrait s’en trouver allégé…

 

Article rédigé par Elena Salette, avec la participation de Sébastien Rochette.


À propos de l'auteur


Commentaires

3 réponses à “Hey ! Quoi de neuf {dplyr} ? Le point sur la v1 !”

  1. Merci pour cet article très complet ! Je voudrais apporter un soupçon de tri et de mise en relief, issu de ma pratique quotidienne, et aussi quelques nuances :

    1) nouveautés très pratiques tout le temps
    – mutate et ses nouveaux paramètres .before et .keep
    – relocate
    – select et sa nouvelle souplesse étendue de filtrages combinés (idem avec rename)

    2) intéressant dans des situations spécifiques
    – rowwise et c_across
    – rows_upsert (génial pour qui pratique SQL)
    – summarising (across + mean, sum…) J’ai un exemple pratique où ça va m’aider
    – summarise et fonctions à x valeurs
    – with_groups

    3) Nuances sur le remplacement des fonctions en _if
    difficile de sauter de joie face à l’invitation à remplacer
    mutate_if(is.factor, as.character) (simple à comprendre et retenir) par
    mutate( across(where(is.factor), as.character) )

    H. Wickham reconnait lui même qu’il n’y a pas encore d’équivalent simple pour :
    filter_if( is.numeric, any_vars( !is.na(.) ) )
    ou un select_if impliquant une fonction à plusieurs paramètres.

    Je cite une réponse qu’il m’a faite en écho :
    “I think you are reading too much into what “superseded” means — we are committed to keeping these functions around for a long time. The soonest we would remove them is in several years, but if we look around and see that people are still using them, we would keep them around for longer. You absolutely do not need to commit to learning replacements for them right now, partly because, as you have discovered, the replacements are not fully mature yet.”

    Donc je nuancerais votre rubrique sur les fonctions “has-been” et la phrase :
    “la fonction across()… Elle signe l’arrêt de mort à venir des fonctions en _if , _at , _all”
    car je pense que certaines fonctions en _if gardent leur pertinence

    4) mention manquante : cur_column (avec mutate et across, permettant de faire intervenir une liste externe d’infos sur les noms de colonne). Répond à un vrai besoin

  2. Merci pour ce récapitulatif. Je cherchais un document comme cela.

    > Donc je nuancerais votre rubrique sur les fonctions “has-been” et la phrase : “la fonction across()… Elle signe l’arrêt de mort à venir des fonctions en _if , _at , _all” car je pense que certaines fonctions en _if gardent leur pertinence

    Idem à ceci près que si j’utilise très peu de fonction _if, en revanche j’utilise beaucoup de mutate_at() , select_at() et group_by_at()

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


À lire également

Nos formations Certifiantes à R sont finançables à 100% via le CPF

Nos formations disponibles via moncompteformation.gouv.fr permettent de délivrer des Certificats reconnues par l’état, enregistrées au répertoire spécifique de France Compétences. 3 niveaux de certifications existent :

Contactez-nous pour en savoir plus.

Calendrier

04/06/2024

14/05/2024

14/05/2024