38  Transformations multiples

38.1 Transformations multiples sur les colonnes

Il est souvent utile d’effectuer la même opération sur plusieurs colonnes, mais le copier-coller est à la fois fastidieux et source d’erreurs :

df %>% 
  group_by(g1, g2) %>% 
  summarise(
    a = mean(a),
    b = mean(b),
    c = mean(c),
    d = mean(d)
  )

Dans cette section, nous allons introduire dplyr::across() qui permets de réécrire la même commande de manière plus succincte.

df %>% 
  group_by(g1, g2) %>% 
  summarise(across(a:d, mean))

38.1.1 Usage de base

dplyr::across() a deux arguments principaux :

  • le premier, .cols, permet de sélectionner les colonnes sur lesquelles on souhaite agir et accepte la même syntaxe de dplyr::select() ;
  • le second, .fns, est une fonction (ou une liste de fonctions) à appliquer à chaque colonne sélectionnée.

Voici quelques exemples avec dplyr::summarise().

Dans ce premier exemple, nous utilisons tidyselect::where() qui permet de sélectionner les colonnes en fonction de leur type (ici les colonnes textuelles car where() est utilisé en conjonction avec la fonction is.character()). Notez que l’on passe is.character() sans ajouter de parenthèse. En effet, is.character renvoie la fonction du même nom, tandis que is.character() appelle la fonction pour l’exécuter. La fonction dplyr::n_distinct(), quant à elle, compte le nombre de valeurs uniques. Le tableau ci-dessous renvoie donc, pour chaque variable textuelle, le nombre de valeurs uniques observées dans les données.

library(tidyverse)
starwars |>  
  summarise(across(where(is.character), n_distinct))
# A tibble: 1 × 8
   name hair_color skin_color eye_color   sex gender homeworld species
  <int>      <int>      <int>     <int> <int>  <int>     <int>   <int>
1    87         12         31        15     5      3        49      38

Dans ce second exemple, nous indiquons simplement la liste de nos variables d’intérêt.

starwars |> 
  group_by(species) |> 
  filter(n() > 1) |>  
  summarise(across(c(sex, gender, homeworld), n_distinct))
# A tibble: 9 × 4
  species    sex gender homeworld
  <chr>    <int>  <int>     <int>
1 Droid        1      2         3
2 Gungan       1      1         1
3 Human        2      2        15
4 Kaminoan     2      2         1
5 Mirialan     1      1         1
6 Twi'lek      2      2         1
7 Wookiee      1      1         1
8 Zabrak       1      1         2
9 <NA>         1      1         3

Dans ce troisième exemple, nous allons calculer la moyenne pour chaque variable numérique.

starwars %>% 
  group_by(homeworld) |>  
  filter(n() > 1) |>  
  summarise(across(where(is.numeric), mean))
# A tibble: 10 × 4
   homeworld height  mass birth_year
   <chr>      <dbl> <dbl>      <dbl>
 1 Alderaan    176.  NA           NA
 2 Corellia    175   78.5         25
 3 Coruscant   174.  NA           NA
 4 Kamino      208.  NA           NA
 5 Kashyyyk    231  124           NA
 6 Mirial      168   53.1         49
 7 Naboo       177.  NA           NA
 8 Ryloth      179   NA           NA
 9 Tatooine    170.  NA           NA
10 <NA>         NA   NA           NA

Il y a beaucoup de valeurs manquantes. Nous devons donc passer na.rm = TRUE à mean(). Différentes approches sont possibles :

  • écrire notre propre fonction ma_fonction() ;
  • utiliser purrr::partial() qui permet de renvoyer une fonction avec des valeurs par défaut différentes ;
  • la syntaxe native de R pour déclarer des fonctions anonymes avec le raccourci \(arg) expr ;
  • une formule définissant une fonction dans le style du package purrr, c’est-à-dire une formule commençant par ~ et dont le premier argument sera noté .x1.

1 Cette syntaxe particulière n’est compatible que dans certaines fonctions du tidyverse. Ce n’est pas une syntaxe standard de R.

