23  Enquêtes longitudinales et panels

ImportantQuestion de départ

Une moyenne qui change entre deux dates signifie-t-elle que les mêmes personnes ont changé, ou seulement que la composition des personnes observées n’est plus la même ?

23.1 Objectifs d’apprentissage

À la fin de ce chapitre, vous saurez :

  • distinguer coupes répétées et panel ;
  • apparier des vagues avec un identifiant stable ;
  • diagnostiquer l’attrition comme un problème de sélection ;
  • modéliser descriptivement la probabilité de rester dans le panel ;
  • construire des formats large et long ;
  • distinguer évolution agrégée, changement individuel et transition d’état ;
  • utiliser un poids longitudinal avec les informations du plan d’origine ;
  • interpréter un modèle en différences à deux vagues ;
  • expliquer ce qu’un panel améliore — et ce qu’il ne résout pas.

23.2 Plusieurs dates ne suffisent pas à faire un panel

Une transversale répétée interroge à chaque date un nouvel échantillon comparable. Elle permet d’étudier l’évolution agrégée de la population.

Un panel réinterroge les mêmes unités. Il permet d’étudier des transitions individuelles : entrée dans l’emploi, changement de revenu, évolution de satisfaction ou changement d’opinion.

NoteLe temps ne suffit pas

Une variable annee ne rend pas un fichier longitudinal. Il faut un identifiant stable reliant les observations d’une même unité et une documentation permettant de savoir qui était éligible à chaque vague.

23.3 La seconde vague d’EnquêteR

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

tibble::tibble(
  `Lignes vague 2` = nrow(vague2),
  Variables = ncol(vague2),
  `Identifiants uniques` = dplyr::n_distinct(vague2$id),
  `IDs dupliqués` = sum(duplicated(vague2$id))
) |>
  knitr::kable()
Lignes vague 2 Variables Identifiants uniques IDs dupliqués
2679 8 2679 0

La vague 2 comporte de nouvelles mesures du revenu, du statut d’emploi, de la santé, de la satisfaction et de la confiance dans la science, ainsi qu’un poids_longitudinal.

23.4 Avant le changement : analyser l’attrition

La première question longitudinale n’est pas « qu’est-ce qui a changé ? », mais qui est encore là pour que nous puissions observer le changement ?

Afficher le code R
design_attr <- update(
  design,
  repond_v2 = as.numeric(id %in% vague2$id),
  statut_panel = factor(
    ifelse(id %in% vague2$id, "Réinterrogé·e", "Attrition"),
    levels = c("Réinterrogé·e", "Attrition")
  )
)

taux_retention <- survey::svymean(~repond_v2, design_attr)
ci_retention <- confint(taux_retention)
Afficher le code R
tibble::tibble(
  `Rétention pondérée` = fmt_pct(coef(taux_retention)[1], .1),
  `IC 95 %` = paste0(fmt_pct(ci_retention[1, 1], .1), " – ", fmt_pct(ci_retention[1, 2], .1)),
  `N vague 1` = nrow(enqueter),
  `N vague 2` = nrow(vague2)
) |>
  knitr::kable()
Rétention pondérée IC 95 % N vague 1 N vague 2
74,3% 72,7% – 75,8% 3592 2679

Un taux de rétention global ne dit pas si l’attrition déforme la composition du panel.

23.5 Profiler l’attrition

Afficher le code R
design_attr |>
  tbl_svysummary(
    by = statut_panel,
    include = c(age_classe_f, diplome_4, sexe_f, satisfaction_vie, revenu_mensuel),
    statistic = list(
      satisfaction_vie ~ "{mean}",
      revenu_mensuel ~ "{mean}",
      all_categorical() ~ "{p}%"
    ),
    missing = "no",
    label = list(
      age_classe_f ~ "Classe d'âge",
      diplome_4 ~ "Niveau de diplôme",
      sexe_f ~ "Sexe / genre",
      satisfaction_vie ~ "Satisfaction de vie en vague 1",
      revenu_mensuel ~ "Revenu en vague 1"
    )
  ) |>
  bold_labels()
Caractéristique Réinterrogé·e
N = 2 6681
Attrition
N = 9241
Classe d'âge

    18-29 17% 11%
    30-44 32% 26%
    45-59 32% 35%
    60-79 18% 27%
