22  Calibration, redressement et poids avancés

ImportantQuestion de départ

Si l’on connaît la structure de la population selon l’âge, le sexe et le territoire, comment utiliser cette information pour améliorer les poids — et comment vérifier que le redressement n’a pas créé une nouvelle source d’instabilité ?

22.1 Objectifs d’apprentissage

À la fin de ce chapitre, vous saurez :

  • distinguer poids de base, ajustement de non-réponse et calibration ;
  • expliquer ce que post-stratification et raking imposent aux poids ;
  • définir clairement le domaine auquel la calibration s’applique ;
  • construire des contraintes de population compatibles avec {survey} ;
  • auditer numériquement l’atteinte des marges ;
  • comparer des estimations avant et après redressement ;
  • diagnostiquer dispersion, coefficient de variation et taille effective de Kish ;
  • traiter le trimming comme une analyse de sensibilité ;
  • expliquer le compromis potentiel entre correction de composition et perte de précision.

22.2 Un poids final est le résultat d’une chaîne de décisions

Une écriture schématique est :

\[ w_i^{final}=w_i^{base}\times a_i^{NR}\times g_i, \]

où le poids de base reflète le mécanisme de sélection, \(a_i^{NR}\) un ajustement de non-réponse et \(g_i\) un facteur de calibration. Les enquêtes réelles peuvent utiliser des procédures plus complexes, mais cette décomposition aide à comprendre qu’un « poids final » n’est pas une donnée brute.

Afficher le code R
tab_poids_init <- bind_rows(
  resume_poids(enqueter$poids_base) |> mutate(Type = "Poids de base", .before = 1),
  resume_poids(enqueter$poids_final) |> mutate(Type = "Poids final fourni", .before = 1)
) |>
  select(Type, Minimum, Médiane, Moyenne, Maximum, `CV des poids`, `N effectif de Kish`) |>
  mutate(across(where(is.numeric), ~round(.x, 2)))

knitr::kable(tab_poids_init)
Type Minimum Médiane Moyenne Maximum CV des poids N effectif de Kish
Poids de base 0.48 0.95 1 2.03 0.26 3359.12
Poids final fourni 0.50 0.95 1 2.18 0.27 3350.44
AvertissementNe jamais recalibrer à l’aveugle un poids officiel

Si un producteur fournit un poids final documenté, il faut d’abord comprendre ce qu’il incorpore. Recalibrer sans connaître les étapes déjà appliquées peut corriger deux fois la même dimension et rendre l’estimation difficile à interpréter.

22.3 Calibration : quelle information externe ajoute-t-on ?

La calibration utilise des informations auxiliaires connues sur la population. Une variable auxiliaire est particulièrement utile lorsqu’elle est :

  • mesurée de façon comparable dans l’enquête et dans la source externe ;
  • disponible pour la grande majorité des répondants ;
  • liée au mécanisme de sélection ou de non-réponse ;
  • et/ou liée aux variables d’intérêt dont on souhaite améliorer l’estimation.

La dernière condition explique pourquoi la calibration est substantive : calibrer sur âge et territoire n’a pas le même intérêt selon que l’on étudie emploi, santé, vote ou pratique culturelle.

Dans enqueter, les marges sont entièrement synthétiques. Elles servent à apprendre la méthode, pas à reconstruire une population réelle.

22.4 Définir le domaine de calibration

Les contraintes ne peuvent pas être appliquées à une unité pour laquelle l’auxiliaire est absent. Commençons par documenter la perte d’observations.

Afficher le code R
n_total <- nrow(enqueter)
n_cal <- sum(
  !is.na(enqueter$sexe_f) &
    !is.na(enqueter$age_classe_f) &
    !is.na(enqueter$territoire_f)
)

tibble::tibble(
  Élément = c("Total", "Sexe manquant", "Âge manquant", "Territoire manquant", "Domaine complet"),
  N = c(
    n_total,
    sum(is.na(enqueter$sexe_f)),
    sum(is.na(enqueter$age_classe_f)),
    sum(is.na(enqueter$territoire_f)),
    n_cal
  )
) |>
  mutate(`Part du fichier` = fmt_pct(N / n_total, .1)) |>
  knitr::kable()