ma_fonction <- function(x) {mean(x, na.rm = TRUE)}
starwars %>% 
  group_by(homeworld) |>  
  filter(n() > 1) |>  
  summarise(across(where(is.numeric), ma_fonction))
# A tibble: 10 × 4
   homeworld height  mass birth_year
   <chr>      <dbl> <dbl>      <dbl>
 1 Alderaan    176.  64         43  
 2 Corellia    175   78.5       25  
 3 Coruscant   174.  50         91  
 4 Kamino      208.  83.1       31.5
 5 Kashyyyk    231  124        200  
 6 Mirial      168   53.1       49  
 7 Naboo       177.  64.2       55  
 8 Ryloth      179   55         48  
 9 Tatooine    170.  85.4       54.6
10 <NA>        139.  82        334. 
starwars %>% 
  group_by(homeworld) |>  
  filter(n() > 1) |>  
  summarise(across(where(is.numeric), purrr::partial(mean, na.rm = TRUE)))
# A tibble: 10 × 4
   homeworld height  mass birth_year
   <chr>      <dbl> <dbl>      <dbl>
 1 Alderaan    176.  64         43  
 2 Corellia    175   78.5       25  
 3 Coruscant   174.  50         91  
 4 Kamino      208.  83.1       31.5
 5 Kashyyyk    231  124        200  
 6 Mirial      168   53.1       49  
 7 Naboo       177.  64.2       55  
 8 Ryloth      179   55         48  
 9 Tatooine    170.  85.4       54.6
10 <NA>        139.  82        334. 
starwars %>% 
  group_by(homeworld) |>  
  filter(n() > 1) |>  
  summarise(across(where(is.numeric), \(x) {mean(x, na.rm = TRUE)}))
# A tibble: 10 × 4
   homeworld height  mass birth_year
   <chr>      <dbl> <dbl>      <dbl>
 1 Alderaan    176.  64         43  
 2 Corellia    175   78.5       25  
 3 Coruscant   174.  50         91  
 4 Kamino      208.  83.1       31.5
 5 Kashyyyk    231  124        200  
 6 Mirial      168   53.1       49  
 7 Naboo       177.  64.2       55  
 8 Ryloth      179   55         48  
 9 Tatooine    170.  85.4       54.6
10 <NA>        139.  82        334. 
starwars %>% 
  group_by(homeworld) |>  
  filter(n() > 1) |>  
  summarise(across(where(is.numeric), ~ mean(.x, na.rm = TRUE)))
# A tibble: 10 × 4
   homeworld height  mass birth_year
   <chr>      <dbl> <dbl>      <dbl>
 1 Alderaan    176.  64         43  
 2 Corellia    175   78.5       25  
 3 Coruscant   174.  50         91  
 4 Kamino      208.  83.1       31.5
 5 Kashyyyk    231  124        200  
 6 Mirial      168   53.1       49  
 7 Naboo       177.  64.2       55  
 8 Ryloth      179   55         48  
 9 Tatooine    170.  85.4       54.6
10 <NA>        139.  82        334. 

Comme dplyr::across() est souvent utilisée au sein de dplyr::mutate() ou de dplyr::summarise(), les variables de groupement ne sont jamais sélectionnée par dplyr::across() pour éviter tout accident.

df <- data.frame(
  g = c(1, 1, 2),
  x = c(-1, 1, 3),
  y = c(-1, -4, -9)
)
df %>% 
  group_by(g) %>% 
  summarise(across(where(is.numeric), sum))
# A tibble: 2 × 3
      g     x     y
  <dbl> <dbl> <dbl>
1     1     0    -5
2     2     3    -9

38.1.2 Fonctions multiples

Vous pouvez transformer chaque variable avec plus d’une fonction en fournissant une liste nommée de fonctions dans le deuxième argument :

min_max <- list(
  min = \(x) min(x, na.rm = TRUE), 
  max = \(x) max(x, na.rm = TRUE)
)
starwars |> 
  summarise(across(where(is.numeric), min_max))
# A tibble: 1 × 6
  height_min height_max mass_min mass_max birth_year_min birth_year_max
       <int>      <int>    <dbl>    <dbl>          <dbl>          <dbl>
1         66        264       15     1358              8            896