Niveau de diplôme

    Inférieur au bac 45% 49%
    Bac 20% 18%
    Bac+2 16% 15%
    Supérieur long 19% 19%
Sexe / genre

    Homme 48% 51%
    Femme 50% 49%
    Autre / non-binaire 1,7% 0,9%
Satisfaction de vie en vague 1 6,80 6,85
Revenu en vague 1 2 192 2 236
1

La table permet de repérer les dimensions sur lesquelles les personnes réinterrogées diffèrent des personnes perdues de vue.

23.6 Visualiser la rétention selon l’âge

Afficher le code R
ret_age <- survey::svyby(
  ~repond_v2,
  ~age_classe_f,
  design_attr,
  survey::svymean,
  vartype = c("se", "ci"),
  na.rm = TRUE
)

ret_age_df <- svyby_ci_df(ret_age, "repond_v2")
Afficher le code R
ggplot(ret_age_df, aes(x = age_classe_f, y = estimation)) +
  geom_errorbar(aes(ymin = bas, ymax = haut), width = .10, colour = couleurs_enqueter[["gris"]]) +
  geom_point(size = 3, colour = couleurs_enqueter[["principal"]]) +
  scale_y_continuous(labels = scales::percent_format(accuracy = 1), limits = c(0, 1)) +
  labs(
    x = "Âge en vague 1",
    y = "Part réinterrogée",
    title = "L'attrition peut modifier la composition du panel",
    subtitle = "Rétention pondérée et IC à 95 %",
    caption = "Jeu synthétique EnquêteR."
  ) +
  theme_enqueter()

La rétention au panel varie selon la classe d'âge ; chaque estimation est accompagnée d'un intervalle de confiance.

23.7 Un modèle de réponse au panel comme diagnostic

Un modèle de rétention ne « corrige » pas l’attrition à lui seul. Il aide à voir si plusieurs caractéristiques de vague 1 restent associées à la probabilité d’être réinterrogé·e.

Afficher le code R
ids_attr_cc <- enqueter |>
  filter(
    !is.na(age_classe_f),
    !is.na(diplome_4),
    !is.na(sexe_f),
    !is.na(satisfaction_vie)
  ) |>
  pull(id)

design_attr_cc <- subset(design_attr, id %in% ids_attr_cc)

mod_attrition <- survey::svyglm(
  repond_v2 ~ age_classe_f + diplome_4 + sexe_f + satisfaction_vie,
  design = design_attr_cc,
  family = quasibinomial()
)

tbl_regression(
  mod_attrition,
  exponentiate = TRUE,
  label = list(
    age_classe_f ~ "Classe d'âge",
    diplome_4 ~ "Niveau de diplôme",
    sexe_f ~ "Sexe / genre",
    satisfaction_vie ~ "Satisfaction en vague 1"
  )
) |>
  bold_labels()
Caractéristique OR 95% IC p-valeur
Classe d'âge


    age_classe_f.L 0,53 0,43 – 0,66 <0,001
    age_classe_f.Q 0,92 0,78 – 1,08 0,3
    age_classe_f.C 1,03 0,90 – 1,18 0,7
Niveau de diplôme


    Inférieur au bac
    Bac 1,21 0,97 – 1,51 0,085
    Bac+2 1,04 0,81 – 1,32 0,8
    Supérieur long 1,00 0,79 – 1,26 >0,9
Sexe / genre


    Homme
    Femme 1,09 0,92 – 1,28 0,3
    Autre / non-binaire 2,00 0,90 – 4,43 0,088
Satisfaction en vague 1 0,96 0,92 – 1,00 0,053
Abréviations: IC = intervalle de confiance, OR = rapport de cotes
NoteLecture sociologique

L’attrition peut être socialement structurée : mobilité, santé, stabilité résidentielle, disponibilité, rapport à l’enquête et position dans le cycle de vie peuvent tous influencer la probabilité de rester observable. Le modèle identifie des associations avec la rétention ; il ne révèle pas automatiquement le mécanisme concret de perte de suivi.

23.8 Construire le panel au format large

Le format large place les deux mesures sur une même ligne et facilite les différences individuelles.

