DiD avec Effets Fixes

Module 3 — Le modèle TWFE et les données de panel

Master GPE — UCA FERDI IHEDD

2026-03-19

Exemples africains de TWFE

Le TWFE en politiques de développement :

  • Impact d’une réforme fiscale (TVA) introduite progressivement par province
  • Effet de la construction de routes sur la production agricole par commune
  • Impact d’un programme de transferts conditionnels introduit par vagues régionales
  • Evaluation des réformes de décentralisation dans les collectivités locales

→ Dans tous ces cas : données de panel (régions × années), traitement qui varie dans le temps.

Notre exemple de cours :

Données mpdta (Callaway & Sant’Anna, 2021) : hausses de salaire minimum dans les comtés américains.

Structure identique à ce que vous utiliseriez pour évaluer une réforme salariale, un programme de subvention, ou une politique de santé dans vos pays.

De la DiD 2×2 au panel

Les données de panel offrent :

  • \(N\) unités (individus, comtés, pays…)
  • \(T\) périodes
  • Un traitement qui peut varier dans le temps
  • Plus de précision statistique
  • Possibilité de contrôler les effets fixes

Notation :

  • \(i = 1, \ldots, N\) : unités
  • \(t = 1, \ldots, T\) : périodes
  • \(D_{it} \in \{0, 1\}\) : statut de traitement
  • \(Y_{it}\) : résultat observé

Le modèle à Effets Fixes Bidirectionnels (TWFE)

Two-Way Fixed Effects (TWFE) :

\[Y_{it} = \alpha_i + \lambda_t + \delta D_{it} + \varepsilon_{it}\]

  • \(\alpha_i\) : effet fixe individuel (capture tout ce qui est constant pour \(i\) dans le temps)
  • \(\lambda_t\) : effet fixe temporel (capture les chocs communs à tous à la période \(t\))
  • \(\delta\) : estimateur DiD — effet causal du traitement
  • \(\varepsilon_{it}\) : terme d’erreur

Note

Le modèle TWFE est une généralisation directe de la DiD 2×2 au cas avec \(N\) unités et \(T\) périodes.

Intuition des effets fixes

Effets fixes individuels \(\alpha_i\) : Absorbent les différences permanentes entre unités : - Capacité institutionnelle d’un pays - Taille d’un comté - Culture organisationnelle - Localisation géographique

→ On travaille en déviations par rapport à la moyenne individuelle

Effets fixes temporels \(\lambda_t\) : Absorbent les chocs communs à toutes les unités : - Crises économiques globales - Changements législatifs nationaux - Saisonnalité - Inflation

→ On compare les évolutions relatives des unités

Les données du cours — mpdta

Package did (Callaway & Sant’Anna) — Données sur le salaire minimum aux États-Unis :

Voir le code R
library(did)
data(mpdta)
mpdta$treat_tv <- as.integer(mpdta$first.treat > 0 & mpdta$year >= mpdta$first.treat)
glimpse(mpdta)
#> Rows: 2,500
#> Columns: 7
#> $ year        <int> 2003, 2004, 2005, 2006, 2007, 2003, 2004, 2005, 2006, 2007…
#> $ countyreal  <dbl> 8001, 8001, 8001, 8001, 8001, 8019, 8019, 8019, 8019, 8019…
#> $ lpop        <dbl> 5.896761, 5.896761, 5.896761, 5.896761, 5.896761, 2.232377…
#> $ lemp        <dbl> 8.461469, 8.336870, 8.340217, 8.378161, 8.487352, 4.997212…
#> $ first.treat <dbl> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…
#> $ treat       <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ treat_tv    <int> 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1…

Description de mpdta

Voir le code R
# Structure des données
cat("Nombre de comtés :", n_distinct(mpdta$countyreal), "\n")
#> Nombre de comtés : 500
Voir le code R
cat("Années disponibles :", sort(unique(mpdta$year)), "\n")
#> Années disponibles : 2003 2004 2005 2006 2007
Voir le code R
cat("Année de premier traitement :", sort(unique(mpdta$first.treat)), "\n")
#> Année de premier traitement : 0 2004 2006 2007
Voir le code R
# Aperçu des variables
mpdta |>
  head(10) |>
  select(countyreal, year, lemp, lpop, first.treat, treat)