On peut contrôler le nom des variables produites avec l’option .names qui prend une chaîne de caractère au format du package glue.

starwars |> 
  summarise(
    across(
      where(is.numeric),
      min_max,
      .names = "{.fn}.{.col}"
    )
  )
# A tibble: 1 × 6
  min.height max.height min.mass max.mass min.birth_year max.birth_year
       <int>      <int>    <dbl>    <dbl>          <dbl>          <dbl>
1         66        264       15     1358              8            896

38.1.3 Accéder à la colonne courante

Si vous en avez besoin, vous pouvez accéder au nom de la colonne courante à l’intérieur d’une fonction en appelant dplyr::cur_column(). Cela peut être utile si vous voulez effectuer une sorte de transformation dépendante du contexte qui est déjà encodée dans un vecteur :

df <- tibble(x = 1:3, y = 3:5, z = 5:7)
mult <- list(x = 1, y = 10, z = 100)

df |> 
  mutate(
    across(
      all_of(names(mult)), 
      ~ .x * mult[[cur_column()]]
    )
  )
# A tibble: 3 × 3
      x     y     z
  <dbl> <dbl> <dbl>
1     1    30   500
2     2    40   600
3     3    50   700

Jusqu’à présent, nous nous sommes concentrés sur l’utilisation de across() avec summarise(), mais cela fonctionne avec n’importe quel autre verbe dplyr qui utilise le masquage de données.

Par exemple, nous pouvons rééchelonner toutes les variables numériques pour se situer entre 0 et 1.

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}
df <- tibble(x = 1:4, y = rnorm(4))
df |> 
  mutate(across(where(is.numeric), rescale01))
# A tibble: 4 × 2
      x     y
  <dbl> <dbl>
1 0     1    
2 0.333 0.458
3 0.667 0.762
4 1     0    

38.1.4 pick()

Pour certains verbes, comme dplyr::group_by(), dplyr::count() et dplyr::distinct(), il n’est pas nécessaire de fournir une fonction de résumé, mais il peut être utile de pouvoir sélectionner dynamiquement un ensemble de colonnes.

Dans ce cas, nous recommandons d’utiliser le complément de dplyr::across(), dplyr::pick(), qui fonctionne comme across() mais n’applique aucune fonction et renvoie à la place un cadre de données contenant les colonnes sélectionnées.

starwars |> 
  distinct(pick(contains("color")))
# A tibble: 67 × 3
   hair_color    skin_color  eye_color
   <chr>         <chr>       <chr>    
 1 blond         fair        blue     
 2 <NA>          gold        yellow   
 3 <NA>          white, blue red      
 4 none          white       yellow   
 5 brown         light       brown    
 6 brown, grey   light       blue     
 7 brown         light       blue     
 8 <NA>          white, red  red      
 9 black         light       brown    
10 auburn, white fair        blue-gray
# ℹ 57 more rows
starwars |> 
  count(pick(contains("color")), sort = TRUE)
# A tibble: 67 × 4
   hair_color skin_color eye_color     n
   <chr>      <chr>      <chr>     <int>
 1 brown      light      brown         6
 2 brown      fair       blue          4
 3 none       grey       black         4
 4 black      dark       brown         3
 5 blond      fair       blue          3
 6 black      fair       brown         2
 7 black      tan        brown         2
 8 black      yellow     blue          2
 9 brown      fair       brown         2
10 none       white      yellow        2
# ℹ 57 more rows

dplyr::across() ne fonctionne pas avec dplyr::select() ou dplyr::rename() parce qu’ils utilisent déjà une syntaxe de sélection dynamique. Si vous voulez transformer les noms de colonnes avec une fonction, vous pouvez utiliser dplyr::rename_with().

38.2 Sélection de lignes à partir d’une sélection de colonnes

Nous ne pouvons pas utiliser directement across() dans dplyr::filter() car nous avons besoin d’une étape supplémentaire pour combiner les résultats. À cette fin, filter() dispose de deux fonctions complémentaires spéciales :

dplyr::if_any() conserve les lignes pour lesquelles le prédicat est vrai pour au moins une colonne sélectionnée :

starwars |> 
  filter(if_any(everything(), ~ !is.na(.x)))