Afficher le code R
v1 <- enqueter |>
  select(
    id, psu, strate,
    age, age_classe_f, diplome_4, sexe_f,
    revenu_mensuel, statut_emploi, sante,
    satisfaction_vie, confiance_science
  ) |>
  rename_with(~paste0(.x, "_v1"), -id)

v2 <- vague2 |>
  rename_with(~paste0(.x, "_v2"), -id)

panel <- inner_join(v1, v2, by = "id") |>
  mutate(
    delta_satisfaction = satisfaction_vie_v2 - satisfaction_vie_v1,
    delta_revenu_k = (revenu_mensuel_v2 - revenu_mensuel_v1) / 1000,
    emploi_v1 = statut_emploi_v1 == 1,
    emploi_v2 = statut_emploi_v2 == 1,
    transition_emploi = case_when(
      is.na(emploi_v1) | is.na(emploi_v2) ~ NA_character_,
      emploi_v1 & emploi_v2 ~ "En emploi aux deux vagues",
      emploi_v1 & !emploi_v2 ~ "Sortie de l'emploi",
      !emploi_v1 & emploi_v2 ~ "Entrée dans l'emploi",
      TRUE ~ "Hors emploi aux deux vagues"
    ),
    transition_emploi_f = factor(
      transition_emploi,
      levels = c(
        "En emploi aux deux vagues", "Sortie de l'emploi",
        "Entrée dans l'emploi", "Hors emploi aux deux vagues"
      )
    )
  )

tibble::tibble(
  `Personnes observées aux deux vagues` = nrow(panel),
  `Part du fichier vague 1` = fmt_pct(nrow(panel) / nrow(enqueter), .1)
) |>
  knitr::kable()
Personnes observées aux deux vagues Part du fichier vague 1
2679 74,6%

23.9 Le format long : une ligne par personne × vague

Afficher le code R
long <- bind_rows(
  panel |>
    transmute(
      id,
      psu_v1,
      strate_v1,
      vague = 1,
      satisfaction_vie = satisfaction_vie_v1,
      revenu_mensuel = revenu_mensuel_v1,
      statut_emploi = statut_emploi_v1,
      confiance_science = confiance_science_v1,
      poids_longitudinal = poids_longitudinal_v2
    ),
  panel |>
    transmute(
      id,
      psu_v1,
      strate_v1,
      vague = 2,
      satisfaction_vie = satisfaction_vie_v2,
      revenu_mensuel = revenu_mensuel_v2,
      statut_emploi = statut_emploi_v2,
      confiance_science = confiance_science_v2,
      poids_longitudinal = poids_longitudinal_v2
    )
)

Un identifiant apparaît maintenant deux fois. Ce n’est pas un doublon à supprimer : c’est la structure du panel.

23.10 Conserver le plan d’origine dans l’analyse longitudinale

Le poids longitudinal ajuste la population suivie ; il ne remplace pas les informations de stratification et de grappes de la vague d’origine. Nous conservons donc strate_v1 et psu_v1.

Pour le fichier long, l’individu est ajouté comme niveau de cluster afin de reconnaître les deux observations répétées d’une même personne.

Afficher le code R
design_long <- survey::svydesign(
  ids = ~psu_v1 + id,
  strata = ~strate_v1,
  weights = ~poids_longitudinal,
  data = long,
  nest = TRUE
)

moy_sat_vague <- survey::svyby(
  ~satisfaction_vie,
  ~vague,
  design_long,
  survey::svymean,
  vartype = c("se", "ci"),
  na.rm = TRUE
)

moy_sat_vague |>
  as.data.frame() |>
  knitr::kable(digits = 2)
vague satisfaction_vie se ci_l ci_u
1 6.78 0.03 6.73 6.83
2 6.80 0.03 6.74 6.87

La comparaison des deux moyennes décrit une évolution agrégée parmi la population longitudinale représentée par le poids. Elle ne dit pas encore comment les individus se sont déplacés autour de cette moyenne.

23.11 Une moyenne stable peut masquer beaucoup de mobilité individuelle

Afficher le code R
set.seed(2026)
ids_graph <- sample(unique(long$id), size = min(80, length(unique(long$id))))

