---
title: "Enquêtes longitudinales et panels"
---
```{r}
#| label: setup-analyse
#| include: false
source("R/04-initialiser-analyse.R")
suppressPackageStartupMessages(library(gtsummary))
```
::: {.callout-important title="Question 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 ?
:::
## 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.
## 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.
::: {.callout-note title="Le 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.
:::
## La seconde vague d'EnquêteR
```{r}
#| label: tab-ch23-vague2
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()
```
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`.
## 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 ?**
```{r}
#| label: ch23-attrition-design
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)
```
```{r}
#| label: tab-ch23-retention
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()
```
Un taux de rétention global ne dit pas si l'attrition déforme la composition du panel.
## Profiler l'attrition
```{r}
#| label: tab-ch23-attrition-profile
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()
```
La table permet de repérer les dimensions sur lesquelles les personnes réinterrogées diffèrent des personnes perdues de vue.
## Visualiser la rétention selon l'âge
```{r}
#| label: ch23-ret-age
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")
```
```{r}
#| label: plot-ch23-retention-age
#| fig-alt: "La rétention au panel varie selon la classe d'âge ; chaque estimation est accompagnée d'un intervalle de confiance."
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()
```
## 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.
```{r}
#| label: ch23-attrition-model
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()
```
::: {.callout-note title="Lecture 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.
:::
## Construire le panel au format large
Le format large place les deux mesures sur une même ligne et facilite les différences individuelles.
```{r}
#| label: ch23-panel-wide
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()
```
## Le format long : une ligne par personne × vague
```{r}
#| label: ch23-panel-long
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.
## 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.
```{r}
#| label: ch23-design-long
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)
```
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.
## Une moyenne stable peut masquer beaucoup de mobilité individuelle
```{r}
#| label: plot-ch23-trajectories
#| fig-alt: "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."
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()
```
## Quantifier le changement individuel avec le design longitudinal
```{r}
#| label: ch23-design-panel
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)
```
```{r}
#| label: tab-ch23-delta
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()
```
Un changement moyen proche de zéro n'implique pas une immobilité individuelle : hausses et baisses peuvent se compenser.
```{r}
#| label: plot-ch23-delta-sat
#| fig-alt: "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."
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()
```
## Transitions d'emploi : les flux derrière les taux
Pour une variable d'état, les transitions sont souvent plus informatives que deux taux successifs.
```{r}
#| label: tab-ch23-transitions
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()
```
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é.
## Une première analyse du changement
Avec deux vagues, le modèle en différences est particulièrement lisible :
```{r}
#| label: tab-ch23-delta-model
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()
```
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.
::: {.callout-note title="Lecture 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.
:::
::: {.callout-warning title="Ce 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.
:::
## 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.
```{r, eval=FALSE}
# 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 ».
## 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.
## Exercice de synthèse
### Niveau A — Appliquer
Calculez le taux de rétention global et selon le diplôme. Produisez une figure avec intervalles de confiance.
### 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.
### 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é.
## À 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.