7  Comprendre et traiter les valeurs manquantes

ImportantQuestion de départ

Une cellule sans réponse signifie-t-elle la même chose lorsqu’une personne refuse d’indiquer son revenu, lorsqu’elle ne connaît pas la réponse ou lorsqu’une question ne lui était pas destinée ?

7.1 Objectifs d’apprentissage

À la fin de ce chapitre, vous saurez :

  • distinguer NA dans R des codes spéciaux présents dans un fichier d’enquête ;
  • différencier non-réponse, refus, « ne sait pas » et « sans objet » ;
  • transformer des codes spéciaux en valeurs manquantes sans perdre inutilement leur provenance ;
  • calculer et visualiser la proportion de données manquantes par variable ;
  • distinguer valeur manquante structurelle et non-réponse à une question applicable ;
  • comprendre l’intuition des mécanismes MCAR, MAR et MNAR sans les traiter comme des diagnostics automatiques ;
  • expliquer pourquoi la suppression complète et l’imputation simple ne sont pas des solutions universelles ;
  • préparer une base propre pour les analyses descriptives et l’imputation multiple abordée plus loin.

7.2 Prérequis

Il faut savoir consulter le dictionnaire de variables, utiliser mutate(), across(), replace() et les fonctions is.na() / sum().

7.3 Dans le fichier brut, une non-réponse n’est pas forcément NA

Dans un CSV d’enquête, une valeur manquante peut être codée :

98
99
999
99998
"Refus"
"NSP"
""

R ne peut pas deviner que 99998 signifie « refus de répondre au revenu ». Tant que nous ne le recodons pas, ce nombre participe à une moyenne comme n’importe quelle autre valeur.

Le dictionnaire du jeu EnquêteR documente par exemple :

Variable Code Signification
diplome 98 Ne sait pas
diplome 99 Refus
revenu_mensuel 99997 Ne sait pas
revenu_mensuel 99998 Refus
heures_travail 97 Sans objet
heures_travail 98 Ne sait pas
heures_travail 99 Refus
NotePremière règle

Avant de compter les NA, il faut savoir comment la non-réponse est codée dans la source. Sinon, le diagnostic sous-estime souvent fortement les valeurs manquantes.

7.4 Quatre situations à distinguer

7.4.1 Valeur manquante technique

Une cellule est vide ou importée directement comme NA. La cause peut être inconnue.

7.4.2 Ne sait pas

Le répondant a reçu la question mais indique ne pas connaître la réponse. C’est une information sur le processus de réponse.

7.4.3 Refus

La question s’appliquait, mais le répondant a choisi de ne pas fournir la réponse.

7.4.4 Sans objet / question non posée

La question ne s’appliquait pas. Par exemple, heures_travail n’est normalement pas demandée à une personne retraitée dans notre questionnaire.

Ces catégories peuvent toutes devenir NA dans une variable analytique, mais elles ne doivent pas être considérées comme identiques au moment du diagnostic.

7.5 Mesurer les codes spéciaux avant recodage

Afficher le code R
raw <- readr::read_csv(
  "data/raw/enqueter_brut.csv",
  show_col_types = FALSE
)

raw |>
  summarise(
    n_revenu_nsp = sum(revenu_mensuel == 99997),
    n_revenu_refus = sum(revenu_mensuel == 99998),
    n_age_nonreponse = sum(age == 999)
  )
# A tibble: 1 × 3
  n_revenu_nsp n_revenu_refus n_age_nonreponse
         <int>          <int>            <int>
1           81            246               19

Dans le fichier fourni, le revenu comporte environ 9 % de codes spéciaux. Ce taux est nettement plus élevé que pour plusieurs variables sociodémographiques. Cette hétérogénéité est réaliste pédagogiquement : toutes les questions ne produisent pas le même niveau de non-réponse.

7.6 Conserver la raison de la non-réponse

Avant de convertir le revenu en NA, on peut créer une variable auxiliaire :

