Afficher le code R
library(tidyverse)
raw <- readr::read_csv(
"data/raw/enqueter_brut.csv",
show_col_types = FALSE
)Avant de supprimer ou recoder une seule valeur, comment établir un diagnostic qui distingue doublons, codes spéciaux, valeurs impossibles et incohérences entre questions ?
À la fin de ce chapitre, vous saurez :
Il faut savoir importer le fichier brut et consulter le dictionnaire de variables. Les codes spéciaux seront identifiés ici, mais leur traitement méthodologique est approfondi au chapitre 7.
Un nettoyage rigoureux ne consiste pas à supprimer ce qui paraît inhabituel. Il vise à vérifier si les observations respectent :
Une valeur extrême peut être vraie. Une valeur très fréquente peut être fausse. C’est pourquoi le diagnostic doit précéder la correction.
Un revenu très élevé est peut-être rare mais possible. Un âge de -2 ou 120 dans une enquête limitée aux 18–79 ans est, lui, incompatible avec le champ documenté. Les règles de nettoyage doivent s’appuyer sur la définition de la variable, pas sur une préférence esthétique.
library(tidyverse)
raw <- readr::read_csv(
"data/raw/enqueter_brut.csv",
show_col_types = FALSE
)Nous utilisons volontairement le nom raw pour rappeler que cet objet reproduit la source. Toutes les transformations seront enregistrées dans un nouvel objet.
dim(raw)[1] 3600 36
head(names(raw), 12) [1] "id" "strate" "psu"
[4] "poids_base" "poids_final" "territoire"
[7] "mode_collecte" "sexe" "age"
[10] "diplome" "statut_emploi" "situation_familiale"
raw |>
dplyr::select(id, age, sexe, diplome, revenu_mensuel, poids_final) |>
dplyr::glimpse()Rows: 3,600
Columns: 6
$ id <dbl> 100001, 100002, 100003, 100004, 100005, 100006, 100007,…
$ age <dbl> 70, 64, 30, 41, 48, 63, 51, 27, 35, 34, 31, 28, 33, 54,…
$ sexe <dbl> 2, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 1, 1, 1, 2, 2, 1, 2, 2…
$ diplome <dbl> 2, 2, 3, 7, 1, 2, 7, 7, 5, 3, 1, 2, 5, 3, 2, 4, 1, 6, 4…
$ revenu_mensuel <dbl> 1750, 99997, 2600, 3400, 2200, 2350, 99998, 2500, 2350,…
$ poids_final <dbl> 1.2692, 0.7122, 1.2943, 0.8282, 1.3215, 0.9727, 0.8254,…
Le fichier pédagogique comporte 3 600 lignes et 36 colonnes. Cette information est connue grâce à la documentation du kit.
Un contrôle automatisé :
stopifnot(nrow(raw) == 3600)
stopifnot(ncol(raw) == 36)
stopifnot("id" %in% names(raw))Dans une enquête réelle, les valeurs attendues peuvent changer entre deux vagues ou livraisons. Le contrôle doit donc être adapté à la version du fichier.
Un identifiant individuel devrait être unique si chaque ligne représente un répondant.
n_distinct(raw$id)[1] 3592
sum(duplicated(raw$id))[1] 8
Dans le jeu pédagogique, il y a 3 592 identifiants distincts pour 3 600 lignes, soit huit lignes dont l’identifiant est déjà apparu.
Pour les inspecter :
doublons_id <- raw |>
filter(id %in% id[duplicated(id)] | duplicated(id, fromLast = TRUE)) |>
arrange(id)
doublons_id |>
select(id, sexe, age, diplome, territoire)# A tibble: 16 × 5
id sexe age diplome territoire
<dbl> <dbl> <dbl> <dbl> <dbl>
1 100011 2 31 1 1
2 100011 2 79 5 5
3 100022 2 30 7 3
4 100022 1 24 6 1
5 100030 1 42 1 5
6 100030 2 42 1 1
7 100034 1 62 5 5
8 100034 2 48 2 2
9 100037 2 66 4 4
10 100037 2 52 4 5
11 100041 1 47 2 2
12 100041 2 79 1 2
13 100051 2 36 4 4
14 100051 2 68 3 3
15 100086 1 59 4 1
16 100086 1 38 7 2
Il existe plusieurs situations :
On ne peut donc pas déduire automatiquement la correction de duplicated().
Dans ce jeu pédagogique uniquement, la règle documentée consiste à conserver la première occurrence :
enqueter <- raw |>
distinct(id, .keep_all = TRUE)Dans une enquête réelle, on remonterait d’abord à la source ou au protocole de collecte.
distinct()
Écrire immédiatement distinct(id, .keep_all = TRUE) ferait disparaître le symptôme sans expliquer son origine. Conserver le nombre et les lignes concernées dans un audit rend la décision vérifiable.
Le questionnaire indique un champ d’âge de 18 à 79 ans et un code spécial 999 pour la non-réponse.
raw |>
filter(!between(age, 18, 79), age != 999) |>
count(age)# A tibble: 5 × 2
age n
<dbl> <int>
1 -2 1
2 15 1
3 16 1
4 105 1
5 120 1
Le jeu contient cinq âges volontairement impossibles hors code spécial : -2, 15, 16, 105 et 120.
999 du contrôle ?Parce que 999 n’est pas un âge aberrant : c’est un code documenté de non-réponse. Il sera transformé plus tard en valeur manquante, mais sa signification n’est pas la même qu’une erreur de plage.
Au lieu de supprimer immédiatement :
audit_age <- raw |>
mutate(
age_code_special = age == 999,
age_hors_champ = !between(age, 18, 79) & age != 999
) |>
count(age_code_special, age_hors_champ)
audit_age# A tibble: 3 × 3
age_code_special age_hors_champ n
<lgl> <lgl> <int>
1 FALSE FALSE 3576
2 FALSE TRUE 5
3 TRUE FALSE 19
Cette logique de flags est très utile : elle permet de mesurer et documenter les anomalies avant correction.
Le dictionnaire indique une plage pédagogique attendue de 1 à 8 :
raw |>
filter(!between(taille_menage, 1, 8)) |>
count(taille_menage)# A tibble: 3 × 2
taille_menage n
<dbl> <int>
1 0 2
2 12 1
3 15 1
Le fichier contient quatre observations hors plage attendue.
Les codes 98 et 99 sont réservés à la non-réponse. Parmi les réponses substantives, nous considérons ici que plus de 14 heures quotidiennes est une anomalie pédagogique documentée :
raw |>
filter(heures_tv > 14, heures_tv < 98) |>
arrange(desc(heures_tv)) |>
select(id, heures_tv)# A tibble: 10 × 2
id heures_tv
<dbl> <dbl>
1 101200 35
2 100594 30
3 103584 30
4 101839 24
5 101882 24
6 102368 24
7 102457 24
8 102622 24
9 101531 18
10 103502 18
Dix valeurs ont été introduites à cet effet.
Le seuil de 14 heures est une règle du jeu pédagogique, pas une règle universelle de sociologie des médias. Dans une enquête réelle, une valeur de 16 heures peut être improbable sans être impossible. Il faut documenter les bornes et, lorsque nécessaire, effectuer une analyse de sensibilité.
Les questions à réponses multiples sont stockées en plusieurs colonnes binaires :
info_tv
info_presse
info_radio
info_reseaux
info_podcasts
Chaque colonne doit contenir 0 ou 1.
raw |>
count(info_reseaux)# A tibble: 3 × 2
info_reseaux n
<dbl> <int>
1 0 1646
2 1 1947
3 2 7
Sept lignes contiennent le code 2, volontairement invalide.
On peut automatiser le contrôle :
vars_info <- c(
"info_tv", "info_presse", "info_radio",
"info_reseaux", "info_podcasts"
)
controle_binaire <- raw |>
summarise(
across(
all_of(vars_info),
~ sum(!.x %in% c(0, 1)),
.names = "invalides_{.col}"
)
)
controle_binaire# A tibble: 1 × 5
invalides_info_tv invalides_info_presse invalides_info_radio
<int> <int> <int>
1 0 0 0
# ℹ 2 more variables: invalides_info_reseaux <int>,
# invalides_info_podcasts <int>
Cette approche est préférable à une inspection manuelle de chaque colonne lorsque l’enquête contient de nombreuses batteries binaires.
Les questions heures_travail et teletravail_jours ne s’appliquent qu’aux personnes en emploi.
Les codes possibles :
97 : sans objet ;98 : ne sait pas ;99 : refus ;raw |>
filter(
statut_emploi != 1,
heures_travail < 97
) |>
select(id, statut_emploi, heures_travail)# A tibble: 35 × 3
id statut_emploi heures_travail
<dbl> <dbl> <dbl>
1 100119 98 44.7
2 100135 2 22.7
3 100157 4 30.2
4 100312 2 27.7
5 100386 99 41.6
6 100472 98 40.4
7 100491 98 28.1
8 100676 98 36
9 100722 6 31.3
10 100902 98 33
# ℹ 25 more rows
raw |>
filter(
statut_emploi == 1,
heures_travail == 97
) |>
select(id, statut_emploi, heures_travail)# A tibble: 9 × 3
id statut_emploi heures_travail
<dbl> <dbl> <dbl>
1 100294 1 97
2 100498 1 97
3 100754 1 97
4 101002 1 97
5 101113 1 97
6 102046 1 97
7 102486 1 97
8 103233 1 97
9 103549 1 97
raw |>
filter(heures_travail > 80, heures_travail < 97)# A tibble: 5 × 36
id strate psu poids_base poids_final territoire mode_collecte sexe
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 100398 2 117 0.952 0.899 2 1 2
2 100764 6 169 1.45 1.44 3 1 1
3 101731 5 157 1.35 1.45 4 3 1
4 102640 3 137 0.890 0.883 2 1 1
5 103600 4 150 1.15 1.14 5 1 1
# ℹ 28 more variables: age <dbl>, diplome <dbl>, statut_emploi <dbl>,
# situation_familiale <dbl>, taille_menage <dbl>, nb_enfants <dbl>,
# revenu_mensuel <dbl>, sante <dbl>, satisfaction_vie <dbl>,
# internet_freq <dbl>, sport_freq <dbl>, heures_tv <dbl>, livres_12m <dbl>,
# confiance_science <dbl>, confiance_gouvernement <dbl>,
# confiance_medias <dbl>, conf_parlement <dbl>, conf_justice <dbl>,
# conf_police <dbl>, conf_mairie <dbl>, conf_associations <dbl>, …
raw |>
filter(teletravail_jours > 5, teletravail_jours < 97)# A tibble: 6 × 36
id strate psu poids_base poids_final territoire mode_collecte sexe
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 100731 3 129 0.726 0.685 1 1 2
2 100868 2 125 0.882 0.875 1 1 1
3 101957 1 101 0.706 0.667 2 3 2
4 102208 4 149 1.06 1.05 5 1 1
5 103149 3 131 0.934 0.927 4 1 1
6 103435 4 144 1.12 1.11 3 1 1
# ℹ 28 more variables: age <dbl>, diplome <dbl>, statut_emploi <dbl>,
# situation_familiale <dbl>, taille_menage <dbl>, nb_enfants <dbl>,
# revenu_mensuel <dbl>, sante <dbl>, satisfaction_vie <dbl>,
# internet_freq <dbl>, sport_freq <dbl>, heures_tv <dbl>, livres_12m <dbl>,
# confiance_science <dbl>, confiance_gouvernement <dbl>,
# confiance_medias <dbl>, conf_parlement <dbl>, conf_justice <dbl>,
# conf_police <dbl>, conf_mairie <dbl>, conf_associations <dbl>, …
Ici, le diagnostic combine deux formes de connaissance :
heures_travail s’applique aux personnes en emploi) ;Les contrôles ne se limitent pas aux filtres explicites. On peut explorer des relations attendues :
nb_enfants devrait être compatible avec certaines situations familiales, sans imposer de règle trop stricte ;Mais une « incohérence sociale » apparente ne doit pas être transformée en règle arbitraire. Par exemple, une personne de 25 ans peut légitimement être retraitée pour invalidité selon le questionnaire. Les règles du terrain doivent primer sur les stéréotypes.
Une bonne pratique consiste à rassembler les principaux contrôles :
audit <- tibble::tibble(
controle = c(
"Identifiants dupliqués",
"Âges hors 18-79 (hors 999)",
"Taille ménage hors 1-8",
"Heures TV > 14 (hors codes spéciaux)",
"Code invalide info_reseaux"
),
n = c(
sum(duplicated(raw$id)),
sum(!between(raw$age, 18, 79) & raw$age != 999),
sum(!between(raw$taille_menage, 1, 8)),
sum(raw$heures_tv > 14 & raw$heures_tv < 98),
sum(!raw$info_reseaux %in% c(0, 1))
)
)
audit# A tibble: 5 × 2
controle n
<chr> <int>
1 Identifiants dupliqués 8
2 Âges hors 18-79 (hors 999) 5
3 Taille ménage hors 1-8 4
4 Heures TV > 14 (hors codes spéciaux) 10
5 Code invalide info_reseaux 7
Un script compact équivalent est fourni dans R/02-audit-enqueter.R.
Dans un projet réel :
| Contrôle | n | Décision | Justification |
|---|---|---|---|
| âge hors champ | 5 | passer à NA |
champ 18–79 documenté |
code 999 âge |
… | traiter comme non-réponse | dictionnaire |
| identifiant dupliqué | 8 | investiguer | identité attendue unique |
| heures TV >14 | 10 | flag + analyse | règle pédagogique |
Ce journal est beaucoup plus informatif qu’une succession de filter() dont on ne connaît plus la justification.
Créons un nouvel objet :
enqueter <- raw |>
distinct(id, .keep_all = TRUE) |>
mutate(
age = if_else(
between(age, 18, 79) | age == 999,
as.numeric(age),
NA_real_
),
taille_menage = if_else(
between(taille_menage, 1, 8),
as.numeric(taille_menage),
NA_real_
),
heures_tv = if_else(
heures_tv <= 14 | heures_tv %in% c(98, 99),
as.numeric(heures_tv),
NA_real_
),
info_reseaux = if_else(
info_reseaux %in% c(0, 1),
as.numeric(info_reseaux),
NA_real_
)
)Cette étape traite les valeurs impossibles ou invalides, mais laisse volontairement les codes spéciaux documentés. Ils seront traités au chapitre 7.
NA plutôt qu’une valeur « corrigée » inventée ?Si un âge vaut 120, nous ne savons pas s’il devait être 20, 70 ou autre chose. Le remplacer par une valeur plausible invente de l’information. Sauf source externe permettant une correction certaine, nous préférons marquer la valeur comme manquante et conserver la trace de la règle.
Corriger une faute connue à partir de la source n’est pas la même chose qu’imputer une valeur inconnue. Les deux opérations doivent être documentées séparément.
Dans des fichiers réels, les noms peuvent contenir espaces, accents ou ponctuation. {janitor} propose :
janitor::clean_names(enquete)Dans le jeu du livre, les noms sont déjà normalisés. Nous n’appliquons donc pas cette fonction uniquement « parce qu’elle existe ». Une transformation sans besoin explicite ajoute du bruit au workflow.
Une règle de nettoyage doit être testée :
sum(!between(enqueter$taille_menage, 1, 8), na.rm = TRUE)[1] 0
sum(enqueter$heures_tv > 14 & enqueter$heures_tv < 98, na.rm = TRUE)[1] 0
sum(!enqueter$info_reseaux %in% c(0, 1), na.rm = TRUE)[1] 7
Les contrôles doivent retourner zéro pour les anomalies ciblées. Ce principe est puissant : chaque transformation importante devrait être accompagnée d’une vérification de l’effet attendu.
Pourquoi la commande suivante peut-elle être dangereuse ?
enquete <- distinct(enquete)Sans préciser l’identifiant et sans inspecter les doublons, elle supprime uniquement les lignes strictement identiques et ne résout pas nécessairement un conflit d’identifiants. Elle peut aussi faire disparaître des répétitions légitimes selon la structure des données. Le diagnostic doit préciser l’unité statistique et la clé attendue.
Expliquez pourquoi revenu = 10 000 € et age = 120 ne doivent pas être traités de la même manière dans une enquête limitée aux 18–79 ans.
Le revenu est élevé mais peut être substantivement possible. L’âge viole directement le champ documenté. Une valeur atypique nécessite souvent une vérification ; une valeur impossible au regard du protocole justifie une règle de correction ou de mise à manquant.
Produisez les cinq contrôles du tableau audit ci-dessus et vérifiez leurs résultats sur enqueter_brut.csv.
Vous trouvez 20 personnes non employées avec une valeur de heures_travail. Donnez au moins trois explications possibles et précisez quelles informations vous consulteriez avant de modifier les données.
Écrivez une fonction simple controle_plage() qui reçoit une variable, une borne minimale et une borne maximale et retourne le nombre de valeurs hors plage, en ignorant les NA. Appliquez-la à taille_menage.
controle_plage <- function(x, min, max) {
sum(x < min | x > max, na.rm = TRUE)
}
controle_plage(raw$taille_menage, 1, 8)La fonction compte les violations d’une règle simple. Dans une vraie enquête, il faut souvent ajouter un argument pour exclure les codes spéciaux ou créer une règle différente selon l’univers de la question.
Les fonctions de manipulation utilisées dans ce chapitre relèvent principalement de {dplyr} (Wickham et al. 2026). La logique méthodologique est cependant indépendante du package : un bon nettoyage repose d’abord sur la documentation de l’enquête et des règles explicites.
Le chapitre suivant traite d’un problème plus subtil : une valeur peut être parfaitement cohérente informatiquement tout en signifiant « refus », « ne sait pas », « sans objet » ou absence de donnée. Ces situations ne doivent pas être fusionnées sans réflexion.