Élément N Part du fichier
Total 3592 100,0%
Sexe manquant 32 0,9%
Âge manquant 24 0,7%
Territoire manquant 0 0,0%
Domaine complet 3536 98,4%
Afficher le code R
design_cal <- subset(
  design,
  !is.na(sexe_f) & !is.na(age_classe_f) & !is.na(territoire_f)
)
NoteLe domaine de calibration devient une partie de l’estimand

Dans cet exemple, les analyses avant/après calibration portent sur les répondants dont les trois auxiliaires sont observés. Si ces personnes diffèrent des autres, le changement de domaine peut lui-même modifier l’estimation. Dans une enquête réelle, le traitement des auxiliaires manquantes doit être documenté : imputation, catégorie explicite, autre jeu d’auxiliaires ou restriction justifiée.

22.5 Post-stratification : une distribution connue

Lorsque l’on connaît une distribution de population pour une variable ou un croisement de variables, postStratify() ajuste les poids pour reproduire ces totaux (Lumley et al. 2026).

Afficher le code R
pop_sexe <- data.frame(
  sexe_f = factor(
    c("Homme", "Femme", "Autre / non-binaire"),
    levels = levels(enqueter$sexe_f)
  ),
  Freq = c(49000, 49500, 1500)
)

design_post <- survey::postStratify(
  design_cal,
  ~sexe_f,
  pop_sexe
)
Afficher le code R
prop.table(survey::svytable(~sexe_f, design_post)) |>
  as.data.frame() |>
  transmute(
    `Sexe / genre` = sexe_f,
    `Part après post-stratification` = fmt_pct(Freq, .1)
  ) |>
  knitr::kable()
Sexe / genre Part après post-stratification
Homme 49,0%
Femme 49,5%
Autre / non-binaire 1,5%

Atteindre cette marge ne garantit rien sur diplôme, revenu, santé ou opinions. La post-stratification ne « rend pas l’échantillon représentatif » au sens général ; elle impose une structure précise sur les auxiliaires retenus.

22.6 Raking : plusieurs marges séparées

Lorsque l’on connaît plusieurs distributions marginales mais pas leur croisement complet, le raking ajuste itérativement les poids pour respecter ces marges (Lumley et al. 2026).

Afficher le code R
pop_age <- data.frame(
  age_classe_f = factor(
    c("18-29", "30-44", "45-59", "60-79"),
    levels = levels(enqueter$age_classe_f)
  ),
  Freq = c(18000, 27000, 27000, 28000)
)

pop_territoire <- data.frame(
  territoire_f = factor(
    c("Île-de-France", "Nord-Ouest", "Nord-Est", "Sud-Ouest", "Sud-Est"),
    levels = levels(enqueter$territoire_f)
  ),
  Freq = c(19000, 21000, 18000, 18000, 24000)
)

design_rake <- survey::rake(
  design = design_cal,
  sample.margins = list(~sexe_f, ~age_classe_f, ~territoire_f),
  population.margins = list(pop_sexe, pop_age, pop_territoire)
)

22.7 Auditer les marges : le calcul doit être vérifiable

Construisons une table unique : structure avant, structure après, cible et écart résiduel.

Afficher le code R
prop_design <- function(des, variable) {
  tab <- prop.table(
    survey::svytable(stats::as.formula(paste0("~", variable)), des)
  )
  tibble(modalite = names(tab), proportion = as.numeric(tab))
}

cibles <- bind_rows(
  tibble(variable = "Sexe / genre", modalite = as.character(pop_sexe$sexe_f), cible = pop_sexe$Freq / sum(pop_sexe$Freq)),
  tibble(variable = "Classe d'âge", modalite = as.character(pop_age$age_classe_f), cible = pop_age$Freq / sum(pop_age$Freq)),
  tibble(variable = "Territoire", modalite = as.character(pop_territoire$territoire_f), cible = pop_territoire$Freq / sum(pop_territoire$Freq))
)