Afficher le code R
enqueter <- raw |>
  mutate(
    revenu_nonreponse = case_when(
      revenu_mensuel == 99997 ~ "Ne sait pas",
      revenu_mensuel == 99998 ~ "Refus",
      TRUE ~ "Réponse observée"
    ),
    revenu_mensuel = replace(
      revenu_mensuel,
      revenu_mensuel %in% c(99997, 99998),
      NA
    )
  )

Nous pouvons ainsi calculer la moyenne du revenu sur les réponses observées tout en conservant la possibilité d’étudier séparément les refus et les « ne sait pas ».

7.6.1 Une version factorisée

Afficher le code R
enqueter <- enqueter |>
  mutate(
    revenu_nonreponse = factor(
      revenu_nonreponse,
      levels = c("Réponse observée", "Ne sait pas", "Refus")
    )
  )
AstuceBon réflexe

Lorsque la raison du manque peut être utile pour l’audit ou l’analyse, créer un indicateur avant de remplacer les codes par NA.

7.7 Recoder plusieurs familles de codes

Les codes dépendent des variables. Il serait dangereux d’écrire une règle générale du type « toutes les valeurs supérieures à 90 deviennent NA ».

Une transformation explicite :

Afficher le code R
enqueter <- enqueter |>
  mutate(
    sexe = replace(sexe, sexe == 9, NA),
    age = replace(age, age == 999, NA),
    across(
      c(diplome, statut_emploi, situation_familiale, nb_enfants),
      ~ replace(.x, .x %in% c(98, 99), NA)
    ),
    revenu_mensuel = replace(
      revenu_mensuel,
      revenu_mensuel %in% c(99997, 99998),
      NA
    ),
    across(
      c(sante, internet_freq, confiance_science, confiance_gouvernement,
        confiance_medias, conf_parlement, conf_justice, conf_police,
        conf_mairie, conf_associations),
      ~ replace(.x, .x %in% c(8, 9), NA)
    )
  )

Cette écriture est plus longue qu’une règle générique, mais elle rend visibles les conventions de codage.

7.8 Le cas particulier des questions filtrées

heures_travail contient beaucoup de 97 = sans objet parce que la question n’est destinée qu’aux personnes en emploi. Si l’on calcule :

mean(is.na(enquete$heures_travail))

après recodage, le pourcentage manquant sera élevé. Cela ne signifie pas que près de la moitié de l’échantillon a « oublié de répondre ». Une grande partie des valeurs est structurellement absente.

7.8.1 Restreindre le dénominateur à l’univers de la question

Pour mesurer la non-réponse parmi les personnes concernées :

Afficher le code R
raw |>
  filter(statut_emploi == 1) |>
  summarise(
    n_eligibles = n(),
    n_nsp = sum(heures_travail == 98),
    n_refus = sum(heures_travail == 99),
    part_nonreponse = mean(heures_travail %in% c(98, 99))
  )
# A tibble: 1 × 4
  n_eligibles n_nsp n_refus part_nonreponse
        <int> <int>   <int>           <dbl>
1        2147    30      21          0.0238

Le dénominateur change la signification de l’indicateur. Cette habitude sera reprise tout au long du livre.

AvertissementUne proportion de manquants sans univers peut être trompeuse

Dire « 41 % de valeurs manquantes pour les heures de travail » mélange les personnes non concernées et les non-répondants si l’on a transformé 97, 98 et 99 en NA. Il faut distinguer le champ de la question de la non-réponse conditionnelle.

7.9 Calculer la part de NA par variable

Après traitement des codes spéciaux :

Afficher le code R
taux_na <- enqueter |>
  summarise(
    across(
      everything(),
      ~ mean(is.na(.x))
    )
  ) |>
  tidyr::pivot_longer(
    everything(),
    names_to = "variable",
    values_to = "proportion_na"
  ) |>
  arrange(desc(proportion_na))

taux_na
# A tibble: 37 × 2
   variable               proportion_na
   <chr>                          <dbl>
 1 revenu_mensuel                0.0908
 2 diplome                       0.0178
 3 conf_parlement                0.0169
 4 conf_mairie                   0.0169
 5 confiance_gouvernement        0.0144
 6 conf_associations             0.0139
 7 confiance_science             0.0117
 8 confiance_medias              0.0117
 9 conf_justice                  0.0117
