6  Diagnostiquer et nettoyer un fichier d’enquête

ImportantQuestion de départ

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 ?

6.1 Objectifs d’apprentissage

À la fin de ce chapitre, vous saurez :

  • séparer diagnostic et correction ;
  • vérifier dimensions, identifiants, doublons et plages plausibles ;
  • utiliser le questionnaire pour définir des règles de cohérence ;
  • repérer des valeurs impossibles sans confondre valeur rare et erreur ;
  • contrôler les questions filtrées et leur univers ;
  • créer un tableau d’audit avant nettoyage ;
  • documenter une décision de correction plutôt que modifier silencieusement les données ;
  • produire une première version nettoyée tout en conservant le fichier brut.

6.2 Prérequis

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.

6.3 Nettoyer ne signifie pas « rendre les données jolies »

Un nettoyage rigoureux ne consiste pas à supprimer ce qui paraît inhabituel. Il vise à vérifier si les observations respectent :

  1. la structure attendue du fichier ;
  2. les règles du questionnaire ;
  3. les plages définies par la documentation ;
  4. la cohérence entre variables liées ;
  5. les décisions explicites du protocole d’analyse.

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.

AvertissementValeur atypique ≠ erreur

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.

6.4 Charger le fichier brut sans le modifier

Afficher le code R
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.

6.5 Étape 1 — Vérifier la structure générale

Afficher le code R
dim(raw)
[1] 3600   36
Afficher le code R
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"
Afficher le code R
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é :

Afficher le code R
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.

6.6 Étape 2 — Vérifier l’identifiant

Un identifiant individuel devrait être unique si chaque ligne représente un répondant.

Afficher le code R
n_distinct(raw$id)
[1] 3592
Afficher le code R
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 :

Afficher le code R
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

6.6.1 Que faire d’un doublon ?

Il existe plusieurs situations :

  • la même ligne a été dupliquée techniquement ;
  • deux interviews partagent par erreur le même identifiant ;
  • l’identifiant n’est pas réellement individuel ;
  • plusieurs lignes sont légitimes parce que le fichier est longitudinal ou au format « long ».

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 :

Afficher le code R
enqueter <- raw |>
  distinct(id, .keep_all = TRUE)

Dans une enquête réelle, on remonterait d’abord à la source ou au protocole de collecte.

NoteLe diagnostic doit précéder 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.

6.7 Étape 3 — Contrôler les plages de valeurs

Le questionnaire indique un champ d’âge de 18 à 79 ans et un code spécial 999 pour la non-réponse.

Afficher le code R
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.

6.7.1 Pourquoi exclure 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.

6.7.2 Créer des indicateurs d’anomalie

Au lieu de supprimer immédiatement :

Afficher le code R
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.

6.8 Étape 4 — Définir des bornes à partir du questionnaire

6.8.1 Taille du ménage

Le dictionnaire indique une plage pédagogique attendue de 1 à 8 :

Afficher le code R
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.

6.8.2 Temps quotidien de télévision/vidéo

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 :

Afficher le code R
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.

AvertissementUne borne doit être justifiée

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é.

6.9 Étape 5 — Vérifier les variables binaires

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.

Afficher le code R
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 :

Afficher le code R
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.

6.10 Étape 6 — Vérifier les filtres du questionnaire

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 ;
  • une valeur numérique plausible lorsque la personne est en emploi.

6.10.1 Personne non employée avec une durée de travail

Afficher le code R
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

6.10.2 Personne en emploi codée « sans objet »

Afficher le code R
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

6.10.3 Plages propres aux questions filtrées

Afficher le code R
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>, …
Afficher le code R
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 :

  • une règle de domaine (heures_travail s’applique aux personnes en emploi) ;
  • une règle de plage (0–80 h, 0–5 jours dans le cadre pédagogique).

6.11 Étape 7 — Chercher les incohérences entre variables

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 ;
  • un âge très faible combiné à un statut de retraite peut attirer l’attention ;
  • un nombre de jours de télétravail supérieur au nombre de jours travaillés pourrait être incohérent si les deux mesures sont disponibles.

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.

6.12 Construire un tableau d’audit

Une bonne pratique consiste à rassembler les principaux contrôles :

Afficher le code R
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.

6.12.1 Ajouter une colonne de décision

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.

6.13 Corriger sans écraser la source

Créons un nouvel objet :

Afficher le code R
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.

6.13.1 Pourquoi 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.

NoteCorrection déterministe versus imputation

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.

6.14 Nettoyer les noms de variables

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.

6.15 Vérifier après correction

Une règle de nettoyage doit être testée :

Afficher le code R
sum(!between(enqueter$taille_menage, 1, 8), na.rm = TRUE)
[1] 0
Afficher le code R
sum(enqueter$heures_tv > 14 & enqueter$heures_tv < 98, na.rm = TRUE)
[1] 0
Afficher le code R
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.

6.16 Mini-exercices

6.16.1 Mini-exercice 1 — Doublons

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.

6.16.2 Mini-exercice 2 — Valeur rare ou impossible

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.

6.17 Exercice de synthèse

6.17.1 Niveau A — Appliquer

Produisez les cinq contrôles du tableau audit ci-dessus et vérifiez leurs résultats sur enqueter_brut.csv.

6.17.2 Niveau B — Choisir

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.

6.17.3 Niveau C — Analyser

É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.

6.17.4 Correction détaillée

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.

6.18 À retenir

  • Diagnostiquer et corriger sont deux étapes distinctes.
  • L’unicité d’un identifiant dépend de l’unité statistique et du format des données.
  • Une valeur rare n’est pas nécessairement une erreur.
  • Les plages doivent venir du questionnaire, du protocole ou d’une justification explicite.
  • Les codes spéciaux doivent être distingués des valeurs impossibles.
  • Les questions filtrées nécessitent des contrôles de cohérence entre variables.
  • Des indicateurs d’anomalie et un tableau d’audit rendent le nettoyage traçable.
  • Lorsque la vraie valeur est inconnue, inventer une « correction plausible » est généralement une mauvaise solution.
  • Toute règle de nettoyage importante doit être suivie d’un contrôle vérifiant son effet.

6.19 Pour aller plus loin

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.