Leçon 4 sur 8
Unité · Des données qui gardent leur sens
Les facteurs, et l'ordre qu'exige un rapport
Ce qu'est réellement un facteur, le piège de as.numeric() qui renvoie silencieusement des positions de niveaux, et les verbes forcats qui mettent une échelle JMP dans l'ordre de l'échelle plutôt que dans celui de l'alphabet.
Un facteur est un entier assorti d’une table de correspondance
service <- factor(c("basic", "limited", "unimproved", "basic"))
levels(service)
#> [1] "basic" "limited" "unimproved"
as.integer(service)
#> [1] 1 2 3 1
En dessous, R stocke 1 2 3 1 et un vecteur de caractères contenant les niveaux.
Tout ce qui suit — le problème d’ordre, le piège de as.numeric(), les niveaux
inutilisés — découle de ce seul fait.
Remarquez que les niveaux sont alphabétiques. Rien dans basic, limited,
unimproved n’indique que ce soit l’ordre, et c’est exactement l’inverse pour
une échelle de service du JMP, où non améliorée est le pire et où le service de
base vaut mieux que le service limité. Laissé tel quel, chaque tableau et chaque
graphique bâti sur cette colonne se lit dans un ordre qui ne signifie rien.
Le piège de as.numeric()
C’est l’erreur de facteur la plus coûteuse, et elle n’avertit pas.
age <- factor(c("10", "9", "8"))
as.numeric(age)
#> [1] 1 3 2
Ce sont des positions de niveaux, non des valeurs. Les niveaux sont
alphabétiques — "10", "8", "9" — donc "10" est en position 1 et "9" en
position 3.
as.numeric(as.character(age))
#> [1] 10 9 8
as.character() d’abord, systématiquement. Le cas survient dès qu’une colonne
numérique a été lue comme du texte — une colonne Excel avec un "n/a" égaré, un
CSV lu en col_character() — puis convertie en facteur en chemin.
Méfiez-vous d’une moyenne suspectement proche du nombre de catégories.
Fixer l’ordre voulu
library(forcats)
ladder <- fct_relevel(service, "unimproved", "limited", "basic")
levels(ladder)
#> [1] "unimproved" "limited" "basic"
fct_relevel() nomme l’ordre explicitement. Déclarez-le une fois, près de
l’endroit où la colonne est créée, et chaque tableau, graphique et modèle en aval
en hérite.
Deux verbes forcats gagnent immédiatement leur place :
fct_infreq(source) # le niveau le plus frequent en premier
fct_reorder(name, value) # ordonner un facteur par une autre colonne
fct_reorder() est ce qui transforme un graphique en barres illisible en
graphique lisible — les communes triées par leur taux de MAG plutôt que par
l’alphabet — et il le fait sans un levels = manuel à mettre à jour quand les
données changent.
library(dplyr)
by_commune |>
mutate(commune = fct_reorder(commune, gam_rate)) |>
ggplot2::ggplot(ggplot2::aes(gam_rate, commune)) +
ggplot2::geom_col()
Les facteurs ordonnés
fct_relevel() fixe l’ordre des niveaux. Il ne rend pas le facteur
comparable :
service[1] >= service[2]
#> Warning: '>=' not meaningful for factors
#> [1] NA
Pour une échelle, où « au moins de base » est une vraie question, déclarez-la ordonnée :
ladder <- factor(
c("basic", "limited"),
levels = c("unimproved", "limited", "basic"),
ordered = TRUE
)
ladder[1] >= ladder[2]
#> [1] TRUE
>= "basic" calcule alors directement l’indicateur de couverture du JMP. Sans
ordered = TRUE, l’expression renvoie NA avec un avertissement — qui, s’il
n’est pas lu, devient un chiffre de couverture à NA ou, pire, un filtre qui ne
sélectionne rien.
Employez les facteurs ordonnés avec parcimonie. Ils changent la façon dont les modèles traitent la variable (contrastes polynomiaux plutôt qu’indicatrices), ce qui est rarement souhaité dans une régression. Pour une échelle de service, une bande de sévérité ou une phase IPC, l’ordre en vaut la peine. Pour une commune, non.
Les niveaux inutilisés, et les deux comportements qui divergent
Un niveau sans ligne reste un niveau. Son apparition dans votre sortie dépend de la fonction interrogée, et R n’est pas cohérent avec lui-même sur ce point.
d <- tibble::tibble(k = factor(c("a", "b", "a"), levels = c("a", "b", "c")))
nrow(dplyr::count(d, k))
#> [1] 2
length(table(d$k))
#> [1] 3
count() écarte le niveau vide ; table() le conserve. Les deux sont justes
pour des questions différentes, et la question est celle-là même que le cours
Python pour les données de programme soulève à propos d’observed= : rapporter
sur les formations ayant transmis des données veut le niveau vide en moins, et
rapporter la couverture face à une liste de formations qui auraient dû
transmettre le veut présent, car une formation à zéro ligne est justement le
constat.
Dites lequel vous voulez :
dplyr::count(d, k, .drop = FALSE) # conserver le niveau vide
droplevels(d$k) # retirer les niveaux sans ligne
forcats::fct_drop(d$k) # la meme chose, en forcats
Réduire une longue traîne
Dix sources d’eau, c’est trop de lignes pour un tableau de rapport et trop de couleurs pour un graphique.
source <- factor(wash$water_source)
length(levels(source))
#> [1] 10
levels(fct_lump_n(source, 4))
#> [1] "borehole" "piped-into-dwelling" "piped-into-yard" "public-tap" "Other"
fct_lump_n() conserve les quatre plus fréquents et replie le reste dans
Other.
Ne repliez pas avant de calculer un indicateur. Other mêle ici les sources
protégées, qui sont améliorées, et l’eau de surface, qui ne l’est pas : un taux
de couverture calculé après repli est donc faux. Repliez pour la présentation,
sur une copie, une fois les chiffres arrêtés.
fct_recode(source,
improved = "borehole",
improved = "protected-well",
unimproved = "surface-water"
)
fct_recode() est l’alternative explicite et c’est ce qu’il faut pour une
classification : elle nomme chaque correspondance, si bien qu’une source
apparaissant au prochain export sans figurer dans la liste reste elle-même au
lieu de rejoindre silencieusement un groupe.
Facteurs et valeurs manquantes
NA n’est pas un niveau : il survit donc à tout réordonnancement et n’apparaît
dans table() que si vous le demandez :
table(muac$outcome, useNA = "ifany")
Pour faire de l’absence une catégorie explicite — ce qui est souvent juste dans un rapport, où « non renseigné » est un constat et non un trou :
muac <- muac |> mutate(outcome = fct_na_value_to_level(outcome, level = "non renseigne"))
Une fois devenue un niveau, elle sera comptée, tracée et sommée comme les autres. C’est le but, et c’est aussi le risque : un taux calculé sur un facteur incluant « non renseigné » n’a pas le même dénominateur qu’un taux qui l’exclut.
Là où les facteurs mordent dans une jointure
left_join(muac, sites, by = "commune")
Si commune est un facteur dans un tableau et du caractère dans l’autre, dplyr
convertit et avertit. Si c’est un facteur dans les deux avec des niveaux
différents, la jointure fonctionne quand même — dplyr compare les étiquettes,
non les entiers — mais les niveaux du résultat sont l’union des deux, ce qui peut
discrètement réintroduire des catégories vides.
L’habitude la plus simple : joignez sur des colonnes de caractères, convertissez en facteur ensuite. Les facteurs sont un dispositif de présentation et de modélisation, non un type de clé.
Ce qui vient ensuite
Les colonnes portent désormais leur sens et leur ordre. L’unité suivante les travaille : les verbes dplyr, puis l’appel de regroupement où naissent la plupart des erreurs d’indicateurs.