10 sante                         0.0111
# ℹ 27 more rows

Pour obtenir des pourcentages :

taux_na |>
  mutate(pourcentage_na = 100 * proportion_na)

7.9.1 Pourquoi calculer une proportion plutôt qu’un nombre seul ?

300 valeurs manquantes représentent 30 % dans un fichier de 1 000 observations, mais seulement 3 % dans un fichier de 10 000. Nombre et proportion sont complémentaires.

7.10 Visualiser la structure des manquants

Un graphique simple :

Afficher le code R
taux_na |>
  filter(proportion_na > 0) |>
  ggplot(aes(x = reorder(variable, proportion_na), y = proportion_na)) +
  geom_col() +
  coord_flip() +
  scale_y_continuous(labels = scales::percent) +
  labs(
    x = NULL,
    y = "Part de valeurs manquantes",
    title = "Valeurs manquantes après recodage des codes spéciaux"
  )

Ce graphique est utile pour repérer les variables problématiques, mais il doit être lu avec les univers de questions.

7.11 NA dans les calculs R

7.11.1 Moyenne

Afficher le code R
mean(enqueter$revenu_mensuel)
[1] NA
Afficher le code R
mean(enqueter$revenu_mensuel, na.rm = TRUE)
[1] 2210.999

La seconde moyenne porte uniquement sur les valeurs observées. Il faut pouvoir le dire explicitement dans le texte.

7.11.2 Comptages

Afficher le code R
sum(is.na(enqueter$revenu_mensuel))
[1] 327

7.11.3 Tableaux

Base R :

table(enqueter$sexe, useNA = "ifany")

Avec dplyr :

count(enqueter, sexe, .drop = FALSE)

Le comportement vis-à-vis des NA dépend de la fonction. Ne jamais supposer qu’ils sont automatiquement « gérés » de la manière souhaitée.

7.12 Pourquoi na.omit() n’est pas un réflexe universel

enquete_complete <- na.omit(enquete)

Cette commande supprime toute ligne qui contient au moins un NA parmi les colonnes de l’objet. Dans un fichier d’enquête large, elle peut éliminer une grande partie de l’échantillon.

Plus problématique : l’échantillon restant peut différer systématiquement de l’échantillon initial.

Une analyse en cas complets peut être raisonnable dans certaines situations, mais elle doit être justifiée variable par variable ou modèle par modèle.

7.12.1 Mesurer d’abord la perte

Afficher le code R
vars_modele <- c("revenu_mensuel", "age", "sexe", "diplome")

n_total <- nrow(enqueter)
n_complets <- enqueter |>
  select(all_of(vars_modele)) |>
  tidyr::drop_na() |>
  nrow()

c(n_total = n_total, n_complets = n_complets)
   n_total n_complets 
      3600       3167 

Avant une régression, nous comparerons aussi les caractéristiques des cas inclus et exclus lorsque la perte est substantielle.

7.13 Pourquoi remplacer par la moyenne est généralement une mauvaise idée

Une imputation simple :

enquete$revenu_mensuel[is.na(enquete$revenu_mensuel)] <-
  mean(enquete$revenu_mensuel, na.rm = TRUE)

réduit artificiellement la variabilité, crée un groupe de personnes possédant exactement la même valeur et ne reflète pas l’incertitude sur les valeurs manquantes.

Cette méthode peut être utile comme illustration pédagogique, mais elle ne doit pas être présentée comme la solution standard à la non-réponse.

Le chapitre 21 introduira l’imputation multiple.

7.14 MCAR, MAR et MNAR : trois idées, pas trois étiquettes faciles

La littérature distingue souvent trois mécanismes conceptuels.

7.14.1 MCAR — Missing Completely At Random

La probabilité qu’une valeur manque ne dépend ni de la valeur manquante elle-même ni d’autres variables observées pertinentes.