avant <- bind_rows(
  prop_design(design_cal, "sexe_f") |> mutate(variable = "Sexe / genre"),
  prop_design(design_cal, "age_classe_f") |> mutate(variable = "Classe d'âge"),
  prop_design(design_cal, "territoire_f") |> mutate(variable = "Territoire")
) |>
  rename(avant = proportion)

apres <- bind_rows(
  prop_design(design_rake, "sexe_f") |> mutate(variable = "Sexe / genre"),
  prop_design(design_rake, "age_classe_f") |> mutate(variable = "Classe d'âge"),
  prop_design(design_rake, "territoire_f") |> mutate(variable = "Territoire")
) |>
  rename(apres = proportion)

marges_compare <- cibles |>
  left_join(avant, by = c("variable", "modalite")) |>
  left_join(apres, by = c("variable", "modalite")) |>
  mutate(ecart = apres - cible)

marges_compare |>
  transmute(
    Variable = variable,
    Modalité = modalite,
    Avant = fmt_pct(avant, .1),
    Après = fmt_pct(apres, .1),
    Cible = fmt_pct(cible, .1),
    `Écart après-cible` = fmt_pp(ecart, .01)
  ) |>
  knitr::kable()
Variable Modalité Avant Après Cible Écart après-cible
Sexe / genre Homme 48,8% 49,0% 49,0% 0,00 points
Sexe / genre Femme 49,7% 49,5% 49,5% 0,00 points
Sexe / genre Autre / non-binaire 1,5% 1,5% 1,5% 0,00 points
Classe d’âge 18-29 15,7% 18,0% 18,0% 0,00 points
Classe d’âge 30-44 30,6% 27,0% 27,0% 0,00 points
Classe d’âge 45-59 33,0% 27,0% 27,0% 0,00 points
Classe d’âge 60-79 20,7% 28,0% 28,0% 0,00 points
Territoire Île-de-France 17,3% 19,0% 19,0% 0,00 points
Territoire Nord-Ouest 23,5% 21,0% 21,0% 0,00 points
Territoire Nord-Est 17,3% 18,0% 18,0% 0,00 points
Territoire Sud-Ouest 22,2% 18,0% 18,0% 0,00 points
Territoire Sud-Est 19,6% 24,0% 24,0% 0,00 points

L’écart maximal aux marges après raking est ici de 0,00 points. Ce contrôle est plus informatif que « la fonction n’a pas renvoyé d’erreur ».

AstuceContrôle indispensable

Après toute calibration, vérifier : niveaux des facteurs, totaux externes, marges atteintes, distribution des poids et estimations substantives. La réussite informatique n’est qu’une étape.

22.8 Ce que le redressement change — et ce qu’il ne change pas

Comparons quelques paramètres sur le même domaine avant et après raking.

Afficher le code R
resume_estim <- function(des, nom) {
  a <- survey::svymean(
    ~sport_regulier + confiance_science + satisfaction_vie,
    des,
    na.rm = TRUE
  )
  tibble::tibble(
    Pondération = nom,
    `Sport régulier` = as.numeric(coef(a)["sport_regulier"]),
    `Confiance science` = as.numeric(coef(a)["confiance_science"]),
    Satisfaction = as.numeric(coef(a)["satisfaction_vie"])
  )
}

estim_compare <- bind_rows(
  resume_estim(design_cal, "Avant raking"),
  resume_estim(design_rake, "Après raking")
)

estim_compare |>
  transmute(
    Pondération,
    `Sport régulier` = fmt_pct(`Sport régulier`, .1),
    `Confiance science (1–5)` = fmt_num(`Confiance science`, .2),
    `Satisfaction (0–10)` = fmt_num(Satisfaction, .2)
  ) |>
  knitr::kable()
Pondération Sport régulier Confiance science (1–5) Satisfaction (0–10)
Avant raking 31,6% 3,6 6,8
Après raking 30,4% 3,6 6,8