long |>
  filter(id %in% ids_graph) |>
  ggplot(aes(x = factor(vague), y = satisfaction_vie, group = id)) +
  geom_line(alpha = .18, colour = couleurs_enqueter[["gris"]]) +
  geom_point(alpha = .25, size = 1.1, colour = couleurs_enqueter[["principal"]]) +
  stat_summary(
    aes(group = 1), fun = mean, geom = "line",
    linewidth = 1.2, colour = couleurs_enqueter[["accent"]]
  ) +
  stat_summary(
    aes(group = 1), fun = mean, geom = "point",
    size = 3, colour = couleurs_enqueter[["accent"]]
  ) +
  labs(
    x = "Vague",
    y = "Satisfaction de vie",
    title = "Une moyenne agrégée peut masquer des trajectoires opposées",
    subtitle = "80 individus tirés uniquement pour la lisibilité de la figure"
  ) +
  theme_enqueter()

Trajectoires de satisfaction entre les deux vagues pour un sous-échantillon de personnes, avec une moyenne générale plus stable que de nombreuses trajectoires individuelles.

23.12 Quantifier le changement individuel avec le design longitudinal

Afficher le code R
design_panel <- survey::svydesign(
  ids = ~psu_v1,
  strata = ~strate_v1,
  weights = ~poids_longitudinal_v2,
  data = panel,
  nest = TRUE
)

moy_delta <- survey::svymean(
  ~delta_satisfaction,
  design_panel,
  na.rm = TRUE
)

ci_delta <- confint(moy_delta)
Afficher le code R
tibble::tibble(
  `Changement moyen` = fmt_num(coef(moy_delta)[1], .02),
  `IC 95 %` = paste0(fmt_num(ci_delta[1, 1], .02), " – ", fmt_num(ci_delta[1, 2], .02)),
  `N personnes` = nrow(panel)
) |>
  knitr::kable()
Changement moyen IC 95 % N personnes
0,02 0,00 – 0,06 2679

Un changement moyen proche de zéro n’implique pas une immobilité individuelle : hausses et baisses peuvent se compenser.

Afficher le code R
ggplot(panel, aes(x = delta_satisfaction, weight = poids_longitudinal_v2)) +
  geom_histogram(
    binwidth = 1,
    boundary = -.5,
    fill = couleurs_enqueter[["principal"]],
    alpha = .82
  ) +
  geom_vline(xintercept = 0, linetype = 2) +
  labs(
    x = "Vague 2 – vague 1 (points)",
    y = "Effectif pondéré",
    title = "Le changement longitudinal se lit dans la distribution des différences",
    caption = "Données synthétiques EnquêteR."
  ) +
  theme_enqueter()

Histogramme pondéré des différences individuelles de satisfaction entre vague 2 et vague 1, avec des valeurs positives et négatives autour de zéro.

23.13 Transitions d’emploi : les flux derrière les taux

Pour une variable d’état, les transitions sont souvent plus informatives que deux taux successifs.

Afficher le code R
transitions <- survey::svytable(
  ~factor(emploi_v1, levels = c(FALSE, TRUE), labels = c("Hors emploi", "En emploi")) +
    factor(emploi_v2, levels = c(FALSE, TRUE), labels = c("Hors emploi", "En emploi")),
  design_panel
)

prop.table(transitions, margin = 1) |>
  as.data.frame.matrix() |>
  tibble::rownames_to_column("Situation vague 1") |>
  mutate(across(-1, ~fmt_pct(.x, .1))) |>
  knitr::kable()
Situation vague 1 Hors emploi En emploi
Hors emploi 95,4% 4,6%
En emploi 4,2% 95,8%

Deux populations peuvent avoir le même taux d’emploi à deux dates tout en connaissant des volumes très différents d’entrées et de sorties. Le panel donne accès à cette mobilité.

23.14 Une première analyse du changement

Avec deux vagues, le modèle en différences est particulièrement lisible :

Afficher le code R
mod_delta <- survey::svyglm(
  delta_satisfaction ~ delta_revenu_k + transition_emploi_f,
  design = design_panel
)