C’est une hypothèse forte.

7.14.2 MAR — Missing At Random

Conditionnellement aux informations observées, la probabilité de manque ne dépend plus de la valeur manquante non observée.

Exemple intuitif : la non-réponse au revenu dépend de l’âge et du diplôme, qui sont observés, mais pas du revenu lui-même une fois ces variables prises en compte.

7.14.3 MNAR — Missing Not At Random

Même après prise en compte des informations observées, le mécanisme dépend encore de la valeur non observée ou d’informations non disponibles.

Exemple intuitif : les personnes ayant les revenus les plus élevés refusent davantage précisément en raison du niveau de leur revenu.

AvertissementOn ne déduit pas MCAR/MAR/MNAR d’un simple pourcentage

Ces mécanismes concernent le processus générateur de la non-réponse. Une table de fréquences ou un test unique ne suffit généralement pas à « prouver » MAR ou MNAR. La connaissance du terrain, les variables auxiliaires et les analyses de sensibilité sont importantes.

7.15 Explorer qui répond et qui ne répond pas

Créons un indicateur de revenu observé :

Afficher le code R
enqueter <- enqueter |>
  mutate(revenu_observe = !is.na(revenu_mensuel))

Puis comparons selon le diplôme :

Afficher le code R
enqueter |>
  count(diplome, revenu_observe) |>
  group_by(diplome) |>
  mutate(part = n / sum(n))
# A tibble: 16 × 4
# Groups:   diplome [8]
   diplome revenu_observe     n   part
     <dbl> <lgl>          <int>  <dbl>
 1       1 FALSE             35 0.0745
 2       1 TRUE             435 0.926 
 3       2 FALSE             51 0.114 
 4       2 TRUE             396 0.886 
 5       3 FALSE             65 0.0945
 6       3 TRUE             623 0.906 
 7       4 FALSE             64 0.0909
 8       4 TRUE             640 0.909 
 9       5 FALSE             44 0.0801
10       5 TRUE             505 0.920 
11       6 FALSE             42 0.113 
12       6 TRUE             329 0.887 
13       7 FALSE             21 0.0684
14       7 TRUE             286 0.932 
15      NA FALSE              5 0.0781
16      NA TRUE              59 0.922 

Ou selon l’âge :

Afficher le code R
enqueter |>
  group_by(revenu_observe) |>
  summarise(
    n = n(),
    age_moyen = mean(age, na.rm = TRUE)
  )
# A tibble: 2 × 3
  revenu_observe     n age_moyen
  <lgl>          <int>     <dbl>
1 FALSE            327      46.5
2 TRUE            3273      46.0

Une différence signale que les données complètes et incomplètes ne sont pas identiques sur les variables observées. Elle ne suffit pas, seule, à déterminer le mécanisme complet de non-réponse.

7.16 Une stratégie de préparation plus riche

Pour les variables importantes, nous pouvons conserver simultanément :

revenu_mensuel_brut     # éventuellement dans raw uniquement
revenu_mensuel          # valeur analytique, codes -> NA
revenu_nonreponse       # observée / NSP / refus
revenu_observe          # TRUE/FALSE

Il n’est pas nécessaire de multiplier ces indicateurs pour chaque variable. On les crée lorsqu’ils répondent à un besoin d’audit ou d’analyse.

7.17 Vérifier que les codes spéciaux ont disparu des valeurs analytiques

Après recodage :

Afficher le code R
stopifnot(!any(enqueter$revenu_mensuel %in% c(99997, 99998), na.rm = TRUE))
stopifnot(!any(enqueter$diplome %in% c(98, 99), na.rm = TRUE))

Cette vérification est très utile dans les pipelines : elle empêche un code spécial oublié d’entrer dans les analyses ultérieures.

7.18 Mini-exercices

7.18.1 Mini-exercice 1 — Dénominateur

Une enquête compte 1 000 personnes, dont 600 sont en emploi. La variable heures_travail est absente pour 430 personnes au total, mais 400 sont hors champ de la question et 30 personnes en emploi n’ont pas répondu. Quel taux décririez-vous comme « non-réponse à la question » ?