#>     countyreal year     lemp     lpop first.treat treat
#> 866       8001 2003 8.461469 5.896761        2007     1
#> 841       8001 2004 8.336870 5.896761        2007     1
#> 842       8001 2005 8.340217 5.896761        2007     1
#> 819       8001 2006 8.378161 5.896761        2007     1
#> 827       8001 2007 8.487352 5.896761        2007     1
#> 937       8019 2003 4.997212 2.232377        2007     1
#> 938       8019 2004 5.081404 2.232377        2007     1
#> 939       8019 2005 4.787492 2.232377        2007     1
#> 940       8019 2006 4.990433 2.232377        2007     1
#> 941       8019 2007 5.036953 2.232377        2007     1

Distribution du traitement dans mpdta

Voir le code R
# Combien de comtés par vague de traitement ?
mpdta |>
  filter(year == min(year)) |>
  count(first.treat) |>
  mutate(first.treat = if_else(first.treat == 0, "Jamais traité", as.character(first.treat))) |>
  rename(`Année 1er traitement` = first.treat, `Nb de comtés` = n)
#>   Année 1er traitement Nb de comtés
#> 1        Jamais traité          309
#> 2                 2004           20
#> 3                 2006           40
#> 4                 2007          131

Astuce

Les traitements sont échelonnés dans le temps — c’est ce qui posera problème au TWFE et motivera le Module 4.

Estimation TWFE en R — package fixest

Voir le code R
library(fixest)

# Modèle TWFE
twfe <- feols(
  lemp ~ treat_tv | countyreal + year,  # | séparateur effets fixes
  data    = mpdta,
  cluster = ~countyreal              # erreurs groupées par comté
)

summary(twfe)
#> OLS estimation, Dep. Var.: lemp
#> Observations: 2,500
#> Fixed-effects: countyreal: 500,  year: 5
#> Standard-errors: Clustered (countyreal) 
#>           Estimate Std. Error  t value Pr(>|t|)    
#> treat_tv -0.036549   0.013265 -2.75526 0.006079 ** 
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> RMSE: 0.124223     Adj. R2: 0.991505
#>                  Within R2: 0.004169

Interprétation du résultat TWFE

Variable Estimate Std. Error t value Pr(>|t|)
treat_tv -0.0365 0.0133 -2.7553 0.0061

Note

Lecture : Une hausse du salaire minimum est associée à une baisse d’environ 3,8 % de l’emploi des jeunes, toutes choses égales par ailleurs (effets comté et année contrôlés).

Ce coefficient est-il fiable ? → Les modules 4 et 5 répondront à cette question.

Tester les pré-tendances — l’event study

Idée : Avant le traitement, les groupes traités et contrôles doivent avoir des tendances similaires.

On estime des effets par période relative au traitement (\(k\) = périodes avant/après) et on vérifie que les coefficients avant le traitement sont proches de zéro.

On utilise le package did pour une event study cohérente avec le reste du cours :

Voir le code R
library(did)
att_gt_m3 <- att_gt(
  yname = "lemp", tname = "year", idname = "countyreal",
  gname = "first.treat", data = mpdta,
  control_group = "nevertreated", print_details = FALSE
)
agg_dynamic_m3 <- aggte(att_gt_m3, type = "dynamic")

Event study — graphique

Voir le code R
ggdid(agg_dynamic_m3,
      title = "Event Study — Impact du salaire minimum sur l'emploi des jeunes",
      ylab  = "ATT estimé (log emploi)",
      xlab  = "Périodes relatives au traitement (k)")

Lecture d’un graphique event study

Ce qu’on regarde :

  1. Périodes pré-traitement (\(k < 0\)) : Les coefficients doivent être non significativement différents de zéro (validation des tendances parallèles)

  2. Période de traitement (\(k = 0\)) : L’effet s’enclenche-t-il immédiatement ?

  3. Périodes post-traitement (\(k > 0\)) : L’effet persiste-t-il ? S’amplifie-t-il ?

Si les coefficients pré-traitement sont significatifs :

→ Les tendances parallèles sont violées

→ L’estimateur DiD est biaisé

→ Revoir la stratégie d’identification

Ajout de covariables au TWFE

Voir le code R
# TWFE avec covariable (log population)
twfe_cov <- feols(
  lemp ~ treat_tv + lpop | countyreal + year,
  data    = mpdta,
  cluster = ~countyreal
)