# A tibble: 87 × 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 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

dplyr::if_all() sélectionne les lignes pour lesquelles le prédicat est vrai pour toutes les colonnes sélectionnées :

starwars |>  
  filter(if_all(everything(), ~ !is.na(.x)))
# A tibble: 29 × 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 Chewbac…    228   112 brown      unknown    blue           200   male  mascu…
10 Han Solo    180    80 brown      fair       brown           29   male  mascu…
# ℹ 19 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

38.3 Transformations multiples sur les lignes

dplyr, et R de manière générale, sont particulièrement bien adaptés à l’exécution d’opérations sur les colonnes, alors que l’exécution d’opérations sur les lignes est beaucoup plus difficile. Ici, nous verrons comment réaliser des calculs ligne par ligne avec dplyr::rowwise().

38.3.1 Création

Les opérations par ligne requièrent un type spécial de regroupement où chaque groupe est constitué d’une seule ligne. Vous créez ce type de groupe avec dplyr::rowwise() :

df <- tibble(x = 1:2, y = 3:4, z = 5:6)
df |> rowwise()
# A tibble: 2 × 3
# Rowwise: 
      x     y     z
  <int> <int> <int>
1     1     3     5
2     2     4     6

Comme group_by(), rowwise() ne fait rien en soi ; elle modifie simplement le fonctionnement des autres verbes. Par exemple, comparez les résultats de mutate() dans le code suivant :

df |> 
  mutate(m = mean(c(x, y, z)))
# A tibble: 2 × 4
      x     y     z     m
  <int> <int> <int> <dbl>
1     1     3     5   3.5
2     2     4     6   3.5
df |> 
  rowwise() |> 
  mutate(m = mean(c(x, y, z)))
# A tibble: 2 × 4
# Rowwise: 
      x     y     z     m
  <int> <int> <int> <dbl>
1     1     3     5     3
2     2     4     6     4

Si vous utilisez mutate() avec un tableau de données classique, il calcule la moyenne de x, y et z sur toutes les lignes. Si vous l’appliquez à un tableau de données row-wise, il calcule la moyenne séparément pour chaque ligne.

Vous pouvez optionnellement fournir des variables identifiantes dans votre appel à rowwise(). Ces variables sont conservées lorsque vous appelez summarise(), de sorte qu’elles se comportent de manière similaire aux variables de regroupement passées à group_by():

df <- tibble(
  name = c("Mara", "Hadley"),
  x = 1:2,
  y = 3:4,
  z = 5:6
)

df |> 
  rowwise() |> 
  summarise(m = mean(c(x, y, z)))
# A tibble: 2 × 1
      m
  <dbl>
1     3
2     4
df |> 
  rowwise(name) |> 
  summarise(m = mean(c(x, y, z)))
`summarise()` has grouped output by 'name'. You can override using the
`.groups` argument.
# A tibble: 2 × 2
# Groups:   name [2]
  name       m
  <chr>  <dbl>
1 Mara       3
2 Hadley     4

rowwise() n’est qu’une forme spéciale de regroupement : donc si vous voulez enlever sa déclaration, appelez simplement ungroup().

38.3.2 Statistiques ligne par ligne

dplyr::summarise() permet de résumer facilement les valeurs d’une ligne à l’autre à l’intérieur d’une colonne. Combinée à rowwise(), elle permet également de résumer les valeurs de plusieurs colonnes à l’intérieur d’une même ligne. Pour voir comment, commençons par créer un petit jeu de données :

df <- tibble(
  id = 1:6,
  w = 10:15,
  x = 20:25,
  y = 30:35,
  z = 40:45
)
df
# A tibble: 6 × 5
     id     w     x     y     z
  <int> <int> <int> <int> <int>
1     1    10    20    30    40
2     2    11    21    31    41
3     3    12    22    32    42
4     4    13    23    33    43
5     5    14    24    34    44
6     6    15    25    35    45

Supposons que nous voulions calculer la somme de w, x, y et z pour chaque ligne. Nous pouvons utiliser mutate() pour ajouter une nouvelle colonne ou summarise() pour renvoyer ce seul résumé :