Le taux de non-réponse parmi les personnes éligibles est 30 / 600 = 5 %. Dire « 43 % de données manquantes » décrit la matrice de données mais pas la non-réponse au questionnaire, car cela inclut les 400 personnes hors champ.

7.18.2 Mini-exercice 2 — Préserver les raisons

Créez une variable diplome_nonreponse distinguant réponse observée, NSP (98) et refus (99) avant de convertir les deux codes en NA.

enqueter <- raw |>
  mutate(
    diplome_nonreponse = case_when(
      diplome == 98 ~ "Ne sait pas",
      diplome == 99 ~ "Refus",
      TRUE ~ "Réponse observée"
    ),
    diplome = replace(diplome, diplome %in% c(98, 99), NA)
  )

7.19 Exercice de synthèse

7.19.1 Niveau A — Appliquer

Pour revenu_mensuel :

  1. comptez 99997 et 99998 dans le brut ;
  2. créez revenu_nonreponse ;
  3. convertissez les deux codes en NA ;
  4. calculez le pourcentage de NA ;
  5. calculez la moyenne parmi les réponses observées.

7.19.2 Niveau B — Choisir

La variable teletravail_jours comporte environ 40 % de valeurs 97, 98 ou 99. Expliquez pourquoi le pourcentage global n’est pas suffisant. Proposez au moins deux indicateurs plus informatifs.

7.19.3 Niveau C — Analyser

Construisez un tableau classant toutes les variables selon leur part de NA après recodage, puis identifiez les cinq plus élevées. Pour chacune, consultez l’univers dans le dictionnaire et classez le manque principalement comme : structurel, non-réponse, mélange des deux, ou à investiguer.

7.19.4 Correction détaillée

Deux indicateurs utiles sont :

  1. part de personnes hors champ (97) parmi l’ensemble de l’échantillon ;
  2. taux de NSP/refus (98/99) parmi les personnes en emploi, donc dans l’univers de la question.

On peut en plus distinguer NSP et refus. Cette décomposition est plus informative qu’un unique pourcentage de NA après fusion des codes.

taux_na <- enqueter |>
  summarise(across(everything(), ~ mean(is.na(.x)))) |>
  pivot_longer(everything(), names_to = "variable", values_to = "part_na") |>
  arrange(desc(part_na))

head(taux_na, 5)

La correction n’est pas complète tant que l’on n’a pas consulté dictionnaire_variables.csv. Les variables liées au travail ont une forte part structurelle ; le revenu comporte surtout de la non-réponse ; d’autres variables nécessitent une interprétation variable par variable.

7.20 À retenir

  • NA est une représentation R ; la non-réponse d’enquête peut être codée autrement dans le brut.
  • NSP, refus et sans objet ont des significations différentes.
  • La raison du manque peut être conservée dans une variable auxiliaire avant recodage.
  • Le pourcentage de valeurs manquantes doit être calculé avec un dénominateur cohérent avec l’univers de la question.
  • Une forte proportion de NA peut être structurelle et non problématique.
  • na.omit() ou drop_na() peuvent réduire fortement et sélectivement l’échantillon.
  • Remplacer toutes les valeurs manquantes par une moyenne n’est pas une solution méthodologique satisfaisante par défaut.
  • MCAR, MAR et MNAR sont des hypothèses sur le mécanisme de manque, pas de simples catégories déduites d’un tableau.
  • Avant l’analyse, vérifier que les codes spéciaux ne subsistent pas comme valeurs numériques.

7.21 Pour aller plus loin

Le chapitre 21 reviendra sur la non-réponse avec l’imputation multiple et les analyses de sensibilité. Pour l’instant, notre objectif est plus fondamental : produire une base où les valeurs manquantes sont correctement représentées et documentées.

Le chapitre suivant s’appuie sur cette base pour construire les variables réellement utilisées dans l’analyse : catégories regroupées, indicateurs binaires, scores et questions à réponses multiples.