# Comparaison des modèles
etable(twfe, twfe_cov,
       headers = c("TWFE simple", "TWFE + lpop"),
       digits  = 4)
#>                               twfe           twfe_cov
#>                        TWFE simple        TWFE + lpop
#> Dependent Var.:               lemp               lemp
#>                                                      
#> treat_tv        -0.0365** (0.0133) -0.0365** (0.0133)
#> Fixed-Effects:  ------------------ ------------------
#> countyreal                     Yes                Yes
#> year                           Yes                Yes
#> _______________ __________________ __________________
#> S.E.: Clustered     by: countyreal     by: countyreal
#> Observations                 2,500              2,500
#> R2                         0.99322            0.99322
#> Within R2                  0.00417            0.00417
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Les erreurs standard groupées

Pourquoi grouper les erreurs standard ?

Dans les DiD, les observations d’une même unité sont corrélées dans le temps → ignorer cela sous-estime les erreurs standard → trop de rejets de H₀.

Bertrand, Duflo & Mullainathan (2004) : Regrouper les erreurs au niveau de l’unité traitée (ici : le comté).

Voir le code R
# Comparaison : erreurs standard OLS vs groupées
twfe_ols     <- feols(lemp ~ treat_tv | countyreal + year, data = mpdta)
twfe_cluster <- feols(lemp ~ treat_tv | countyreal + year, data = mpdta, cluster = ~countyreal)

etable(twfe_ols, twfe_cluster,
       headers = c("OLS std.", "Std. groupées"),
       digits  = 4)
#>                           twfe_ols       twfe_cluster
#>                           OLS std.     Std. groupées
#> Dependent Var.:               lemp               lemp
#>                                                      
#> treat_tv        -0.0365** (0.0126) -0.0365** (0.0133)
#> Fixed-Effects:  ------------------ ------------------
#> countyreal                     Yes                Yes
#> year                           Yes                Yes
#> _______________ __________________ __________________
#> S.E. type                      IID     by: countyreal
#> Observations                 2,500              2,500
#> R2                         0.99322            0.99322
#> Within R2                  0.00417            0.00417
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Limites du TWFE — prélude au Module 4

  1. Hétérogénéité des effets de traitement : Le TWFE suppose que l’effet \(\delta\) est identique pour toutes les unités et toutes les périodes. Cette hypothèse est rarement réaliste.

  2. Traitement échelonné (staggered adoption) : Quand différentes unités sont traitées à différentes dates, le TWFE “compare” parfois des unités déjà traitées à d’autres. Ce sont des mauvais groupes de comparaison !

  3. Goodman-Bacon (2021) : Montre que le coefficient TWFE est une moyenne pondérée de DiD 2×2, avec des poids négatifs possibles pour certains termes.

Illustration du problème TWFE — Goodman-Bacon

Résumé du Module 3

À retenir :

  1. Le TWFE \(Y_{it} = \alpha_i + \lambda_t + \delta D_{it} + \varepsilon_{it}\) est la généralisation de la DiD 2×2 au panel
  2. Les effets fixes \(\alpha_i\) et \(\lambda_t\) contrôlent l’hétérogénéité permanente et les tendances communes
  3. On implémente le TWFE en R avec fixest::feols() et les erreurs groupées
  4. L’event study permet de tester visuellement les pré-tendances
  5. Le TWFE est biaisé quand les traitements sont échelonnés et les effets hétérogènes

Module 4 : L’approche Callaway & Sant’Anna pour corriger ce biais

Communiquer les résultats TWFE

Comment présenter ce résultat à votre ministre ?

“L’analyse par effets fixes bidirectionnels — qui compare chaque unité à elle-même dans le temps et contrôle les tendances communes — indique qu’une hausse du salaire minimum est associée à une baisse de 3,8 % de l’emploi des jeunes. Cette méthode est robuste aux différences structurelles entre comtés et aux chocs économiques communs.”

→ Toujours donner : (1) le chiffre, (2) le sens (hausse/baisse), (3) la variable, (4) une phrase sur pourquoi la méthode est fiable.

Avertissement

Avant de présenter ces résultats : Vérifiez TOUJOURS le graphique d’event study et précisez que la validité dépend de l’hypothèse de tendances parallèles.