Utilisation de case_when dans dplyr : cas des variables facteurs
Le verbe case_when
est un incontournable du traitement de données avec dplyr
. Il permet de créer une variable conditionnellement à une ou plusieurs variables existantes. Sa syntaxe est très lisible et permet à votre code de rester clair. Cependant, vous avez peut-être déjà eu quelques problèmes de compatibilité avec les variables facteurs. Dans cette note, on vous présente ce verbe bien pratique de dplyr
, ses nouveautés et comment l'utiliser pour créer directement des variables facteurs.
Syntaxe de case_when dans dplyr et différences avec if_else
Pour cet article, nous nous reposons sur un dataset complètement fictif qui a la structure suivante :
## tibble [200 × 4] (S3: tbl_df/tbl/data.frame)
## $ id : int [1:200] 1 2 3 4 5 6 7 8 9 10 ...
## $ groupe: chr [1:200] "C" "C" "C" "B" ...
## $ var1 : int [1:200] NA 20 10 28 19 30 NA 22 20 20 ...
## $ statut: Factor w/ 2 levels "ko","ok": NA 1 NA 1 1 2 1 NA 2 2 ...
Vous pouvez retrouver le code ayant servi à le générer sur le dépôt github des notes de blog de Statoscop.
Le verbe case_when
comporte plusieurs différences avec if_else
, mais deux nous semblent particulièrement importantes :
- sa syntaxe rend la lecture de plusieurs conditions bien plus aisée
- par défaut, il ne renvoie pas NA s'il croise une valeur manquante dans la condition
Défintion de conditions multiples
Pour la première différence, la syntaxe de case_when
va permettre de définir les différentes conditions et la valeur associée sur une ligne dédiée alors que celle de if_else
oblige à chaîner les appels à la fonction. En fait, case_when
a même été créé pour mettre de généraliser if_else
à des multiples conditions puisqu'il est indiqué dans sa description de la page d'aide que c'est une fonction permettant de vectoriser l'appel à plusieurs if_else
. Illustrons cela en codant des deux manières une variable égale à :
- "cas 1" si groupe = "A" et var1 < 25
- "cas 2" si groupe = "B" ou "C" et var1 >= 25
- "cas 3" sinon
Le code avec les deux syntaxes est le suivant :
df <- df |> mutate(
# syntaxe case_when
cond_case = case_when(
groupe == "A" & var1 < 25 ~ "cas 1",
groupe %in% c("B", "C") & var1 >= 25 ~ "cas 2",
.default = "cas 3"),
# syntaxe if_else
cond_ifelse = if_else(
groupe == "A" & var1 < 25,
"cas 1",
if_else(
groupe %in% c("B", "C") & var1 >= 25,
"cas 2",
"cas 3"))
)
La syntaxe de case_when permet de produire un code plus lisible et aéré. À noter que si l'on ne définissait pas de valeur par défaut explicitement, on aurait à la place des valeurs manquantes.
Gestion des valeurs manquantes
Pour la seconde différence, case_when
considère les valeurs manquantes comme une valeur à part entière alors que if_else
renvoie automatiquement une valeur manquante s'il trouve une valeur manquante dans la condition. Si l'on observe nos deux variables on constate en effet qu'elles ne sont pas toujours égales :
df |> head(10) |>
knitr::kable()
id | groupe | var1 | statut | cond_case | cond_ifelse |
---|---|---|---|---|---|
1 | C | NA | NA | cas 3 | NA |
2 | C | 20 | ko | cas 3 | cas 3 |
3 | C | 10 | NA | cas 3 | cas 3 |
4 | B | 28 | ko | cas 2 | cas 2 |
5 | C | 19 | ko | cas 3 | cas 3 |
6 | B | 30 | ok | cas 2 | cas 2 |
7 | B | NA | ko | cas 3 | NA |
8 | B | 22 | NA | cas 3 | cas 3 |
9 | C | 20 | ok | cas 3 | cas 3 |
10 | A | 20 | ok | cas 1 | cas 1 |
Ainsi, lorsque la valeur de la variable var1
est manquante, la méthode if_else
renvoie une valeur manquante alors que case_when
lui donne la valeur "cas 3" car elle ne correspond pas aux deux premières conditions. Il faut donc traiter explicitement les valeurs manquantes dans les conditions de case_when
si l'on souhaite qu'elles ne soient pas prises en compte.
👋 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?
Gestion des types facteur avec case_when
La création d'une variable avec case_when
doit respecter le fait que la variable créée ait un type unique. Cela peut poser problème lorsque l'on souhaite créer directement une variable facteur.
.default
pour définir la valeur de base
Le paramètre .default
permet de définir la valeur que prend la variable lorsqu'aucune des conditions n'est respectée. Il remplace l'utilisation de la syntaxe .TRUE ~ valeur
depuis dplyr 1.1.0. Il peut correspondre à une valeur, comme dans l'exemple précédent, ou à une variable existante. Il faut veiller à ce que le type de la variable corresponde bien à celui des modalités définies précédemment. En effet, on obtient sinon l'erreur suivante :
df |> mutate(
statut_bis = case_when(
is.na(var1) ~ FALSE, # modalité booléen
.default = statut)) # variable facteur
## Error in `mutate()`:
## ℹ In argument: `statut_bis = case_when(is.na(var1) ~ FALSE, .default =
## statut)`.
## Caused by error in `case_when()`:
## ! Can't combine `..1 (right)` <logical> and `.default` <factor<3d285>>.
Le message d'erreur nous indique bien l'impossibilité pour case_when
de combiner des valeurs booléennes (le FALSE
) et une variable facteur (la variable statut
donnant la valeur par défaut).
À noter qu'auparavant, case_when
sortait également une erreur lorsqu'on définissait des modalités caractères et une variable facteur par défaut. Cela n'est plus le cas depuis dplyr 1.1.0, puisque case_when
transforme automatiquement les variables facteur en variable caractère. Ainsi, le code suivant fonctionne :
df |> mutate(
statut_bis = case_when(
is.na(var1) ~ "inconnu", # modalité caractère
.default = statut)) |> # variable facteur convertie automatiquement en caractère
count(statut_bis) # affichage des modalités de statut_bis
## # A tibble: 4 × 2
## statut_bis n
## <chr> <int>
## 1 inconnu 5
## 2 ko 62
## 3 ok 61
## 4 <NA> 72
.ptype
pour créer directement un facteur
Il n'est ici pas possible de définir directement un facteur et l'ordre de ses niveaux, à moins de le faire dans une nouvelle instruction dédiée. C'est là qu'entre en jeu l'argument .ptype
de la fonction case_when
. Par défaut, le type de la variable est en effet déduit des valeurs définies après les conditions. L'argument ptype
permet d'imposer un type spécifique quand cela est compatible avec les expressions utilisées. Dans notre exemple précédent, on peut ainsi spécifier que l'on souhaite une variable facteur en sortie, à condition de bien en définir les niveaux :
df |> mutate(
statut_bis = case_when(
is.na(var1) ~ "inconnu", # modalité caractère
.default = statut,
.ptype = factor(levels = c("ok", "ko", "inconnu")))) |>
count(statut_bis) # affichage des modalités de statut_bis
## # A tibble: 4 × 2
## statut_bis n
## <fct> <int>
## 1 ok 61
## 2 ko 62
## 3 inconnu 5
## 4 <NA> 72
On a ainsi pu définir notre variable facteur avec l'ordre des niveaux voulu directement dans case_when
.
C'est la fin de cet article! N'hésitez pas à visiter notre site et à 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.