39 Transformations multiples
39.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 :
Dans cette section, nous allons introduire dplyr::across()
qui permets de réécrire la même commande de manière plus succincte.
39.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 dedplyr::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.
# 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.
# 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é.x
1.
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.
39.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.
39.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.
39.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.
# 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
# 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()
.
39.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 :
# 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 :
# 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>
39.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()
.
39.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()
:
# 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 :
# A tibble: 2 × 4
x y z m
<int> <int> <int> <dbl>
1 1 3 5 3.5
2 2 4 6 3.5
# 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
`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()
.
39.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 :
# 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é :
# 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
`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 :
# 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
# 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
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.
# 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
# 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