tbl_regression(
  mod_delta,
  label = list(
    delta_revenu_k ~ "Variation de revenu (+1 000 €)",
    transition_emploi_f ~ "Transition d'emploi"
  )
) |>
  bold_labels()
Caractéristique Beta 95% IC p-valeur
Variation de revenu (+1 000 €) 0,00 -0,14 – 0,14 >0,9
Transition d'emploi


    En emploi aux deux vagues
    Sortie de l'emploi 0,08 -0,12 – 0,27 0,4
    Entrée dans l'emploi -0,04 -0,21 – 0,14 0,7
    Hors emploi aux deux vagues 0,03 -0,05 – 0,10 0,5
Abréviation: IC = intervalle de confiance

Le modèle relie des changements chez les mêmes personnes. Certaines différences stables entre personnes disparaissent mécaniquement de la variable dépendante en différence, mais cela ne transforme pas le modèle en expérience causale.

NoteLecture sociologique

Le passage du transversal au longitudinal déplace la question. On ne demande plus seulement pourquoi certaines personnes ont une satisfaction plus élevée que d’autres, mais si des modifications de ressources ou de position sont associées à des modifications de satisfaction pour une même personne. Cette perspective rapproche l’analyse de la dynamique sociale plutôt que de la seule stratification entre individus.

AvertissementCe que nous ne pouvons pas conclure

Une association entre variation de revenu et variation de satisfaction n’identifie pas automatiquement un effet causal. Des événements concomitants, la causalité inverse, des erreurs de mesure du changement et l’attrition sélective restent possibles. Avec seulement deux vagues, la temporalité elle-même est encore peu détaillée.

23.15 Effets fixes : l’intuition, pas la magie

Avec davantage de vagues, un modèle à effets fixes individuels exploite les variations au sein des personnes et élimine les caractéristiques individuelles invariantes dans le temps.

Afficher le code R
# Exemple d'extension avec davantage de vagues.
# library(plm)
# mod_fe <- plm(
#   satisfaction_vie ~ I(revenu_mensuel / 1000) + en_emploi,
#   data = long,
#   index = c("id", "vague"),
#   model = "within"
# )

Cela ne contrôle pas les facteurs non observés qui changent dans le temps et sont corrélés aux changements étudiés. « Effets fixes » ne signifie donc pas « causalité garantie ».

23.16 Pourquoi un poids longitudinal spécifique ?

Un poids transversal vise une population à une vague donnée. Un poids longitudinal doit en plus traiter la probabilité de rester observable au fil des vagues. Copier simplement poids_final de vague 1 ignorerait l’attrition.

Dans une enquête réelle, il faut savoir :

  • quelle population longitudinale le poids vise ;
  • quelles variables ont servi à l’ajustement de l’attrition ;
  • si le poids est valide pour toutes les vagues ou seulement certaines combinaisons ;
  • si les strates et PSU d’origine doivent être conservées dans la variance.

23.17 Exercice de synthèse

23.17.1 Niveau A — Appliquer

Calculez le taux de rétention global et selon le diplôme. Produisez une figure avec intervalles de confiance.

23.17.2 Niveau B — Choisir

Comparez évolution moyenne de satisfaction et distribution des changements individuels. Expliquez pourquoi les deux résumés peuvent conduire à des lectures très différentes.

23.17.3 Niveau C — Analyser

Choisissez une variable disponible aux deux vagues. Construisez : un diagnostic d’attrition, un modèle de rétention, une statistique de transition ou de changement, une figure et un modèle en différences. Rédigez un paragraphe distinguant changement individuel, sélection par attrition, poids longitudinal et causalité.

23.18 À retenir

  • Plusieurs dates ne créent pas un panel sans identifiant stable.
  • L’attrition doit être analysée avant les trajectoires.
  • Formats large et long servent des opérations différentes.
  • Le poids longitudinal n’efface pas la nécessité de conserver les informations du plan d’origine.
  • Une moyenne stable peut masquer d’importants changements individuels.
  • Les transitions sont souvent plus informatives qu’une comparaison de deux proportions indépendantes.
  • Un modèle en différences réduit certaines hétérogénéités stables mais ne garantit pas une interprétation causale.
  • Le longitudinal améliore l’analyse du changement ; il introduit aussi de nouveaux problèmes de sélection et de mesure.