Une estimation peut changer beaucoup, peu ou pratiquement pas selon son association avec les auxiliaires corrigés. Le fait qu’elle bouge n’est pas une preuve qu’elle devient vraie ; le fait qu’elle bouge peu n’est pas une preuve que la calibration était inutile.

NoteLecture sociologique

Calibrer sur âge, sexe et territoire revient à imposer une certaine représentation de la structure démographique et territoriale. Si ces dimensions sont liées à une pratique sociale, l’estimation de cette pratique peut se déplacer. Le redressement formalise donc une hypothèse sur les dimensions observées de la sélection ; il ne reconstruit pas les dimensions absentes du modèle de réponse.

22.9 Diagnostiquer la dispersion des poids

Deux jeux de poids peuvent atteindre les mêmes marges tout en ayant des propriétés de variance très différentes.

Afficher le code R
w_avant <- weights(design_cal)
w_apres <- weights(design_rake)

diagnostic_poids <- bind_rows(
  resume_poids(w_avant) |> mutate(Poids = "Avant raking", .before = 1),
  resume_poids(w_apres) |> mutate(Poids = "Après raking", .before = 1)
) |>
  select(Poids, Minimum, Médiane, Moyenne, Maximum, `CV des poids`, `N effectif de Kish`) |>
  mutate(across(where(is.numeric), ~round(.x, 2)))

knitr::kable(diagnostic_poids)
Poids Minimum Médiane Moyenne Maximum CV des poids N effectif de Kish
Avant raking 0.50 0.95 1.00 2.18 0.27 3297.45
Après raking 10.29 25.72 28.28 86.59 0.40 3055.40
Afficher le code R
tibble(`Avant raking` = w_avant, `Après raking` = w_apres) |>
  pivot_longer(everything(), names_to = "Type", values_to = "Poids") |>
  ggplot(aes(x = Poids, colour = Type, fill = Type)) +
  geom_density(alpha = .10, linewidth = .8) +
  scale_fill_manual(values = palette_enqueter[c(2, 3)]) +
  scale_colour_manual(values = palette_enqueter[c(2, 3)]) +
  labs(
    x = "Valeur du poids",
    y = "Densité",
    title = "Atteindre les marges peut modifier fortement la dispersion des poids",
    subtitle = "L'alignement externe et la précision doivent être diagnostiqués ensemble",
    caption = "Données et marges entièrement synthétiques."
  ) +
  theme_enqueter()

Deux densités comparent la distribution des poids avant et après raking.

Le coefficient de variation et la taille effective de Kish donnent une intuition de la concentration des poids. Ils ne remplacent pas les erreurs standards calculées avec le design.

22.10 Trimming : une analyse de sensibilité, pas une finition esthétique

Le trimming borne certains poids extrêmes. Il peut réduire la variance, mais il peut aussi éloigner les marges et modifier l’estimation.

Afficher le code R
bornes <- quantile(weights(design_rake), probs = c(.01, .99))

design_trim <- survey::trimWeights(
  design_rake,
  lower = unname(bornes[1]),
  upper = unname(bornes[2]),
  strict = TRUE
)

Comparons raking et trimming sur les diagnostics de poids :

Afficher le code R
bind_rows(
  resume_poids(weights(design_rake)) |> mutate(Poids = "Raking", .before = 1),
  resume_poids(weights(design_trim)) |> mutate(Poids = "Raking + trimming", .before = 1)
) |>
  select(Poids, Minimum, Médiane, Maximum, `CV des poids`, `N effectif de Kish`) |>
  mutate(across(where(is.numeric), ~round(.x, 2))) |>
  knitr::kable()
Poids Minimum Médiane Maximum CV des poids N effectif de Kish
Raking 10.29 25.72 86.59 0.40 3055.4
Raking + trimming 12.55 25.78 65.40 0.39 3075.3

Puis les estimations :

Afficher le code R
sensibilite_cal <- bind_rows(
  resume_estim(design_cal, "Avant raking"),
  resume_estim(design_rake, "Raking"),
  resume_estim(design_trim, "Raking + trimming")
)

