---
title: "Comprendre et traiter les valeurs manquantes"
---
```{r}
#| label: setup-chapitre
#| include: false
suppressPackageStartupMessages({
library(dplyr)
library(tidyr)
library(ggplot2)
})
```
::: {.callout-important title="Question 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 ?
:::
## 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.
## Prérequis
Il faut savoir consulter le dictionnaire de variables, utiliser `mutate()`, `across()`, `replace()` et les fonctions `is.na()` / `sum()`.
## 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 :
```text
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 |
::: {.callout-note title="Premiè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.
:::
## Quatre situations à distinguer
### Valeur manquante technique
Une cellule est vide ou importée directement comme `NA`. La cause peut être inconnue.
### 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.
### Refus
La question s'appliquait, mais le répondant a choisi de ne pas fournir la réponse.
### 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.
## Mesurer les codes spéciaux avant recodage
```{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)
)
```
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.
## Conserver la raison de la non-réponse
Avant de convertir le revenu en `NA`, on peut créer une variable auxiliaire :
```{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 ».
### Une version factorisée
```{r}
enqueter <- enqueter |>
mutate(
revenu_nonreponse = factor(
revenu_nonreponse,
levels = c("Réponse observée", "Ne sait pas", "Refus")
)
)
```
::: {.callout-tip title="Bon 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`.
:::
## 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 :
```{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.
## 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 :
```r
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**.
### Restreindre le dénominateur à l'univers de la question
Pour mesurer la non-réponse parmi les personnes concernées :
```{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))
)
```
Le **dénominateur** change la signification de l'indicateur. Cette habitude sera reprise tout au long du livre.
::: {.callout-warning title="Une 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.
:::
## Calculer la part de `NA` par variable
Après traitement des codes spéciaux :
```{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
```
Pour obtenir des pourcentages :
```r
taux_na |>
mutate(pourcentage_na = 100 * proportion_na)
```
### 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.
## Visualiser la structure des manquants
Un graphique simple :
```{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**.
## `NA` dans les calculs R
### Moyenne
```{r}
mean(enqueter$revenu_mensuel)
mean(enqueter$revenu_mensuel, na.rm = TRUE)
```
La seconde moyenne porte uniquement sur les valeurs observées. Il faut pouvoir le dire explicitement dans le texte.
### Comptages
```{r}
sum(is.na(enqueter$revenu_mensuel))
```
### Tableaux
Base R :
```r
table(enqueter$sexe, useNA = "ifany")
```
Avec `dplyr` :
```r
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.
## Pourquoi `na.omit()` n'est pas un réflexe universel
```r
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.
### Mesurer d'abord la perte
```{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)
```
Avant une régression, nous comparerons aussi les caractéristiques des cas inclus et exclus lorsque la perte est substantielle.
## Pourquoi remplacer par la moyenne est généralement une mauvaise idée
Une imputation simple :
```r
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.
## MCAR, MAR et MNAR : trois idées, pas trois étiquettes faciles
La littérature distingue souvent trois mécanismes conceptuels.
### 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.
### 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.
### 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.
::: {.callout-warning title="On 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.
:::
## Explorer qui répond et qui ne répond pas
Créons un indicateur de revenu observé :
```{r}
enqueter <- enqueter |>
mutate(revenu_observe = !is.na(revenu_mensuel))
```
Puis comparons selon le diplôme :
```{r}
enqueter |>
count(diplome, revenu_observe) |>
group_by(diplome) |>
mutate(part = n / sum(n))
```
Ou selon l'âge :
```{r}
enqueter |>
group_by(revenu_observe) |>
summarise(
n = n(),
age_moyen = mean(age, na.rm = TRUE)
)
```
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.
## Une stratégie de préparation plus riche
Pour les variables importantes, nous pouvons conserver simultanément :
```text
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.
## Vérifier que les codes spéciaux ont disparu des valeurs analytiques
Après recodage :
```{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.
## Mini-exercices
### 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 » ?
::: {.callout-caution collapse="true" title="Correction"}
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.
:::
### 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`.
::: {.callout-caution collapse="true" title="Correction"}
```r
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)
)
```
:::
## Exercice de synthèse
### 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.
### 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.
### 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.
### Correction détaillée
::: {.callout-caution collapse="true" title="Correction — Niveau B"}
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.
:::
::: {.callout-caution collapse="true" title="Correction — Niveau C"}
```r
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.
:::
## À 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.
## 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.