if_any et if_all : appliquer la même condition sur plusieurs variables dans dplyr
Au début de l'année 2021, la sortie de dplyr 1.0.4 consacre l'arrivée de deux petits nouveaux : if_any
et if_all
. Ces deux fonctions viennent notamment compléter le verbe across
, qui avait été introduit quelques mois plus tôt dans dplyr 1.0.0, et que nous avons déjà présenté dans un précédent article.
Ici nous vous proposons donc un rapide tour des possibilités offertes par ces fonctions. Nous commençons par montrer comment ils peuvent être utilisés dans filter()
pour filtrer facilement notre dataframe en appliquant la même condition à plusieurs variables. Puis nous montrons comment ils peuvent être avantageusement utilisés dans des instructions mutate()
pour synthétiser les valeurs de plusieurs variables.
Syntaxe de if_any
et if_all
La syntaxe de ces deux verbes est exactement la même que celle d'across()
:
if_any(.cols, .fns, ...)
if_all(.cols, .fns, ...)
Comme pour across
, le paramètre .cols
permet de sélectionner les variables sur lesquelles on souhaite appliquer notre filtre. Pour cela on utilise les méthodes du tidyselect
:
- where(fn)
pour sélectionner les colonnes sur une condition (where(is.numeric)
par exemple).
- starts_with(match)
, ends_with(match)
, contains(match)
, matches(match)
pour sélectionner les variables sur une caractéristique de leur nom (starts_with("v_")
par exemple).
- c(var1, var2, var3)
, all_of(c("var1", "var2", "var3"))
ou any_of(c("var1", "var2", "var3"))
pour sélectionner directement les variables par leur nom.
Le paramètre .fns
permet de définir la fonction de filtrage sur les variables sélectionnées. Ce sera soit une fonction déjà existante (is.na
par exemple), soit une fonction définie en par l'utilisateur en amont ou à la volée (avec la syntaxe ~ .x > 50
par exemple).
Ces deux fonctions vont créer en sortie un vecteur booléen qui va nous permettre de filtrer nos observations de deux façons :
- avec
if_any
si la condition définie dans.fns
est respectée pour au moins une des variables. Cela revient à coder la condition pour chaque colonne avec l'opérateur|
(OU
). - avec
if_all
si cette condition est respectée pour toutes les variables. Cela revient à coder la condition pour chaque colonne avec l'opérateur&
(ET
).
Voyons concrètement comment nous pouvons les utiliser dans deux cas distincts : filtrer notre dataframe sur une conditions portant sur plusieurs variables à la fois et créer une nouvelle variable en fonction des valeurs de plusieurs autres variables.
👋 Nous c'est Antoine et Louis de Statoscop, une coopérative de statisticiens / data scientists. Vous voulez en savoir plus sur ce que l'on fait?
Filtrer un dataframe avec if_any
et if_all
Pour illustrer l'utilisation de ces deux verbes, on s'appuie sur des données Kaggle de notes d'étudiants à différentes matières. On a modifié ces données pour avoir une colonne pour chaque note de chaque matière. Notre dataset stud_grades
a maintenant ce format :
## Rows: 10,000
## Columns: 6
## $ student.id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, …
## $ math <chr> "F", "F", "F", "D", "C", "A", "D", "F", "A", "F", "…
## $ science <chr> "C", "A", "A", "B", "C", "C", "F", "F", "C", "A", "…
## $ history <chr> "A", "B", "A", "F", "C", "C", "A", "A", "D", "B", "…
## $ english <chr> "F", "C", "A", "D", "A", "C", "D", "C", "F", "F", "…
## $ physical_education <chr> "C", "A", "C", "B", "C", "F", "B", "B", "B", "F", "…
Les notes possibles sont A
, B
, C
, D
et F
.
On présente plusieurs opérations de filtrage qui peuvent être utiles dans notre exploration des données.
Repérer des valeurs en particulier avec if_any
La première question que l'on peut se poser est de savoir s'il manque des informations dans notre dataset. if_any
va nous permettre de renvoyer les lignes contenant au moins une valeur manquante parmi les variables que l'on sélectionne :
stud_grades |>
filter(if_any(c(math:physical_education), is.na)) |>
print()
## # A tibble: 0 × 6
## # ℹ 6 variables: student.id <int>, math <chr>, science <chr>, history <chr>,
## # english <chr>, physical_education <chr>
On voit tout de suite que le dataframe ne contient pas de valeurs manquantes, d'une manière plus directe et élégante que de coder la condition is.na(math) | is.na(science) | is.na(history) | is.na(english) | is.na(physical_education)
.
Dans la même logique on pourrait vouloir identifier les cas où des élèves ont reçu la note "F" :
stud_grades |>
# On définit ici notre fonction "à la volée" avec ~ function(.x)
filter(if_any(c(math:physical_education), ~ .x == "F")) |>
# on renvoie le nombre d'observations
nrow()
## [1] 6749
On apprend ainsi que 6749 élèves sur 10 000 ont eu au moins un F à une des matières!
Repérer les très bons (ou les très mauvais) résultats avec if_all
Avec if_all
, on peut facilement repérer les excellents résultats en filtrant par exemple sur les élèves ayant eu des "A" à toutes les matières :
stud_grades |>
# notez qu'on sélectionne les mêmes colonnes que précédemment
# avec une méthode différente
filter(if_all(where(is.character), ~ .x == "A")) |>
print()
## # A tibble: 2 × 6
## student.id math science history english physical_education
## <int> <chr> <chr> <chr> <chr> <chr>
## 1 6933 A A A A A
## 2 8749 A A A A A
On voit ainsi directement que seulement deux élèves ont réussi cet exploit, avec une syntaxe bien plus directe que de coder math == "A" & science == "A" & history == "A" & english == "A" & physical_education == "A"
.
On peut bien sûr également repérer les élèves en difficulté, en regardant ceux qui n'ont eu que des "F" à toutes les matières.
Créer une variable synthétique avec if_any
et if_all
dans case_when
On conclut cet article en vous montrant comment profiter avantageusement de ces verbes dans une instruction mutate()
, dans le but par exemple de créer une variable synthéthique qui prenne en compte l'ensemble de ces notes. Les modalités pourraient être les suivantes :
1 - Facilités en sciences et en lettres (au moins B dans toutes ces matières)
2 - Facilités en sciences, difficultés en lettres (au moins B en maths et science, C ou moins dans une matière littéraire)
3 - Facilités en lettres, difficultés en sciences (au moins B en histoire et anglais, C ou moins dans une matière scientifique)
4 - Difficultés en lettres et sciences (jamais mieux que C dans aucune de ces 4 matières)
5 - Difficultés surtout en sciences (jamais mieux que C dans une matière scientifique)
6 - Difficultés surtout en lettres (jamais mieux que C dans une matière littéraire)
7 - Résultats irréguliers (aucune des conditions précédentes : donc bons résultats dans seulement au moins une des matières scientifiques et littéraire)
Ces conditions s'écrivent ainsi :
stud_grades <- stud_grades |>
mutate(
lvl_sc_lit = case_when(
if_all(c(math:english), ~ .x %in% c("A", "B")) ~ "Facilités en sciences et en lettres",
if_all(c(math, science), ~ .x %in% c("A", "B")) &
if_any(c(history, english), ~ .x %in% c("C", "D", "F")) ~ "Facilités en sciences, difficultés en lettres",
if_all(c(history, english), ~ .x %in% c("A", "B")) &
if_any(c(math, science), ~ .x %in% c("C", "D", "F")) ~ "Facilités en lettres, difficultés en sciences",
if_all(c(math:english), ~ .x %in% c("C", "D", "F")) ~ "Difficultés en lettres et sciences",
if_all(c(math, science), ~ .x %in% c("C", "D", "F")) &
if_any(c(history, english), ~ .x %in% c("A", "B")) ~ "Difficultés surtout en sciences",
if_all(c(history, english), ~ .x %in% c("C", "D", "F")) &
if_any(c(math, science), ~ .x %in% c("A", "B")) ~ "Difficultés surtout en lettres",
TRUE ~ "Résultats irréguliers"))
On voit ainsi qu'on peut combiner des conditions de if_any
et if_all
avec les &
et |
, ce qui permet une grande flexibilité dans la création de nos conditions. On utilise également le paramètre TRUE
de case_when
pour tous les cas ne tombant dans aucune des conditions que nous avons imaginées, c'est à dire les élèves n'ayant pas de profil particulièrement littéraire ou scientifique mais plutôt des résultats très irréguliers dans les différentes matières. Voici comment se répartissent nos élèves dans cette nouvelle variable synthétique :
## # A tibble: 7 × 2
## lvl_sc_lit n
## <chr> <int>
## 1 Difficultés en lettres et sciences 1334
## 2 Difficultés surtout en lettres 1790
## 3 Difficultés surtout en sciences 1678
## 4 Facilités en lettres, difficultés en sciences 1334
## 5 Facilités en sciences et en lettres 259
## 6 Facilités en sciences, difficultés en lettres 1337
## 7 Résultats irréguliers 2268
C'est tout pour aujourd'hui! On espère que cette note vous permettra de mieux exploiter la puissance de ces deux petits verbes bien pratiques. Si vous avez besoin de conseils en programmation pour l'analyse de données, n'hésitez pas à continuer votre navigation sur notre site ou à nous suivre sur BlueSky et Linkedin. Pour retrouver l'ensemble du code ayant servi à générer cette note, vous pouvez vous rendre sur le github de Statoscop.