df |> 
  rowwise(id) |> 
  mutate(total = sum(c(w, x, y, z)))
# A tibble: 6 × 6
# Rowwise:  id
     id     w     x     y     z total
  <int> <int> <int> <int> <int> <int>
1     1    10    20    30    40   100
2     2    11    21    31    41   104
3     3    12    22    32    42   108
4     4    13    23    33    43   112
5     5    14    24    34    44   116
6     6    15    25    35    45   120
df |> 
  rowwise(id) |> 
  summarise(total = sum(c(w, x, y, z)))
`summarise()` has grouped output by 'id'. You can override using the `.groups`
argument.
# A tibble: 6 × 2
# Groups:   id [6]
     id total
  <int> <int>
1     1   100
2     2   104
3     3   108
4     4   112
5     5   116
6     6   120

Bien sûr, si vous avez beaucoup de variables, il sera fastidieux de taper chaque nom de variable. Au lieu de cela, vous pouvez utiliser dplyr::c_across() qui utilise une syntaxe tidy selection afin de sélectionner succinctement de nombreuses variables :

df |> 
  rowwise(id) |> 
  mutate(total = sum(c_across(w:z)))
# A tibble: 6 × 6
# Rowwise:  id
     id     w     x     y     z total
  <int> <int> <int> <int> <int> <int>
1     1    10    20    30    40   100
2     2    11    21    31    41   104
3     3    12    22    32    42   108
4     4    13    23    33    43   112
5     5    14    24    34    44   116
6     6    15    25    35    45   120
df |> 
  rowwise(id) |> 
  mutate(total = sum(c_across(where(is.numeric))))
# A tibble: 6 × 6
# Rowwise:  id
     id     w     x     y     z total
  <int> <int> <int> <int> <int> <int>
1     1    10    20    30    40   100
2     2    11    21    31    41   104
3     3    12    22    32    42   108
4     4    13    23    33    43   112
5     5    14    24    34    44   116
6     6    15    25    35    45   120

Vous pouvez combiner cela avec des opérations par colonne (voir la section précédente) pour calculer la proportion du total pour chaque colonne :

df |> 
  rowwise(id) |> 
  mutate(total = sum(c_across(w:z))) |> 
  ungroup() |> 
  mutate(across(w:z, ~ . / total))
# A tibble: 6 × 6
     id     w     x     y     z total
  <int> <dbl> <dbl> <dbl> <dbl> <int>
1     1 0.1   0.2   0.3   0.4     100
2     2 0.106 0.202 0.298 0.394   104
3     3 0.111 0.204 0.296 0.389   108
4     4 0.116 0.205 0.295 0.384   112
5     5 0.121 0.207 0.293 0.379   116
6     6 0.125 0.208 0.292 0.375   120
Important

L’approche rowwise() fonctionne pour n’importe quelle fonction de résumé. Mais si vous avez besoin d’une plus grande rapidité, il est préférable de rechercher une variante intégrée de votre fonction de résumé. Celles-ci sont plus efficaces car elles opèrent sur l’ensemble du cadre de données ; elles ne le divisent pas en lignes, ne calculent pas le résumé et ne joignent pas à nouveau les résultats.

Par exemple, R fournit nativement les fonctions rowSums() et rowMeans() pour calculer des sommes et des moyennes par ligne. Elles sont de fait bien plus efficaces.

df  |> 
  mutate(total = rowSums(pick(where(is.numeric), -id)))
# A tibble: 6 × 6
     id     w     x     y     z total
  <int> <int> <int> <int> <int> <dbl>
1     1    10    20    30    40   100
2     2    11    21    31    41   104
3     3    12    22    32    42   108
4     4    13    23    33    43   112
5     5    14    24    34    44   116
6     6    15    25    35    45   120
df |> 
  mutate(mean = rowMeans(pick(where(is.numeric), -id)))
# A tibble: 6 × 6
     id     w     x     y     z  mean
  <int> <int> <int> <int> <int> <dbl>
1     1    10    20    30    40    25
2     2    11    21    31    41    26
3     3    12    22    32    42    27
4     4    13    23    33    43    28
5     5    14    24    34    44    29
6     6    15    25    35    45    30