sensibilite_cal |>
  transmute(
    Pondération,
    `Sport régulier` = fmt_pct(`Sport régulier`, .1),
    `Confiance science` = fmt_num(`Confiance science`, .2),
    Satisfaction = fmt_num(Satisfaction, .2)
  ) |>
  knitr::kable()
Pondération Sport régulier Confiance science Satisfaction
Avant raking 31,6% 3,6 6,8
Raking 30,4% 3,6 6,8
Raking + trimming 30,5% 3,6 6,8

Le trimming doit ensuite être suivi d’un nouvel audit des marges. Une baisse du CV des poids ne suffit pas à conclure qu’il s’agit d’une meilleure pondération.

22.11 calibrate() : formulation plus générale

survey::calibrate() permet de travailler avec des totaux de population et différentes fonctions de calibration (Lumley et al. 2026). Lorsque les contraintes deviennent plus complexes, la correspondance entre la matrice du modèle et les totaux externes doit être contrôlée explicitement.

Afficher le code R
# Logique générale : inspecter d'abord la matrice.
# X <- model.matrix(
#   ~ sexe_f + age_classe_f + territoire_f,
#   data = design_cal$variables
# )
# colnames(X)
#
# Les totaux fournis à calibrate() doivent correspondre exactement
# aux colonnes de cette matrice, intercept et références compris.

22.12 Trois questions à poser avant de redresser

22.12.1 1. Quel problème de sélection voulons-nous réduire ?

Le choix des auxiliaires doit être relié au mécanisme de sélection ou de non-réponse, pas seulement à la disponibilité de marges publiques.

22.12.2 2. Quel prix payons-nous en variance ?

Une calibration très agressive peut concentrer le poids sur peu d’observations et réduire l’information effective.

22.12.3 3. Quelle conclusion reste hors de portée ?

Même une calibration parfaite sur plusieurs marges ne corrige pas automatiquement :

  • une mauvaise mesure ;
  • une sous-couverture non expliquée par les auxiliaires ;
  • une non-réponse conditionnelle à des variables non observées ;
  • une mauvaise définition du champ.
AvertissementReproduire quelques marges n’est pas être représentatif de tout

La calibration améliore la cohérence avec des informations externes précises. Elle n’autorise pas une déclaration générale de « représentativité » indépendante des variables auxiliaires, du mécanisme de réponse et de la question étudiée.

22.13 Exercice de synthèse

22.13.1 Niveau A — Appliquer

Calibrez le design uniquement sur l’âge. Vérifiez les marges et comparez la prévalence de pratique sportive avant/après.

22.13.2 Niveau B — Choisir

Comparez une calibration âge seul à une calibration âge + sexe + territoire. Discutez simultanément évolution des estimations, CV des poids et taille effective de Kish.

22.13.3 Niveau C — Analyser

Rédigez une note méthodologique pour une enquête réelle sur la confiance institutionnelle. Pour chaque auxiliaire proposé, justifiez : comparabilité avec la source externe, lien plausible avec la réponse, lien avec l’indicateur d’intérêt et risque de poids extrêmes. Ajoutez un plan de sensibilité au trimming.

22.14 À retenir

  • Un poids final est une construction méthodologique.
  • La calibration doit être reliée à une information externe et à un mécanisme plausible de sélection/non-réponse.
  • Le domaine de calibration doit être documenté, notamment lorsque des auxiliaires sont manquants.
  • Post-stratification et raking doivent être suivis d’un audit numérique des marges.
  • Il faut comparer les estimations et la dispersion des poids avant/après.
  • La taille effective de Kish est un diagnostic de concentration des poids, pas un substitut au calcul de variance du design.
  • Le trimming est une analyse de sensibilité biais–variance ; il peut dégrader les marges.
  • Reproduire quelques marges ne garantit pas une représentativité générale.

22.15 Pour aller plus loin

Les fonctions postStratify(), rake(), calibrate() et trimWeights() sont documentées dans {survey} (Lumley et al. 2026). Dans une application réelle, la documentation de l’enquête et de la source externe des marges reste aussi importante que le choix de la fonction R.