<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Difference-in-Differences (DiD) | Carlos Mendez</title><link>https://carlos-mendez.org/category/difference-in-differences-did/</link><atom:link href="https://carlos-mendez.org/category/difference-in-differences-did/index.xml" rel="self" type="application/rss+xml"/><description>Difference-in-Differences (DiD)</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-us</language><copyright>Carlos Mendez</copyright><lastBuildDate>Thu, 26 Mar 2026 00:00:00 +0000</lastBuildDate><image><url>https://carlos-mendez.org/media/icon_huedfae549300b4ca5d201a9bd09a3ecd5_79625_512x512_fill_lanczos_center_3.png</url><title>Difference-in-Differences (DiD)</title><link>https://carlos-mendez.org/category/difference-in-differences-did/</link></image><item><title>Difference-in-Differences for Policy Evaluation: A Tutorial using R</title><link>https://carlos-mendez.org/post/r_did/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_did/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Does raising the minimum wage reduce employment among young workers? This question has been at the center of one of the longest-running debates in labor economics, and the &lt;strong>Difference-in-Differences (DID)&lt;/strong> method has been the primary tool for answering it. In this tutorial, we analyze how state-level minimum wage increases between 2001 and 2007 affected teen employment in the United States &amp;mdash; a period when the federal minimum wage was frozen at \$5.15 per hour, while individual states raised their own minimum wages at different times. This variation in treatment timing creates a natural experiment ideally suited for DID.&lt;/p>
&lt;p>For decades, applied researchers implemented DID using a simple &lt;strong>two-way fixed effects (TWFE)&lt;/strong> regression &amp;mdash; a panel regression with unit and time fixed effects. Recent research has revealed that this approach can produce severely biased estimates when there is &lt;strong>staggered treatment adoption&lt;/strong> (units treated at different times) and &lt;strong>treatment effect heterogeneity&lt;/strong> (effects that vary across groups or over time). The TWFE regression implicitly makes &amp;ldquo;forbidden comparisons&amp;rdquo; that use already-treated units as the comparison group, and it assigns negative weights to some group-time treatment effects. These problems are not theoretical curiosities &amp;mdash; they lead to meaningful differences in empirical estimates.&lt;/p>
&lt;p>This tutorial walks through the complete modern DID workflow. We begin with the traditional TWFE regression and demonstrate its limitations. We then introduce the &lt;strong>Callaway and Sant&amp;rsquo;Anna (2021)&lt;/strong> framework for estimating group-time average treatment effects, $ATT(g,t)$, that cleanly separate identification from estimation. We extend the analysis with covariates using doubly robust estimation, assess the sensitivity of results to violations of parallel trends using &lt;strong>HonestDiD&lt;/strong> (Rambachan and Roth, 2023), and explore how to handle heterogeneous treatment doses across states. The tutorial is based on Callaway&amp;rsquo;s (2022) chapter &amp;ldquo;Difference-in-Differences for Policy Evaluation&amp;rdquo; and the accompanying LSU workshop materials.&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand the parallel trends assumption and why TWFE regressions break down with staggered treatment adoption and treatment effect heterogeneity&lt;/li>
&lt;li>Estimate group-time average treatment effects using &lt;code>att_gt()&lt;/code> from the &lt;code>did&lt;/code> package and aggregate them into overall ATTs and event studies&lt;/li>
&lt;li>Diagnose TWFE bias through weight decomposition, identifying negative weights and pre-treatment contamination&lt;/li>
&lt;li>Apply doubly robust estimation with conditional parallel trends and assess robustness to base period and comparison group choices&lt;/li>
&lt;li>Conduct HonestDiD sensitivity analysis to evaluate how robust findings are to violations of parallel trends&lt;/li>
&lt;/ul>
&lt;h2 id="2-setup">2. Setup&lt;/h2>
&lt;pre>&lt;code class="language-r"># Install packages if needed
cran_packages &amp;lt;- c(&amp;quot;did&amp;quot;, &amp;quot;fixest&amp;quot;, &amp;quot;HonestDiD&amp;quot;, &amp;quot;DRDID&amp;quot;, &amp;quot;BMisc&amp;quot;,
&amp;quot;modelsummary&amp;quot;, &amp;quot;ggplot2&amp;quot;, &amp;quot;dplyr&amp;quot;, &amp;quot;pte&amp;quot;)
missing &amp;lt;- cran_packages[!sapply(cran_packages, requireNamespace, quietly = TRUE)]
if (length(missing) &amp;gt; 0) install.packages(missing)
# twfeweights is GitHub-only
if (!requireNamespace(&amp;quot;twfeweights&amp;quot;, quietly = TRUE)) {
remotes::install_github(&amp;quot;bcallaway11/twfeweights&amp;quot;)
}
# pte may also require GitHub install if not on CRAN
if (!requireNamespace(&amp;quot;pte&amp;quot;, quietly = TRUE)) {
remotes::install_github(&amp;quot;bcallaway11/pte&amp;quot;)
}
library(did)
library(fixest)
library(twfeweights)
library(HonestDiD)
library(DRDID)
library(BMisc)
library(modelsummary)
library(ggplot2)
library(dplyr)
&lt;/code>&lt;/pre>
&lt;h2 id="3-data-loading-and-exploration">3. Data Loading and Exploration&lt;/h2>
&lt;p>The dataset comes from Callaway and Sant&amp;rsquo;Anna (2021) and contains county-level panel data on teen employment and state minimum wages across the United States from 2001 to 2007. During this period, the federal minimum wage remained constant at \$5.15 per hour, while several states raised their state-level minimum wages above the federal floor at different points in time. States that raised their minimum wages form the &amp;ldquo;treated&amp;rdquo; groups, identified by the year their first increase took effect. States that never raised their minimum wage above the federal level during this period form the &amp;ldquo;never-treated&amp;rdquo; comparison group.&lt;/p>
&lt;pre>&lt;code class="language-r"># Load data from Callaway's GitHub repository
load(url(&amp;quot;https://github.com/bcallaway11/did_chapter/raw/master/mw_data_ch2.RData&amp;quot;))
# Filter: keep groups 0 (never-treated), 2004, 2006; drop Northeast region
mw_data_ch2 &amp;lt;- subset(mw_data_ch2,
(G %in% c(2004, 2006, 2007, 0)) &amp;amp; (region != &amp;quot;1&amp;quot;))
# Main analysis subset: drop G=2007, keep year &amp;gt;= 2003
data2 &amp;lt;- subset(mw_data_ch2, G != 2007 &amp;amp; year &amp;gt;= 2003)
head(data2[, c(&amp;quot;id&amp;quot;, &amp;quot;year&amp;quot;, &amp;quot;G&amp;quot;, &amp;quot;lemp&amp;quot;, &amp;quot;lpop&amp;quot;, &amp;quot;region&amp;quot;)])
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> id year G lemp lpop region
6 1001 2003 0 5.253534 10.07352 3
7 1001 2004 0 5.288267 10.06966 3
8 1001 2005 0 5.267858 10.06235 3
9 1001 2006 0 5.298317 10.05546 3
10 1001 2007 0 5.232025 10.04953 3
31 1003 2003 0 6.822197 11.16740 3
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-r"># Counties by treatment group
data2 %&amp;gt;%
filter(year == 2003) %&amp;gt;%
group_by(G) %&amp;gt;%
summarise(n_counties = n(), .groups = &amp;quot;drop&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> G n_counties
1 0 1417
2 2004 102
3 2006 226
&lt;/code>&lt;/pre>
&lt;p>The dataset contains 8,725 county-year observations spanning 1,745 counties over five years (2003&amp;ndash;2007). There are two treatment groups: 102 counties in states that first raised their minimum wage in 2004 (G=2004) and 226 counties in states that did so in 2006 (G=2006). The remaining 1,417 counties are in states that kept their minimum wage at the federal level throughout the period and serve as the never-treated comparison group. We drop the G=2007 group (states raising their minimum wage right before the federal increase) to maintain a cleaner analysis window, following the workshop approach.&lt;/p>
&lt;pre>&lt;code class="language-r"># Summary statistics
summary(data2[, c(&amp;quot;lemp&amp;quot;, &amp;quot;lpop&amp;quot;, &amp;quot;lavg_pay&amp;quot;)])
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> lemp lpop lavg_pay
Min. : 1.099 Min. : 6.397 Min. : 9.646
1st Qu.: 4.615 1st Qu.: 9.149 1st Qu.:10.117
Median : 5.517 Median : 9.931 Median :10.225
Mean : 5.594 Mean :10.030 Mean :10.245
3rd Qu.: 6.458 3rd Qu.:10.762 3rd Qu.:10.352
Max. :11.173 Max. :15.492 Max. :11.223
&lt;/code>&lt;/pre>
&lt;p>The outcome variable &lt;code>lemp&lt;/code> is log teen employment, with a mean of 5.59 (corresponding to roughly 270 teen workers per county). The covariates &lt;code>lpop&lt;/code> (log county population, mean 10.03) and &lt;code>lavg_pay&lt;/code> (log average county pay, mean 10.25) capture differences in county size and economic conditions that could affect employment trends. These covariates will become important when we condition the parallel trends assumption on observables in Section 7.&lt;/p>
&lt;h2 id="4-the-basic-did-framework">4. The Basic DID Framework&lt;/h2>
&lt;h3 id="41-did-intuition-and-parallel-trends">4.1 DID Intuition and Parallel Trends&lt;/h3>
&lt;p>The core idea behind Difference-in-Differences is simple: compare how outcomes change over time for the treated group relative to a comparison group. If the treated and comparison groups would have followed &lt;strong>parallel trends&lt;/strong> in the absence of treatment, then any divergence after treatment can be attributed to the treatment itself. Formally, the Average Treatment Effect on the Treated (ATT) is identified as:&lt;/p>
&lt;p>$$ATT = E[\Delta Y_{t^{\ast}} \mid D=1] - E[\Delta Y_{t^{\ast}} \mid D=0]$$&lt;/p>
&lt;p>where $\Delta Y_{t^{\ast}}$ is the change in outcomes from the pre-treatment period to the post-treatment period, $D=1$ indicates treated units, and $D=0$ indicates untreated units. The ATT equals the change in outcomes for the treated group, adjusted by the change in outcomes for the comparison group.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
subgraph &amp;quot;Before Treatment&amp;quot;
A[&amp;quot;Treated Group&amp;lt;br/&amp;gt;Pre-treatment Y&amp;quot;]
B[&amp;quot;Control Group&amp;lt;br/&amp;gt;Pre-treatment Y&amp;quot;]
end
subgraph &amp;quot;After Treatment&amp;quot;
C[&amp;quot;Treated Group&amp;lt;br/&amp;gt;Post-treatment Y&amp;quot;]
D[&amp;quot;Control Group&amp;lt;br/&amp;gt;Post-treatment Y&amp;quot;]
end
A --&amp;gt;|&amp;quot;ΔY treated&amp;quot;| C
B --&amp;gt;|&amp;quot;ΔY control&amp;quot;| D
C -.-&amp;gt;|&amp;quot;ATT = ΔY treated − ΔY control&amp;quot;| E[&amp;quot;Causal Effect&amp;quot;]
style A fill:#d97757,stroke:#141413,color:#fff
style C fill:#d97757,stroke:#141413,color:#fff
style B fill:#6a9bcc,stroke:#141413,color:#fff
style D fill:#6a9bcc,stroke:#141413,color:#fff
style E fill:#00d4c8,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>In the textbook case with exactly two periods and two groups, the TWFE regression $Y_{it} = \theta_t + \eta_i + \alpha D_{it} + v_{it}$ delivers an estimate of $\alpha$ that is numerically identical to the simple DID estimator, even in the presence of treatment effect heterogeneity. Here, $\theta_t$ represents time fixed effects (captured by &lt;code>year&lt;/code> in the regression), $\eta_i$ represents unit fixed effects (captured by &lt;code>id&lt;/code>), $D_{it}$ is the treatment indicator (&lt;code>post&lt;/code>), and $v_{it}$ are idiosyncratic unobservables.&lt;/p>
&lt;p>However, this equivalence breaks down when there are &lt;strong>multiple time periods&lt;/strong> and &lt;strong>variation in treatment timing&lt;/strong>. In our application, states raised their minimum wages at different times (2004 and 2006), creating a staggered treatment adoption design.&lt;/p>
&lt;p>The TWFE regression implicitly makes two types of comparisons: (1) &amp;ldquo;good comparisons&amp;rdquo; that compare treated groups to not-yet-treated groups, and (2) &amp;ldquo;bad comparisons&amp;rdquo; (sometimes called &amp;ldquo;forbidden comparisons&amp;rdquo;) that use already-treated groups as the comparison group. To see why this is problematic, imagine grading a student&amp;rsquo;s improvement by comparing them to classmates who already took the test last week &amp;mdash; those &amp;ldquo;comparison&amp;rdquo; students are themselves affected by the test, so they no longer represent a valid counterfactual. Similarly, already-treated units may themselves be experiencing treatment effects, contaminating the estimate.&lt;/p>
&lt;p>Moreover, under treatment effect heterogeneity, the TWFE coefficient $\alpha$ is a weighted average of underlying group-time treatment effects, and some of these weights can be &lt;strong>negative&lt;/strong>. It is as if you tried to compute an average score but accidentally gave some students a negative weight &amp;mdash; their positive performance would drag the average down. This means TWFE could, in principle, produce a negative estimate even when all true treatment effects are positive.&lt;/p>
&lt;h3 id="42-twfe-regression">4.2 TWFE Regression&lt;/h3>
&lt;p>Let us start with the traditional TWFE approach to establish a baseline estimate.&lt;/p>
&lt;pre>&lt;code class="language-r">twfe_res &amp;lt;- fixest::feols(lemp ~ post | id + year,
data = data2,
cluster = &amp;quot;id&amp;quot;)
summary(twfe_res)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">OLS estimation, Dep. Var.: lemp
Observations: 8,725
Fixed-effects: id: 1,745, year: 5
Standard-errors: Clustered (id)
Estimate Std. Error t value Pr(&amp;gt;|t|)
post -0.03812 0.008489 -4.49036 7.5762e-06 ***
---
RMSE: 0.116264 Adj. R2: 0.9926
Within R2: 0.003711
&lt;/code>&lt;/pre>
&lt;p>The TWFE regression estimates that minimum wage increases reduced log teen employment by 0.038 (SE = 0.008), which is statistically significant. Interpreted naively, this suggests that states raising their minimum wage experienced a 3.8% decline in teen employment relative to states that did not. However, this single coefficient attempts to summarize the entire treatment effect across two different treatment groups, multiple post-treatment periods, and varying lengths of exposure &amp;mdash; a task that, as we will show, is not well-served by TWFE under treatment effect heterogeneity.&lt;/p>
&lt;p>&lt;img src="r_did_01_twfe_event_study.png" alt="TWFE Event Study based on the Sun-Abraham interaction-weighted estimator.">&lt;/p>
&lt;p>The TWFE event study above uses &lt;code>fixest::sunab()&lt;/code> to estimate dynamic treatment effects within the TWFE framework. The coefficients suggest a small pre-trend violation at event time $-3$ and increasingly negative post-treatment effects. While the Sun-Abraham correction improves upon the standard TWFE event study by addressing some of the weighting issues, we will see that the Callaway-Sant&amp;rsquo;Anna approach provides a more principled decomposition of the treatment effect.&lt;/p>
&lt;h2 id="5-group-time-att-the-callaway-santanna-approach">5. Group-Time ATT: The Callaway-Sant&amp;rsquo;Anna Approach&lt;/h2>
&lt;h3 id="51-estimating-attgt">5.1 Estimating ATT(g,t)&lt;/h3>
&lt;p>The Callaway and Sant&amp;rsquo;Anna (2021) framework addresses the limitations of TWFE by working with &lt;strong>group-time average treatment effects&lt;/strong>:&lt;/p>
&lt;p>$$ATT(g,t) = E[Y_t(g) - Y_t(0) \mid G = g]$$&lt;/p>
&lt;p>where $Y_t(g)$ is the potential outcome at time $t$ if first treated in period $g$, $Y_t(0)$ is the untreated potential outcome, and $G = g$ identifies units in treatment group $g$. In words, $ATT(g,t)$ is the average treatment effect for units first treated in period $g$, measured at time $t$. These building-block parameters are identified under the parallel trends assumption using clean comparisons: each treated group is compared only to units that are never treated (or not yet treated), avoiding the forbidden comparisons that plague TWFE.&lt;/p>
&lt;pre>&lt;code class="language-r">attgt &amp;lt;- did::att_gt(yname = &amp;quot;lemp&amp;quot;,
idname = &amp;quot;id&amp;quot;,
gname = &amp;quot;G&amp;quot;,
tname = &amp;quot;year&amp;quot;,
data = data2,
control_group = &amp;quot;nevertreated&amp;quot;,
base_period = &amp;quot;universal&amp;quot;)
tidy(attgt)[, 1:5]
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> term group time estimate std.error
ATT(2004,2003) 2004 2003 0.00000000 NA
ATT(2004,2004) 2004 2004 -0.03266653 0.02149279
ATT(2004,2005) 2004 2005 -0.06827991 0.02098524
ATT(2004,2006) 2004 2006 -0.12335404 0.02089502
ATT(2004,2007) 2004 2007 -0.13109136 0.02326712
ATT(2006,2003) 2006 2003 -0.03408910 0.01165128
ATT(2006,2004) 2006 2004 -0.01669977 0.00817406
ATT(2006,2005) 2006 2005 0.00000000 NA
ATT(2006,2006) 2006 2006 -0.01939335 0.00892409
ATT(2006,2007) 2006 2007 -0.06607568 0.00965073
&lt;/code>&lt;/pre>
&lt;p>The &lt;code>att_gt()&lt;/code> function estimates each $ATT(g,t)$ separately. For the G=2004 group, the treatment effect grows over time: $-0.033$ on impact (2004), $-0.068$ one year later (2005), $-0.123$ two years later (2006), and $-0.131$ three years later (2007). This pattern suggests &lt;strong>treatment effect dynamics&lt;/strong> &amp;mdash; the negative employment effect of minimum wage increases deepens with longer exposure. For the G=2006 group, the on-impact effect is smaller ($-0.019$) and grows to $-0.066$ after one year. The pre-treatment estimates for G=2006 show a concerning value of $-0.034$ at event time $-3$ (year 2003), suggesting a possible violation of the parallel trends assumption for this group &amp;mdash; a point we will revisit in the sensitivity analysis.&lt;/p>
&lt;p>&lt;img src="r_did_02_attgt.png" alt="Group-time average treatment effects for each treatment cohort, estimated with the Callaway-Sant&amp;rsquo;Anna method.">&lt;/p>
&lt;h3 id="52-aggregation-overall-att-and-event-study">5.2 Aggregation: Overall ATT and Event Study&lt;/h3>
&lt;p>Group-time ATTs are informative but numerous. The &lt;code>aggte()&lt;/code> function aggregates them into summary parameters. The &lt;strong>overall ATT&lt;/strong> weights each $ATT(g,t)$ by the group size and the number of post-treatment periods:&lt;/p>
&lt;pre>&lt;code class="language-r">attO &amp;lt;- did::aggte(attgt, type = &amp;quot;group&amp;quot;)
summary(attO)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Overall summary of ATT's based on group/cohort aggregation:
ATT Std. Error [ 95% Conf. Int.]
-0.0571 0.008 -0.0727 -0.0415 *
Group Effects:
Group Estimate Std. Error [95% Simult. Conf. Band]
2004 -0.0888 0.0197 -0.1309 -0.0468 *
2006 -0.0427 0.0083 -0.0604 -0.0251 *
&lt;/code>&lt;/pre>
&lt;p>The overall ATT is $-0.057$ (SE = 0.008), substantially larger in magnitude than the TWFE estimate of $-0.038$. The Callaway-Sant&amp;rsquo;Anna framework reveals that TWFE &lt;strong>understated&lt;/strong> the negative employment effect by about one-third. The group-level results show that the G=2004 group experienced a larger average effect ($-0.089$) than the G=2006 group ($-0.043$), which makes sense because the G=2004 group has been treated for more periods and thus accumulates more treatment effect dynamics.&lt;/p>
&lt;p>The &lt;strong>event study&lt;/strong> aggregation is equally informative:&lt;/p>
&lt;pre>&lt;code class="language-r">attes &amp;lt;- did::aggte(attgt, type = &amp;quot;dynamic&amp;quot;)
summary(attes)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Overall summary of ATT's based on event-study/dynamic aggregation:
ATT Std. Error [ 95% Conf. Int.]
-0.0862 0.0124 -0.1106 -0.0618 *
Dynamic Effects:
Event time Estimate Std. Error [95% Simult. Conf. Band]
-3 -0.0341 0.0119 -0.0623 -0.0059 *
-2 -0.0167 0.0076 -0.0348 0.0014
-1 0.0000 NA NA NA
0 -0.0235 0.0081 -0.0426 -0.0044 *
1 -0.0668 0.0086 -0.0870 -0.0465 *
2 -0.1234 0.0203 -0.1714 -0.0753 *
3 -0.1311 0.0230 -0.1855 -0.0767 *
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="r_did_03_cs_event_study.png" alt="Event study aggregation of group-time ATTs showing the trajectory of treatment effects relative to the treatment year.">&lt;/p>
&lt;p>The event study reveals a clear pattern: the on-impact effect at $e=0$ is $-0.024$, growing to $-0.067$ at $e=1$, $-0.123$ at $e=2$, and $-0.131$ at $e=3$. The post-treatment effects are all statistically significant and increasingly negative, consistent with the minimum wage having a cumulative negative effect on teen employment over time. However, the pre-trend at $e=-3$ is $-0.034$ and marginally significant, which raises a flag about the validity of the parallel trends assumption. The pre-trend at $e=-2$ is smaller ($-0.017$) and not significant. We will formally assess the robustness of these results to parallel trends violations using HonestDiD in Section 8.&lt;/p>
&lt;h3 id="53-twfe-weight-decomposition">5.3 TWFE Weight Decomposition&lt;/h3>
&lt;p>Why does TWFE produce a different estimate than Callaway-Sant&amp;rsquo;Anna? Both the TWFE coefficient and the overall $ATT^O$ can be written as weighted averages of the same underlying $ATT(g,t)$ values:&lt;/p>
&lt;p>$$ATT^O = \sum_{g,t} w^O(g,t) \cdot ATT(g,t)$$&lt;/p>
&lt;p>The difference lies in the weights. The proper $ATT^O$ weights reflect group size and number of post-treatment periods, while the TWFE weights are driven by the estimation method and can assign nonzero weight to pre-treatment periods or even negative weight to some post-treatment cells. The &lt;code>twfeweights&lt;/code> package makes these weights explicit.&lt;/p>
&lt;pre>&lt;code class="language-r">tw_obj &amp;lt;- twfeweights::twfe_weights(attgt)
tw &amp;lt;- tw_obj$weights_df
wO_obj &amp;lt;- twfeweights::attO_weights(attgt)
wO &amp;lt;- wO_obj$weights_df
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">TWFE estimate from weights: -0.0381
ATT^O estimate from weights: -0.0571
TWFE post-treatment component: -0.0503
Pre-treatment contamination: 0.0122
Total TWFE bias: 0.019
Fraction of bias from pre-treatment: 0.6422
Fraction of bias from post-treatment weighting: 0.3578
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="r_did_04_twfe_weights.png" alt="TWFE weight scatter plot showing how each group-time ATT is weighted. Circles are TWFE weights; teal diamonds are the proper ATT-O weights for post-treatment cells.">&lt;/p>
&lt;p>The weight decomposition is revealing. The TWFE estimate ($-0.038$) differs from the proper overall ATT ($-0.057$) by a total bias of $0.019$ &amp;mdash; meaning TWFE attenuates the negative employment effect toward zero. Of this bias, &lt;strong>64.2%&lt;/strong> comes from pre-treatment contamination: the TWFE regression assigns nonzero weights to pre-treatment $ATT(g,t)$ values, which should receive zero weight in any proper treatment effect parameter. The remaining &lt;strong>35.8%&lt;/strong> of the bias comes from TWFE assigning different post-treatment weights than the proper $ATT^O$ weights. The figure shows this visually: the orange pre-treatment dots receive nonzero TWFE weights (horizontal position), and the post-treatment TWFE weights (blue circles) differ systematically from the proper $ATT^O$ weights (teal diamonds).&lt;/p>
&lt;h2 id="6-relaxing-parallel-trends">6. Relaxing Parallel Trends&lt;/h2>
&lt;h3 id="61-conditional-parallel-trends-with-covariates">6.1 Conditional Parallel Trends with Covariates&lt;/h3>
&lt;p>The unconditional parallel trends assumption may be too strong if treatment and comparison groups differ on observable characteristics that affect outcome trends. For example, states that raised their minimum wages may have larger populations or higher average pay levels, and these characteristics could correlate with employment trends even absent the minimum wage change. &lt;strong>Conditional parallel trends&lt;/strong> weakens the assumption: trends need only be parallel after conditioning on covariates. The &lt;code>did&lt;/code> package offers three estimation methods for this setting. Regression adjustment models the outcome as a function of covariates; inverse probability weighting (IPW) reweights the comparison group to match the treated group&amp;rsquo;s covariate distribution; and the &lt;strong>doubly robust&lt;/strong> (DR) estimator combines both approaches, remaining consistent if either the outcome model or the propensity score model is correctly specified &amp;mdash; like wearing both a belt and suspenders.&lt;/p>
&lt;pre>&lt;code class="language-r"># Regression adjustment
cs_reg &amp;lt;- att_gt(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
xformla = ~lpop + lavg_pay,
control_group = &amp;quot;nevertreated&amp;quot;, base_period = &amp;quot;universal&amp;quot;,
est_method = &amp;quot;reg&amp;quot;, data = data2)
attO_reg &amp;lt;- aggte(cs_reg, type = &amp;quot;group&amp;quot;)
# Inverse probability weighting
cs_ipw &amp;lt;- att_gt(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
xformla = ~lpop + lavg_pay,
control_group = &amp;quot;nevertreated&amp;quot;, base_period = &amp;quot;universal&amp;quot;,
est_method = &amp;quot;ipw&amp;quot;, data = data2)
attO_ipw &amp;lt;- aggte(cs_ipw, type = &amp;quot;group&amp;quot;)
# Doubly robust
cs_dr &amp;lt;- att_gt(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
xformla = ~lpop + lavg_pay,
control_group = &amp;quot;nevertreated&amp;quot;, base_period = &amp;quot;universal&amp;quot;,
est_method = &amp;quot;dr&amp;quot;, data = data2)
attO_dr &amp;lt;- aggte(cs_dr, type = &amp;quot;group&amp;quot;)
&lt;/code>&lt;/pre>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Overall ATT&lt;/th>
&lt;th>SE&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Unconditional&lt;/td>
&lt;td>$-0.057$&lt;/td>
&lt;td>0.008&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Regression adj.&lt;/td>
&lt;td>$-0.064$&lt;/td>
&lt;td>0.008&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IPW&lt;/td>
&lt;td>$-0.065$&lt;/td>
&lt;td>0.008&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Doubly robust&lt;/td>
&lt;td>$-0.065$&lt;/td>
&lt;td>0.008&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Controlling for log population and log average pay increases the estimated negative employment effect from $-0.057$ to approximately $-0.065$ across all three conditional methods. The three estimation methods produce nearly identical estimates, which is reassuring. The fact that all three methods agree suggests that covariate adjustment is not introducing model-dependence artifacts.&lt;/p>
&lt;p>&lt;img src="r_did_05_dr_event_study.png" alt="Event study from the doubly robust estimator conditioning on log population and log average pay.">&lt;/p>
&lt;p>The doubly robust event study shows the same qualitative pattern as the unconditional analysis: near-zero pre-trends (the pre-trend at $e=-3$ shrinks from $-0.034$ to $-0.022$ and is no longer significant) and increasingly negative post-treatment effects ($-0.027$ at $e=0$, $-0.077$ at $e=1$, $-0.135$ at $e=2$, $-0.147$ at $e=3$). The improved pre-trend behavior after conditioning on covariates suggests that some of the apparent pre-trend violations in the unconditional analysis were driven by differences in county characteristics between treatment and comparison groups.&lt;/p>
&lt;h3 id="62-robustness-base-period-comparison-group-and-anticipation">6.2 Robustness: Base Period, Comparison Group, and Anticipation&lt;/h3>
&lt;p>The Callaway-Sant&amp;rsquo;Anna framework allows the researcher to make several important choices. We now check that our results are robust to these choices.&lt;/p>
&lt;p>&lt;strong>Varying base period:&lt;/strong> Instead of comparing all pre-treatment and post-treatment periods to a single universal base period ($t = g-1$), we can use a varying base period that compares each period $t$ to period $t-1$.&lt;/p>
&lt;pre>&lt;code class="language-r">cs_varying &amp;lt;- att_gt(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
xformla = ~lpop + lavg_pay,
control_group = &amp;quot;nevertreated&amp;quot;, base_period = &amp;quot;varying&amp;quot;,
est_method = &amp;quot;dr&amp;quot;, data = data2)
attO_varying &amp;lt;- aggte(cs_varying, type = &amp;quot;group&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Varying base period ATT^O: -0.0646 (SE: 0.0081)
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Not-yet-treated comparison group:&lt;/strong> Instead of using only the never-treated group as the comparison, we can also include units that are not yet treated at time $t$.&lt;/p>
&lt;pre>&lt;code class="language-r">cs_nyt &amp;lt;- att_gt(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
xformla = ~lpop + lavg_pay,
control_group = &amp;quot;notyettreated&amp;quot;, base_period = &amp;quot;universal&amp;quot;,
est_method = &amp;quot;dr&amp;quot;, data = data2)
attO_nyt &amp;lt;- aggte(cs_nyt, type = &amp;quot;group&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Not-yet-treated ATT^O: -0.0649 (SE: 0.008)
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Anticipation:&lt;/strong> If states announced their minimum wage increases before they took effect, workers and firms might adjust their behavior in anticipation. We allow for one period of anticipation by setting &lt;code>anticipation = 1&lt;/code>.&lt;/p>
&lt;pre>&lt;code class="language-r">cs_antic &amp;lt;- att_gt(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
xformla = ~lpop + lavg_pay,
control_group = &amp;quot;nevertreated&amp;quot;, base_period = &amp;quot;universal&amp;quot;,
est_method = &amp;quot;dr&amp;quot;, anticipation = 1, data = data2)
attO_antic &amp;lt;- aggte(cs_antic, type = &amp;quot;group&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">With anticipation (1 period) ATT^O: -0.0396 (SE: 0.0098)
&lt;/code>&lt;/pre>
&lt;p>The results are reassuringly stable across specifications. Switching to a varying base period ($-0.065$) or using the not-yet-treated comparison group ($-0.065$) produces virtually identical estimates to our baseline doubly robust result ($-0.065$). Allowing for one period of anticipation reduces the estimated ATT to $-0.040$ (SE = 0.010), which makes sense &amp;mdash; if some of the treatment effect occurs before the official implementation date, excluding that period from post-treatment narrows the estimated effect. The consistency across the first three specifications gives us confidence that the main findings are not driven by specific methodological choices.&lt;/p>
&lt;h2 id="7-sensitivity-analysis-when-parallel-trends-may-fail">7. Sensitivity Analysis: When Parallel Trends May Fail&lt;/h2>
&lt;p>Even after conditioning on covariates, the parallel trends assumption is not directly testable &amp;mdash; pre-trends being close to zero is necessary but not sufficient for parallel trends to hold in post-treatment periods. The &lt;strong>HonestDiD&lt;/strong> approach of Rambachan and Roth (2023) provides a principled sensitivity analysis: it asks how large violations of parallel trends can be before the post-treatment results break down. The &amp;ldquo;relative magnitude&amp;rdquo; variant compares the size of potential post-treatment violations to the observed size of pre-treatment deviations from parallel trends.&lt;/p>
&lt;p>The &lt;code>HonestDiD&lt;/code> package requires a small helper function to interface with the &lt;code>did&lt;/code> package&amp;rsquo;s event study objects. This helper (available in the companion R script and in &lt;a href="https://github.com/bcallaway11/did_chapter" target="_blank" rel="noopener">Callaway&amp;rsquo;s workshop materials&lt;/a>) extracts the influence function (a statistical tool for computing standard errors in complex estimators) and variance-covariance matrix from the event study, then passes them to &lt;code>HonestDiD&lt;/code>&amp;rsquo;s sensitivity routines. The parameter $\bar{M}$ bounds the ratio of the maximum post-treatment deviation from parallel trends to the maximum pre-treatment deviation &amp;mdash; in other words, it is a stress test asking &amp;ldquo;how much worse can things get after treatment compared to what we already see before treatment?&amp;rdquo;&lt;/p>
&lt;pre>&lt;code class="language-r"># Helper function from Callaway's workshop (references/honest_did.R)
# Bridges the did package's AGGTEobj to HonestDiD's sensitivity functions
source(&amp;quot;references/honest_did.R&amp;quot;)
attgt_hd &amp;lt;- did::att_gt(yname = &amp;quot;lemp&amp;quot;, idname = &amp;quot;id&amp;quot;, gname = &amp;quot;G&amp;quot;,
tname = &amp;quot;year&amp;quot;, data = data2,
control_group = &amp;quot;nevertreated&amp;quot;,
base_period = &amp;quot;universal&amp;quot;)
cs_es_hd &amp;lt;- aggte(attgt_hd, type = &amp;quot;dynamic&amp;quot;)
hd_rm &amp;lt;- honest_did(es = cs_es_hd, e = 0, type = &amp;quot;relative_magnitude&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Original CI: [-0.0404, -0.0066]
Robust CIs:
lb ub Mbar
-0.0401 -0.00871 0.000
-0.0435 -0.00523 0.222
-0.0470 -0.00174 0.444
-0.0505 0.00523 0.667
-0.0575 0.01220 0.889
-0.0644 0.01920 1.111
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="r_did_06_honestdid.png" alt="HonestDiD sensitivity analysis showing how the confidence interval for the on-impact effect widens as the allowed magnitude of parallel trends violations increases.">&lt;/p>
&lt;p>The sensitivity analysis reveals that the on-impact effect ($e=0$) is robust to moderate violations of parallel trends, but not to large ones. The original 95% confidence interval is $[-0.040, -0.007]$, comfortably below zero. As $\bar{M}$ increases &amp;mdash; meaning we allow post-treatment violations of parallel trends to be larger relative to pre-treatment violations &amp;mdash; the confidence interval widens. The &lt;strong>breakdown point&lt;/strong> is at $\bar{M} \approx 0.67$: if post-treatment violations are no more than about 67% as large as the pre-treatment deviations from parallel trends, the negative employment effect remains statistically significant. Beyond that threshold, the confidence interval includes zero and we can no longer rule out a null effect. Given the moderate pre-trend violations we observed (especially at $e=-3$), this suggests that the results should be interpreted with some caution &amp;mdash; the evidence is suggestive of a negative employment effect, but it is not bulletproof.&lt;/p>
&lt;h2 id="8-more-complicated-treatment-regimes">8. More Complicated Treatment Regimes&lt;/h2>
&lt;h3 id="81-heterogeneous-treatment-doses">8.1 Heterogeneous Treatment Doses&lt;/h3>
&lt;p>So far, we have treated all minimum wage increases as a binary &amp;ldquo;treated or not&amp;rdquo; event. But states raised their minimum wages by very different amounts &amp;mdash; some by as little as \$0.10 above the federal floor, others by over \$1.00. A \$0.25 increase and a \$1.70 increase should not be expected to have the same employment effect. To account for this, we can normalize the treatment effect by the size of the minimum wage increase, computing an &lt;strong>ATT per dollar&lt;/strong>.&lt;/p>
&lt;pre>&lt;code class="language-r"># Use full data including G=2007 for more treated states
data3 &amp;lt;- subset(mw_data_ch2, year &amp;gt;= 2003)
treated_state_list &amp;lt;- unique(subset(data3, G != 0)$state_name)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="r_did_07_state_mw.png" alt="Minimum wage trajectories showing the heterogeneous timing and magnitude of state minimum wage increases above the federal floor.">&lt;/p>
&lt;p>The figure reveals substantial variation across states. Illinois raised its minimum wage early (2004) and by a relatively large amount, while Florida and Colorado made smaller increases later. This heterogeneity in treatment dose motivates the per-dollar normalization.&lt;/p>
&lt;h3 id="82-att-per-dollar-event-study">8.2 ATT Per Dollar Event Study&lt;/h3>
&lt;p>We compute state-specific ATTs using the doubly robust panel DID estimator from the &lt;code>DRDID&lt;/code> package, then divide each by the size of the minimum wage increase above the federal level.&lt;/p>
&lt;pre>&lt;code class="language-r"># For each treated state and post-treatment period, compute ATT
# using the doubly robust panel estimator, then normalize by dose
for (state in treated_state_list) {
g &amp;lt;- unique(subset(data3, state_name == state)$G)
for (period in 2004:2007) {
Y1 &amp;lt;- c(subset(data3, state_name == state &amp;amp; year == period)$lemp,
subset(data3, G == 0 &amp;amp; year == period)$lemp)
Y0 &amp;lt;- c(subset(data3, state_name == state &amp;amp; year == g - 1)$lemp,
subset(data3, G == 0 &amp;amp; year == g - 1)$lemp)
D &amp;lt;- c(rep(1, sum(data3$state_name == state &amp;amp; data3$year == period)),
rep(0, sum(data3$G == 0 &amp;amp; data3$year == period)))
attst &amp;lt;- DRDID::drdid_panel(Y1, Y0, D, covariates = NULL)
treat_amount &amp;lt;- unique(subset(data3, state_name == state &amp;amp;
year == period)$state_mw) - 5.15
att_per_dollar &amp;lt;- attst$ATT / treat_amount
}
}
# Note: this is a simplified excerpt. See analysis.R for the full
# implementation with result storage, event study aggregation, and plots.
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Overall ATT per dollar: -0.0297 (SE: 0.0155)
Event study ATT per dollar:
event_time att se ci_lower ci_upper
0 -0.028 0.020 -0.066 0.010
1 -0.055 0.012 -0.079 -0.031
2 -0.091 0.015 -0.120 -0.062
3 -0.097 0.017 -0.130 -0.064
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="r_did_08_att_per_dollar.png" alt="Event study of treatment effects normalized by the dollar amount of the minimum wage increase, showing the employment response per dollar of additional minimum wage.">&lt;/p>
&lt;p>The dose-normalized results tell a consistent story. The on-impact effect per dollar is $-0.028$ (not quite significant at the 5% level), but the effect grows substantially with exposure: $-0.055$ after one year, $-0.091$ after two years, and $-0.097$ after three years. These per-dollar estimates imply that a \$1 increase in the minimum wage is associated with a decline of 0.055 log points in teen employment after one year (approximately 5.3%) and 0.097 log points after three years (approximately 9.2%). The post-treatment estimates from $e=1$ onward are all statistically significant. The overall ATT per dollar of $-0.030$ (SE = 0.016) averages across all post-treatment periods, but the event study makes clear that the cumulative effects are substantially larger.&lt;/p>
&lt;h2 id="9-alternative-identification-strategies">9. Alternative Identification Strategies&lt;/h2>
&lt;p>The DID framework relies on the parallel trends assumption. Alternative identification strategies relax this assumption in different ways. The &lt;code>pte&lt;/code> package implements a &lt;strong>lagged outcomes&lt;/strong> strategy, which conditions on lagged outcome values rather than assuming parallel trends. Instead of assuming that treated and untreated groups would have followed the same trend, this approach assumes that controlling for the previous period&amp;rsquo;s outcome level makes treatment assignment as good as random &amp;mdash; counties with the same employment level last year are equally likely to be in a state that raised its minimum wage, regardless of which state they are in.&lt;/p>
&lt;pre>&lt;code class="language-r">library(pte)
data2_lo &amp;lt;- data2
data2_lo$G2 &amp;lt;- data2_lo$G
lo_res &amp;lt;- pte::pte_default(yname = &amp;quot;lemp&amp;quot;, tname = &amp;quot;year&amp;quot;, idname = &amp;quot;id&amp;quot;,
gname = &amp;quot;G2&amp;quot;, data = data2_lo,
d_outcome = FALSE, lagged_outcome_cov = TRUE)
summary(lo_res)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Overall ATT: -0.061 (SE: 0.008, 95% CI: [-0.077, -0.045])
Dynamic Effects:
Event Time Estimate Std. Error [95% Conf. Band]
-2 0.014 0.008 -0.010 0.038
-1 0.010 0.007 -0.009 0.030
0 -0.024 0.009 -0.049 0.000
1 -0.074 0.008 -0.097 -0.050 *
2 -0.129 0.019 -0.185 -0.073 *
3 -0.140 0.023 -0.206 -0.074 *
&lt;/code>&lt;/pre>
&lt;p>The lagged outcomes strategy produces an overall ATT of $-0.061$ (SE = 0.008), very close to the DID estimates with covariates ($-0.065$). The pre-trends under this alternative identification strategy are close to zero (0.014 at $e=-2$ and 0.010 at $e=-1$, both insignificant), and the post-treatment trajectory ($-0.024$ on impact, $-0.074$ at $e=1$, $-0.129$ at $e=2$, $-0.140$ at $e=3$) closely mirrors the DID event study. The convergence of results across different identification strategies strengthens the case that the estimated negative employment effects are reflecting a genuine causal relationship rather than an artifact of any particular set of assumptions.&lt;/p>
&lt;h2 id="10-discussion-and-takeaways">10. Discussion and Takeaways&lt;/h2>
&lt;p>This tutorial demonstrates why &lt;strong>TWFE regressions are unreliable&lt;/strong> with staggered treatment adoption and treatment effect heterogeneity, and how modern DID methods provide a principled alternative. The TWFE coefficient of $-0.038$ understates the true overall ATT of $-0.057$ by about one-third, with the bias driven primarily by pre-treatment contamination (64% of the total bias) and improper post-treatment weighting (36%). The Callaway-Sant&amp;rsquo;Anna framework cleanly separates identification from estimation by first computing group-time ATTs and then aggregating them into target parameters of interest.&lt;/p>
&lt;p>The substantive findings suggest that state-level minimum wage increases above the federal floor reduced teen employment, with effects that grew over time. The doubly robust estimator with covariates yields an overall ATT of $-0.065$ (SE = 0.008), and the dose-normalized analysis finds effects of approximately $-0.055$ per dollar after one year and $-0.097$ per dollar after three years. These results are robust across estimation methods (regression adjustment, IPW, doubly robust), comparison group definitions (never-treated, not-yet-treated), and base period choices (universal, varying).&lt;/p>
&lt;p>However, the results come with important caveats. The HonestDiD sensitivity analysis shows that the on-impact effect loses statistical significance when post-treatment parallel trends violations exceed about 67% of the pre-treatment deviations. The pre-treatment coefficient at $e=-3$ is moderately significant in the unconditional analysis, though it shrinks after covariate adjustment. These patterns suggest that while the evidence points toward negative employment effects, the magnitude should be interpreted with some caution. As Callaway (2022) notes, this application is primarily intended to illustrate the methodology rather than to settle the minimum wage debate.&lt;/p>
&lt;p>The modern DID toolkit demonstrated here &amp;mdash; &lt;code>did&lt;/code> for group-time ATTs, &lt;code>twfeweights&lt;/code> for diagnosing TWFE problems, &lt;code>HonestDiD&lt;/code> for sensitivity analysis, and &lt;code>DRDID&lt;/code> for doubly robust estimation &amp;mdash; provides applied researchers with a complete workflow for credible causal inference in staggered treatment settings. The key lesson is that DID is not just a regression &amp;mdash; it is an identification strategy that requires careful attention to the structure of the treatment, the comparison group, and the plausibility of the underlying assumptions.&lt;/p>
&lt;p>&lt;strong>Key takeaways:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>TWFE understates the true ATT by ~33% ($-0.038$ vs $-0.057$), with 64% of the bias from pre-treatment contamination and 36% from improper post-treatment weighting&lt;/li>
&lt;li>The doubly robust ATT of $-0.065$ is stable across estimation methods (regression, IPW, DR), comparison groups (never-treated, not-yet-treated), and base periods (universal, varying)&lt;/li>
&lt;li>Employment effects accumulate over time: $-0.027$ on impact, growing to $-0.147$ after three years under the doubly robust specification&lt;/li>
&lt;li>The on-impact effect is robust to parallel trends violations up to 67% of pre-trend magnitude ($\bar{M} \approx 0.67$), but not beyond&lt;/li>
&lt;li>Per-dollar normalization reveals that a \$1 minimum wage increase reduces teen employment by approximately 5.3% after one year and 9.2% after three years&lt;/li>
&lt;/ol>
&lt;h2 id="11-exercises">11. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Expand the sample:&lt;/strong> Re-run the analysis using &lt;code>data3&lt;/code> (which includes the G=2007 group) and compare the results. Does including the additional treatment group change the overall ATT or the event study pattern?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Alternative covariates:&lt;/strong> Experiment with different covariate specifications in the doubly robust estimator. What happens if you include only &lt;code>lpop&lt;/code>? Only &lt;code>lavg_pay&lt;/code>? Does the choice of covariates meaningfully affect the pre-trends?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Smoothness sensitivity:&lt;/strong> Run the HonestDiD smoothness-based sensitivity analysis (&lt;code>type = &amp;quot;smoothness&amp;quot;&lt;/code>) in addition to the relative magnitude analysis. How do the two approaches compare in terms of the robustness of the results?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="12-references">12. References&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>Callaway, B. (2022). Difference-in-Differences for Policy Evaluation. In &lt;em>Handbook of Labor, Human Resources, and Population Economics&lt;/em>. Springer. &lt;a href="https://link.springer.com/referenceworkentry/10.1007/978-3-319-57365-6_352-1" target="_blank" rel="noopener">Published version&lt;/a> | &lt;a href="https://bcallaway11.github.io/files/Callaway-Chapter-2022/main.pdf" target="_blank" rel="noopener">Working paper&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Callaway, B. and Sant&amp;rsquo;Anna, P.H.C. (2021). Difference-in-Differences with Multiple Time Periods. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 200&amp;ndash;230. &lt;a href="https://doi.org/10.1016/j.jeconom.2020.12.001" target="_blank" rel="noopener">doi:10.1016/j.jeconom.2020.12.001&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Goodman-Bacon, A. (2021). Difference-in-differences with variation in treatment timing. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 254&amp;ndash;277. &lt;a href="https://doi.org/10.1016/j.jeconom.2021.03.014" target="_blank" rel="noopener">doi:10.1016/j.jeconom.2021.03.014&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Rambachan, A. and Roth, J. (2023). A More Credible Approach to Parallel Trends. &lt;em>Review of Economic Studies&lt;/em>, 90(5), 2555&amp;ndash;2591. &lt;a href="https://doi.org/10.1093/restud/rdad018" target="_blank" rel="noopener">doi:10.1093/restud/rdad018&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>de Chaisemartin, C. and D&amp;rsquo;Haultfoeuille, X. (2020). Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects. &lt;em>American Economic Review&lt;/em>, 110(9), 2964&amp;ndash;2996.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Sun, L. and Abraham, S. (2021). Estimating dynamic treatment effects in event studies with heterogeneous treatment effects. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 175&amp;ndash;199.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>did&lt;/code> package: &lt;a href="https://cran.r-project.org/package=did" target="_blank" rel="noopener">CRAN&lt;/a> | &lt;a href="https://github.com/bcallaway11/did" target="_blank" rel="noopener">GitHub&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>fixest&lt;/code> package: &lt;a href="https://cran.r-project.org/package=fixest" target="_blank" rel="noopener">CRAN&lt;/a> | &lt;a href="https://lrberge.github.io/fixest/" target="_blank" rel="noopener">Documentation&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>twfeweights&lt;/code> package: &lt;a href="https://github.com/bcallaway11/twfeweights" target="_blank" rel="noopener">GitHub&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>HonestDiD&lt;/code> package: &lt;a href="https://cran.r-project.org/package=HonestDiD" target="_blank" rel="noopener">CRAN&lt;/a> | &lt;a href="https://github.com/asheshrambachan/HonestDiD" target="_blank" rel="noopener">GitHub&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h4 id="acknowledgements">Acknowledgements&lt;/h4>
&lt;p>AI tools (Claude Code, Gemini, NotebookLM) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Sensitivity Analysis for Parallel Trends in Difference-in-Differences Using honestdid in Stata</title><link>https://carlos-mendez.org/post/stata_honestdid/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/stata_honestdid/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Difference-in-differences (DiD) is one of the most widely used methods for estimating causal effects in the social sciences. But every DiD estimate rests on a single critical assumption &amp;mdash; &lt;strong>parallel trends&lt;/strong> &amp;mdash; and that assumption is fundamentally untestable. With only two periods of data, researchers cannot check whether treated and control groups followed similar trends before treatment. With multiple periods, researchers can run a pre-trends test, but as Roth (2022) demonstrated, these tests have low statistical power and can create a false sense of security.&lt;/p>
&lt;p>So what can researchers do? The &lt;code>honestdid&lt;/code> package, developed by Rambachan and Roth (2023), provides a formal &lt;strong>sensitivity analysis&lt;/strong> framework. Instead of asking the binary question &amp;ldquo;Do parallel trends hold?&amp;rdquo; it asks a more useful question: &amp;ldquo;How large would violations of parallel trends need to be before my conclusion changes?&amp;rdquo; The answer &amp;mdash; called the &lt;strong>breakdown value&lt;/strong> &amp;mdash; is a single number that tells the reader exactly how robust the result is.&lt;/p>
&lt;p>This tutorial teaches the method in two self-contained parts. &lt;strong>Part 1&lt;/strong> starts with the simplest possible DiD &amp;mdash; two groups, two periods &amp;mdash; where parallel trends cannot be tested at all. We show how &lt;code>honestdid&lt;/code> can still provide meaningful robustness analysis in this limited-data setting. &lt;strong>Part 2&lt;/strong> extends to a multi-period event study, where we have more pre-treatment data and can deploy the full toolkit, including both relative magnitudes and smoothness restrictions. Throughout, we use data from the Affordable Care Act&amp;rsquo;s Medicaid expansion to study the effect of expanding health insurance eligibility on insurance coverage.&lt;/p>
&lt;h3 id="learning-objectives">Learning objectives&lt;/h3>
&lt;ul>
&lt;li>Construct a simple 2x2 difference-in-differences estimate and understand the parallel trends assumption&lt;/li>
&lt;li>Recognize that parallel trends &lt;strong>cannot be tested&lt;/strong> with only two periods of data&lt;/li>
&lt;li>Apply &lt;code>honestdid&lt;/code> with relative magnitudes (DeltaRM) to assess robustness even in the 2x2 case&lt;/li>
&lt;li>Interpret breakdown values as a quantitative measure of how robust a DiD result is&lt;/li>
&lt;li>Estimate a multi-period event study and run a conventional pre-trends test&lt;/li>
&lt;li>Explain why pre-trends tests have low power and can mislead researchers&lt;/li>
&lt;li>Apply both DeltaRM and smoothness restrictions (DeltaSD) to multi-period DiD&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="2-study-context-----medicaid-expansion">2. Study context &amp;mdash; Medicaid expansion&lt;/h2>
&lt;p>The Affordable Care Act (ACA) gave US states the option to expand Medicaid eligibility to low-income adults. Some states expanded in 2014, while others chose not to expand at all. This creates a natural quasi-experiment: states that expanded serve as the &lt;strong>treatment group&lt;/strong>, and states that never expanded serve as the &lt;strong>control group&lt;/strong>. The outcome of interest is the share of the population with health insurance coverage (&lt;code>dins&lt;/code>).&lt;/p>
&lt;p>This is an &lt;strong>observational study&lt;/strong>, not a randomized experiment. States were not randomly assigned to expand Medicaid &amp;mdash; they chose to do so based on political and economic factors. This means that the parallel trends assumption is a genuine concern: states that chose to expand may have been on different insurance coverage trajectories than non-expanders even before 2014.&lt;/p>
&lt;p>Our target estimand is the &lt;strong>average treatment effect on the treated (ATT)&lt;/strong> &amp;mdash; the effect of Medicaid expansion on insurance coverage in the states that expanded. We will use this dataset in two ways: first restricted to a narrow window around the treatment year (Part 1), then with the full panel spanning 2008&amp;ndash;2015 (Part 2).&lt;/p>
&lt;h3 id="variables">Variables&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Type&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>stfips&lt;/code>&lt;/td>
&lt;td>State FIPS code&lt;/td>
&lt;td>Panel ID&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>year&lt;/code>&lt;/td>
&lt;td>Calendar year (2008&amp;ndash;2015)&lt;/td>
&lt;td>Time variable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dins&lt;/code>&lt;/td>
&lt;td>Share of population with health insurance&lt;/td>
&lt;td>Outcome (0&amp;ndash;1)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>yexp2&lt;/code>&lt;/td>
&lt;td>Year of Medicaid expansion (missing if never)&lt;/td>
&lt;td>Treatment timing&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="3-analytical-roadmap">3. Analytical roadmap&lt;/h2>
&lt;p>The diagram below shows how the tutorial progresses. Each part is self-contained, with its own estimation and sensitivity analysis.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;2x2 DiD&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Estimation&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Sensitivity&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Relative Magnitudes&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;Event Study&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Estimation&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;Sensitivity&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;RM + Smoothness&amp;quot;]
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#00d4c8,stroke:#141413,color:#141413
style D fill:#141413,stroke:#d97757,color:#fff
&lt;/code>&lt;/pre>
&lt;p>Part 1 uses a simple before-and-after comparison where parallel trends is untestable. Part 2 leverages the full panel to run richer sensitivity analyses, including smoothness restrictions that require multiple pre-treatment periods.&lt;/p>
&lt;hr>
&lt;h2 id="4-setup-----data-loading-and-packages">4. Setup &amp;mdash; data loading and packages&lt;/h2>
&lt;p>We begin by installing the required packages and loading the Medicaid expansion dataset.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Install required packages
capture ssc install require, replace
capture ssc install ftools, replace
capture ssc install reghdfe, replace
capture ssc install coefplot, replace
capture ssc install drdid, replace
capture ssc install csdid, replace
capture net install honestdid, from(&amp;quot;https://raw.githubusercontent.com/mcaceresb/stata-honestdid/main&amp;quot;) replace
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">(output omitted)
&lt;/code>&lt;/pre>
&lt;p>Now we load the data and examine its structure. The dataset contains state-level panel data on health insurance coverage from 2008 to 2015, with information on when each state expanded Medicaid eligibility.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Load data
use &amp;quot;https://raw.githubusercontent.com/Mixtape-Sessions/Advanced-DID/main/Exercises/Data/ehec_data.dta&amp;quot;, clear
* Examine the data
des
tab year
tab yexp2, m
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Contains data
obs: 552
vars: 5
variable name type format label variable label
stfips byte %8.0g STATEFIP state FIPS code
year int %8.0g YEAR Census/ACS survey year
dins float %9.0g Insurance Rate among low-income
childless adults
yexp2 float %9.0g Year of Medicaid Expansion
W float %9.0g total survey weight
year | Freq.
-----------+----------
2008 | 46
2009 | 46
... | ...
2019 | 46
-----------+----------
Total | 552
yexp2 | Freq.
------------+----------
2014 | 264
2015 | 36
2016 | 24
2017 | 12
2019 | 24
. | 192
------------+----------
Total | 552
&lt;/code>&lt;/pre>
&lt;p>The data contains 552 observations across 46 states and 12 years (2008&amp;ndash;2019). States expanded Medicaid in different years &amp;mdash; 22 in 2014, 3 in 2015, 2 in 2016, 1 in 2017, and 2 in 2019 &amp;mdash; while 16 states never expanded (missing &lt;code>yexp2&lt;/code>). For a clean two-group comparison, we restrict the sample to 2014-expanders and never-expanders, and keep only years 2008&amp;ndash;2015.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Restrict to 2008--2015, keep only 2014 expanders and never-expanders
keep if (year &amp;lt;= 2015) &amp;amp; (missing(yexp2) | (yexp2 == 2014))
* Create treatment indicator
gen byte D = (yexp2 == 2014)
* Verify sample
tab D
tab year
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> D | Freq.
------------+----------
0 | 128
1 | 176
------------+----------
Total | 304
year | Freq.
------------+----------
2008 | 38
2009 | 38
2010 | 38
2011 | 38
2012 | 38
2013 | 38
2014 | 38
2015 | 38
------------+----------
Total | 304
&lt;/code>&lt;/pre>
&lt;p>Our analysis sample contains 38 states observed across 8 years (2008&amp;ndash;2015): 22 treatment states that expanded Medicaid in 2014 and 16 control states that never expanded. This balanced panel provides the foundation for both parts of the tutorial.&lt;/p>
&lt;hr>
&lt;h1 id="part-1-simple-2x2-difference-in-differences">Part 1: Simple 2x2 Difference-in-Differences&lt;/h1>
&lt;h2 id="5-the-2x2-did-----concept-and-estimation">5. The 2x2 DiD &amp;mdash; concept and estimation&lt;/h2>
&lt;h3 id="51-collapsing-to-two-periods">5.1 Collapsing to two periods&lt;/h3>
&lt;p>The 2x2 DiD is the simplest version of difference-in-differences: two groups (treated and control) observed in two time periods (before and after treatment). We collapse our multi-year data into a single pre-treatment average (2008&amp;ndash;2013) and a single post-treatment average (2014&amp;ndash;2015).&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
PRE_T[&amp;quot;&amp;lt;b&amp;gt;Treated States&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pre-2014 average&amp;quot;]
PRE_C[&amp;quot;&amp;lt;b&amp;gt;Control States&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pre-2014 average&amp;quot;]
POST_T[&amp;quot;&amp;lt;b&amp;gt;Treated States&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Post-2014 average&amp;quot;]
POST_C[&amp;quot;&amp;lt;b&amp;gt;Control States&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Post-2014 average&amp;quot;]
DID[&amp;quot;&amp;lt;b&amp;gt;DiD Estimate&amp;lt;/b&amp;gt;&amp;quot;]
PRE_T --&amp;gt;|&amp;quot;Change in Treated&amp;quot;| POST_T
PRE_C --&amp;gt;|&amp;quot;Change in Control&amp;quot;| POST_C
POST_T --&amp;gt; DID
POST_C --&amp;gt; DID
style PRE_T fill:#00d4c8,stroke:#141413,color:#141413
style POST_T fill:#00d4c8,stroke:#141413,color:#141413
style PRE_C fill:#6a9bcc,stroke:#141413,color:#fff
style POST_C fill:#6a9bcc,stroke:#141413,color:#fff
style DID fill:#d97757,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>To see the four means that define the 2x2 DiD, we create a post-treatment indicator and compute group averages.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Create post indicator
gen byte post = (year &amp;gt;= 2014)
* Compute the four group means
preserve
collapse (mean) dins, by(D post)
list, clean noobs
restore
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> D post dins
0 0 .6189702
0 1 .6836083
1 0 .6544622
1 1 .7808657
&lt;/code>&lt;/pre>
&lt;p>The four cells of the 2x2 table reveal the raw pattern. Control states (D = 0) saw insurance coverage rise from 61.90% to 68.36% &amp;mdash; a gain of 6.46 percentage points reflecting nationwide trends. Treated states (D = 1) saw a larger increase from 65.45% to 78.09% &amp;mdash; a gain of 12.64 percentage points. The DiD estimate is the difference of these two changes: 12.64 - 6.46 = &lt;strong>6.18 percentage points&lt;/strong>. This is the causal effect of Medicaid expansion on insurance coverage among low-income childless adults, under the parallel trends assumption.&lt;/p>
&lt;h3 id="52-regression-based-2x2-did">5.2 Regression-based 2x2 DiD&lt;/h3>
&lt;p>The same estimate emerges from a regression. The 2x2 DiD regression specification is:&lt;/p>
&lt;p>$$Y_{it} = \alpha + \beta \cdot \text{Treat}_i + \gamma \cdot \text{Post}_t + \delta \cdot (\text{Treat}_i \times \text{Post}_t) + \varepsilon_{it}$$&lt;/p>
&lt;p>In words, the outcome for state $i$ in period $t$ equals a baseline level ($\alpha$), a treatment group fixed effect ($\beta$), a post-period fixed effect ($\gamma$), and the interaction ($\delta$) &amp;mdash; which is the DiD estimate. The coefficient $\delta$ captures how much the treated group&amp;rsquo;s outcome changed relative to the control group&amp;rsquo;s change.&lt;/p>
&lt;pre>&lt;code class="language-stata">* 2x2 DiD regression
reg dins i.D##i.post, cluster(stfips)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Linear regression Number of obs = 304
F(3, 37) = 182.58
Prob &amp;gt; F = 0.0000
R-squared = 0.4722
Root MSE = .05526
(Std. err. adjusted for 38 clusters in stfips)
------------------------------------------------------------------------------
| Robust
dins | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
1.D | .035492 .0176856 2.01 0.052 -.0003425 .0713265
1.post | .0646382 .0052781 12.25 0.000 .0539437 .0753326
|
D#post |
1 1 | .0617653 .0085367 7.24 0.000 .0444682 .0790624
|
_cons | .6189702 .0122906 50.36 0.000 .5940671 .6438732
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The regression confirms the manual calculation: the interaction coefficient (&lt;code>1.D#1.post&lt;/code>) is 0.0618, corresponding to a 6.18 percentage point increase in insurance coverage. The effect is highly statistically significant (t = 7.24, p &amp;lt; 0.001), with a 95% confidence interval of [4.45, 7.91] percentage points. The standard errors are clustered at the state level to account for within-state correlation over time.&lt;/p>
&lt;h3 id="53-the-parallel-trends-problem-in-the-2x2">5.3 The parallel trends problem in the 2x2&lt;/h3>
&lt;p>This estimate relies on a crucial assumption: absent Medicaid expansion, treated and control states would have followed the &lt;strong>same trend&lt;/strong> in insurance coverage. Formally, the parallel trends assumption states:&lt;/p>
&lt;p>$$E[Y_{it}(0) | \text{Treat}_i = 1] - E[Y_{it-1}(0) | \text{Treat}_i = 1] = E[Y_{it}(0) | \text{Treat}_i = 0] - E[Y_{it-1}(0) | \text{Treat}_i = 0]$$&lt;/p>
&lt;p>In words, the change in untreated potential outcomes would have been the same for both groups. The problem is that &lt;strong>with only two periods of data, we have no way to test this&lt;/strong>. We observe each group once before treatment and once after. There is no earlier period to check whether trends were already diverging.&lt;/p>
&lt;p>Imagine you have a single photograph of two runners side by side before a race. They appear to be at the same speed. You assume they were always running at the same pace &amp;mdash; but what if one had been accelerating? With only one snapshot, you cannot know. This is exactly the situation in the 2x2 DiD: we assume parallel trends because we have no evidence against it, but we also have no evidence for it.&lt;/p>
&lt;p>The &lt;code>honestdid&lt;/code> package provides a way forward. Instead of assuming parallel trends holds perfectly, it asks: &lt;strong>&amp;ldquo;How large would the violation of parallel trends need to be before the DiD result breaks down?&amp;quot;&lt;/strong> The next section makes this precise.&lt;/p>
&lt;p>&lt;img src="stata_honestdid_2x2_means.png" alt="Line plot showing treated and control group means before and after Medicaid expansion, with a dashed counterfactual line showing where the treated group would have been under parallel trends. The gap between the actual treated line and the counterfactual is the DiD estimate.">
&lt;em>Figure 1: Group means and counterfactual trend. The dashed line shows where treated states would have been without Medicaid expansion (parallel trends assumption). The gap between the solid treated line and the dashed counterfactual is the DiD estimate of 6.18 pp.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="6-sensitivity-analysis-for-the-2x2-did">6. Sensitivity analysis for the 2x2 DiD&lt;/h2>
&lt;p>Before applying sensitivity analysis, note that the 2x2 DiD estimate of 6.18 pp averages across all pre-treatment years (2008&amp;ndash;2013) and all post-treatment years (2014&amp;ndash;2015). The event study estimates in this section and in Part 2 measure year-specific effects relative to the reference year 2013. These are different parameters &amp;mdash; the event study will show 4.23 pp for 2014 and 6.87 pp for 2015, which bracket the 2x2 average.&lt;/p>
&lt;h3 id="61-setting-up-the-event-study-for-honestdid">6.1 Setting up the event study for honestdid&lt;/h3>
&lt;p>To apply &lt;code>honestdid&lt;/code>, we need coefficients in an event study format &amp;mdash; at least one pre-treatment coefficient and one post-treatment coefficient, relative to a reference period. We restrict the data to a narrow three-year window around the treatment year: 2012 (one year before the reference), 2013 (the reference period, just before treatment), and 2014 (the treatment year).&lt;/p>
&lt;p>This gives us the simplest event study possible: one pre-period coefficient (the 2012 vs 2013 difference between treated and control) and one post-period coefficient (the 2014 vs 2013 difference). The pre-period coefficient tells us whether the groups were already diverging before treatment.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Restrict to 3-year window: 2012, 2013, 2014
preserve
keep if inrange(year, 2012, 2014)
* Create Dyear variable (treatment-year interaction)
gen Dyear = cond(D, year, 2013)
* Event study with 2013 as reference
reghdfe dins b2013.Dyear, absorb(stfips year) cluster(stfips) noconstant
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">HDFE Linear regression Number of obs = 114
Absorbing 2 HDFE groups F( 2, 37) = 16.27
R-squared = 0.9604
Number of clusters (stfips) = 38 Root MSE = 0.0174
(Std. err. adjusted for 38 clusters in stfips)
------------------------------------------------------------------------------
| Robust
dins | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
Dyear |
2012 | -.0062865 .0059107 -1.06 0.294 -.0182626 .0056897
2014 | .0423401 .0082657 5.12 0.000 .0255923 .059088
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The pre-period coefficient for 2012 is -0.0063, which is small in magnitude and statistically insignificant (t = -1.06, p = 0.294). This suggests that treated and control states were on similar trajectories in the year before treatment. The post-period coefficient for 2014 is 0.0423, indicating that Medicaid expansion increased insurance coverage by 4.23 percentage points relative to the reference year, a highly significant effect (t = 5.12, p &amp;lt; 0.001).&lt;/p>
&lt;h3 id="62-introducing-relative-magnitudes-deltarm">6.2 Introducing relative magnitudes (DeltaRM)&lt;/h3>
&lt;p>Now we apply the core innovation of Rambachan and Roth (2023). The &lt;strong>relative magnitudes&lt;/strong> restriction bounds the post-treatment violation of parallel trends relative to the largest pre-treatment violation:&lt;/p>
&lt;p>$$\Delta^{RM}(\bar{M}): \quad |\delta_t^{\text{post}}| \leq \bar{M} \cdot \max_{s \in \text{pre}} |\delta_s|$$&lt;/p>
&lt;p>In words, this restriction says: &amp;ldquo;the true deviation from parallel trends after treatment can be at most $\bar{M}$ times as large as the largest true deviation in the pre-treatment period.&amp;rdquo; We do not observe these true deviations directly &amp;mdash; the package uses the estimated pre-period coefficients and their uncertainty to construct valid confidence intervals. The parameter $\bar{M}$ &amp;mdash; read as &amp;ldquo;M-bar&amp;rdquo; &amp;mdash; controls how much violation we allow:&lt;/p>
&lt;ul>
&lt;li>$\bar{M} = 0$: exact parallel trends in the &lt;strong>post-treatment&lt;/strong> period (strongest assumption), though pre-treatment deviations are still allowed&lt;/li>
&lt;li>$\bar{M} = 1$: post-treatment violation can be as large as the worst pre-treatment violation&lt;/li>
&lt;li>$\bar{M} = 2$: post-treatment violation can be twice the worst pre-treatment violation&lt;/li>
&lt;/ul>
&lt;p>Think of the breakdown value like a bridge stress test. Engineers do not just ask &amp;ldquo;Can the bridge hold the expected load?&amp;rdquo; They ask &amp;ldquo;How much MORE load can it take before it fails?&amp;rdquo; The &lt;strong>breakdown value&lt;/strong> is that safety margin for your DiD estimate &amp;mdash; the value of $\bar{M}$ at which the confidence interval first includes zero and the conclusion reverses.&lt;/p>
&lt;p>The diagram below summarizes the &lt;code>honestdid&lt;/code> workflow &amp;mdash; from the event study coefficients all the way to the breakdown value.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;Event Study&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Coefficients + VCV&amp;quot;] --&amp;gt; B[&amp;quot;&amp;lt;b&amp;gt;Choose Restriction&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;DeltaRM or DeltaSD&amp;quot;]
B --&amp;gt; C[&amp;quot;&amp;lt;b&amp;gt;Set M Values&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;mvec(0, 0.5, 1, ...)&amp;quot;]
C --&amp;gt; D[&amp;quot;&amp;lt;b&amp;gt;Robust CIs&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;for each M&amp;quot;]
D --&amp;gt; E[&amp;quot;&amp;lt;b&amp;gt;Breakdown Value&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;CI first includes zero&amp;quot;]
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#d97757,stroke:#141413,color:#fff
style D fill:#00d4c8,stroke:#141413,color:#141413
style E fill:#141413,stroke:#d97757,color:#fff
&lt;/code>&lt;/pre>
&lt;h3 id="63-running-honestdid">6.3 Running honestdid&lt;/h3>
&lt;p>We apply &lt;code>honestdid&lt;/code> to the event study results from the three-year window. With &lt;code>pre(1/1)&lt;/code>, we tell the package that coefficient position 1 (the 2012 coefficient) is the pre-period, and &lt;code>post(3/3)&lt;/code> specifies position 3 (the 2014 coefficient) as the post-period, skipping the omitted 2013 reference at position 2.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Sensitivity analysis: relative magnitudes
honestdid, pre(1/1) post(3/3) mvec(0(0.5)2)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">| M | lb | ub |
| ------- | ------ | ------ |
| . | 0.026 | 0.059 | (Original)
| 0.0000 | 0.026 | 0.059 |
| 0.5000 | 0.022 | 0.060 |
| 1.0000 | 0.017 | 0.064 |
| 1.5000 | 0.010 | 0.069 |
| 2.0000 | 0.003 | 0.076 |
(method = C-LF, Delta = DeltaRM)
&lt;/code>&lt;/pre>
&lt;p>The table shows robust confidence intervals for different values of $\bar{M}$, constructed using the C-LF (conditional least-favorable) method &amp;mdash; a procedure that accounts for both sampling uncertainty and the worst-case bias allowed by the restriction. The first row ($\bar{M}$ = .) shows the original confidence interval without any sensitivity adjustment: [0.026, 0.059]. As $\bar{M}$ increases, we allow larger violations of parallel trends, and the confidence interval widens. Even at $\bar{M}$ = 2 &amp;mdash; allowing post-treatment violations twice as large as the pre-treatment difference &amp;mdash; the lower bound remains positive at 0.003, still above zero. The result is remarkably robust: the conclusion that Medicaid expansion increased insurance coverage survives even generous assumptions about parallel trends violations.&lt;/p>
&lt;h3 id="64-the-sensitivity-plot">6.4 The sensitivity plot&lt;/h3>
&lt;p>We can visualize the sensitivity analysis with a plot that shows how the confidence interval expands as we relax the parallel trends assumption.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Generate the sensitivity plot
honestdid, pre(1/1) post(3/3) mvec(0(0.5)2) coefplot
graph export &amp;quot;stata_honestdid_2x2_rm.png&amp;quot;, replace width(1200)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_honestdid_2x2_rm.png" alt="Sensitivity plot showing robust confidence intervals for the 2x2 DiD estimate under the relative magnitudes restriction, with M-bar on the x-axis and the treatment effect on the y-axis. The confidence interval widens as M-bar increases but remains above zero throughout.">
&lt;em>Figure 2: Relative magnitudes sensitivity for the 2x2 DiD. The CI stays above zero even at M-bar = 2.&lt;/em>&lt;/p>
&lt;p>Each point on the plot shows the robust confidence interval at a given $\bar{M}$. Moving right on the x-axis means allowing progressively larger violations of parallel trends. The breakdown value is where the confidence interval first touches zero. In this case, the confidence interval stays above zero even at $\bar{M}$ = 2 (lower bound = 0.003), meaning the result is robust to post-treatment violations that are at least twice as large as the pre-treatment divergence we observed.&lt;/p>
&lt;h3 id="65-what-did-we-learn">6.5 What did we learn?&lt;/h3>
&lt;p>Even with just three periods of data &amp;mdash; barely more than the textbook 2x2 &amp;mdash; &lt;code>honestdid&lt;/code> lets us go far beyond the simple assertion &amp;ldquo;we assume parallel trends holds.&amp;rdquo; We can now say: &amp;ldquo;Our result is robust to post-treatment violations of parallel trends that are at least twice as large as the pre-treatment difference between groups.&amp;rdquo; This is a much more informative and credible statement.&lt;/p>
&lt;p>However, with only one pre-period coefficient, we are limited to the relative magnitudes restriction. The &lt;strong>smoothness restriction&lt;/strong> (DeltaSD) &amp;mdash; which bounds how quickly the trend can change direction &amp;mdash; requires at least two pre-period coefficients to compute second differences. To unlock this richer analysis, we need more pre-treatment data. That is exactly what Part 2 provides.&lt;/p>
&lt;p>Now that we have established Part 1&amp;rsquo;s results, we restore the full dataset and move to the multi-period analysis.&lt;/p>
&lt;pre>&lt;code class="language-stata">restore
&lt;/code>&lt;/pre>
&lt;hr>
&lt;h1 id="part-2-multi-period-difference-in-differences">Part 2: Multi-period Difference-in-Differences&lt;/h1>
&lt;h2 id="7-from-2x2-to-event-study">7. From 2x2 to event study&lt;/h2>
&lt;h3 id="71-why-more-periods-help">7.1 Why more periods help&lt;/h3>
&lt;p>With the full panel (2008&amp;ndash;2015), we have five pre-treatment years instead of just one. This gives us two advantages. First, we can &lt;strong>visually inspect&lt;/strong> whether treated and control groups were on similar trajectories before 2014. Second, &lt;code>honestdid&lt;/code> has richer information to calibrate the scale of potential violations, and we unlock the smoothness restriction that was unavailable in Part 1.&lt;/p>
&lt;p>The multi-period event study estimates a separate treatment effect for each year relative to a reference year. The specification is:&lt;/p>
&lt;p>$$Y_{it} = \alpha_i + \lambda_t + \sum_{k \neq -1} \beta_k \cdot \mathbb{1}[K_{it} = k] + \varepsilon_{it}$$&lt;/p>
&lt;p>In words, the outcome for state $i$ in year $t$ depends on state fixed effects ($\alpha_i$), year fixed effects ($\lambda_t$), and a set of event-time indicators. $K_{it}$ measures event time &amp;mdash; years relative to treatment onset (2014). The reference period $k = -1$ (year 2013) is omitted, so each $\beta_k$ measures the treated-control difference in year $k$ relative to the year just before treatment. The pre-treatment coefficients ($\beta_{-6}$ through $\beta_{-2}$) show whether trends were already diverging; the post-treatment coefficients ($\beta_0$ and $\beta_1$) capture the treatment effect.&lt;/p>
&lt;p>&lt;strong>Variable mapping:&lt;/strong> $Y$ = &lt;code>dins&lt;/code>, $\alpha_i$ = state dummies (absorbed by &lt;code>reghdfe&lt;/code>), $\lambda_t$ = year dummies, and $K_{it}$ = &lt;code>Dyear&lt;/code> interaction variable.&lt;/p>
&lt;h3 id="72-estimation">7.2 Estimation&lt;/h3>
&lt;p>We now estimate the event study using all eight years of data. The variable &lt;code>Dyear&lt;/code> interacts treatment status with calendar year, and we omit 2013 as the reference.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Create Dyear for event study (full sample)
gen Dyear = cond(D, year, 2013)
* Full event study: 2008--2015 with 2013 as reference
reghdfe dins b2013.Dyear, absorb(stfips year) cluster(stfips) noconstant
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">HDFE Linear regression Number of obs = 304
Absorbing 2 HDFE groups F( 7, 37) = 10.37
R-squared = 0.9505
Number of clusters (stfips) = 38 Root MSE = 0.0185
(Std. err. adjusted for 38 clusters in stfips)
------------------------------------------------------------------------------
| Robust
dins | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
-------------+----------------------------------------------------------------
Dyear |
2008 | -.0095956 .0076769 -1.25 0.219 -.0251505 .0059593
2009 | -.0132771 .0073502 -1.81 0.079 -.02817 .0016159
2010 | -.0018712 .0067698 -0.28 0.784 -.0155881 .0118457
2011 | -.0064012 .0070425 -0.91 0.369 -.0206707 .0078682
2012 | -.0062865 .005944 -1.06 0.297 -.0183302 .0057573
2014 | .0423401 .0083124 5.09 0.000 .0254977 .0591826
2015 | .0687134 .0108512 6.33 0.000 .0467268 .0906999
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The five pre-treatment coefficients (2008&amp;ndash;2012) are all small in magnitude and statistically insignificant, ranging from -0.0133 to -0.0019. This suggests that treated and control states followed similar insurance coverage trajectories before Medicaid expansion. The post-treatment coefficients show a sharp break: insurance coverage jumped by 4.23 percentage points in 2014 and 6.87 percentage points in 2015, both highly significant. The growing effect over time is consistent with gradual Medicaid enrollment &amp;mdash; eligible individuals signing up over the first two years of the program.&lt;/p>
&lt;p>We visualize these coefficients in a standard event study plot.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Event study plot
coefplot, vertical yline(0, lcolor(gs8)) ///
xline(5.5, lpattern(dash) lcolor(gs8)) ///
ciopts(recast(rcap)) ///
ytitle(&amp;quot;Effect on insurance share&amp;quot;) xtitle(&amp;quot;Year&amp;quot;) ///
title(&amp;quot;Event Study: Medicaid Expansion and Insurance Coverage&amp;quot;)
graph export &amp;quot;stata_honestdid_event_study.png&amp;quot;, replace width(1200)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_honestdid_event_study.png" alt="Event study plot showing pre-treatment coefficients clustered around zero from 2008 to 2012 and a sharp positive jump in 2014 and 2015, with a dashed vertical line marking the treatment year.">
&lt;em>Figure 3: Event study coefficients. Pre-treatment coefficients hover near zero; post-treatment effects are large and significant.&lt;/em>&lt;/p>
&lt;p>The event study plot makes the pattern visually clear. Pre-treatment coefficients hover around zero with no discernible trend, while post-treatment coefficients jump sharply upward. The dashed vertical line marks the onset of treatment in 2014.&lt;/p>
&lt;h3 id="73-conventional-pre-trends-test">7.3 Conventional pre-trends test&lt;/h3>
&lt;p>The standard approach is to conduct a joint F-test of all pre-treatment coefficients. If we fail to reject the null that all pre-period coefficients are jointly zero, we conclude that parallel trends &amp;ldquo;holds.&amp;rdquo;&lt;/p>
&lt;pre>&lt;code class="language-stata">* Joint test of pre-treatment coefficients
test 2008.Dyear 2009.Dyear 2010.Dyear 2011.Dyear 2012.Dyear
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> ( 1) 2008.Dyear = 0
( 2) 2009.Dyear = 0
( 3) 2010.Dyear = 0
( 4) 2011.Dyear = 0
( 5) 2012.Dyear = 0
F( 5, 37) = 0.86
Prob &amp;gt; F = 0.5178
&lt;/code>&lt;/pre>
&lt;p>The pre-trends test yields an F-statistic of 0.86 with a p-value of 0.518, providing no evidence against parallel trends. But should we trust this binary verdict? The next section explains why the answer is no.&lt;/p>
&lt;hr>
&lt;h2 id="8-why-pre-trends-tests-are-not-enough">8. Why pre-trends tests are not enough&lt;/h2>
&lt;p>Your DiD passed the pre-trends test. But should you trust it?&lt;/p>
&lt;p>Think of a pre-trends test as a &lt;strong>smoke detector that only beeps for large fires&lt;/strong>. A fire too small to trigger the alarm can still burn down the house. Similarly, a pre-trends test can fail to detect violations of parallel trends that are large enough to overturn your conclusions. Roth (2022) demonstrated two important problems with conventional pre-trends tests:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Low power.&lt;/strong> Pre-trends tests often cannot detect violations of parallel trends that are economically meaningful. A test with 50 observations per group may require a violation three times larger than the treatment effect to reject the null at 5% significance. Violations smaller than this detection threshold go unnoticed.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Pre-test bias.&lt;/strong> Conditioning on passing the pre-trends test introduces bias. The estimates that survive the pre-test are a selected sample &amp;mdash; they look better than they should. Researchers who report &amp;ldquo;parallel trends holds&amp;rdquo; are unknowingly presenting results that have been filtered to appear more credible than they are.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The fundamental issue is that the pre-trends test asks a binary question &amp;mdash; &amp;ldquo;reject or not?&amp;rdquo; &amp;mdash; when what we really need is a &lt;strong>continuous measure&lt;/strong> of robustness. Instead of asking &amp;ldquo;Are parallel trends exactly satisfied?&amp;rdquo; we should ask &amp;ldquo;How robust are our conclusions to plausible violations?&amp;rdquo;&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
PT[&amp;quot;&amp;lt;b&amp;gt;Parallel Trends Assumption&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(untestable)&amp;quot;]
CONV[&amp;quot;&amp;lt;b&amp;gt;Conventional Approach&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pre-trends test&amp;lt;br/&amp;gt;(binary: reject or not)&amp;quot;]
HONEST[&amp;quot;&amp;lt;b&amp;gt;HonestDiD Approach&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Sensitivity analysis&amp;lt;br/&amp;gt;(how much violation&amp;lt;br/&amp;gt;can we tolerate?)&amp;quot;]
RESULT_C[&amp;quot;Parallel trends holds&amp;lt;br/&amp;gt;(false confidence)&amp;quot;]
RESULT_H[&amp;quot;Results robust up to&amp;lt;br/&amp;gt;M-bar = X violations&amp;lt;br/&amp;gt;(calibrated conclusion)&amp;quot;]
PT --&amp;gt; CONV
PT --&amp;gt; HONEST
CONV --&amp;gt; RESULT_C
HONEST --&amp;gt; RESULT_H
style PT fill:#141413,stroke:#d97757,color:#fff
style CONV fill:#d97757,stroke:#141413,color:#fff
style HONEST fill:#00d4c8,stroke:#141413,color:#141413
style RESULT_C fill:#d97757,stroke:#141413,color:#fff
style RESULT_H fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;p>The &lt;code>honestdid&lt;/code> approach replaces the binary verdict with a quantitative statement: &amp;ldquo;Our result is robust to violations of parallel trends up to $\bar{M}$ times the largest pre-treatment violation.&amp;rdquo; This is like reporting the load at which a bridge fails, rather than just saying &amp;ldquo;the bridge passed inspection.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h2 id="9-sensitivity-analysis-----relative-magnitudes-full-panel">9. Sensitivity analysis &amp;mdash; relative magnitudes (full panel)&lt;/h2>
&lt;h3 id="91-rm-with-5-pre-periods">9.1 RM with 5 pre-periods&lt;/h3>
&lt;p>We now apply the same relative magnitudes restriction from Part 1, but with the richer information from five pre-treatment periods. The equation is the same:&lt;/p>
&lt;p>$$\Delta^{RM}(\bar{M}): \quad |\delta_t^{\text{post}}| \leq \bar{M} \cdot \max_{s \in \text{pre}} |\delta_s|$$&lt;/p>
&lt;p>With five pre-period coefficients instead of one, the &amp;ldquo;max pre-period violation&amp;rdquo; is calibrated from more data points, giving a more reliable scale for what constitutes a plausible violation.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Relative magnitudes: full panel
honestdid, pre(1/5) post(7/8) mvec(0(0.5)2)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">| M | lb | ub |
| ------- | ------ | ------ |
| . | 0.026 | 0.059 | (Original)
| 0.0000 | 0.027 | 0.058 |
| 0.5000 | 0.021 | 0.063 |
| 1.0000 | 0.013 | 0.071 |
| 1.5000 | 0.003 | 0.081 |
| 2.0000 | -0.007 | 0.091 |
(method = C-LF, Delta = DeltaRM)
&lt;/code>&lt;/pre>
&lt;p>With five pre-periods calibrating the scale of violations, the confidence intervals widen faster than in the 2x2 case. At $\bar{M}$ = 0 (exact parallel trends), the robust CI is [0.027, 0.058]. At $\bar{M}$ = 1, allowing violations as large as the worst pre-period deviation, the CI remains positive: [0.013, 0.071]. At $\bar{M}$ = 1.5, the lower bound is barely positive at 0.003. At $\bar{M}$ = 2, the lower bound turns negative at -0.007. The breakdown value is approximately $\bar{M}$ = 1.5&amp;ndash;2 &amp;mdash; the post-treatment violation of parallel trends would need to be about 1.5 to 2 times as large as the worst pre-treatment deviation to overturn the conclusion.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Sensitivity plot: relative magnitudes
honestdid, pre(1/5) post(7/8) mvec(0(0.5)2) coefplot
graph export &amp;quot;stata_honestdid_rm_full.png&amp;quot;, replace width(1200)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_honestdid_rm_full.png" alt="Sensitivity plot for the relative magnitudes restriction with five pre-treatment periods, showing the robust confidence interval widening as M-bar increases from 0 to 2.">
&lt;em>Figure 4: Relative magnitudes sensitivity with 5 pre-periods. The CI crosses zero between M-bar = 1.5 and 2.&lt;/em>&lt;/p>
&lt;p>The sensitivity plot confirms the pattern: the confidence interval steadily widens as we allow larger violations, crossing zero between $\bar{M}$ = 1.5 and 2. Compared to the 2x2 case in Part 1, where the CI stayed positive even at $\bar{M}$ = 2, the full-panel analysis produces a slightly tighter breakdown. This happens because having more pre-period coefficients can produce a larger &amp;ldquo;max pre-period violation&amp;rdquo; (the scaling factor on the right-hand side of the relative magnitudes formula), which scales up the allowed post-treatment violation for any given $\bar{M}$.&lt;/p>
&lt;h3 id="92-focusing-on-the-average-post-treatment-effect">9.2 Focusing on the average post-treatment effect&lt;/h3>
&lt;p>By default, &lt;code>honestdid&lt;/code> examines the first post-treatment period. We can instead ask about the average treatment effect across both post-treatment periods (2014 and 2015) using the &lt;code>l_vec&lt;/code> option, which specifies weights for combining the post-period coefficients.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Average effect across 2014 and 2015
matrix l_vec = 0.5 \ 0.5
honestdid, pre(1/5) post(7/8) mvec(0(0.5)2) l_vec(l_vec)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">| M | lb | ub |
| ------- | ------ | ------ |
| . | 0.039 | 0.072 | (Original)
| 0.0000 | 0.039 | 0.072 |
| 0.5000 | 0.029 | 0.079 |
| 1.0000 | 0.014 | 0.092 |
| 1.5000 | -0.002 | 0.107 |
| 2.0000 | -0.019 | 0.123 |
(method = C-LF, Delta = DeltaRM)
&lt;/code>&lt;/pre>
&lt;p>The average treatment effect across 2014&amp;ndash;2015 has a higher point estimate (the original CI of [0.039, 0.072]) because the 2015 effect is larger than the 2014 effect. The breakdown value for the average effect is between $\bar{M}$ = 1 and 1.5 &amp;mdash; at $\bar{M}$ = 1 the lower bound is still positive (0.014) but at $\bar{M}$ = 1.5 it turns negative (-0.002). Interestingly, the average effect is slightly &lt;em>less&lt;/em> robust than the first-period effect alone (breakdown between 1 and 1.5 vs between 1.5 and 2). This can happen when averaging over a longer horizon amplifies the cumulative impact of potential trend deviations.&lt;/p>
&lt;hr>
&lt;h2 id="10-sensitivity-analysis-----smoothness-restrictions">10. Sensitivity analysis &amp;mdash; smoothness restrictions&lt;/h2>
&lt;h3 id="101-introducing-deltasd">10.1 Introducing DeltaSD&lt;/h3>
&lt;p>Relative magnitudes asks: &amp;ldquo;How large can the violation be?&amp;rdquo; A complementary question is: &amp;ldquo;How quickly can the trend change direction?&amp;rdquo; This is the &lt;strong>smoothness restriction&lt;/strong> (DeltaSD), which bounds the second differences of the trend deviation.&lt;/p>
&lt;p>Think of the two restrictions like driving rules. Relative magnitudes imposes a &lt;strong>speed limit&lt;/strong> &amp;mdash; the violation cannot exceed $\bar{M}$ times the maximum observed pre-treatment violation. Smoothness imposes an &lt;strong>acceleration limit&lt;/strong> &amp;mdash; the violation cannot change direction too sharply between consecutive periods. A car might be going fast but safely if it accelerated gradually; a sudden swerve is dangerous even at moderate speed.&lt;/p>
&lt;p>Formally, the smoothness restriction bounds the second difference:&lt;/p>
&lt;p>$$\Delta^{SD}(M): \quad |(\delta_{t+1} - \delta_t) - (\delta_t - \delta_{t-1})| \leq M \quad \text{for all } t$$&lt;/p>
&lt;p>In words, the &amp;ldquo;acceleration&amp;rdquo; of the parallel trends violation &amp;mdash; how much the slope changes from one period to the next &amp;mdash; cannot exceed $M$ for any consecutive triple of periods. When $M = 0$, the trend deviation is perfectly linear (constant slope). Larger $M$ allows more curvature.&lt;/p>
&lt;p>This restriction was &lt;strong>not available in Part 1&lt;/strong> because it requires at least two pre-period coefficients to compute second differences (you need three points to calculate one &amp;ldquo;acceleration&amp;rdquo;). With five pre-periods, we can now use this richer restriction.&lt;/p>
&lt;h3 id="102-running-honestdid-with-deltasd">10.2 Running honestdid with DeltaSD&lt;/h3>
&lt;pre>&lt;code class="language-stata">* Smoothness restriction
honestdid, pre(1/5) post(7/8) mvec(0(0.005)0.04) delta(sd)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">| M | lb | ub |
| ------- | ------ | ------ |
| . | 0.026 | 0.059 | (Original)
| 0.0000 | 0.026 | 0.058 |
| 0.0050 | 0.013 | 0.061 |
| 0.0100 | 0.007 | 0.065 |
| 0.0150 | 0.002 | 0.070 |
| 0.0200 | -0.003 | 0.075 |
| 0.0250 | -0.008 | 0.080 |
| 0.0300 | -0.013 | 0.085 |
| 0.0350 | -0.018 | 0.090 |
| 0.0400 | -0.023 | 0.095 |
(method = FLCI, Delta = DeltaSD)
&lt;/code>&lt;/pre>
&lt;p>Note that &lt;code>honestdid&lt;/code> automatically selects the FLCI (fixed-length confidence interval) method for smoothness restrictions, rather than the C-LF method used for relative magnitudes. FLCI constructs a confidence interval with optimal length under the smoothness restriction. Under the smoothness restriction, the breakdown value is approximately $M$ = 0.015&amp;ndash;0.02. At $M$ = 0 (perfectly linear trend extrapolation), the robust CI is [0.026, 0.058]. At $M$ = 0.01, the CI is [0.007, 0.065], still comfortably above zero. At $M$ = 0.015, the lower bound is barely positive at 0.002. At $M$ = 0.02, the lower bound turns negative at -0.003. The change in the rate of divergence from parallel trends would need to exceed 1.5&amp;ndash;2 percentage points between consecutive periods to overturn the finding.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Smoothness sensitivity plot
honestdid, pre(1/5) post(7/8) mvec(0(0.005)0.04) delta(sd) coefplot
graph export &amp;quot;stata_honestdid_sd_full.png&amp;quot;, replace width(1200)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_honestdid_sd_full.png" alt="Sensitivity plot for the smoothness restriction showing the robust confidence interval widening as M increases from 0 to 0.04, crossing zero near M = 0.02.">
&lt;em>Figure 5: Smoothness restriction sensitivity. The CI crosses zero near M = 0.02.&lt;/em>&lt;/p>
&lt;p>The smoothness restriction yields a different perspective. Unlike relative magnitudes &amp;mdash; where $\bar{M}$ is a dimensionless multiplier &amp;mdash; the smoothness parameter $M$ is measured in the same units as the outcome (insurance share). A breakdown value of $M$ = 0.015&amp;ndash;0.02 means the rate of divergence from parallel trends would need to shift by about 1.5&amp;ndash;2 percentage points between consecutive periods to invalidate the result.&lt;/p>
&lt;h3 id="103-comparing-rm-vs-sd">10.3 Comparing RM vs SD&lt;/h3>
&lt;p>The two approaches offer complementary views of robustness:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Restriction&lt;/th>
&lt;th>Parameter&lt;/th>
&lt;th>Breakdown Value&lt;/th>
&lt;th>Interpretation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Relative Magnitudes&lt;/td>
&lt;td>$\bar{M}$&lt;/td>
&lt;td>~1.5&amp;ndash;2&lt;/td>
&lt;td>Post violation can be up to 1.5&amp;ndash;2x the max pre violation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Smoothness&lt;/td>
&lt;td>$M$&lt;/td>
&lt;td>~0.015&amp;ndash;0.02&lt;/td>
&lt;td>Rate of trend divergence can shift by up to 1.5&amp;ndash;2 pp between periods&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="104-when-to-choose-which-restriction">10.4 When to choose which restriction&lt;/h3>
&lt;ul>
&lt;li>Use &lt;strong>DeltaRM&lt;/strong> when: (a) you have few pre-periods &amp;mdash; it works with just one, (b) the pre-treatment coefficients look like random noise around zero with no clear trend, or (c) you want a dimensionless measure of robustness that is easy to communicate&lt;/li>
&lt;li>Use &lt;strong>DeltaSD&lt;/strong> when: (a) you have two or more pre-periods, (b) there is a visible pre-trend (non-zero slope) and you want to formalize how much the slope can change, or (c) you want bounds measured in the outcome&amp;rsquo;s units&lt;/li>
&lt;li>&lt;strong>Report both&lt;/strong> when feasible, as we did here, to provide a complete picture&lt;/li>
&lt;/ul>
&lt;p>In general, relative magnitudes is the more popular choice because it is intuitive and works with minimal data. Smoothness restrictions are complementary &amp;mdash; they capture a different form of violation (abrupt changes in trend direction rather than large absolute deviations).&lt;/p>
&lt;h3 id="105-how-to-report-honestdid-results-in-a-paper">10.5 How to report honestdid results in a paper&lt;/h3>
&lt;p>Many readers will want to apply this method in their own work. Here is example text you can adapt for a manuscript:&lt;/p>
&lt;blockquote>
&lt;p>We conduct sensitivity analysis following Rambachan and Roth (2023). Under relative magnitudes restrictions, the treatment effect on insurance coverage remains statistically significant for $\bar{M}$ up to 1.5 (95% robust CI: [0.003, 0.081]). Under smoothness restrictions, the result is robust for $M$ up to 0.015 (95% robust CI: [0.002, 0.070]). These breakdown values indicate that post-treatment deviations from parallel trends would need to be at least 1.5 times the largest pre-treatment deviation to overturn the conclusion.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="11-extension-----staggered-did-with-csdid-and-honestdid">11. Extension &amp;mdash; staggered DiD with csdid and honestdid&lt;/h2>
&lt;h3 id="111-why-staggered-timing-matters">11.1 Why staggered timing matters&lt;/h3>
&lt;p>Our analysis so far restricted attention to states expanding in 2014 and compared them to never-expanders. But different states expanded Medicaid at different times &amp;mdash; some in 2014, others in 2015 or later. Callaway and Sant&amp;rsquo;Anna (2021) showed that standard two-way fixed effects (TWFE) regressions can produce misleading estimates when treatment timing varies across units, especially if treatment effects are heterogeneous over time. The &lt;code>csdid&lt;/code> package provides a heterogeneity-robust estimator that correctly handles staggered treatment adoption.&lt;/p>
&lt;p>We reload the dataset and apply &lt;code>csdid&lt;/code> followed by &lt;code>honestdid&lt;/code>. We keep the same two-group sample (2014-expanders vs never-treated) to demonstrate the &lt;code>csdid&lt;/code> workflow. With a single treatment cohort, the TWFE and Callaway-Sant&amp;rsquo;Anna estimates should agree &amp;mdash; but in settings with multiple treatment cohorts and heterogeneous effects, they can diverge substantially.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Reload full dataset for staggered analysis
use &amp;quot;https://raw.githubusercontent.com/Mixtape-Sessions/Advanced-DID/main/Exercises/Data/ehec_data.dta&amp;quot;, clear
* Restrict to 2008--2015, keep 2014-expanders and never-expanders
keep if (year &amp;lt;= 2015) &amp;amp; (missing(yexp2) | (yexp2 == 2014))
* Replace missing yexp2 with 0 for csdid (never-treated)
replace yexp2 = 0 if missing(yexp2)
* Callaway-Sant'Anna estimator
* long2: compare each post-period to base period (long differences)
* notyet: use not-yet-treated units as additional controls
csdid dins, ivar(stfips) time(year) gvar(yexp2) long2 notyet
* Aggregate to event study
csdid_estat event, window(-5 1) estore(csdid)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">ATT by Periods Before and After treatment
Event Study:Dynamic effects
------------------------------------------------------------------------------
| Coefficient Std. err. z P&amp;gt;|z| [95% conf. interval]
-------------+----------------------------------------------------------------
Pre_avg | -.0074863 .0056726 -1.32 0.187 -.0186045 .0036318
Post_avg | .0555267 .0083153 6.68 0.000 .0392291 .0718244
Tm6 | -.0095956 .0073982 -1.30 0.195 -.0240958 .0049045
Tm5 | -.0132771 .0070833 -1.87 0.061 -.0271601 .000606
Tm4 | -.0018712 .006524 -0.29 0.774 -.0146579 .0109155
Tm3 | -.0064012 .0067868 -0.94 0.346 -.0197031 .0069006
Tm2 | -.0062865 .0057282 -1.10 0.272 -.0175135 .0049406
Tp0 | .0423401 .0080105 5.29 0.000 .0266398 .0580405
Tp1 | .0687134 .0104571 6.57 0.000 .0482177 .089209
------------------------------------------------------------------------------
&lt;/code>&lt;/pre>
&lt;p>The Callaway-Sant&amp;rsquo;Anna event study confirms the pattern from our TWFE analysis: pre-treatment coefficients (Tm6 through Tm2) are all small and insignificant, while the post-treatment effects (Tp0 = 0.0423 in 2014, Tp1 = 0.0687 in 2015) are large and highly significant. The average post-treatment effect is 5.55 percentage points.&lt;/p>
&lt;h3 id="112-applying-honestdid-to-staggered-estimates">11.2 Applying honestdid to staggered estimates&lt;/h3>
&lt;p>We now apply &lt;code>honestdid&lt;/code> to the Callaway-Sant&amp;rsquo;Anna event study estimates. The &lt;code>pre()&lt;/code> and &lt;code>post()&lt;/code> indices refer to the event-time coefficient positions, skipping the Pre_avg and Post_avg summary rows at positions 1&amp;ndash;2.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Restore csdid results and apply honestdid
estimates restore csdid
* csdid_estat stores: Pre_avg(1), Post_avg(2), Tm6(3)..Tm2(7), Tp0(8), Tp1(9)
honestdid, pre(3/7) post(8/9) mvec(0(0.5)2) coefplot
graph export &amp;quot;stata_honestdid_csdid.png&amp;quot;, replace width(1200)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">| M | lb | ub |
| ------- | ------ | ------ |
| . | 0.027 | 0.058 | (Original)
| 0.0000 | 0.027 | 0.058 |
| 0.5000 | 0.022 | 0.062 |
| 1.0000 | 0.014 | 0.071 |
| 1.5000 | 0.004 | 0.080 |
| 2.0000 | -0.007 | 0.090 |
(method = C-LF, Delta = DeltaRM, alpha = 0.050)
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_honestdid_csdid.png" alt="Sensitivity plot for the relative magnitudes restriction applied to the Callaway-Sant&amp;rsquo;Anna staggered DiD estimates.">
&lt;em>Figure 6: Sensitivity analysis for staggered DiD. Breakdown value is consistent with the TWFE analysis.&lt;/em>&lt;/p>
&lt;p>The staggered-robust estimates from &lt;code>csdid&lt;/code> produce a breakdown value between $\bar{M}$ = 1.5 and 2 &amp;mdash; at $\bar{M}$ = 1.5 the lower bound is still positive (0.004) but at $\bar{M}$ = 2 it turns negative (-0.007). This is nearly identical to the TWFE analysis in Section 9. This is reassuring &amp;mdash; it suggests that the TWFE estimates are reliable in this application because we restricted to a single treatment cohort (2014 expanders vs never-treated). In settings with multiple treatment cohorts and heterogeneous effects, the TWFE and staggered estimates can diverge significantly, making this comparison an important robustness check.&lt;/p>
&lt;hr>
&lt;h2 id="12-discussion-and-summary">12. Discussion and summary&lt;/h2>
&lt;h3 id="121-summary-of-all-sensitivity-analyses">12.1 Summary of all sensitivity analyses&lt;/h3>
&lt;p>The table below collects every sensitivity analysis from this tutorial. Scanning across the rows reveals which settings and restrictions yield stronger or weaker robustness.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Analysis&lt;/th>
&lt;th>Setting&lt;/th>
&lt;th>Restriction&lt;/th>
&lt;th>Breakdown Value&lt;/th>
&lt;th>Robustness&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Section 6&lt;/td>
&lt;td>2x2 (1 pre-period)&lt;/td>
&lt;td>DeltaRM&lt;/td>
&lt;td>&amp;gt; 2&lt;/td>
&lt;td>Very robust&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Section 9.1&lt;/td>
&lt;td>Full panel, first period&lt;/td>
&lt;td>DeltaRM&lt;/td>
&lt;td>~1.5&amp;ndash;2&lt;/td>
&lt;td>Robust&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Section 9.2&lt;/td>
&lt;td>Full panel, average effect&lt;/td>
&lt;td>DeltaRM&lt;/td>
&lt;td>~1&amp;ndash;1.5&lt;/td>
&lt;td>Moderately robust&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Section 10&lt;/td>
&lt;td>Full panel, first period&lt;/td>
&lt;td>DeltaSD&lt;/td>
&lt;td>~0.015&amp;ndash;0.02&lt;/td>
&lt;td>Moderately robust&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Section 11&lt;/td>
&lt;td>Staggered (csdid)&lt;/td>
&lt;td>DeltaRM&lt;/td>
&lt;td>~1.5&amp;ndash;2&lt;/td>
&lt;td>Consistent with TWFE&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The first-period treatment effect is the most robust finding across all approaches. The average effect over 2014&amp;ndash;2015 is slightly less robust because it accumulates potential violations over a longer horizon. The smoothness restriction yields a tighter bound than relative magnitudes, reflecting a different type of assumption about how trends can deviate.&lt;/p>
&lt;p>This tutorial demonstrated how to move beyond the binary question &amp;ldquo;Do parallel trends hold?&amp;rdquo; to the much more useful question &amp;ldquo;How robust are my results to violations of parallel trends?&amp;rdquo; The &lt;code>honestdid&lt;/code> package makes this transition straightforward in Stata.&lt;/p>
&lt;h3 id="122-key-takeaways">12.2 Key takeaways&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Method insight &amp;mdash; the breakdown value replaces the pre-trends test.&lt;/strong> The breakdown value is the single most informative number to report alongside any DiD estimate. It tells the reader exactly how much they need to doubt parallel trends before the result breaks down. For the Medicaid expansion, the breakdown value is approximately $\bar{M}$ = 1.5&amp;ndash;2 under relative magnitudes and $M$ = 0.015&amp;ndash;0.02 under smoothness restrictions.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Data insight &amp;mdash; Medicaid expansion robustly increased insurance coverage.&lt;/strong> The 2x2 DiD estimate of 6.18 percentage points survives sensitivity analysis. In the full-panel event study, the 4.23 percentage point effect in 2014 remains significant up to approximately $\bar{M}$ = 1.5&amp;ndash;2, meaning the post-treatment violation would need to be roughly 1.5 to 2 times the worst pre-period deviation to overturn the result.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Practical insight &amp;mdash; honestdid works even with limited data.&lt;/strong> Part 1 showed that sensitivity analysis is possible with just one pre-period coefficient. You do not need a long panel to use this tool &amp;mdash; though more pre-treatment periods unlock richer analyses (DeltaSD).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Limitation &amp;mdash; sensitivity is not identification.&lt;/strong> The breakdown value tells you how much violation is tolerable, not whether violations actually occur. Subject-matter knowledge about the specific policy context remains essential for assessing whether the parallel trends assumption is plausible.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Next step &amp;mdash; apply honestdid to your own DiD.&lt;/strong> Every DiD analysis should report a breakdown value. The package works with &lt;code>reghdfe&lt;/code>, &lt;code>csdid&lt;/code>, &lt;code>did_multiplegt&lt;/code>, and &lt;code>jwdid&lt;/code> &amp;mdash; any estimator that produces event-study coefficients and a variance-covariance matrix. Tip: use &lt;code>honestdid, coefplot cached&lt;/code> to re-plot previous results without recomputation &amp;mdash; useful for customizing graph appearance.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>For policymakers evaluating the ACA&amp;rsquo;s Medicaid expansion, the sensitivity analysis provides calibrated confidence: the insurance coverage gains are genuine and not an artifact of differential trends between expanding and non-expanding states, unless those differential trends were very large relative to the patterns observed before the policy change.&lt;/p>
&lt;hr>
&lt;h2 id="13-exercises">13. Exercises&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Expand the 2x2 window.&lt;/strong> In Part 1, we used a 3-year window (2012&amp;ndash;2014). Expand it to 4 years (2011&amp;ndash;2014) to get 2 pre-periods. Now try the smoothness restriction (&lt;code>delta(sd)&lt;/code>) &amp;mdash; does it change your conclusion about robustness?&lt;/li>
&lt;/ol>
&lt;pre>&lt;code class="language-stata">* Starter code: restrict to 2011--2014 and re-run
keep if inrange(year, 2011, 2014)
gen Dyear = cond(D, year, 2013)
reghdfe dins b2013.Dyear, absorb(stfips year) cluster(stfips) noconstant
honestdid, pre(1/2) post(4/4) mvec(0(0.005)0.04) delta(sd)
&lt;/code>&lt;/pre>
&lt;ol start="2">
&lt;li>&lt;strong>Focus on the 2015 effect.&lt;/strong> In Part 2, modify &lt;code>l_vec&lt;/code> to focus on only the second post-period (2015). Is the 2015 effect more or less robust than the 2014 effect? Why might longer-horizon effects differ in robustness?&lt;/li>
&lt;/ol>
&lt;pre>&lt;code class="language-stata">* Starter code: l_vec selects only the second post-period
matrix l_vec = 0 \ 1
honestdid, pre(1/5) post(7/8) mvec(0(0.5)2) l_vec(l_vec)
&lt;/code>&lt;/pre>
&lt;ol start="3">
&lt;li>&lt;strong>Compare TWFE and staggered estimates.&lt;/strong> Run the relative magnitudes analysis on both the TWFE (Section 9) and staggered (Section 11) estimates with the same &lt;code>mvec()&lt;/code> grid. Are the breakdown values similar? If they differ, what does that tell you about treatment effect heterogeneity?&lt;/li>
&lt;/ol>
&lt;pre>&lt;code class="language-stata">* Starter code: after running both TWFE and csdid analyses,
* compare the breakdown values from these two commands:
* TWFE: honestdid, pre(1/5) post(7/8) mvec(0(0.5)2)
* csdid: honestdid, pre(3/7) post(8/9) mvec(0(0.5)2)
&lt;/code>&lt;/pre>
&lt;hr>
&lt;h2 id="14-references">14. References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1093/restud/rdad018" target="_blank" rel="noopener">Rambachan, A. &amp;amp; Roth, J. (2023). A More Credible Approach to Parallel Trends. &lt;em>Review of Economic Studies&lt;/em>, 90(5), 2555&amp;ndash;2591.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1257/aeri.20210236" target="_blank" rel="noopener">Roth, J. (2022). Pre-test with Caution: Event-Study Estimates after Testing for Parallel Trends. &lt;em>American Economic Review: Insights&lt;/em>, 4(3), 305&amp;ndash;322.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jeconom.2020.12.001" target="_blank" rel="noopener">Callaway, B. &amp;amp; Sant&amp;rsquo;Anna, P.H.C. (2021). Difference-in-Differences with Multiple Time Periods. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 200&amp;ndash;230.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/mcaceresb/stata-honestdid" target="_blank" rel="noopener">HonestDiD Stata Package &amp;mdash; Rambachan &amp;amp; Roth.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/asheshrambachan/HonestDiD" target="_blank" rel="noopener">HonestDiD R Package &amp;mdash; Rambachan &amp;amp; Roth.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/Mixtape-Sessions/Advanced-DID" target="_blank" rel="noopener">Mixtape Sessions &amp;mdash; Advanced DiD (dataset source).&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://friosavila.github.io/stpackages/csdid.html" target="_blank" rel="noopener">csdid Stata Package &amp;mdash; Rios-Avila, F.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://scorreia.com/software/reghdfe/" target="_blank" rel="noopener">reghdfe &amp;mdash; Linear Models with Many Levels of Fixed Effects &amp;mdash; Correia, S.&lt;/a>&lt;/li>
&lt;/ol>
&lt;h4 id="acknowledgements">Acknowledgements&lt;/h4>
&lt;p>AI tools (Claude Code, Gemini, NotebookLM) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Evaluating a Cash Transfer Program (RCT) with Panel Data in Stata</title><link>https://carlos-mendez.org/post/stata_rct/</link><pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/stata_rct/</guid><description>&lt;h2 id="1-overview">1. Overview&lt;/h2>
&lt;p>Cash transfer programs are among the most common development interventions worldwide. Governments and international organizations spend billions of dollars each year providing direct cash transfers to low-income households. But how do we rigorously evaluate whether these programs actually work? This tutorial walks through the complete workflow of analyzing a &lt;strong>randomized controlled trial (RCT)&lt;/strong> with &lt;strong>panel data&lt;/strong> in Stata &amp;mdash; from verifying that randomization succeeded, to estimating treatment effects using increasingly sophisticated methods, to comparing results across all approaches.&lt;/p>
&lt;p>We use simulated data from a hypothetical cash transfer program targeting 2,000 households in a developing country. The key advantage of simulated data is that we know the &lt;strong>true treatment effect&lt;/strong> before we begin: the program increases household consumption by &lt;strong>12%&lt;/strong> (0.12 log points). This known ground truth gives us a perfect benchmark to evaluate how well each econometric method recovers the correct answer.&lt;/p>
&lt;p>The tutorial progresses from simple to sophisticated. We start with basic balance checks, then estimate treatment effects three different ways using only endline data &amp;mdash; regression adjustment (RA), inverse probability weighting (IPW), and doubly robust (DR) methods. Next, we unlock the full power of panel data with difference-in-differences (DiD) and its doubly robust extension (DRDID). Finally, we address the real-world complication of imperfect compliance.&lt;/p>
&lt;h3 id="learning-objectives">Learning objectives&lt;/h3>
&lt;ul>
&lt;li>Verify baseline balance using t-tests, standardized mean differences, and balance plots&lt;/li>
&lt;li>Distinguish between ATE and ATT and identify which estimand each method targets&lt;/li>
&lt;li>Understand three estimation strategies &amp;mdash; regression adjustment, inverse probability weighting, and doubly robust &amp;mdash; and when to use each&lt;/li>
&lt;li>Estimate treatment effects using all three approaches and compare their results&lt;/li>
&lt;li>Leverage panel data structure with difference-in-differences and understand why DiD estimates ATT&lt;/li>
&lt;li>Apply doubly robust difference-in-differences (DRDID) for modern panel data analysis&lt;/li>
&lt;li>Separate the effect of treatment offer from treatment receipt under imperfect compliance&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="2-study-design">2. Study design&lt;/h2>
&lt;p>This RCT evaluates a cash transfer program designed to boost household consumption. The study tracks 2,000 households across two survey waves &amp;mdash; a &lt;strong>baseline&lt;/strong> in 2021 (before the program) and an &lt;strong>endline&lt;/strong> in 2024 (after the program was implemented). The diagram below summarizes the experimental design.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
POP[&amp;quot;&amp;lt;b&amp;gt;2,000 Households&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Balanced panel&amp;lt;br/&amp;gt;(observed in 2021 and 2024)&amp;quot;]
STRAT[&amp;quot;&amp;lt;b&amp;gt;Stratified Randomization&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Within poverty strata&amp;quot;]
TRT[&amp;quot;&amp;lt;b&amp;gt;Treatment Group&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(~1,000 households)&amp;lt;br/&amp;gt;Offered cash transfer&amp;quot;]
CTL[&amp;quot;&amp;lt;b&amp;gt;Control Group&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(~1,000 households)&amp;lt;br/&amp;gt;No offer&amp;quot;]
COMP1[&amp;quot;85% receive&amp;lt;br/&amp;gt;the transfer&amp;quot;]
COMP2[&amp;quot;15% do not&amp;lt;br/&amp;gt;receive&amp;quot;]
COMP3[&amp;quot;5% receive&amp;lt;br/&amp;gt;the transfer&amp;quot;]
COMP4[&amp;quot;95% do not&amp;lt;br/&amp;gt;receive&amp;quot;]
BASE[&amp;quot;&amp;lt;b&amp;gt;Baseline 2021&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pre-treatment survey&amp;quot;]
END[&amp;quot;&amp;lt;b&amp;gt;Endline 2024&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Post-treatment survey&amp;quot;]
POP --&amp;gt; BASE
BASE --&amp;gt; STRAT
STRAT --&amp;gt; TRT
STRAT --&amp;gt; CTL
TRT --&amp;gt; COMP1
TRT --&amp;gt; COMP2
CTL --&amp;gt; COMP3
CTL --&amp;gt; COMP4
COMP1 --&amp;gt; END
COMP2 --&amp;gt; END
COMP3 --&amp;gt; END
COMP4 --&amp;gt; END
style POP fill:#6a9bcc,stroke:#141413,color:#fff
style STRAT fill:#d97757,stroke:#141413,color:#fff
style TRT fill:#00d4c8,stroke:#141413,color:#141413
style CTL fill:#6a9bcc,stroke:#141413,color:#fff
style BASE fill:#6a9bcc,stroke:#141413,color:#fff
style END fill:#d97757,stroke:#141413,color:#fff
style COMP1 fill:#00d4c8,stroke:#141413,color:#141413
style COMP2 fill:#141413,stroke:#d97757,color:#fff
style COMP3 fill:#d97757,stroke:#141413,color:#fff
style COMP4 fill:#141413,stroke:#6a9bcc,color:#fff
&lt;/code>&lt;/pre>
&lt;p>The randomization was &lt;strong>stratified by poverty status&lt;/strong> (block randomization), ensuring that treatment and control groups started with similar proportions of poor and non-poor households. A critical real-world feature of this study is &lt;strong>imperfect compliance&lt;/strong> &amp;mdash; only 85% of households offered the treatment actually received the cash transfer, while 5% of control households received it through other channels.&lt;/p>
&lt;h3 id="variables">Variables&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Type&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>id&lt;/code>&lt;/td>
&lt;td>Household identifier&lt;/td>
&lt;td>Panel ID&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>year&lt;/code>&lt;/td>
&lt;td>Survey year (2021 or 2024)&lt;/td>
&lt;td>Time variable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>post&lt;/code>&lt;/td>
&lt;td>Endline indicator (1 = 2024)&lt;/td>
&lt;td>Binary&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>treat&lt;/code>&lt;/td>
&lt;td>Random assignment to offer (intent-to-treat)&lt;/td>
&lt;td>Binary&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>D&lt;/code>&lt;/td>
&lt;td>Actual receipt of cash transfer&lt;/td>
&lt;td>Binary (endogenous)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>y&lt;/code>&lt;/td>
&lt;td>Log monthly consumption&lt;/td>
&lt;td>Continuous (outcome)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>age&lt;/code>&lt;/td>
&lt;td>Age of household head&lt;/td>
&lt;td>Continuous&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>female&lt;/code>&lt;/td>
&lt;td>Female-headed household&lt;/td>
&lt;td>Binary&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>poverty&lt;/code>&lt;/td>
&lt;td>Poverty status at baseline&lt;/td>
&lt;td>Binary&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>edu&lt;/code>&lt;/td>
&lt;td>Years of education&lt;/td>
&lt;td>Continuous&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>y0&lt;/code>&lt;/td>
&lt;td>Log monthly consumption at baseline (pre-treatment)&lt;/td>
&lt;td>Continuous&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Offer vs. receipt&lt;/strong> &amp;mdash; The variable &lt;code>treat&lt;/code> captures random assignment to the program offer. It is exogenous (determined by randomization) and unrelated to household characteristics. The variable &lt;code>D&lt;/code> captures actual receipt of the cash transfer. It is &lt;strong>endogenous&lt;/strong> &amp;mdash; households that chose to take up the program may differ systematically from those that did not. Most methods in this tutorial estimate the effect of the &lt;strong>offer&lt;/strong> (intent-to-treat). Section 10 addresses the effect of &lt;strong>receipt&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="3-analytical-roadmap">3. Analytical roadmap&lt;/h2>
&lt;p>The diagram below shows the progression of methods we will use. Each stage builds on the previous one, adding complexity and robustness.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
A[&amp;quot;&amp;lt;b&amp;gt;Balance&amp;lt;br/&amp;gt;Checks&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 5&amp;lt;/i&amp;gt;&amp;quot;]
B[&amp;quot;&amp;lt;b&amp;gt;Cross-sectional&amp;lt;br/&amp;gt;RA / IPW / DR&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Sections 7--8&amp;lt;/i&amp;gt;&amp;quot;]
C[&amp;quot;&amp;lt;b&amp;gt;Panel Data&amp;lt;br/&amp;gt;DiD / DR-DiD&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 9&amp;lt;/i&amp;gt;&amp;quot;]
D[&amp;quot;&amp;lt;b&amp;gt;Endogenous&amp;lt;br/&amp;gt;Treatment&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Section 10&amp;lt;/i&amp;gt;&amp;quot;]
A --&amp;gt; B
B --&amp;gt; C
C --&amp;gt; D
style A fill:#6a9bcc,stroke:#141413,color:#fff
style B fill:#d97757,stroke:#141413,color:#fff
style C fill:#00d4c8,stroke:#141413,color:#141413
style D fill:#141413,stroke:#d97757,color:#fff
&lt;/code>&lt;/pre>
&lt;p>We first establish that randomization worked (balance checks). Then we estimate treatment effects three ways using only endline data &amp;mdash; regression adjustment, inverse probability weighting, and doubly robust methods. Next, we leverage the full panel structure with difference-in-differences. Finally, we address imperfect compliance by separating the effect of the offer from the effect of receipt.&lt;/p>
&lt;hr>
&lt;h2 id="4-data-loading-and-exploration">4. Data loading and exploration&lt;/h2>
&lt;p>We begin by loading the simulated dataset from a public GitHub repository and examining its structure.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/ametrics/dataSIM4RCT.dta&amp;quot;, clear
des y age edu female poverty treat D
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Contains data
Observations: 4,000
Variables: 10
Variable Storage Display Value
name type format label Variable label
─────────────────────────────────────────────────────────────
y float %9.0g Log monthly consumption
age float %9.0g
edu float %9.0g
female float %9.0g
poverty float %9.0g
treat float %9.0g Assignment to offer (Z)
D float %9.0g Receipt of cash transfer
&lt;/code>&lt;/pre>
&lt;p>The dataset contains 4,000 observations &amp;mdash; 2,000 households observed at two time points (baseline 2021 and endline 2024). The outcome variable &lt;code>y&lt;/code> is log monthly consumption, &lt;code>treat&lt;/code> is the random assignment indicator, and &lt;code>D&lt;/code> is the actual receipt indicator.&lt;/p>
&lt;p>Now let us examine summary statistics at baseline and endline separately.&lt;/p>
&lt;pre>&lt;code class="language-stata">sum y age edu female poverty treat D if post==0
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Variable | Obs Mean Std. dev. Min Max
─────────────+─────────────────────────────────────────────────────────
y | 2,000 10.0154 .4348886 8.454445 11.48253
age | 2,000 35.126 9.650839 18 68
edu | 2,000 12.0275 1.9889 6 18
female | 2,000 .5085 .5000528 0 1
poverty | 2,000 .3125 .4636283 0 1
treat | 2,000 .518 .4998009 0 1
D | 2,000 0 0 0 0
&lt;/code>&lt;/pre>
&lt;p>At baseline, mean log consumption is approximately 10.02, the average household head is 35 years old with 12 years of education, about 51% of households are female-headed, and 31% are in poverty. Treatment assignment (&lt;code>treat&lt;/code>) is approximately 50%, as expected from the randomization. Crucially, the receipt variable &lt;code>D&lt;/code> is zero for all households at baseline &amp;mdash; the program had not yet been implemented.&lt;/p>
&lt;pre>&lt;code class="language-stata">sum y age edu female poverty treat D if post==1
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Variable | Obs Mean Std. dev. Min Max
─────────────+─────────────────────────────────────────────────────────
y | 2,000 10.1137 .4382183 8.638689 11.55002
age | 2,000 35.126 9.650839 18 68
edu | 2,000 12.0275 1.9889 6 18
female | 2,000 .5085 .5000528 0 1
poverty | 2,000 .3125 .4636283 0 1
treat | 2,000 .518 .4998009 0 1
D | 2,000 .4615 .4986402 0 1
&lt;/code>&lt;/pre>
&lt;p>At endline, mean consumption has risen to approximately 10.11, reflecting both the natural time trend and the treatment effect. The receipt variable &lt;code>D&lt;/code> is now non-zero &amp;mdash; about 46% of all households received the cash transfer (combining treated households who took up the program and control households who received it through other channels).&lt;/p>
&lt;p>Finally, we declare the panel structure so Stata knows we have repeated observations.&lt;/p>
&lt;pre>&lt;code class="language-stata">xtset id year
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Panel variable: id (strongly balanced)
Time variable: year, 2021 to 2024, but with gaps
Delta: 1 unit
&lt;/code>&lt;/pre>
&lt;p>The panel is &lt;strong>strongly balanced&lt;/strong> &amp;mdash; all 2,000 households appear in both survey waves, with no attrition. This is an ideal scenario that simplifies our analysis.&lt;/p>
&lt;hr>
&lt;h2 id="5-baseline-balance-checks">5. Baseline balance checks&lt;/h2>
&lt;p>Before estimating any treatment effects, we must verify that randomization produced comparable treatment and control groups at baseline. This is the most fundamental quality check in any RCT.&lt;/p>
&lt;h3 id="51-t-tests-and-proportion-tests">5.1 T-tests and proportion tests&lt;/h3>
&lt;p>We compare the treatment and control groups on all baseline characteristics using two-sample t-tests for continuous variables and proportion tests for binary variables.&lt;/p>
&lt;pre>&lt;code class="language-stata">ttest y if post==0, by(treat)
ttest age if post==0, by(treat)
ttest edu if post==0, by(treat)
prtest female if post==0, by(treat)
prtest poverty if post==0, by(treat)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Variable | Control Mean Treat Mean Diff p-value
────────────+──────────────────────────────────────────────
y | 10.025 10.006 0.019 0.330
age | 35.335 34.931 0.404 0.350
edu | 11.974 12.077 -0.103 0.247
female | 0.484 0.531 -0.046 0.038 **
poverty | 0.307 0.318 -0.011 0.612
&lt;/code>&lt;/pre>
&lt;p>Most variables show no statistically significant differences between the treatment and control groups. However, the variable &lt;code>female&lt;/code> has a p-value of 0.038 &amp;mdash; a statistically significant imbalance. The treatment group has about 4.6 percentage points more female-headed households than the control group. This imbalance occurred purely by chance but must be addressed in our estimation.&lt;/p>
&lt;h3 id="52-balance-table-with-standardized-mean-differences">5.2 Balance table with standardized mean differences&lt;/h3>
&lt;p>P-values are sensitive to sample size &amp;mdash; a large sample can make tiny differences &amp;ldquo;significant.&amp;rdquo; Standardized mean differences (SMDs) provide a scale-free measure of imbalance that is more informative. The SMD is computed as the difference in group means divided by the pooled standard deviation &amp;mdash; this puts all variables on the same scale regardless of their units. The common rule of thumb is that SMDs below 10% indicate adequate balance.&lt;/p>
&lt;pre>&lt;code class="language-stata">capture ssc install ietoolkit, replace
iebaltab y age edu female poverty if post==0, grpvar(treat)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> (1) (2) (2)-(1)
Control Treatment Difference
y 10.025 10.006 0.019
(0.014) (0.014) (0.019)
age 35.335 34.931 0.404
(0.316) (0.295) (0.432)
edu 11.974 12.077 -0.103
(0.063) (0.063) (0.089)
female 0.484 0.531 -0.046**
(0.016) (0.016) (0.022)
poverty 0.307 0.318 -0.011
(0.015) (0.014) (0.021)
N 964 1,036
&lt;/code>&lt;/pre>
&lt;p>The balance table confirms our t-test findings. With 964 control and 1,036 treatment households, all variables are well balanced except &lt;code>female&lt;/code>, which shows a statistically significant difference (marked with **). The outcome variable &lt;code>y&lt;/code> has a negligible difference of 0.019 at baseline &amp;mdash; the groups started with essentially identical consumption levels.&lt;/p>
&lt;h3 id="53-visual-balance-plot">5.3 Visual balance plot&lt;/h3>
&lt;p>A balance plot provides a visual overview of all SMDs at once, making it easy to spot problematic variables.&lt;/p>
&lt;pre>&lt;code class="language-stata">net install balanceplot, from(&amp;quot;https://tdmize.github.io/data&amp;quot;) replace
balanceplot y age edu i.female i.poverty, group(treat) table nodropdv
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_rct_balance_plot.png" alt="Balance plot showing standardized mean differences for all covariates. All variables fall within the 10% threshold, with female closest at approximately 9.3%.">&lt;/p>
&lt;p>The balance plot shows that all SMDs fall below the 10% threshold (indicated by the dashed vertical lines). The variable &lt;code>female&lt;/code> has the largest SMD at approximately 9.3% &amp;mdash; close to but still below the conventional threshold. The remaining variables &amp;mdash; consumption, age, education, and poverty &amp;mdash; all have SMDs well below 5%. Overall, randomization was successful, but we should control for &lt;code>female&lt;/code> (and other covariates) in our estimation to improve precision.&lt;/p>
&lt;h3 id="54-aipw-as-a-formal-balance-test">5.4 AIPW as a formal balance test&lt;/h3>
&lt;p>As a final and more formal balance check, we can use the Augmented Inverse Probability Weighting (AIPW) estimator on &lt;strong>baseline data only&lt;/strong>. If randomization was successful, the estimated &amp;ldquo;treatment effect&amp;rdquo; at baseline should be zero &amp;mdash; since the program had not yet been implemented, there should be no difference between groups.&lt;/p>
&lt;pre>&lt;code class="language-stata">preserve
keep if post==0
teffects aipw (y age edu i.female i.poverty) (treat age edu i.female i.poverty)
&lt;/code>&lt;/pre>
&lt;blockquote>
&lt;p>&lt;strong>Tip:&lt;/strong> The &lt;code>preserve&lt;/code> command saves a snapshot of the current data. After the balance analysis, use &lt;code>restore&lt;/code> to return to the full dataset. The companion do-file handles this automatically.&lt;/p>
&lt;/blockquote>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : augmented IPW
Outcome model : linear
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATE |
treat |
(1 vs 0) | -.0244086 .018861 -1.29 0.196 -.0613754 .0125582
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.02792 .0138363 724.75 0.000 10.0008 10.05504
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The AIPW-estimated &amp;ldquo;ATE&amp;rdquo; at baseline is -0.024 with a p-value of 0.196 &amp;mdash; not statistically significant. This confirms that there is no detectable pre-treatment difference between the groups after adjusting for covariates. The treatment and control groups were statistically comparable before the program began.&lt;/p>
&lt;p>Now we run the diagnostic checks for the AIPW model.&lt;/p>
&lt;pre>&lt;code class="language-stata">tebalance overid
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Overidentification test for covariate balance
H0: Covariates are balanced
chi2(5) = 3.216
Prob &amp;gt; chi2 = 0.6670
&lt;/code>&lt;/pre>
&lt;p>The overidentification test fails to reject the null hypothesis of covariate balance (p = 0.667). There is no statistical evidence of residual imbalance after weighting.&lt;/p>
&lt;pre>&lt;code class="language-stata">tebalance summarize
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> |Standardized differences Variance ratio
| Raw Weighted Raw Weighted
----------------+------------------------------------------------
age | -.0417918 .0002505 .9318894 .9446877
edu | .0519015 -6.96e-06 1.071677 1.078214
female |
1 | .0929611 6.51e-06 .9970775 .9999996
poverty |
1 | .0226764 .0002864 1.018475 1.000233
&lt;/code>&lt;/pre>
&lt;p>The balance summary reveals that the raw standardized differences (before weighting) show the &lt;code>female&lt;/code> imbalance at 0.093, consistent with our earlier findings. After weighting, all standardized differences shrink to near zero (all below 0.001) &amp;mdash; excellent balance. The variance ratios are all close to 1.0, indicating similar spread across groups.&lt;/p>
&lt;pre>&lt;code class="language-stata">tebalance density y
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_rct_density_y.png" alt="Density plot showing the distribution of log consumption for treatment and control groups, before and after AIPW weighting. The weighted distributions overlap almost perfectly.">&lt;/p>
&lt;p>The density plot confirms that after AIPW weighting, the distributions of log consumption in the treatment and control groups overlap almost perfectly. Any small pre-existing differences in the outcome variable have been eliminated by the weighting scheme.&lt;/p>
&lt;pre>&lt;code class="language-stata">teffects overlap
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="stata_rct_overlap_baseline.png" alt="Overlap plot showing kernel densities of estimated propensity scores for treatment and control groups. Both distributions span approximately 0.43 to 0.55 with substantial overlap.">&lt;/p>
&lt;p>The overlap plot shows that propensity scores for both groups are concentrated between approximately 0.43 and 0.55 &amp;mdash; well within the range where matching and weighting are feasible. There are no extreme propensity scores near 0 or 1, confirming that the common support condition is satisfied. This is expected in a well-designed RCT where treatment probability is approximately 0.50 for all households.&lt;/p>
&lt;pre>&lt;code class="language-stata">restore
&lt;/code>&lt;/pre>
&lt;p>This AIPW-based balance analysis also serves a pedagogical purpose: it introduces the concept of &lt;strong>doubly robust&lt;/strong> estimation before we use it for treatment effect estimation in Section 8.&lt;/p>
&lt;hr>
&lt;h2 id="6-what-are-we-estimating-ate-vs-att">6. What are we estimating? ATE vs. ATT&lt;/h2>
&lt;p>Before diving into estimation, we need to be precise about &lt;strong>what&lt;/strong> we are trying to estimate. There are two fundamental causal quantities in program evaluation.&lt;/p>
&lt;p>The &lt;strong>Average Treatment Effect (ATE)&lt;/strong> answers the policymaker&amp;rsquo;s question: &lt;em>&amp;ldquo;What would happen if we scaled this program to the entire population?&amp;quot;&lt;/em>&lt;/p>
&lt;p>$$ATE = E[Y(1) - Y(0)]$$&lt;/p>
&lt;p>where $Y(1)$ is the potential outcome under treatment and $Y(0)$ is the potential outcome under control, averaged over the &lt;strong>entire population&lt;/strong> (both treated and untreated).&lt;/p>
&lt;p>The &lt;strong>Average Treatment Effect on the Treated (ATT)&lt;/strong> answers the evaluator&amp;rsquo;s question: &lt;em>&amp;ldquo;Did the program benefit those who were assigned to it?&amp;quot;&lt;/em>&lt;/p>
&lt;p>$$ATT = E[Y(1) - Y(0) \mid T = 1]$$&lt;/p>
&lt;p>This averages the treatment effect only over the &lt;strong>treated group&lt;/strong> &amp;mdash; the households that were assigned to receive the cash transfer.&lt;/p>
&lt;p>In a well-designed RCT with &lt;strong>homogeneous treatment effects&lt;/strong> (the program affects everyone equally), ATE and ATT are the same. But when treatment effects are &lt;strong>heterogeneous&lt;/strong> (the program benefits some households more than others), they can differ. For example, if poorer households benefit more from cash transfers and the treatment group has a higher share of poor households, the ATT could be larger than the ATE.&lt;/p>
&lt;p>Understanding this distinction is critical because different methods target different estimands. Cross-sectional methods (RA, IPW, DR) can estimate &lt;strong>either&lt;/strong> ATE or ATT. Difference-in-differences inherently estimates the &lt;strong>ATT only&lt;/strong>. We will return to this point in Section 9.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Note on RCTs&lt;/strong> &amp;mdash; In a randomized experiment, treatment assignment is independent of potential outcomes. This means that simple comparisons between treatment and control groups are already unbiased estimates of the ATE. When we add covariates (regression adjustment, IPW, doubly robust), we are not removing bias &amp;mdash; we are &lt;strong>improving precision&lt;/strong> by accounting for residual variation. This is different from observational studies, where covariate adjustment is needed to address confounding.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="7-three-strategies-for-causal-estimation">7. Three strategies for causal estimation&lt;/h2>
&lt;p>We now understand &lt;em>what&lt;/em> we want to estimate (ATE and ATT from Section 6). The question becomes &lt;em>how&lt;/em> to estimate it. Three families of methods exist, each taking a fundamentally different approach to solving the missing-data problem at the heart of causal inference. Each method models a different part of the data-generating process, and understanding these differences is essential for interpreting results and choosing the right tool.&lt;/p>
&lt;h3 id="71-regression-adjustment-ra-----modeling-the-outcome">7.1 Regression Adjustment (RA) &amp;mdash; modeling the outcome&lt;/h3>
&lt;p>Regression adjustment solves the missing-data problem by &lt;strong>predicting the unobserved potential outcomes&lt;/strong>. It fits separate regression models for treated and untreated groups. For each household, it uses these models to predict two potential outcomes: what consumption would be if treated, $\hat{\mu}_1(X_i)$, and what consumption would be if untreated, $\hat{\mu}_0(X_i)$. Since we only observe one of these for each household, the model fills in the missing counterfactual. The treatment effect for each household is the difference between the two predictions, and the ATE is the average across all households.&lt;/p>
&lt;p>The Stata documentation describes this succinctly: &lt;em>&amp;ldquo;RA estimators use means of predicted outcomes for each treatment level to estimate each POM. ATEs and ATETs are differences in estimated POMs.&amp;quot;&lt;/em>&lt;/p>
&lt;p>&lt;strong>Analogy &amp;mdash; predicting exam scores.&lt;/strong> Imagine two study methods (A and B) being tested on students. You observe each student using only one method. RA fits a model predicting test scores based on student characteristics (prior GPA, hours studied) separately for method-A and method-B users. Then, for &lt;em>every&lt;/em> student, it predicts what their score would have been under &lt;em>both&lt;/em> methods &amp;mdash; even the one they did not use. The average difference in predicted scores is the treatment effect.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
DATA[&amp;quot;&amp;lt;b&amp;gt;Observed Data&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Each household observed&amp;lt;br/&amp;gt;under ONE treatment only&amp;quot;]
M0[&amp;quot;&amp;lt;b&amp;gt;Fit outcome model&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;using control group&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Y = f(age, edu, female, poverty)&amp;lt;/i&amp;gt;&amp;quot;]
M1[&amp;quot;&amp;lt;b&amp;gt;Fit outcome model&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;using treated group&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Y = f(age, edu, female, poverty)&amp;lt;/i&amp;gt;&amp;quot;]
P0[&amp;quot;Predict &amp;lt;b&amp;gt;Ŷ₀&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;for ALL households&amp;quot;]
P1[&amp;quot;Predict &amp;lt;b&amp;gt;Ŷ₁&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;for ALL households&amp;quot;]
ATE[&amp;quot;&amp;lt;b&amp;gt;ATE&amp;lt;/b&amp;gt; = Average of&amp;lt;br/&amp;gt;(Ŷ₁ − Ŷ₀)&amp;quot;]
DATA --&amp;gt; M0
DATA --&amp;gt; M1
M0 --&amp;gt; P0
M1 --&amp;gt; P1
P0 --&amp;gt; ATE
P1 --&amp;gt; ATE
style DATA fill:#141413,stroke:#6a9bcc,color:#fff
style M0 fill:#6a9bcc,stroke:#141413,color:#fff
style M1 fill:#6a9bcc,stroke:#141413,color:#fff
style P0 fill:#6a9bcc,stroke:#141413,color:#fff
style P1 fill:#6a9bcc,stroke:#141413,color:#fff
style ATE fill:#6a9bcc,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>The RA estimator.&lt;/strong> Formally, the ATE under regression adjustment is:&lt;/p>
&lt;p>$$\hat{\tau}_{RA}^{ATE} = \frac{1}{N} \sum_{i=1}^{N} \left[ \hat{\mu}_1(X_i) - \hat{\mu}_0(X_i) \right]$$&lt;/p>
&lt;p>where $\hat{\mu}_1(X)$ is the predicted outcome under treatment (fitted from treated observations) and $\hat{\mu}_0(X)$ is the predicted outcome under control (fitted from untreated observations), both evaluated at each household&amp;rsquo;s covariates $X_i$. In plain language: for each household, the model predicts what their consumption would be if they received the cash transfer and what it would be if they did not. The difference is the household&amp;rsquo;s estimated treatment effect. Averaging these across all $N$ households gives the ATE.&lt;/p>
&lt;p>For the ATT, we restrict the average to treated units only:&lt;/p>
&lt;p>$$\hat{\tau}_{RA}^{ATT} = \frac{1}{N_1} \sum_{i: T_i = 1} \left[ \hat{\mu}_1(X_i) - \hat{\mu}_0(X_i) \right]$$&lt;/p>
&lt;p>where $N_1$ is the number of treated households.&lt;/p>
&lt;p>&lt;strong>Mini example from our data.&lt;/strong> Consider Household A: a 40-year-old female in poverty with 10 years of education. The treated outcome model predicts her consumption at 10.17 log points. The untreated outcome model predicts 10.05. Her estimated individual treatment effect is $10.17 - 10.05 = 0.12$. Averaging such predictions over all 2,000 endline households gives the ATE.&lt;/p>
&lt;p>&lt;strong>Stata implementation.&lt;/strong> The &lt;code>teffects ra&lt;/code> command fits linear outcome models by default. The first parenthesis specifies the outcome model (outcome variable + covariates), and the second specifies the treatment variable: &lt;code>teffects ra (y c.age c.edu i.female i.poverty) (treat), ate&lt;/code>.&lt;/p>
&lt;p>&lt;strong>What can go wrong &amp;mdash; model misspecification.&lt;/strong> RA&amp;rsquo;s Achilles heel is that it relies entirely on the outcome model being correctly specified. If consumption depends on age nonlinearly (for example, a U-shaped relationship), but we assume a linear model, the predictions $\hat{\mu}_1$ and $\hat{\mu}_0$ will be systematically wrong, biasing the ATE. As the Stata manual notes, RA works well when the outcome model is correct, but &amp;ldquo;relying on a correctly specified outcome model with little data is extremely risky.&amp;rdquo; RA gives the right answer &lt;strong>only if the outcome model is correct&lt;/strong>. If it is wrong, the ATE estimate can be biased even with infinite data.&lt;/p>
&lt;p>What if we are unsure about the functional form of the outcome model? Is there an approach that avoids modeling the outcome entirely?&lt;/p>
&lt;h3 id="72-inverse-probability-weighting-ipw-----modeling-the-treatment-assignment">7.2 Inverse Probability Weighting (IPW) &amp;mdash; modeling the treatment assignment&lt;/h3>
&lt;p>IPW takes the opposite approach. Instead of modeling consumption, it models the probability of being assigned to treatment &amp;mdash; the &lt;strong>propensity score&lt;/strong>, defined as $p(X) = \Pr(T = 1 \mid X)$. It then reweights observations so that the treatment and control groups become comparable. The Stata documentation explains: &lt;em>&amp;ldquo;IPW estimators use weighted averages of the observed outcome variable to estimate means of the potential outcomes. The weights account for the missing data inherent in the potential-outcome framework.&amp;quot;&lt;/em>&lt;/p>
&lt;p>The logic is elegant: in a perfectly randomized experiment, every household has the same 50% chance of treatment, and a simple comparison of means is unbiased. When chance imbalances arise (like our 9.3% gender SMD), the estimated propensity scores deviate slightly from 0.50. IPW corrects for these imbalances by making the reweighted sample look as if randomization had been perfect &amp;mdash; without ever modeling the outcome.&lt;/p>
&lt;p>&lt;strong>Analogy &amp;mdash; opinion polling.&lt;/strong> Election pollsters know their survey overrepresents some demographics. If 60% of respondents are college graduates but only 35% of voters are, pollsters give lower weight to each college graduate&amp;rsquo;s response and higher weight to non-graduates. IPW does the same thing for treatment groups &amp;mdash; it reweights households so the treated and control groups have the same covariate distribution.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
DATA[&amp;quot;&amp;lt;b&amp;gt;Observed Data&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Treatment and control groups&amp;lt;br/&amp;gt;may have imbalances&amp;quot;]
PS[&amp;quot;&amp;lt;b&amp;gt;Estimate propensity score&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;p(X) = Pr(T=1 | X)&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;via logistic regression&amp;lt;/i&amp;gt;&amp;quot;]
WT[&amp;quot;&amp;lt;b&amp;gt;Compute weights&amp;lt;/b&amp;gt;&amp;quot;]
WTR[&amp;quot;Treated: weight = 1/p(X)&amp;quot;]
WCT[&amp;quot;Control: weight = 1/(1−p(X))&amp;quot;]
ATE[&amp;quot;&amp;lt;b&amp;gt;ATE&amp;lt;/b&amp;gt; = Weighted mean(treated)&amp;lt;br/&amp;gt;− Weighted mean(control)&amp;quot;]
DATA --&amp;gt; PS
PS --&amp;gt; WT
WT --&amp;gt; WTR
WT --&amp;gt; WCT
WTR --&amp;gt; ATE
WCT --&amp;gt; ATE
style DATA fill:#141413,stroke:#d97757,color:#fff
style PS fill:#d97757,stroke:#141413,color:#fff
style WT fill:#d97757,stroke:#141413,color:#fff
style WTR fill:#d97757,stroke:#141413,color:#fff
style WCT fill:#d97757,stroke:#141413,color:#fff
style ATE fill:#d97757,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>The propensity score.&lt;/strong> The propensity score is estimated via logistic regression:&lt;/p>
&lt;p>$$\hat{p}(X_i) = \Pr(T_i = 1 \mid X_i) = \text{logit}^{-1}(\hat{\alpha} + \hat{\beta}' X_i)$$&lt;/p>
&lt;p>In plain language: we fit a logistic model predicting whether each household was assigned to treatment, based on their covariates (age, education, gender, poverty status). The predicted probability is their propensity score.&lt;/p>
&lt;p>&lt;strong>The IPW estimator.&lt;/strong> The ATE under IPW is:&lt;/p>
&lt;p>$$\hat{\tau}_{IPW}^{ATE} = \frac{1}{N} \sum_{i=1}^{N} \left[ \frac{T_i \cdot Y_i}{\hat{p}(X_i)} - \frac{(1 - T_i) \cdot Y_i}{1 - \hat{p}(X_i)} \right]$$&lt;/p>
&lt;p>Each treated household&amp;rsquo;s outcome is divided by its probability of being treated &amp;mdash; this upweights treated households that &amp;ldquo;look like&amp;rdquo; control households (the Stata manual calls this placing &amp;ldquo;a larger weight on those observations for which $y_{1i}$ is observed even though its observation was not likely&amp;rdquo;). Each control household&amp;rsquo;s outcome is divided by its probability of being in the control group. The reweighting creates a pseudo-population where treatment assignment is independent of covariates.&lt;/p>
&lt;p>For the ATT, only the control group needs reweighting (because the treated group is already the reference population):&lt;/p>
&lt;p>$$\hat{\tau}_{IPW}^{ATT} = \frac{1}{N_1} \sum_{i=1}^{N} \left[ T_i \cdot Y_i - \frac{(1 - T_i) \cdot \hat{p}(X_i) \cdot Y_i}{1 - \hat{p}(X_i)} \right]$$&lt;/p>
&lt;p>&lt;strong>Mini example from our data.&lt;/strong> In our RCT, a female household in poverty might have $\hat{p}(X) = 0.52$ (slightly more likely to be treated due to the gender imbalance). If treated, her weight is $1/0.52 = 1.92$. If in the control group, her weight is $1/(1 - 0.52) = 2.08$. A male non-poor household might have $\hat{p}(X) = 0.49$, giving weights close to 2.0 in either group. These mild adjustments rebalance the groups to remove the chance gender imbalance.&lt;/p>
&lt;p>&lt;strong>Why IPW matters even in RCTs.&lt;/strong> In a perfect RCT, the true propensity score is exactly 0.50 for everyone, and IPW does nothing. But finite samples produce chance imbalances. IPW uses the estimated propensity scores (which deviate slightly from 0.50) to correct for these imbalances without making any assumptions about how covariates affect the outcome.&lt;/p>
&lt;p>&lt;strong>Stata implementation.&lt;/strong> The &lt;code>teffects ipw&lt;/code> command fits a logistic treatment model by default. Note that the first parenthesis specifies only the outcome variable (no covariates &amp;mdash; IPW does not model the outcome), and the second specifies the treatment model: &lt;code>teffects ipw (y) (treat c.age c.edu i.female i.poverty), ate&lt;/code>.&lt;/p>
&lt;p>&lt;strong>What can go wrong &amp;mdash; extreme weights.&lt;/strong> IPW&amp;rsquo;s vulnerability is extreme propensity scores. If $\hat{p}(X) = 0.01$ for some household, the weight becomes $1/0.01 = 100$ &amp;mdash; that single household dominates the ATE estimate, causing high variance and instability. The Stata manual warns: &lt;em>&amp;ldquo;When propensity scores are extreme (near 0 or 1), the inverse weights become very large, producing unstable estimates.&amp;quot;&lt;/em> This happens when the treatment and control groups have poor &lt;strong>overlap&lt;/strong> &amp;mdash; some covariate combinations appear only in one group. In our well-designed RCT, all propensity scores are between 0.43 and 0.55 (we verified this in Section 5.4), so extreme weights are not a concern.&lt;/p>
&lt;p>RA works well if the outcome model is correct but can be biased if it is wrong. IPW works well if the propensity score model is correct but can be unstable if it is wrong. Is there a method that protects us against both types of misspecification?&lt;/p>
&lt;h3 id="73-doubly-robust-dr-----modeling-both">7.3 Doubly Robust (DR) &amp;mdash; modeling both&lt;/h3>
&lt;p>Doubly robust methods combine RA and IPW into a single estimator. They fit an outcome model &lt;strong>and&lt;/strong> estimate a propensity score. The key property &amp;mdash; the reason they are called &amp;ldquo;doubly robust&amp;rdquo; &amp;mdash; is that the estimator is consistent (converges to the true treatment effect with enough data) if &lt;strong>either&lt;/strong> the outcome model &lt;strong>or&lt;/strong> the propensity score model is correctly specified. You do not need both to be right &amp;mdash; just one.&lt;/p>
&lt;p>The Stata manual describes this property: &lt;em>&amp;ldquo;AIPW estimators model both the outcome and the treatment probability. A surprising fact is that only one of the two models must be correctly specified to consistently estimate the treatment effects.&amp;quot;&lt;/em>&lt;/p>
&lt;p>&lt;strong>Analogy &amp;mdash; backup power.&lt;/strong> Think of a house with two independent power sources: the electrical grid (the outcome model) and a solar panel system (the propensity score model). If the grid goes down (outcome model is misspecified), solar power keeps the lights on. If clouds block the solar panels (propensity score model is wrong), the grid still works. As long as at least one power source is functioning, the house stays lit. That is doubly robust estimation &amp;mdash; as long as at least one model is correct, the estimator gives the right answer.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
DATA[&amp;quot;&amp;lt;b&amp;gt;Observed Data&amp;lt;/b&amp;gt;&amp;quot;]
RA_C[&amp;quot;&amp;lt;b&amp;gt;RA component&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Predict Ŷ₁ and Ŷ₀&amp;lt;br/&amp;gt;for each household&amp;quot;]
IPW_C[&amp;quot;&amp;lt;b&amp;gt;IPW component&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Estimate propensity&amp;lt;br/&amp;gt;score p(X)&amp;quot;]
RESID[&amp;quot;&amp;lt;b&amp;gt;Prediction errors&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Y − Ŷ for each&amp;lt;br/&amp;gt;household&amp;quot;]
CORRECT[&amp;quot;&amp;lt;b&amp;gt;Bias-correction term&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;IPW-weighted residuals&amp;quot;]
DR[&amp;quot;&amp;lt;b&amp;gt;DR estimate&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;= RA prediction&amp;lt;br/&amp;gt;+ Bias correction&amp;quot;]
DATA --&amp;gt; RA_C
DATA --&amp;gt; IPW_C
RA_C --&amp;gt; RESID
IPW_C --&amp;gt; CORRECT
RESID --&amp;gt; CORRECT
RA_C --&amp;gt; DR
CORRECT --&amp;gt; DR
style DATA fill:#141413,stroke:#00d4c8,color:#fff
style RA_C fill:#6a9bcc,stroke:#141413,color:#fff
style IPW_C fill:#d97757,stroke:#141413,color:#fff
style RESID fill:#6a9bcc,stroke:#141413,color:#fff
style CORRECT fill:#d97757,stroke:#141413,color:#fff
style DR fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>The AIPW estimator.&lt;/strong> The most common doubly robust form is Augmented Inverse Probability Weighting (AIPW):&lt;/p>
&lt;p>$$\hat{\tau}_{DR}^{ATE} = \frac{1}{N} \sum_{i=1}^{N} \left[ \hat{\mu}_1(X_i) - \hat{\mu}_0(X_i) + \frac{T_i (Y_i - \hat{\mu}_1(X_i))}{\hat{p}(X_i)} - \frac{(1 - T_i)(Y_i - \hat{\mu}_0(X_i))}{1 - \hat{p}(X_i)} \right]$$&lt;/p>
&lt;p>This equation has two clearly interpretable components:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>RA component&lt;/strong> (first two terms): $\hat{\mu}_1(X_i) - \hat{\mu}_0(X_i)$ &amp;mdash; the regression adjustment prediction, exactly as in Section 7.1&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Bias-correction component&lt;/strong> (last two terms): IPW-weighted residuals $(Y_i - \hat{\mu})$ &amp;mdash; the difference between actual and predicted outcomes, weighted by inverse propensity scores&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>In plain language: start with the RA prediction of each household&amp;rsquo;s treatment effect. Then ask: how far off was that prediction from reality? Weight those prediction errors by the propensity score. If RA was already right, the errors average to zero and you just get RA. If RA was wrong but IPW is right, the weighted errors exactly cancel the RA bias.&lt;/p>
&lt;p>&lt;strong>Why the magic works &amp;mdash; four scenarios.&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Outcome model correct, propensity model wrong:&lt;/strong> The residuals $(Y_i - \hat{\mu})$ are zero on average, so the correction terms vanish. DR reduces to RA. Correct answer.&lt;/li>
&lt;li>&lt;strong>Propensity model correct, outcome model wrong:&lt;/strong> The IPW reweighting is valid, so the correction terms fix the RA bias. Correct answer.&lt;/li>
&lt;li>&lt;strong>Both models correct:&lt;/strong> Both components work together, producing the most efficient estimate.&lt;/li>
&lt;li>&lt;strong>Both models wrong:&lt;/strong> Neither safety net catches the error. The estimate can be biased. DR provides insurance, not invincibility.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>AIPW vs. IPWRA in Stata.&lt;/strong> Stata offers two doubly robust commands. &lt;code>teffects aipw&lt;/code> augments the IPW estimator with an outcome-model correction (the equation above). &lt;code>teffects ipwra&lt;/code> applies propensity score weights to the regression adjustment &amp;mdash; arriving at the same property from the other direction. Both are doubly robust and produce nearly identical results in practice.&lt;/p>
&lt;p>&lt;strong>Stata implementation.&lt;/strong> Both commands require specifying the outcome model in the first parenthesis and the treatment model in the second: &lt;code>teffects ipwra (y c.age c.edu i.female i.poverty) (treat c.age c.edu i.female i.poverty), vce(robust)&lt;/code>.&lt;/p>
&lt;p>&lt;strong>What can go wrong.&lt;/strong> DR fails only when &lt;strong>both&lt;/strong> models are wrong. This is much less likely than either single model being wrong &amp;mdash; getting at least one model approximately right is much easier than getting both perfectly right. However, the Stata manual notes: &lt;em>&amp;ldquo;When both the outcome and the treatment model are misspecified, which estimator is more robust is a matter of debate.&amp;quot;&lt;/em> Using flexible specifications (polynomials, interactions) reduces the risk of both models failing simultaneously.&lt;/p>
&lt;h3 id="comparison-of-the-three-approaches">Comparison of the three approaches&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Feature&lt;/th>
&lt;th>RA&lt;/th>
&lt;th>IPW&lt;/th>
&lt;th>DR (AIPW/IPWRA)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Models the outcome?&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Models the treatment?&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Key equation&lt;/td>
&lt;td>$\hat{\mu}_1(X) - \hat{\mu}_0(X)$&lt;/td>
&lt;td>$T \cdot Y / \hat{p}(X)$&lt;/td>
&lt;td>RA + IPW-weighted residuals&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Consistent if outcome model correct?&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&amp;mdash;&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Consistent if treatment model correct?&lt;/td>
&lt;td>&amp;mdash;&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Main vulnerability&lt;/td>
&lt;td>Outcome misspecification&lt;/td>
&lt;td>Extreme weights&lt;/td>
&lt;td>Both models wrong&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stata command&lt;/td>
&lt;td>&lt;code>teffects ra&lt;/code>&lt;/td>
&lt;td>&lt;code>teffects ipw&lt;/code>&lt;/td>
&lt;td>&lt;code>teffects ipwra&lt;/code> / &lt;code>teffects aipw&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;pre>&lt;code class="language-mermaid">graph LR
RA[&amp;quot;&amp;lt;b&amp;gt;Regression Adjustment&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Models the outcome&amp;quot;]
IPW[&amp;quot;&amp;lt;b&amp;gt;Inverse Probability&amp;lt;br/&amp;gt;Weighting&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Models the treatment&amp;quot;]
DR[&amp;quot;&amp;lt;b&amp;gt;Doubly Robust&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Models both&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;Consistent if either&amp;lt;br/&amp;gt;model is correct&amp;lt;/i&amp;gt;&amp;quot;]
RA --&amp;gt; DR
IPW --&amp;gt; DR
style RA fill:#6a9bcc,stroke:#141413,color:#fff
style IPW fill:#d97757,stroke:#141413,color:#fff
style DR fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;p>The doubly robust estimator combines the strengths of both RA and IPW. It is the &lt;strong>standard recommendation in modern causal inference&lt;/strong> because it provides an extra layer of protection against model misspecification. Now that we understand what each method does, what it assumes, and what can go wrong, let us apply all three to our cash transfer data and compare their results.&lt;/p>
&lt;hr>
&lt;h2 id="8-cross-sectional-estimation-at-endline-----ra-ipw-and-dr">8. Cross-sectional estimation at endline &amp;mdash; RA, IPW, and DR&lt;/h2>
&lt;p>We now estimate treatment effects using only endline data. For each method, we compute both the &lt;strong>ATE&lt;/strong> (the policymaker&amp;rsquo;s quantity) and the &lt;strong>ATT&lt;/strong> (the evaluator&amp;rsquo;s quantity).&lt;/p>
&lt;h3 id="81-simple-difference-in-means">8.1 Simple difference in means&lt;/h3>
&lt;p>The simplest approach is to compare mean outcomes between treated and control groups at endline.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/ametrics/dataSIM4RCT.dta&amp;quot;, clear
keep if post==1
reg y treat, robust
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Linear regression Number of obs = 2,000
F(1, 1998) = 35.43
Prob &amp;gt; F = 0.0000
R-squared = 0.0174
Root MSE = .43449
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
treat | .1157465 .0194443 5.95 0.000 .0776132 .1538798
_cons | 10.05374 .014001 718.07 0.000 10.02628 10.0812
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The simple difference in means yields an estimate of 0.116 (SE = 0.019, p &amp;lt; 0.001, 95% CI [0.078, 0.154]). Because the outcome is in logs, this means being offered the cash transfer increased household consumption by approximately 11.6%. This estimate is close to the true effect of 12% and is our benchmark for comparison. However, it does not adjust for the gender imbalance we discovered at baseline.&lt;/p>
&lt;h3 id="82-regression-adjustment-----ate-and-att">8.2 Regression Adjustment &amp;mdash; ATE and ATT&lt;/h3>
&lt;p>Regression adjustment models the outcome as a function of treatment and covariates, then computes predicted outcomes under treatment and control for each observation.&lt;/p>
&lt;pre>&lt;code class="language-stata">* RA: Average Treatment Effect (ATE)
teffects ra (y c.age c.edu i.female i.poverty) (treat), ate
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : regression adjustment
Outcome model : linear
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATE |
treat |
(1 vs 0) | .1125431 .0190927 5.89 0.000 .0751221 .1499641
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.05503 .0138703 724.93 0.000 10.02785 10.08222
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-stata">* RA: Average Treatment Effect on the Treated (ATT)
teffects ra (y c.age c.edu i.female i.poverty) (treat), atet
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : regression adjustment
Outcome model : linear
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATET |
treat |
(1 vs 0) | .1132537 .0191498 5.91 0.000 .0757208 .1507865
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.05623 .0140082 717.88 0.000 10.02878 10.08369
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The RA estimates are ATE = 0.113 (SE = 0.019, 95% CI [0.075, 0.150]) and ATT = 0.113 (SE = 0.019, 95% CI [0.076, 0.151]). The ATE and ATT are nearly identical, which confirms that treatment effects are approximately &lt;strong>homogeneous&lt;/strong> across households. The RA approach models the outcome with covariates (age, education, gender, poverty), which adjusts for the baseline gender imbalance and can improve precision.&lt;/p>
&lt;h3 id="83-inverse-probability-weighting-----ate-and-att">8.3 Inverse Probability Weighting &amp;mdash; ATE and ATT&lt;/h3>
&lt;p>IPW reweights observations based on their estimated probability of treatment, without modeling the outcome.&lt;/p>
&lt;pre>&lt;code class="language-stata">* IPW: Average Treatment Effect (ATE)
teffects ipw (y) (treat c.age c.edu i.female i.poverty), ate
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : inverse-probability weights
Outcome model : weighted mean
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATE |
treat |
(1 vs 0) | .1126713 .0190886 5.90 0.000 .0752583 .1500844
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.05495 .0138651 725.20 0.000 10.02778 10.08213
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-stata">* IPW: Average Treatment Effect on the Treated (ATT)
teffects ipw (y) (treat c.age c.edu i.female i.poverty), atet
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : inverse-probability weights
Outcome model : weighted mean
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATET |
treat |
(1 vs 0) | .1134031 .0191397 5.93 0.000 .0758899 .1509162
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.05608 .0140004 718.27 0.000 10.02864 10.08352
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The IPW estimates are ATE = 0.113 (SE = 0.019, 95% CI [0.075, 0.150]) and ATT = 0.113 (SE = 0.019, 95% CI [0.076, 0.151]). These are very close to the RA results, which is expected in a well-designed RCT where propensity scores are near 0.50 for all households. Notice that IPW does &lt;strong>not&lt;/strong> model the outcome &amp;mdash; it only models the treatment assignment process using the propensity score. The close agreement between RA and IPW gives us confidence that both the outcome model and the treatment model are approximately correct.&lt;/p>
&lt;h3 id="84-doubly-robust-----ate-and-att-ipwra">8.4 Doubly Robust &amp;mdash; ATE and ATT (IPWRA)&lt;/h3>
&lt;p>The doubly robust IPWRA estimator combines outcome modeling and propensity score weighting.&lt;/p>
&lt;pre>&lt;code class="language-stata">* IPWRA: Average Treatment Effect (ATE)
teffects ipwra (y c.age c.edu i.female i.poverty) ///
(treat c.age c.edu i.female i.poverty), vce(robust)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : IPW regression adjustment
Outcome model : linear
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATE |
treat |
(1 vs 0) | .112639 .0190901 5.90 0.000 .0752231 .1500549
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.055 .0138677 725.07 0.000 10.02782 10.08218
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-stata">* IPWRA: Average Treatment Effect on the Treated (ATT)
teffects ipwra (y c.age c.edu i.female i.poverty) ///
(treat c.age c.edu i.female i.poverty), atet vce(robust)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : IPW regression adjustment
Outcome model : linear
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATET |
treat |
(1 vs 0) | .1133162 .0191469 5.92 0.000 .0757889 .1508435
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.05617 .0140019 718.20 0.000 10.02873 10.08361
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The doubly robust IPWRA estimates are ATE = 0.113 (SE = 0.019, 95% CI [0.075, 0.150]) and ATT = 0.113 (SE = 0.019, 95% CI [0.076, 0.151]). These are very close to the RA and IPW estimates, confirming that all three approaches converge in this well-designed RCT. The DR method provides the most reliable cross-sectional estimate because it is protected against misspecification of either the outcome or treatment model.&lt;/p>
&lt;h3 id="85-doubly-robust-----aipw-alternative">8.5 Doubly Robust &amp;mdash; AIPW alternative&lt;/h3>
&lt;p>As a robustness check, we can also compute the doubly robust estimate using the AIPW formulation instead of IPWRA.&lt;/p>
&lt;pre>&lt;code class="language-stata">* AIPW: Average Treatment Effect (ATE)
teffects aipw (y c.age c.edu i.female i.poverty) ///
(treat c.age c.edu i.female i.poverty)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : augmented IPW
Outcome model : linear by ML
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATE |
treat |
(1 vs 0) | .1126412 .0190903 5.90 0.000 .075225 .1500574
─────────────+────────────────────────────────────────────────────────────────
POmean |
treat |
0 | 10.055 .013868 725.05 0.000 10.02782 10.08218
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The AIPW estimate of ATE = 0.113 (SE = 0.019, 95% CI [0.075, 0.150]) is virtually identical to the IPWRA result (0.113). Both are doubly robust &amp;mdash; the difference lies in the computational approach (AIPW augments the IPW estimator with a bias-correction term, while IPWRA applies IPW weights to the regression adjustment), but the theoretical properties and estimates are the same.&lt;/p>
&lt;h3 id="86-cross-sectional-comparison">8.6 Cross-sectional comparison&lt;/h3>
&lt;p>The table below summarizes all cross-sectional estimates.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Approach&lt;/th>
&lt;th>Estimand&lt;/th>
&lt;th style="text-align:center">Estimate&lt;/th>
&lt;th style="text-align:center">SE&lt;/th>
&lt;th style="text-align:center">95% CI&lt;/th>
&lt;th style="text-align:center">Contains 0.12?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Simple regression&lt;/td>
&lt;td>None&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td style="text-align:center">0.116&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.078, 0.154]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Regression Adjustment&lt;/td>
&lt;td>Outcome model&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Regression Adjustment&lt;/td>
&lt;td>Outcome model&lt;/td>
&lt;td>ATT&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.076, 0.151]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Inverse Prob. Weighting&lt;/td>
&lt;td>Treatment model&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Inverse Prob. Weighting&lt;/td>
&lt;td>Treatment model&lt;/td>
&lt;td>ATT&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.076, 0.151]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IPWRA (Doubly Robust)&lt;/td>
&lt;td>Both models&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IPWRA (Doubly Robust)&lt;/td>
&lt;td>Both models&lt;/td>
&lt;td>ATT&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.076, 0.151]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>True effect&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td style="text-align:center">&lt;strong>0.12&lt;/strong>&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Several patterns emerge from this comparison. First, &lt;strong>ATE and ATT are nearly identical&lt;/strong> for every method, confirming that treatment effects are homogeneous across households. Second, &lt;strong>RA, IPW, and DR all give remarkably similar results&lt;/strong> (all approximately 0.113) because, in this well-designed RCT, randomization ensures that both the outcome model and the propensity score model are approximately correct. Third, the simple difference in means (0.116) is slightly higher than the covariate-adjusted estimates (0.113), reflecting the precision improvement from controlling for covariates including the gender imbalance. Finally, all confidence intervals contain the true effect of 0.12 &amp;mdash; every method successfully recovers the correct answer.&lt;/p>
&lt;p>The real value of doubly robust methods becomes apparent in less ideal settings. When one model might be misspecified &amp;mdash; a common situation in practice &amp;mdash; DR methods provide insurance that RA or IPW alone cannot offer.&lt;/p>
&lt;hr>
&lt;h2 id="9-leveraging-panel-data-----difference-in-differences">9. Leveraging panel data &amp;mdash; Difference-in-Differences&lt;/h2>
&lt;p>All estimates in Section 8 used only endline data. But we have panel data &amp;mdash; the same 2,000 households observed before and after the intervention. Can we do better?&lt;/p>
&lt;h3 id="91-why-use-panel-data">9.1 Why use panel data?&lt;/h3>
&lt;p>Cross-sectional methods (RA, IPW, DR) compare treated and control groups at a single point in time &amp;mdash; the endline. They control for &lt;strong>observable&lt;/strong> covariates like age, education, and gender. But there may be &lt;strong>unobservable&lt;/strong> characteristics &amp;mdash; household motivation, geographic advantages, cultural factors &amp;mdash; that differ between groups and affect consumption. No amount of cross-sectional covariate adjustment can control for these, because we simply do not observe them.&lt;/p>
&lt;p>&lt;strong>Analogy &amp;mdash; comparing students across schools.&lt;/strong> Imagine comparing test scores between students at a charter school (treatment) and a traditional school (control). You can adjust for observable differences like family income and prior grades. But what about unmeasured factors &amp;mdash; parental involvement, neighborhood quality, student ambition? A cross-sectional comparison cannot disentangle the school effect from these hidden differences. Now suppose you observe the &lt;em>same students&lt;/em> before and after they switch schools. By comparing each student&amp;rsquo;s score change, you automatically cancel out all fixed student characteristics &amp;mdash; because they are the same at both time points. That is the power of panel data.&lt;/p>
&lt;p>Panel data methods like difference-in-differences (DiD) solve this problem by comparing each household &lt;strong>to itself&lt;/strong> over time. By looking at how each household&amp;rsquo;s consumption changed from baseline to endline, we effectively control for all &lt;strong>time-invariant unobservable characteristics&lt;/strong> (household fixed effects). This is a powerful advantage that cross-sectional methods cannot replicate.&lt;/p>
&lt;h4 id="the-did-estimator">The DiD estimator&lt;/h4>
&lt;p>The DiD estimator computes a simple but powerful quantity &amp;mdash; a &amp;ldquo;difference of differences&amp;rdquo;:&lt;/p>
&lt;p>$$\hat{\tau}_{DiD} = \underbrace{(\bar{Y}_{treat,post} - \bar{Y}_{treat,pre})}_{\text{Change for treated}} - \underbrace{(\bar{Y}_{control,post} - \bar{Y}_{control,pre})}_{\text{Change for control}}$$&lt;/p>
&lt;p>The first difference ($\bar{Y}_{treat,post} - \bar{Y}_{treat,pre}$) captures the treatment group&amp;rsquo;s change over time &amp;mdash; the treatment effect &lt;strong>plus&lt;/strong> any common time trend (e.g., economic growth that affects all households). The second difference ($\bar{Y}_{control,post} - \bar{Y}_{control,pre}$) captures the control group&amp;rsquo;s change &amp;mdash; the common time trend &lt;strong>only&lt;/strong>, since they did not receive treatment. Subtracting the second from the first removes the time trend, isolating the treatment effect.&lt;/p>
&lt;p>&lt;strong>Mini example from our data.&lt;/strong> Suppose the treated group&amp;rsquo;s average log consumption went from 10.01 at baseline to 10.17 at endline (change = +0.16). The control group went from 10.03 to 10.06 (change = +0.03). The DiD estimate is $0.16 - 0.03 = 0.13$ &amp;mdash; close to the true effect of 0.12. The control group&amp;rsquo;s +0.03 change captures the natural time trend that would have affected everyone, and subtracting it isolates the treatment effect.&lt;/p>
&lt;h4 id="the-parallel-trends-assumption">The parallel trends assumption&lt;/h4>
&lt;p>The key identifying assumption of DiD is the &lt;strong>parallel trends assumption (PTA)&lt;/strong>: absent the treatment, the treatment and control groups would have followed the same time trend. Formally:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Notation note&lt;/strong> &amp;mdash; In the DiD literature and in the Sant&amp;rsquo;Anna and Zhao (2020) paper, $D$ denotes treatment group assignment (equivalent to our &lt;code>treat&lt;/code> variable). This differs from our data dictionary where &lt;code>D&lt;/code> is the receipt indicator. In this section and Section 9.4, we follow the paper&amp;rsquo;s convention: $D = 1$ means assigned to treatment, $D = 0$ means assigned to control.&lt;/p>
&lt;/blockquote>
&lt;p>$$E[Y_1(0) - Y_0(0) \mid D = 1] = E[Y_1(0) - Y_0(0) \mid D = 0]$$&lt;/p>
&lt;p>This says that the average change in &lt;em>untreated&lt;/em> potential outcomes is the same for the treated and control groups. Note that this does &lt;strong>not&lt;/strong> require the two groups to have the same &lt;em>level&lt;/em> of consumption &amp;mdash; only the same &lt;em>trend&lt;/em>. The treated group can start higher or lower, as long as their consumption would have evolved at the same rate as the control group in the absence of the program.&lt;/p>
&lt;p>In an RCT, the parallel trends assumption is very plausible because randomization ensures the groups were similar at baseline. Any pre-existing differences between groups occurred by chance and are unlikely to produce different time trends. This makes DiD a strong estimator in our setting.&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph LR
subgraph &amp;quot;Parallel Trends Assumption&amp;quot;
PRE[&amp;quot;&amp;lt;b&amp;gt;Baseline 2021&amp;lt;/b&amp;gt;&amp;quot;]
POST[&amp;quot;&amp;lt;b&amp;gt;Endline 2024&amp;lt;/b&amp;gt;&amp;quot;]
end
PRE --&amp;gt;|&amp;quot;Treated group&amp;lt;br/&amp;gt;change = effect + trend&amp;quot;| POST
PRE --&amp;gt;|&amp;quot;Control group&amp;lt;br/&amp;gt;change = trend only&amp;quot;| POST
style PRE fill:#6a9bcc,stroke:#141413,color:#fff
style POST fill:#d97757,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;h3 id="92-why-does-did-estimate-att-and-not-ate">9.2 Why does DiD estimate ATT and not ATE?&lt;/h3>
&lt;p>This is a point that many beginners miss, so it is worth explaining carefully.&lt;/p>
&lt;p>Recall from Section 6 that the ATT is $E[Y_1(1) - Y_1(0) \mid D = 1]$ &amp;mdash; the effect on those who were treated. Sant&amp;rsquo;Anna and Zhao (2020) make this explicit: the main challenge is computing $E[Y_1(0) \mid D = 1]$ &amp;mdash; what would the treated group&amp;rsquo;s consumption have been at endline &lt;em>without&lt;/em> the program?&lt;/p>
&lt;p>DiD solves this by using the control group&amp;rsquo;s time trend as a stand-in. Specifically, it constructs the counterfactual for the treated group as:&lt;/p>
&lt;p>$$\underbrace{E[Y_1(0) \mid D = 1]}_{\text{Counterfactual}} = \underbrace{E[Y_0 \mid D = 1]}_{\text{Treated at baseline}} + \underbrace{(E[Y_1 \mid D = 0] - E[Y_0 \mid D = 0])}_{\text{Control group&amp;rsquo;s time trend}}$$&lt;/p>
&lt;p>This counterfactual is &lt;strong>specific to the treated group&lt;/strong> &amp;mdash; it starts from their baseline level and adds the control group&amp;rsquo;s trend. DiD therefore estimates what happened to the treated group relative to this counterfactual. This is precisely the ATT.&lt;/p>
&lt;p>&lt;strong>Why not the ATE?&lt;/strong> To estimate the ATE, we would also need the treatment effect for the untreated &amp;mdash; what would happen if we gave the program to those who did not receive it. DiD does not provide this, because the counterfactual it constructs runs in only one direction (control trend applied to treated baseline, not treated trend applied to control baseline).&lt;/p>
&lt;p>&lt;strong>In our RCT context&lt;/strong>, since treatment was randomly assigned, ATE and ATT are likely very similar (as we saw in Section 8). But in observational studies with heterogeneous treatment effects, this distinction matters greatly. A job-training program might have a larger effect on those who voluntarily enrolled (ATT) than it would have on randomly selected workers (ATE).&lt;/p>
&lt;h3 id="93-basic-did-with-panel-fixed-effects">9.3 Basic DiD with panel fixed effects&lt;/h3>
&lt;p>We now implement the basic DiD estimator using Stata&amp;rsquo;s &lt;code>xtdidregress&lt;/code> command, which handles the panel structure and computes clustered standard errors.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/ametrics/dataSIM4RCT.dta&amp;quot;, clear
* Create the treatment-post interaction
gen treat_post = treat * post
label var treat_post &amp;quot;Treated x Post (1 only for treated in 2024)&amp;quot;
* Declare panel structure
xtset id year
* Basic DiD with individual fixed effects
xtdidregress (y) (treat_post), group(id) time(year) vce(cluster id)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text"> Number of obs = 4,000
Number of groups = 2,000
Outcome model : linear
Treatment model: none
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. t P&amp;gt;|t| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATET |
treat_post | .1347161 .0272737 4.94 0.000 .0812282 .188204
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The basic DiD estimate of the ATT is 0.135 (SE = 0.027, p &amp;lt; 0.001, 95% CI [0.081, 0.188]). This is slightly higher than the cross-sectional estimates (0.113&amp;ndash;0.116) but still contains the true effect of 0.12 within its confidence interval. The wider standard error (0.027 vs. 0.019) reflects the additional variability introduced by differencing within households. Standard errors are clustered at the household level to account for serial correlation within panels.&lt;/p>
&lt;p>The key advantage of this DiD estimate is that it controls for all &lt;strong>time-invariant unobservable characteristics&lt;/strong> of each household. In an RCT, randomization already handles confounding, so the cross-sectional and panel estimates are similar. But in observational settings, DiD&amp;rsquo;s ability to absorb household fixed effects can correct biases that cross-sectional methods cannot.&lt;/p>
&lt;h3 id="94-from-cross-sectional-dr-to-panel-dr-----doubly-robust-did-drdid">9.4 From cross-sectional DR to panel DR &amp;mdash; Doubly Robust DiD (DRDID)&lt;/h3>
&lt;p>In Section 7, we saw that doubly robust methods combine outcome modeling and propensity score modeling for cross-sectional data. &lt;strong>DRDID extends this logic to the panel setting.&lt;/strong> It combines the DiD framework (using pre/post variation) with doubly robust covariate adjustment.&lt;/p>
&lt;p>This approach was introduced by Sant&amp;rsquo;Anna and Zhao (2020) in a landmark paper published in the &lt;em>Journal of Econometrics&lt;/em>. They proposed estimators that are &amp;ldquo;consistent if either (but not necessarily both) a propensity score or outcome regression working models are correctly specified&amp;rdquo; &amp;mdash; bringing the doubly robust property from the cross-sectional world into the DiD framework.&lt;/p>
&lt;h4 id="why-do-we-need-drdid">Why do we need DRDID?&lt;/h4>
&lt;p>Recall from Section 9.2 that basic DiD relies on the &lt;strong>parallel trends assumption&lt;/strong> &amp;mdash; absent treatment, the treated and control groups would have followed the same time trend. But what if parallel trends holds only &lt;strong>conditional on covariates&lt;/strong>? For example, what if consumption trends differ between poor and non-poor households, but within each poverty group the trends are parallel?&lt;/p>
&lt;p>In this case, we need a &lt;strong>conditional&lt;/strong> parallel trends assumption:&lt;/p>
&lt;p>$$E[Y_1(0) - Y_0(0) \mid D = 1, X] = E[Y_1(0) - Y_0(0) \mid D = 0, X]$$&lt;/p>
&lt;p>This says that the average change in untreated potential outcomes is the same for treated and control groups &lt;em>who share the same covariates&lt;/em> $X$. Note that this allows for covariate-specific time trends (e.g., different consumption growth rates for poor and non-poor households) while still identifying the ATT.&lt;/p>
&lt;p>Under this conditional parallel trends assumption, there are two ways to estimate the ATT:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Outcome regression (OR) approach&lt;/strong> &amp;mdash; model how the outcome evolves over time for the control group, and use that model to predict the counterfactual evolution for the treated group&lt;/li>
&lt;li>&lt;strong>IPW approach&lt;/strong> &amp;mdash; reweight the control group so its covariate distribution matches the treated group, then compute the standard DiD&lt;/li>
&lt;/ul>
&lt;p>The problem is the same as in the cross-sectional case: OR requires a correctly specified outcome model, and IPW requires a correctly specified propensity score model. Sant&amp;rsquo;Anna and Zhao&amp;rsquo;s insight was that &lt;strong>you can combine both into a single estimator that works if either model is correct&lt;/strong>.&lt;/p>
&lt;h4 id="the-drdid-estimator-for-panel-data">The DRDID estimator for panel data&lt;/h4>
&lt;p>When panel data are available (as in our case &amp;mdash; same households observed at baseline and endline), the DRDID estimator takes a particularly clean form. Let $\Delta Y_i = Y_{i,post} - Y_{i,pre}$ denote each household&amp;rsquo;s change in consumption. The DR DID estimator is:&lt;/p>
&lt;p>$$\hat{\tau}_{DR}^{DiD} = \frac{1}{N_1} \sum_{i=1}^{N} \left[ w_1(D_i) - w_0(D_i, X_i) \right] \left[ \Delta Y_i - \hat{\mu}_{0,\Delta}(X_i) \right]$$&lt;/p>
&lt;p>where:&lt;/p>
&lt;ul>
&lt;li>$w_1(D_i) = D_i / \bar{D}$ assigns equal weight to each treated unit (the fraction treated)&lt;/li>
&lt;li>$w_0(D_i, X_i)$ reweights control units using the propensity score $\hat{p}(X)$, so they resemble the treated group&lt;/li>
&lt;li>$\hat{\mu}_{0,\Delta}(X_i) = \hat{\mu}_{0,post}(X_i) - \hat{\mu}_{0,pre}(X_i)$ is the predicted change in consumption for the control group, fitted from control-group data&lt;/li>
&lt;/ul>
&lt;p>In plain language: for each household, compute the change in consumption over time ($\Delta Y$) and subtract the model-predicted change for the control group ($\hat{\mu}_{0,\Delta}$). This residual captures the treatment effect plus any prediction error. Then reweight these residuals using IPW so that the control group matches the treated group&amp;rsquo;s covariate profile.&lt;/p>
&lt;h4 id="why-is-this-doubly-robust">Why is this doubly robust?&lt;/h4>
&lt;p>The doubly robust property works through the same logic as in the cross-sectional case (Section 7.3), but applied to &lt;strong>changes&lt;/strong> rather than levels:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>If the outcome model is correct&lt;/strong> ($\hat{\mu}_{0,\Delta}(X) = E[\Delta Y \mid D=0, X]$), then the residuals $\Delta Y_i - \hat{\mu}_{0,\Delta}(X_i)$ average to zero for the control group, regardless of the propensity score weights. The estimator reduces to an outcome-regression DiD. Correct answer.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>If the propensity score model is correct&lt;/strong> ($\hat{p}(X) = \Pr(D=1 \mid X)$), the IPW reweighting makes the control group comparable to the treated group, regardless of the outcome model. The correction term fixes any bias from a misspecified outcome model. Correct answer.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>If both are correct&lt;/strong>, the estimator achieves the &lt;strong>semiparametric efficiency bound&lt;/strong> &amp;mdash; it is the most precise estimator possible given the assumptions. Sant&amp;rsquo;Anna and Zhao proved this formally.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>If both are wrong&lt;/strong>, the estimator can be biased &amp;mdash; double robustness provides one layer of insurance, not two.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;pre>&lt;code class="language-mermaid">graph TD
DY[&amp;quot;&amp;lt;b&amp;gt;Panel data&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;ΔY = Y_post − Y_pre&amp;lt;br/&amp;gt;for each household&amp;quot;]
OR[&amp;quot;&amp;lt;b&amp;gt;Outcome model&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Predict control group's&amp;lt;br/&amp;gt;consumption change&amp;lt;br/&amp;gt;μ̂₀,Δ(X)&amp;quot;]
PS[&amp;quot;&amp;lt;b&amp;gt;Propensity score&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Estimate p(X)&amp;lt;br/&amp;gt;= Pr(D=1 | X)&amp;quot;]
RES[&amp;quot;&amp;lt;b&amp;gt;Residuals&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;ΔY − μ̂₀,Δ(X)&amp;quot;]
IPW_W[&amp;quot;&amp;lt;b&amp;gt;IPW reweighting&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Make controls look&amp;lt;br/&amp;gt;like treated group&amp;quot;]
DRDID[&amp;quot;&amp;lt;b&amp;gt;DR-DiD estimate&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;ATT = weighted average&amp;lt;br/&amp;gt;of residuals&amp;quot;]
DY --&amp;gt; RES
OR --&amp;gt; RES
PS --&amp;gt; IPW_W
RES --&amp;gt; DRDID
IPW_W --&amp;gt; DRDID
style DY fill:#141413,stroke:#00d4c8,color:#fff
style OR fill:#6a9bcc,stroke:#141413,color:#fff
style PS fill:#d97757,stroke:#141413,color:#fff
style RES fill:#6a9bcc,stroke:#141413,color:#fff
style IPW_W fill:#d97757,stroke:#141413,color:#fff
style DRDID fill:#00d4c8,stroke:#141413,color:#141413
&lt;/code>&lt;/pre>
&lt;h4 id="what-drdid-adds-over-basic-did-and-twfe">What DRDID adds over basic DiD and TWFE&lt;/h4>
&lt;p>Sant&amp;rsquo;Anna and Zhao (2020) also showed that the standard two-way fixed effects (TWFE) estimator &amp;mdash; the workhorse of applied economics &amp;mdash; can produce misleading results when treatment effects are heterogeneous across covariates. Specifically, the TWFE estimator implicitly assumes (i) that treatment effects are the same for all covariate values, and (ii) that there are no covariate-specific time trends. When these assumptions fail, &amp;ldquo;the estimand is, in general, different from the ATT, and policy evaluation based on it may be misleading.&amp;rdquo; DRDID avoids both of these pitfalls by allowing for flexible outcome models and covariate-specific trends.&lt;/p>
&lt;h4 id="stata-implementation">Stata implementation&lt;/h4>
&lt;p>The &lt;code>drdid&lt;/code> package (Rios-Avila, Sant&amp;rsquo;Anna, and Callaway) implements the estimators from the paper.&lt;/p>
&lt;pre>&lt;code class="language-stata">* Install the drdid package (only needed once)
ssc install drdid, replace
* Doubly Robust DiD with DRIPW estimator
drdid y c.age c.edu i.female i.poverty, ivar(id) time(year) treatment(treat) dripw
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Doubly robust difference-in-differences estimator
Outcome model : least squares
Treatment model: inverse probability
──────────────────────────────────────────────────────────────────────────────
| Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATET | .1374784 .027387 5.02 0.000 .0838008 .191156
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The DRDID estimate of the ATT is 0.137 (SE = 0.027, p &amp;lt; 0.001, 95% CI [0.084, 0.191]). The &lt;code>dripw&lt;/code> option specifies the Doubly Robust Inverse Probability Weighting estimator, which uses a linear least squares model for the outcome evolution of the control group and a logistic model for the propensity score. The result is slightly higher than basic DiD (0.135) and close to the true effect of 0.12.&lt;/p>
&lt;p>&lt;strong>Alternative: Stata 17+ built-in command.&lt;/strong> Stata 17 and later versions include a built-in doubly robust DiD estimator that does not require installing external packages.&lt;/p>
&lt;pre>&lt;code class="language-stata">xthdidregress aipw (y c.age c.edu i.female i.poverty) ///
(treat_post c.age c.edu i.female i.poverty), group(id)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Heterogeneous-treatment-effects regression Number of obs = 4,000
Number of panels = 2,000
Estimator: Augmented IPW
Panel variable: id
Treatment level: id
Control group: Never treated
(Std. err. adjusted for 2,000 clusters in id)
──────────────────────────────────────────────────────────────────────────────
| Robust
Cohort | ATET std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
year |
2024 | .1374784 .027387 5.02 0.000 .0838008 .191156
──────────────────────────────────────────────────────────────────────────────
Note: ATET computed using covariates.
&lt;/code>&lt;/pre>
&lt;p>The &lt;code>xthdidregress aipw&lt;/code> command produces the same ATT estimate of 0.137 (SE = 0.027, 95% CI [0.084, 0.191]) as the &lt;code>drdid&lt;/code> package &amp;mdash; confirming that both implement the same doubly robust DiD methodology. The output labels the result as &amp;ldquo;Cohort year 2024&amp;rdquo; because &lt;code>xthdidregress&lt;/code> is designed for settings with staggered treatment adoption across multiple cohorts; in our two-period design, there is only one treatment cohort (households treated in 2024). As the Stata manual explains, &amp;ldquo;AIPW models both treatment and outcome. If at least one of the models is correctly specified, it provides consistent estimates, a property called double robustness.&amp;rdquo;&lt;/p>
&lt;p>The agreement between &lt;code>drdid&lt;/code> (community package) and &lt;code>xthdidregress aipw&lt;/code> (built-in) provides a useful robustness check &amp;mdash; researchers can verify their results using both implementations.&lt;/p>
&lt;h4 id="panel-data-vs-repeated-cross-sections">Panel data vs. repeated cross-sections&lt;/h4>
&lt;p>An important result from Sant&amp;rsquo;Anna and Zhao (2020) is that panel data are &lt;strong>strictly more efficient&lt;/strong> than repeated cross-sections for estimating the ATT under the DiD framework. The intuition is straightforward: with panel data, we observe each household&amp;rsquo;s individual change over time ($\Delta Y_i$), which eliminates household-level variation. With repeated cross-sections, we can only compare group averages at different time points, which introduces additional noise. The efficiency gain is larger when the sample sizes in the pre and post periods are more imbalanced.&lt;/p>
&lt;p>In our study, we have a balanced panel (same 2,000 households at baseline and endline), so we benefit from this efficiency advantage.&lt;/p>
&lt;h3 id="95-cross-sectional-vs-panel-comparison">9.5 Cross-sectional vs. panel comparison&lt;/h3>
&lt;p>The table below compares our best cross-sectional estimates with the panel-based DiD estimates.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Approach&lt;/th>
&lt;th>Estimand&lt;/th>
&lt;th>Data Used&lt;/th>
&lt;th style="text-align:center">Estimate&lt;/th>
&lt;th style="text-align:center">SE&lt;/th>
&lt;th style="text-align:center">95% CI&lt;/th>
&lt;th style="text-align:center">Contains 0.12?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Simple regression&lt;/td>
&lt;td>None&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td>Endline only&lt;/td>
&lt;td style="text-align:center">0.116&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.078, 0.154]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RA&lt;/td>
&lt;td>Outcome model&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td>Endline only&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IPW&lt;/td>
&lt;td>Treatment model&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td>Endline only&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DR (IPWRA)&lt;/td>
&lt;td>Both models&lt;/td>
&lt;td>ATE&lt;/td>
&lt;td>Endline only&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Basic DiD&lt;/td>
&lt;td>Panel FE&lt;/td>
&lt;td>&lt;strong>ATT&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Both waves&lt;/strong>&lt;/td>
&lt;td style="text-align:center">0.135&lt;/td>
&lt;td style="text-align:center">0.027&lt;/td>
&lt;td style="text-align:center">[0.081, 0.188]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DR-DiD (&lt;code>drdid&lt;/code>)&lt;/td>
&lt;td>Both + Panel&lt;/td>
&lt;td>&lt;strong>ATT&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Both waves&lt;/strong>&lt;/td>
&lt;td style="text-align:center">0.137&lt;/td>
&lt;td style="text-align:center">0.027&lt;/td>
&lt;td style="text-align:center">[0.084, 0.191]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DR-DiD (&lt;code>xthdidregress&lt;/code>)&lt;/td>
&lt;td>Both + Panel&lt;/td>
&lt;td>&lt;strong>ATT&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Both waves&lt;/strong>&lt;/td>
&lt;td style="text-align:center">0.137&lt;/td>
&lt;td style="text-align:center">0.027&lt;/td>
&lt;td style="text-align:center">[0.084, 0.191]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>True effect&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td style="text-align:center">&lt;strong>0.12&lt;/strong>&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Several important patterns emerge from this comparison. Cross-sectional methods estimate &lt;strong>ATE&lt;/strong> using only endline data, while DiD methods estimate &lt;strong>ATT&lt;/strong> using both survey waves. The two DR-DiD implementations (&lt;code>drdid&lt;/code> and &lt;code>xthdidregress aipw&lt;/code>) produce identical results, confirming methodological consistency. The DiD estimates (0.135&amp;ndash;0.137) are slightly higher than the cross-sectional estimates (0.113), but &lt;strong>all confidence intervals contain the true effect of 0.12&lt;/strong>. DiD&amp;rsquo;s wider standard errors (0.027 vs. 0.019) reflect the additional variability from differencing within households.&lt;/p>
&lt;p>The key value of DiD is &lt;strong>not&lt;/strong> tighter standard errors &amp;mdash; it is &lt;strong>robustness to time-invariant unobservables.&lt;/strong> In observational settings where randomization does not hold, DiD can correct biases that cross-sectional methods cannot address. In this RCT, randomization already handles confounding, so the estimates are similar. DRDID adds doubly robust protection on top of DiD, making it the most robust panel method available.&lt;/p>
&lt;hr>
&lt;h2 id="10-offer-vs-receipt-----endogenous-treatment-advanced">10. Offer vs. receipt &amp;mdash; endogenous treatment (advanced)&lt;/h2>
&lt;blockquote>
&lt;p>&lt;strong>Note:&lt;/strong> This section addresses the advanced topic of imperfect compliance and endogenous treatment. Readers new to causal inference may wish to skip this section on a first reading and return to it later.&lt;/p>
&lt;/blockquote>
&lt;h3 id="101-the-compliance-problem">10.1 The compliance problem&lt;/h3>
&lt;p>All estimates in Sections 8 and 9 measure the effect of &lt;strong>being offered&lt;/strong> the cash transfer (&lt;code>treat&lt;/code>), not the effect of &lt;strong>actually receiving&lt;/strong> it (&lt;code>D&lt;/code>). This is the intent-to-treat (ITT) approach &amp;mdash; it captures the policy-relevant effect of the offer, regardless of whether households complied.&lt;/p>
&lt;p>But what about the effect of actual receipt? This is more complex because compliance is &lt;strong>not random&lt;/strong>. Only 85% of treated households received the transfer, and 5% of control households received it through other channels. The households that chose to take up the program may differ systematically from those that did not &amp;mdash; they may be more motivated, more financially constrained, or better connected. Naively comparing receivers to non-receivers would introduce &lt;strong>selection bias&lt;/strong>.&lt;/p>
&lt;p>The solution is to use the random assignment (&lt;code>treat&lt;/code>) as an &lt;strong>instrumental variable&lt;/strong> for actual receipt (&lt;code>D&lt;/code>). Because &lt;code>treat&lt;/code> was randomly assigned, it is independent of household characteristics and satisfies the requirements for a valid instrument. This allows us to isolate the causal effect of receipt, at least for the subset of households whose receipt was determined by the offer (the &amp;ldquo;compliers&amp;rdquo;).&lt;/p>
&lt;p>&lt;strong>Analogy &amp;mdash; prescriptions and pills.&lt;/strong> Imagine a doctor randomly prescribes a medication to some patients, but not all patients fill their prescription. We cannot simply compare those who took the pill to those who did not, because pill-takers may be more health-conscious. Instead, we use the random prescription (the &amp;ldquo;offer&amp;rdquo;) as a nudge &amp;mdash; it strongly predicts whether you take the pill but does not directly affect your health except through the pill. That is the instrumental variable approach: using the random offer to estimate the causal effect of actual receipt.&lt;/p>
&lt;h3 id="102-endogenous-treatment-regression">10.2 Endogenous treatment regression&lt;/h3>
&lt;p>Stata&amp;rsquo;s &lt;code>etregress&lt;/code> command estimates the effect of an endogenous treatment variable, using the random assignment as an excluded instrument.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/ametrics/dataSIM4RCT.dta&amp;quot;, clear
keep if post==1
* Endogenous treatment regression
etregress y c.age i.female i.poverty c.edu, ///
treat(D = treat c.age i.female i.poverty c.edu) vce(robust)
* Mark estimation sample
gen byte esample = e(sample)
* ATE of receipt
margins r.D if esample==1
* ATT of receipt
margins, predict(cte) subpop(if D==1 &amp;amp; esample==1)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Linear regression with endogenous treatment Number of obs = 2,000
Estimator: Maximum likelihood Wald chi2(5) = 92.23
Log pseudolikelihood = -1797.6297 Prob &amp;gt; chi2 = 0.0000
──────────────────────────────────────────────────────────────────────────────
| Robust
| Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
y |
age | .003187 .0010016 3.18 0.001 .001224 .0051501
1.female | .0801465 .0189552 4.23 0.000 .042995 .117298
1.poverty | -.1030302 .0205984 -5.00 0.000 -.1434023 -.062658
edu | .0182634 .0045243 4.04 0.000 .0093959 .0271308
1.D | .1471 .0246775 5.96 0.000 .0987329 .1954671
_cons | 9.705642 .0694641 139.72 0.000 9.569495 9.841789
─────────────+────────────────────────────────────────────────────────────────
D |
treat | 2.55806 .0802103 31.89 0.000 2.40085 2.715269
_cons | -1.844408 .2847883 -6.48 0.000 -2.402582 -1.286233
─────────────+────────────────────────────────────────────────────────────────
/athrho | -.0060068 .0481062 -0.12 0.901 -.1002933 .0882796
sigma | .4245195 .0066426 .411698 .4377404
──────────────────────────────────────────────────────────────────────────────
Wald test of indep. eqns. (rho = 0): chi2(1) = 0.02 Prob &amp;gt; chi2 = 0.9006
ATE of receipt (margins r.D):
──────────────────────────────────────────────────────────────────────────────
D | Contrast std. err. [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
(1 vs 0) | .1471 .0246775 .0987329 .1954671
──────────────────────────────────────────────────────────────────────────────
ATT of receipt (margins, predict(cte)):
──────────────────────────────────────────────────────────────────────────────
_cons | Margin std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
| .1471 .0246775 5.96 0.000 .0987329 .1954671
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The &lt;code>etregress&lt;/code> output reveals several important findings. The coefficient on &lt;code>D&lt;/code> (receipt) is 0.147 (SE = 0.025, p &amp;lt; 0.001, 95% CI [0.099, 0.195]), which is the estimated effect of actually receiving the cash transfer. This is larger than the offer-based estimates (0.113&amp;ndash;0.116) because not everyone who was offered the program received it &amp;mdash; the per-recipient effect is naturally larger than the per-offer effect. The Wald test of independent equations (rho = 0) has p = 0.901, indicating no evidence of endogeneity &amp;mdash; consistent with a well-designed RCT where unobservable factors do not drive both treatment receipt and consumption. The &lt;code>margins&lt;/code> commands confirm that both the ATE and ATT of receipt are 0.147 (identical in this case because the model assumes a constant treatment effect).&lt;/p>
&lt;h3 id="103-doubly-robust-estimation-of-receipt-effect">10.3 Doubly robust estimation of receipt effect&lt;/h3>
&lt;p>We can also estimate the receipt effect using a doubly robust approach, incorporating the baseline outcome &lt;code>y0&lt;/code> as an additional control variable (an ANCOVA-style adjustment) and including &lt;code>treat&lt;/code> (the random assignment) as a covariate in the treatment model for &lt;code>D&lt;/code>.&lt;/p>
&lt;pre>&lt;code class="language-stata">use &amp;quot;https://github.com/quarcs-lab/data-open/raw/master/ametrics/dataSIM4RCT.dta&amp;quot;, clear
keep if post==1
* Doubly robust ATE of receipt, controlling for baseline outcome
teffects ipwra (y y0 c.age i.female i.poverty c.edu) ///
(D c.age i.female i.poverty c.edu treat), vce(robust)
* Diagnostic checks
tebalance summarize age edu i.female i.poverty
tebalance summarize, baseline
tebalance density y0
tebalance density age
teffects overlap
&lt;/code>&lt;/pre>
&lt;pre>&lt;code class="language-text">Treatment-effects estimation Number of obs = 2,000
Estimator : IPW regression adjustment
Outcome model : linear
Treatment model: logit
──────────────────────────────────────────────────────────────────────────────
| Robust
y | Coefficient std. err. z P&amp;gt;|z| [95% conf. interval]
─────────────+────────────────────────────────────────────────────────────────
ATE |
D |
(1 vs 0) | .1172686 .0322495 3.64 0.000 .0540608 .1804764
─────────────+────────────────────────────────────────────────────────────────
POmean |
D |
0 | 10.03361 .0171459 585.19 0.000 10 10.06722
──────────────────────────────────────────────────────────────────────────────
&lt;/code>&lt;/pre>
&lt;p>The doubly robust estimate of the ATE of receipt is 0.117 (SE = 0.032, 95% CI [0.054, 0.180]). This is slightly lower than the &lt;code>etregress&lt;/code> estimate (0.147) and closer to the true effect of 0.12. The wider standard error (0.032 vs. 0.025) reflects the additional flexibility of the doubly robust approach. This specification includes &lt;code>y0&lt;/code> (the baseline outcome) in the outcome model, which controls for pre-treatment differences in consumption levels. The variable &lt;code>treat&lt;/code> appears in the treatment model for &lt;code>D&lt;/code> because random assignment is the strongest predictor of receipt.&lt;/p>
&lt;p>The diagnostic graphs below verify adequate covariate balance and propensity score overlap for the receipt model.&lt;/p>
&lt;p>&lt;img src="stata_rct_density_y0_receipt.png" alt="Density plot of baseline consumption (y0) for receivers and non-receivers, before and after IPWRA weighting.">&lt;/p>
&lt;p>&lt;img src="stata_rct_overlap_receipt.png" alt="Overlap plot showing propensity score distributions for receivers and non-receivers of the cash transfer.">&lt;/p>
&lt;p>The density and overlap plots confirm that the IPWRA weighting achieves good balance between receivers and non-receivers. After weighting, the effective sample sizes are approximately 999 treated and 1,001 control (rebalanced from the raw 923 receivers and 1,077 non-receivers). The weighted covariate means are closely aligned &amp;mdash; for example, the weighted mean age is 35.0 for receivers versus 35.2 for non-receivers, and the weighted poverty rate is 31.1% versus 31.4%. The propensity scores show sufficient overlap for reliable estimation.&lt;/p>
&lt;hr>
&lt;h2 id="11-comparing-all-estimates-----the-big-picture">11. Comparing all estimates &amp;mdash; the big picture&lt;/h2>
&lt;p>The table below brings together all estimates from the tutorial, providing a comprehensive overview of how different methods, estimands, and data structures relate to each other.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Method&lt;/th>
&lt;th>Approach&lt;/th>
&lt;th>Estimand&lt;/th>
&lt;th>Data&lt;/th>
&lt;th style="text-align:center">Estimate&lt;/th>
&lt;th style="text-align:center">SE&lt;/th>
&lt;th style="text-align:center">95% CI&lt;/th>
&lt;th style="text-align:center">Contains 0.12?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>Simple regression&lt;/td>
&lt;td>None&lt;/td>
&lt;td>ATE (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.116&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.078, 0.154]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>Regression Adjustment&lt;/td>
&lt;td>Outcome model&lt;/td>
&lt;td>ATE (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>Regression Adjustment&lt;/td>
&lt;td>Outcome model&lt;/td>
&lt;td>ATT (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.076, 0.151]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Inverse Prob. Weighting&lt;/td>
&lt;td>Treatment model&lt;/td>
&lt;td>ATE (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>Inverse Prob. Weighting&lt;/td>
&lt;td>Treatment model&lt;/td>
&lt;td>ATT (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.076, 0.151]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>IPWRA (Doubly Robust)&lt;/td>
&lt;td>Both models&lt;/td>
&lt;td>ATE (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.075, 0.150]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>IPWRA (Doubly Robust)&lt;/td>
&lt;td>Both models&lt;/td>
&lt;td>ATT (offer)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.113&lt;/td>
&lt;td style="text-align:center">0.019&lt;/td>
&lt;td style="text-align:center">[0.076, 0.151]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>Basic DiD&lt;/td>
&lt;td>Panel FE&lt;/td>
&lt;td>ATT (offer)&lt;/td>
&lt;td>Panel&lt;/td>
&lt;td style="text-align:center">0.135&lt;/td>
&lt;td style="text-align:center">0.027&lt;/td>
&lt;td style="text-align:center">[0.081, 0.188]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>DR-DiD (&lt;code>drdid&lt;/code>)&lt;/td>
&lt;td>Both + Panel&lt;/td>
&lt;td>ATT (offer)&lt;/td>
&lt;td>Panel&lt;/td>
&lt;td style="text-align:center">0.137&lt;/td>
&lt;td style="text-align:center">0.027&lt;/td>
&lt;td style="text-align:center">[0.084, 0.191]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>DR-DiD (&lt;code>xthdidregress&lt;/code>)&lt;/td>
&lt;td>Both + Panel&lt;/td>
&lt;td>ATT (offer)&lt;/td>
&lt;td>Panel&lt;/td>
&lt;td style="text-align:center">0.137&lt;/td>
&lt;td style="text-align:center">0.027&lt;/td>
&lt;td style="text-align:center">[0.084, 0.191]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>11&lt;/td>
&lt;td>Endogenous treatment (&lt;code>etregress&lt;/code>)&lt;/td>
&lt;td>IV&lt;/td>
&lt;td>ATE (receipt)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.147&lt;/td>
&lt;td style="text-align:center">0.025&lt;/td>
&lt;td style="text-align:center">[0.099, 0.195]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12&lt;/td>
&lt;td>DR receipt (&lt;code>teffects ipwra&lt;/code>)&lt;/td>
&lt;td>Both models&lt;/td>
&lt;td>ATE (receipt)&lt;/td>
&lt;td>Endline&lt;/td>
&lt;td style="text-align:center">0.117&lt;/td>
&lt;td style="text-align:center">0.032&lt;/td>
&lt;td style="text-align:center">[0.054, 0.180]&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;/td>
&lt;td>&lt;strong>True effect&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;td style="text-align:center">&lt;strong>0.12&lt;/strong>&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;td style="text-align:center">&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="four-key-takeaways">Four key takeaways&lt;/h3>
&lt;p>&lt;strong>1. RA vs. IPW vs. DR.&lt;/strong> In this well-designed RCT, all three cross-sectional approaches give remarkably similar results (0.113&amp;ndash;0.116). This convergence occurs because randomization ensures that both the outcome model and the propensity score model are approximately correct. The differences are small &amp;mdash; but in observational studies, where one model might be misspecified, the choice of method matters much more. Doubly robust methods are the safest bet because they remain consistent if either model is correct.&lt;/p>
&lt;p>&lt;strong>2. ATE vs. ATT.&lt;/strong> For all cross-sectional methods, ATE and ATT are nearly identical (0.113&amp;ndash;0.116). This confirms that treatment effects are roughly homogeneous across households in this simulation. When treatment effects are heterogeneous &amp;mdash; for example, if the program benefits poorer households more &amp;mdash; ATE and ATT can diverge. The researcher must choose the estimand that matches their policy question: ATE for scaling decisions, ATT for program evaluation.&lt;/p>
&lt;p>&lt;strong>3. Cross-sectional vs. DiD.&lt;/strong> DiD estimates (0.135&amp;ndash;0.137) are slightly higher than cross-sectional estimates (0.113&amp;ndash;0.116), but all confidence intervals contain the true effect of 0.12. DiD&amp;rsquo;s main advantage is controlling for &lt;strong>time-invariant unobservable&lt;/strong> household characteristics &amp;mdash; less important in an RCT (where randomization handles confounding) but critical in quasi-experimental settings. DRDID extends the doubly robust logic to the panel setting, providing the most robust estimator in our toolkit. DiD inherently estimates the &lt;strong>ATT&lt;/strong> because its counterfactual is constructed specifically for the treated group.&lt;/p>
&lt;p>&lt;strong>4. Offer vs. receipt.&lt;/strong> The effect of actually receiving the cash transfer (0.117&amp;ndash;0.147) is larger than the effect of being offered it (0.113&amp;ndash;0.116), because imperfect compliance dilutes the offer-based estimates. The doubly robust receipt estimate (0.117) is closest to the true effect of 0.12, while the endogenous treatment model (0.147) is slightly higher. All confidence intervals contain 0.12.&lt;/p>
&lt;hr>
&lt;h2 id="12-summary-and-key-takeaways">12. Summary and key takeaways&lt;/h2>
&lt;p>The cash transfer program increased household consumption by approximately &lt;strong>11&amp;ndash;14%&lt;/strong> across all estimation methods, close to the true effect of &lt;strong>12%&lt;/strong>. Every confidence interval contained the true value, demonstrating that all methods successfully recovered the correct answer.&lt;/p>
&lt;h3 id="seven-methodological-lessons">Seven methodological lessons&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Always verify baseline balance&lt;/strong> before estimating treatment effects. Even with randomization, chance imbalances can occur &amp;mdash; as we saw with the gender variable (SMD = 9.3%).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Be explicit about your estimand.&lt;/strong> ATE answers the policymaker&amp;rsquo;s question (&amp;ldquo;What if we scale this up?&amp;quot;), while ATT answers the evaluator&amp;rsquo;s question (&amp;ldquo;Did it help the participants?&amp;quot;). Different methods target different estimands.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Regression adjustment models the outcome; IPW models treatment assignment; doubly robust does both.&lt;/strong> These three approaches represent fundamentally different strategies for causal estimation. Understanding what each models &amp;mdash; and what can go wrong &amp;mdash; is essential for choosing the right method.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>In a well-designed RCT, all three approaches converge.&lt;/strong> But doubly robust methods provide insurance against model misspecification, making them the standard recommendation in modern causal inference.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Panel data controls for time-invariant unobservables&lt;/strong> that cross-sectional methods cannot address. By comparing each household to itself over time, DiD absorbs household fixed effects &amp;mdash; motivation, geography, family culture &amp;mdash; that are invisible to cross-sectional approaches.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>DiD inherently estimates the ATT&lt;/strong> because its counterfactual is specific to the treated group. The control group&amp;rsquo;s time trend provides a counterfactual for what the treated group would have experienced without the program &amp;mdash; but it does not tell us what would happen if the program were given to the untreated.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Doubly robust DiD (DRDID)&lt;/strong> extends the DR logic to the panel setting. It combines the power of DiD (controlling for household fixed effects) with the robustness of doubly robust estimation (protection against model misspecification), making it the most robust panel estimator available.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="limitations">Limitations&lt;/h3>
&lt;ul>
&lt;li>This tutorial uses &lt;strong>simulated data&lt;/strong> with known parameters. Real-world data may exhibit more complex compliance patterns, heterogeneous effects, and missing data.&lt;/li>
&lt;li>The panel has only &lt;strong>two periods&lt;/strong> (baseline and endline), limiting our ability to test for pre-treatment trends or estimate dynamic treatment effects.&lt;/li>
&lt;li>Treatment effects are &lt;strong>homogeneous&lt;/strong> by construction. In practice, researchers should explore heterogeneity across subgroups.&lt;/li>
&lt;/ul>
&lt;h3 id="next-steps">Next steps&lt;/h3>
&lt;ul>
&lt;li>Apply these methods to &lt;strong>real-world RCT data&lt;/strong> from actual cash transfer programs&lt;/li>
&lt;li>Explore &lt;strong>heterogeneous treatment effects&lt;/strong> by gender, poverty status, or education level&lt;/li>
&lt;li>Extend to &lt;strong>multi-period panels&lt;/strong> with staggered treatment adoption, using modern DiD methods (Callaway and Sant&amp;rsquo;Anna, 2021)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="13-exercises">13. Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Heterogeneous effects by gender.&lt;/strong> Estimate treatment effects separately for male-headed and female-headed households using IPWRA. Are the effects different? Does ATE still equal ATT when you restrict to subgroups?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Model misspecification.&lt;/strong> Compare the RA, IPW, and DR estimates when you deliberately misspecify the outcome model by omitting &lt;code>edu&lt;/code> and &lt;code>age&lt;/code> from the covariate list. Which method is most robust to this misspecification? What does this tell you about the value of doubly robust estimation?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Basic DiD vs. doubly robust DiD.&lt;/strong> Re-run the DiD analysis using the basic &lt;code>xtdidregress&lt;/code> command (no covariates) and compare it with the &lt;code>drdid&lt;/code> results (with covariates). How much do the estimates differ? What does this tell you about the role of covariate adjustment in DiD?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://www.stata.com/manuals/teteffects.pdf" target="_blank" rel="noopener">Stata &lt;code>teffects&lt;/code> documentation &amp;mdash; Treatment-effects estimation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jeconom.2020.06.003" target="_blank" rel="noopener">Sant&amp;rsquo;Anna, P.H.C. &amp;amp; Zhao, J. (2020). Doubly Robust Difference-in-Differences Estimators. &lt;em>Journal of Econometrics&lt;/em>, 219(1), 101&amp;ndash;122&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1017/CBO9781139025751" target="_blank" rel="noopener">Imbens, G. &amp;amp; Rubin, D. (2015). &lt;em>Causal Inference for Statistics, Social, and Biomedical Sciences&lt;/em>. Cambridge University Press&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://friosavila.github.io/stpackages/drdid.html" target="_blank" rel="noopener">Rios-Avila, F., Sant&amp;rsquo;Anna, P.H.C., &amp;amp; Callaway, B. &lt;code>drdid&lt;/code> &amp;mdash; Doubly Robust DID estimators for Stata&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://dimewiki.worldbank.org/iebaltab" target="_blank" rel="noopener">World Bank &lt;code>ietoolkit&lt;/code> / &lt;code>iebaltab&lt;/code> documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tdmize.github.io/data/" target="_blank" rel="noopener">Mize, T. &lt;code>balanceplot&lt;/code> &amp;mdash; Stata module for covariate balance visualization&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://youtu.be/Gr_fu5deDMk" target="_blank" rel="noopener">RCT Analysis: Cash Transfers, Panel Data, and Doubly Robust Estimation (YouTube)&lt;/a>&lt;/li>
&lt;/ol>
&lt;h4 id="acknowledgements">Acknowledgements&lt;/h4>
&lt;p>AI tools (Claude Code, Gemini, NotebookLM) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Introduction to Difference-in-Differences in Python</title><link>https://carlos-mendez.org/post/python_did/</link><pubDate>Thu, 19 Mar 2026 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/python_did/</guid><description>&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>An education ministry rolls out AI tutoring bots in some cities but not others. Did the AI tools actually improve learning, or were those cities already on an upward trajectory? This is the core challenge of &lt;strong>policy evaluation&lt;/strong>: separating the genuine effect of an intervention from pre-existing trends and selection differences between treated and untreated groups. The seminal study by &lt;a href="https://www.jstor.org/stable/2118030" target="_blank" rel="noopener">Card and Krueger (1994)&lt;/a> pioneered this approach in a different context &amp;mdash; examining how a minimum wage increase in New Jersey affected fast-food employment compared to neighboring Pennsylvania.&lt;/p>
&lt;p>&lt;strong>Difference-in-Differences (DiD)&lt;/strong> is the workhorse method for answering such questions. The idea is elegantly simple: compare the change in outcomes over time between a group that received treatment and a group that did not. If both groups were evolving similarly before treatment &amp;mdash; the &lt;em>parallel trends&lt;/em> assumption &amp;mdash; then the difference in their changes isolates the causal effect. Think of it as using the control group as a mirror: it shows what would have happened to the treated group had the policy never been implemented.&lt;/p>
&lt;p>The &lt;strong>&lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">diff-diff&lt;/a>&lt;/strong> Python package, developed by &lt;a href="https://github.com/igerber/diff-diff" target="_blank" rel="noopener">Gerber (2026)&lt;/a>, provides a unified, scikit-learn-style API for 13+ DiD estimators validated against their R counterparts. These range from the classic 2x2 design to modern methods for staggered adoption. In this tutorial, we start with the simplest case, build up to event studies and multi-cohort designs, and finish with sensitivity analysis that quantifies how robust the findings are to violations of parallel trends. All examples use synthetic &lt;strong>panel data&lt;/strong> &amp;mdash; datasets where the same units (cities, firms, individuals) are observed repeatedly over multiple time periods &amp;mdash; with known true effects, so every estimate can be verified against ground truth.&lt;/p>
&lt;p>&lt;strong>Learning objectives:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Understand the logic of the 2x2 DiD design and why it identifies causal effects under parallel trends&lt;/li>
&lt;li>Estimate the Average Treatment Effect on the Treated (ATT) using classic DiD&lt;/li>
&lt;li>Test the parallel trends assumption with pre-treatment trend comparisons&lt;/li>
&lt;li>Interpret event study plots that reveal dynamic treatment effects over time&lt;/li>
&lt;li>Recognize why Two-Way Fixed Effects fails under staggered adoption and how Callaway-Sant&amp;rsquo;Anna corrects for it&lt;/li>
&lt;li>Assess robustness of causal conclusions using Bacon decomposition diagnostics and HonestDiD sensitivity analysis&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://colab.research.google.com/github/cmg777/starter-academic-v501/blob/master/content/post/python_did/notebook.ipynb" target="_blank">&lt;img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab">&lt;/a>&lt;/p>
&lt;h2 id="conceptual-framework-what-is-difference-in-differences">Conceptual framework: What is Difference-in-Differences?&lt;/h2>
&lt;p>Imagine a school district deploys AI tutoring bots in some schools but not others, and you want to know whether the AI tools improved learning outcomes. You could compare learning scores at AI-equipped schools versus non-equipped schools after deployment. But AI-equipped schools might have had stronger students to begin with &amp;mdash; perhaps the district piloted the technology in its highest-performing schools. A simple post-treatment comparison confounds the AI effect with pre-existing differences. Alternatively, you could compare a single school before and after the AI rollout &amp;mdash; but learning scores might have been rising everywhere due to a new curriculum or improved teacher training, not the AI tools.&lt;/p>
&lt;p>DiD combines these two simpler approaches so that selection bias and the effect of time are, in turns, eliminated. The logic proceeds through &lt;strong>successive differencing&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>First difference&lt;/strong>: Compare a unit before and after treatment. This eliminates time-invariant differences between groups (e.g., one school always scores higher than another), but confounds the treatment effect with common time trends (e.g., district-wide learning improvements from a new curriculum).&lt;/li>
&lt;li>&lt;strong>Second difference&lt;/strong>: Difference the first differences between treated and control groups. This eliminates the common time trends, leaving only the treatment effect.&lt;/li>
&lt;/ul>
&lt;pre>&lt;code class="language-mermaid">graph TB
subgraph &amp;quot;Before Treatment&amp;quot;
A[&amp;quot;&amp;lt;b&amp;gt;Treated Group&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pre-treatment outcome&amp;quot;]
B[&amp;quot;&amp;lt;b&amp;gt;Control Group&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Pre-treatment outcome&amp;quot;]
end
subgraph &amp;quot;After Treatment&amp;quot;
C[&amp;quot;&amp;lt;b&amp;gt;Treated Group&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Post-treatment outcome&amp;quot;]
D[&amp;quot;&amp;lt;b&amp;gt;Control Group&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Post-treatment outcome&amp;quot;]
end
A --&amp;gt;|&amp;quot;Change in&amp;lt;br/&amp;gt;treated&amp;quot;| C
B --&amp;gt;|&amp;quot;Change in&amp;lt;br/&amp;gt;control&amp;quot;| D
style A fill:#d97757,stroke:#141413,color:#fff
style C fill:#d97757,stroke:#141413,color:#fff
style B fill:#6a9bcc,stroke:#141413,color:#fff
style D fill:#6a9bcc,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;h3 id="the-did-estimator">The DiD estimator&lt;/h3>
&lt;p>The 2x2 DiD estimator formalizes this double comparison. Let $k$ denote the treated group and $U$ the untreated group:&lt;/p>
&lt;p>$$\hat{\delta}^{2 \times 2}_{kU} = \big( \bar{Y}_k^{Post} - \bar{Y}_k^{Pre} \big) - \big( \bar{Y}_U^{Post} - \bar{Y}_U^{Pre} \big)$$&lt;/p>
&lt;p>In words: take the before-and-after change in the treated group, subtract the before-and-after change in the control group, and the remainder is the treatment effect. Here $\bar{Y}_k^{Post}$ is the average outcome for treated units in the post-treatment period (rows where &lt;code>treated = 1&lt;/code> and &lt;code>post = 1&lt;/code>), and similarly for the other three terms.&lt;/p>
&lt;h3 id="what-did-actually-estimates-the-potential-outcomes-framework">What DiD actually estimates: The potential outcomes framework&lt;/h3>
&lt;p>The sample-means formula above tells us &lt;em>how to compute&lt;/em> DiD from data, but it does not tell us &lt;em>what causal quantity&lt;/em> DiD recovers or &lt;em>under what assumptions&lt;/em> it is valid. To answer these deeper questions, we need the &lt;strong>potential outcomes framework&lt;/strong> (&lt;a href="https://doi.org/10.1037/h0037350" target="_blank" rel="noopener">Rubin, 1974&lt;/a>).&lt;/p>
&lt;p>The key idea is that every unit has &lt;em>two&lt;/em> potential outcomes at every point in time, but we only ever observe one of them:&lt;/p>
&lt;ul>
&lt;li>$Y^1_{i}$ &amp;mdash; the outcome unit $i$ would experience &lt;strong>with&lt;/strong> treatment&lt;/li>
&lt;li>$Y^0_{i}$ &amp;mdash; the outcome unit $i$ would experience &lt;strong>without&lt;/strong> treatment&lt;/li>
&lt;/ul>
&lt;p>For a treated city, we observe $Y^1$ (what actually happened after adopting AI tutoring) but never $Y^0$ (what &lt;em>would have&lt;/em> happened had the city not adopted AI). For a control city, we observe $Y^0$ but never $Y^1$. This is the &lt;strong>fundamental problem of causal inference&lt;/strong>: for any individual unit, the causal effect $Y^1_{i} - Y^0_{i}$ is unobservable because one potential outcome is always missing.&lt;/p>
&lt;p>Since we cannot measure individual effects, we aim for the &lt;strong>Average Treatment Effect on the Treated (ATT)&lt;/strong> &amp;mdash; the average causal effect across all treated units in the post-treatment period:&lt;/p>
&lt;p>$$ATT = E[Y^1_k - Y^0_k | Post]$$&lt;/p>
&lt;p>In words: what is the average difference between what treated units actually experienced and what they &lt;em>would have&lt;/em> experienced without treatment, measured in the post-treatment period? Here $E[\cdot]$ denotes the expected value (population average), $k$ indexes the treated group, and the conditioning on $Post$ restricts attention to the post-treatment period. In our data, $E[Y^1_k | Post]$ corresponds to the average &lt;code>outcome&lt;/code> for rows where &lt;code>treated = 1&lt;/code> and &lt;code>post = 1&lt;/code> &amp;mdash; that is, $\bar{Y}_k^{Post}$ from the previous formula.&lt;/p>
&lt;p>The challenge is that $E[Y^0_k | Post]$ &amp;mdash; the average untreated outcome for the treated group after treatment &amp;mdash; is a &lt;strong>counterfactual&lt;/strong> that we never observe. Treated cities received the policy, so we cannot see what their outcomes would have been without it. This is where DiD&amp;rsquo;s clever trick comes in.&lt;/p>
&lt;h3 id="from-sample-means-to-potential-outcomes">From sample means to potential outcomes&lt;/h3>
&lt;p>Let us now connect the sample-means formula to potential outcomes by rewriting each $\bar{Y}$ term. For the &lt;strong>control group&lt;/strong>, which never receives treatment, the observed outcome always equals the untreated potential outcome: $Y_U = Y^0_U$ in both periods. For the &lt;strong>treated group&lt;/strong>, the observed outcome equals the untreated potential outcome before treatment ($Y_k = Y^0_k$ when $Pre$) and the treated potential outcome after ($Y_k = Y^1_k$ when $Post$). Substituting these into the DiD formula:&lt;/p>
&lt;p>$$\hat{\delta}^{2 \times 2}_{kU} = \big( \underbrace{\bar{Y}_k^{Post}}_{= E[Y^1_k | Post]} - \underbrace{\bar{Y}_k^{Pre}}_{= E[Y^0_k | Pre]} \big) - \big( \underbrace{\bar{Y}_U^{Post}}_{= E[Y^0_U | Post]} - \underbrace{\bar{Y}_U^{Pre}}_{= E[Y^0_U | Pre]} \big)$$&lt;/p>
&lt;p>On the left of the outer subtraction, the treated group&amp;rsquo;s pre-treatment mean uses $Y^0_k$ (no treatment yet) and post-treatment mean uses $Y^1_k$ (treatment is active). On the right, both control group means use $Y^0_U$ (never treated). Now we apply a standard algebraic trick: &lt;strong>add and subtract&lt;/strong> the unobserved counterfactual $E[Y^0_k | Post]$ inside the first parenthesis:&lt;/p>
&lt;p>$$= \big( E[Y^1_k | Post] - E[Y^0_k | Post] + E[Y^0_k | Post] - E[Y^0_k | Pre] \big) - \big( E[Y^0_U | Post] - E[Y^0_U | Pre] \big)$$&lt;/p>
&lt;p>Rearranging by grouping the first two terms and the last three:&lt;/p>
&lt;p>$$= \underbrace{E[Y^1_k | Post] - E[Y^0_k | Post]}_{ATT} + \underbrace{\big( E[Y^0_k | Post] - E[Y^0_k | Pre] \big) - \big( E[Y^0_U | Post] - E[Y^0_U | Pre] \big)}_{Bias}$$&lt;/p>
&lt;p>This is the fundamental decomposition of the DiD estimator (&lt;a href="https://mixtape.scunning.com/09-difference_in_differences" target="_blank" rel="noopener">Cunningham, 2021&lt;/a>). The first term is the &lt;strong>ATT&lt;/strong> &amp;mdash; the causal quantity we want. The second term is the &lt;strong>non-parallel trends bias&lt;/strong> &amp;mdash; the difference in how the two groups' untreated outcomes would have evolved over time. The bias term compares the untreated trajectory of the treated group ($E[Y^0_k | Post] - E[Y^0_k | Pre]$) against the untreated trajectory of the control group ($E[Y^0_U | Post] - E[Y^0_U | Pre]$). If the bias term is zero, the DiD estimator cleanly identifies the ATT.&lt;/p>
&lt;h3 id="parallel-trends-assumption">Parallel trends assumption&lt;/h3>
&lt;p>The bias term vanishes when the treated and control groups would have followed the same trajectory absent treatment:&lt;/p>
&lt;p>$$E[Y^0_k | Post] - E[Y^0_k | Pre] = E[Y^0_U | Post] - E[Y^0_U | Pre]$$&lt;/p>
&lt;p>This is the &lt;strong>parallel trends assumption&lt;/strong>. It does not require the groups to have the same outcome levels &amp;mdash; only the same &lt;em>trends&lt;/em>. Two cities can have different learning scores, but if their learning scores were rising at the same speed before the AI rollout, DiD can credibly estimate the policy&amp;rsquo;s impact. Importantly, this assumption is &lt;strong>fundamentally untestable&lt;/strong> because the counterfactual outcome $E[Y^0_k | Post]$ &amp;mdash; what would have happened to the treated group absent treatment &amp;mdash; is never observed. We can check whether trends were parallel in the pre-treatment period, but this does not guarantee they would have remained parallel afterward. This limitation is why Section 11 introduces HonestDiD sensitivity analysis.&lt;/p>
&lt;h3 id="regression-formulation">Regression formulation&lt;/h3>
&lt;p>In practice, DiD is implemented as a regression with an interaction term:&lt;/p>
&lt;p>$$Y_{it} = \alpha + \gamma \cdot Treated_i + \lambda \cdot Post_t + \delta \cdot (Treated_i \times Post_t) + \varepsilon_{it}$$&lt;/p>
&lt;p>where $Treated_i$ is the group indicator (our &lt;code>treated&lt;/code> column), $Post_t$ is the time indicator (our &lt;code>post&lt;/code> column), and $\delta$ is the DiD treatment effect. The coefficient $\gamma$ captures the pre-existing level difference between groups, and $\lambda$ captures the common time trend. This regression mechanically constructs the counterfactual using the control group&amp;rsquo;s trajectory &amp;mdash; it always estimates the $\delta$ coefficient as the extra change in the treated group, which is only valid if the counterfactual trend truly equals the control group&amp;rsquo;s trend.&lt;/p>
&lt;p>&lt;strong>Estimand clarity:&lt;/strong> DiD targets the &lt;strong>Average Treatment Effect on the Treated (ATT)&lt;/strong> &amp;mdash; the average impact of treatment on those units that actually received it. This differs from the Average Treatment Effect (ATE), which averages over the entire population including units that were never treated. The ATT answers: &amp;ldquo;For the units that received the policy, how much did it change their outcomes?&amp;rdquo; This is typically the policy-relevant question, since the decision-maker wants to know whether the intervention helped the people it was aimed at.&lt;/p>
&lt;p>Now that we understand the logic, let us implement it step by step using the &lt;code>diff-diff&lt;/code> package.&lt;/p>
&lt;h2 id="setup-and-imports">Setup and imports&lt;/h2>
&lt;p>Before running the analysis, install the required package:&lt;/p>
&lt;pre>&lt;code class="language-python"># Run in terminal (or use !pip install in a notebook)
pip install diff-diff
&lt;/code>&lt;/pre>
&lt;p>The following code imports all necessary libraries and sets configuration variables. The &lt;code>diff-diff&lt;/code> package provides &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>generate_did_data()&lt;/code>&lt;/a> to create synthetic panel data with known treatment effects, &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>DifferenceInDifferences()&lt;/code>&lt;/a> for the classic 2x2 estimator, and several advanced estimators for multi-period and staggered designs.&lt;/p>
&lt;pre>&lt;code class="language-python">import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from diff_diff import (
DifferenceInDifferences,
MultiPeriodDiD,
CallawaySantAnna,
BaconDecomposition,
HonestDiD,
generate_did_data,
generate_staggered_data,
check_parallel_trends,
)
# Reproducibility
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
# Site color palette
STEEL_BLUE = &amp;quot;#6a9bcc&amp;quot;
WARM_ORANGE = &amp;quot;#d97757&amp;quot;
NEAR_BLACK = &amp;quot;#141413&amp;quot;
TEAL = &amp;quot;#00d4c8&amp;quot;
# Dark-theme palette
DARK_NAVY = &amp;quot;#0f1729&amp;quot;
GRID_LINE = &amp;quot;#1f2b5e&amp;quot;
LIGHT_TEXT = &amp;quot;#c8d0e0&amp;quot;
WHITE_TEXT = &amp;quot;#e8ecf2&amp;quot;
&lt;/code>&lt;/pre>
&lt;h2 id="classic-2x2-did-design">Classic 2x2 DiD design&lt;/h2>
&lt;p>The simplest DiD setup has two groups (treated and control) observed at two time points (before and after treatment). We start here because the 2x2 case makes the mechanics of DiD transparent before moving to more complex designs.&lt;/p>
&lt;h3 id="generating-synthetic-panel-data">Generating synthetic panel data&lt;/h3>
&lt;p>We use &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>generate_did_data()&lt;/code>&lt;/a> to create a synthetic panel where the true treatment effect is exactly 5.0 units. This known ground truth lets us verify that the estimator recovers the correct answer. The function creates a balanced panel with &lt;code>n_units&lt;/code> units observed over &lt;code>n_periods&lt;/code> periods, where &lt;code>treatment_fraction&lt;/code> of units receive treatment starting at &lt;code>treatment_period&lt;/code>.&lt;/p>
&lt;pre>&lt;code class="language-python">data_2x2 = generate_did_data(
n_units=100,
n_periods=10,
treatment_effect=5.0,
treatment_period=5,
treatment_fraction=0.5,
seed=RANDOM_SEED,
)
print(f&amp;quot;Dataset shape: {data_2x2.shape}&amp;quot;)
print(f&amp;quot;Columns: {data_2x2.columns.tolist()}&amp;quot;)
print(f&amp;quot;\nTreatment groups:&amp;quot;)
print(data_2x2.groupby(&amp;quot;treated&amp;quot;)[&amp;quot;unit&amp;quot;].nunique().rename(
{0: &amp;quot;Control&amp;quot;, 1: &amp;quot;Treated&amp;quot;}))
print(f&amp;quot;\nPeriods: {sorted(int(p) for p in data_2x2['period'].unique())}&amp;quot;)
print(f&amp;quot;Treatment period: 5 (post = 1 for periods &amp;gt;= 5)&amp;quot;)
print(f&amp;quot;True treatment effect: 5.0&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>Dataset shape: (1000, 6)
Columns: ['unit', 'period', 'treated', 'post', 'outcome', 'true_effect']
Treatment groups:
treated
Control 50
Treated 50
Name: unit, dtype: int64
Periods: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Treatment period: 5 (post = 1 for periods &amp;gt;= 5)
True treatment effect: 5.0
&lt;/code>&lt;/pre>
&lt;p>The synthetic panel contains 1,000 observations: 100 units observed across 10 periods (0 through 9). Half the units (50) are assigned to treatment, which begins at period 5. The dataset includes a &lt;code>true_effect&lt;/code> column that equals 0.0 in pre-treatment periods and 5.0 in post-treatment periods for treated units, providing a built-in benchmark. The &lt;code>post&lt;/code> indicator is 1 for periods 5&amp;ndash;9 and 0 for periods 0&amp;ndash;4, matching the binary time dimension of the classic 2x2 framework.&lt;/p>
&lt;h3 id="exploring-the-2x2-dataset">Exploring the 2x2 dataset&lt;/h3>
&lt;p>Before estimating any model, we inspect the raw data to understand its structure. The &lt;code>.head()&lt;/code> method shows the first rows so we can see how each observation is organized as a unit-period pair.&lt;/p>
&lt;pre>&lt;code class="language-python">data_2x2.head(10)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> unit period treated post outcome true_effect
0 0 1 0 10.231272 0.0
0 1 1 0 12.408662 0.0
0 2 1 0 11.253170 0.0
0 3 1 0 12.846950 0.0
0 4 1 0 11.675816 0.0
0 5 1 1 17.903997 5.0
0 6 1 1 17.659412 5.0
0 7 1 1 18.770401 5.0
0 8 1 1 20.449742 5.0
0 9 1 1 18.382114 5.0
&lt;/code>&lt;/pre>
&lt;p>Each row is one unit in one period. The &lt;code>unit&lt;/code> column identifies the individual, &lt;code>period&lt;/code> tracks time, &lt;code>treated&lt;/code> indicates group assignment (time-invariant), and &lt;code>post&lt;/code> flags observations after the treatment period. The &lt;code>outcome&lt;/code> column is what we aim to explain, and &lt;code>true_effect&lt;/code> is the ground truth we will try to recover. This unit-period structure is the hallmark of &lt;strong>panel data&lt;/strong> &amp;mdash; repeated observations on the same units over time.&lt;/p>
&lt;p>Summary statistics confirm the design parameters:&lt;/p>
&lt;pre>&lt;code class="language-python">data_2x2.describe()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> unit period treated post outcome true_effect
count 1000.000000 1000.000000 1000.00000 1000.00000 1000.000000 1000.000000
mean 49.500000 4.500000 0.50000 0.50000 13.380874 1.250000
std 28.880514 2.873719 0.50025 0.50025 3.752000 2.166147
min 0.000000 0.000000 0.00000 0.00000 4.965883 0.000000
25% 24.750000 2.000000 0.00000 0.00000 10.716817 0.000000
50% 49.500000 4.500000 0.50000 0.50000 12.558536 0.000000
75% 74.250000 7.000000 1.00000 1.00000 15.926784 1.250000
max 99.000000 9.000000 1.00000 1.00000 24.294992 5.000000
&lt;/code>&lt;/pre>
&lt;p>The means of &lt;code>treated&lt;/code> and &lt;code>post&lt;/code> are both exactly 0.50, confirming a perfectly balanced design: half the units are treated, and half the time periods are post-treatment. The outcome ranges from about 5.0 to 24.3 with a mean of 13.4, reflecting the combination of time trends, unit effects, and treatment effects. The &lt;code>true_effect&lt;/code> mean of 1.25 comes from the fact that only 25% of observations (treated units in post-treatment periods) have a non-zero effect of 5.0.&lt;/p>
&lt;p>A crosstab reveals the 2x2 structure that gives DiD its name:&lt;/p>
&lt;pre>&lt;code class="language-python">pd.crosstab(data_2x2[&amp;quot;treated&amp;quot;], data_2x2[&amp;quot;post&amp;quot;], margins=True)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>post 0 1 All
treated
0 250 250 500
1 250 250 500
All 500 500 1000
&lt;/code>&lt;/pre>
&lt;p>This is the core of the 2x2 design: 250 observations in each of the four cells (control-pre, control-post, treated-pre, treated-post). The balanced allocation means each cell has equal weight in the estimator, which maximizes statistical power. In observational studies, these cell sizes are rarely equal, but the DiD estimator adjusts for imbalance automatically.&lt;/p>
&lt;p>Finally, we examine how the outcome varies across the four cells:&lt;/p>
&lt;pre>&lt;code class="language-python">data_2x2.groupby([&amp;quot;treated&amp;quot;, &amp;quot;post&amp;quot;])[&amp;quot;outcome&amp;quot;].describe()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> count mean std min 25% 50% 75% max
treated post
0 0 250.0 10.614957 1.871283 5.670539 9.261649 10.781139 11.866492 15.825691
1 250.0 13.086386 1.968271 8.158302 11.777457 13.149548 14.600075 18.372485
1 0 250.0 11.114546 2.015353 4.965883 9.909285 11.065526 12.494486 16.804462
1 250.0 18.707609 1.905034 13.182572 17.296981 18.870692 20.070330 24.294992
&lt;/code>&lt;/pre>
&lt;p>In the pre-treatment period, both groups have similar mean outcomes: 10.61 for the control group and 11.11 for the treated group &amp;mdash; a negligible difference of 0.50 that suggests the groups started on comparable footing. In the post-treatment period, the control group mean rises to 13.09 (a gain of 2.47), while the treated group mean jumps to 18.71 (a gain of 7.59). The extra gain for the treated group (7.59 - 2.47 = 5.12) closely approximates the treatment effect that DiD will formally estimate. The raw numbers already hint that something happened to the treated group beyond the natural time trend.&lt;/p>
&lt;p>The box plot below visualizes these distributions:&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
groups = [
(&amp;quot;Control, Pre&amp;quot;, data_2x2[(data_2x2[&amp;quot;treated&amp;quot;] == 0) &amp;amp; (data_2x2[&amp;quot;post&amp;quot;] == 0)][&amp;quot;outcome&amp;quot;]),
(&amp;quot;Control, Post&amp;quot;, data_2x2[(data_2x2[&amp;quot;treated&amp;quot;] == 0) &amp;amp; (data_2x2[&amp;quot;post&amp;quot;] == 1)][&amp;quot;outcome&amp;quot;]),
(&amp;quot;Treated, Pre&amp;quot;, data_2x2[(data_2x2[&amp;quot;treated&amp;quot;] == 1) &amp;amp; (data_2x2[&amp;quot;post&amp;quot;] == 0)][&amp;quot;outcome&amp;quot;]),
(&amp;quot;Treated, Post&amp;quot;, data_2x2[(data_2x2[&amp;quot;treated&amp;quot;] == 1) &amp;amp; (data_2x2[&amp;quot;post&amp;quot;] == 1)][&amp;quot;outcome&amp;quot;]),
]
bp = ax.boxplot(
[g[1] for g in groups],
tick_labels=[g[0] for g in groups],
patch_artist=True,
widths=0.5,
medianprops=dict(color=WHITE_TEXT, linewidth=2),
)
box_colors = [STEEL_BLUE, STEEL_BLUE, WARM_ORANGE, WARM_ORANGE]
for patch, color in zip(bp[&amp;quot;boxes&amp;quot;], box_colors):
patch.set_facecolor(color)
patch.set_alpha(0.6)
ax.set_ylabel(&amp;quot;Outcome&amp;quot;)
ax.set_title(&amp;quot;Outcome Distribution by Treatment Group and Period&amp;quot;)
plt.savefig(&amp;quot;did_outcome_distribution.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_outcome_distribution.png" alt="Box plot showing outcome distributions for control and treated groups in pre and post periods. Both groups start with similar distributions, but the treated group shifts markedly upward in the post period.">&lt;/p>
&lt;p>The box plot makes the treatment effect visible at a glance. In the pre-treatment period, control (steel blue) and treated (warm orange) boxes overlap almost completely, centered around 10.6&amp;ndash;11.1. Both groups shift upward in the post period due to the natural time trend, but the treated group shifts &lt;em>more&lt;/em> &amp;mdash; its median jumps to around 18.9, compared to 13.1 for the control. The extra upward shift for the treated group is the treatment effect that DiD will formally estimate. Notice also that the spread (box height) remains similar across all four groups, suggesting that treatment affects the level but not the variability of outcomes.&lt;/p>
&lt;h3 id="visualizing-parallel-trends">Visualizing parallel trends&lt;/h3>
&lt;p>Before estimating the treatment effect, we check whether the treated and control groups followed similar trajectories in the pre-treatment period. This visual inspection is the first step in assessing whether the parallel trends assumption is plausible. If the two groups were diverging before treatment, any post-treatment difference could reflect pre-existing trends rather than a causal effect.&lt;/p>
&lt;pre>&lt;code class="language-python">treated_means = data_2x2[data_2x2[&amp;quot;treated&amp;quot;] == 1].groupby(&amp;quot;period&amp;quot;)[&amp;quot;outcome&amp;quot;].mean()
control_means = data_2x2[data_2x2[&amp;quot;treated&amp;quot;] == 0].groupby(&amp;quot;period&amp;quot;)[&amp;quot;outcome&amp;quot;].mean()
fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
ax.plot(control_means.index, control_means.values, &amp;quot;o-&amp;quot;,
color=STEEL_BLUE, linewidth=2, markersize=7, label=&amp;quot;Control group&amp;quot;)
ax.plot(treated_means.index, treated_means.values, &amp;quot;s-&amp;quot;,
color=WARM_ORANGE, linewidth=2, markersize=7, label=&amp;quot;Treated group&amp;quot;)
ax.axvline(x=4.5, color=LIGHT_TEXT, linestyle=&amp;quot;--&amp;quot;, linewidth=1.5,
alpha=0.7, label=&amp;quot;Treatment onset&amp;quot;)
ax.set_xlabel(&amp;quot;Period&amp;quot;)
ax.set_ylabel(&amp;quot;Average Outcome&amp;quot;)
ax.set_title(&amp;quot;Parallel Trends: Treatment vs Control Groups&amp;quot;)
ax.legend(loc=&amp;quot;upper left&amp;quot;)
ax.set_xticks(range(10))
plt.savefig(&amp;quot;did_parallel_trends.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_parallel_trends.png" alt="Parallel trends plot showing treatment and control groups tracking closely in pre-treatment periods 0-4, then diverging sharply after treatment onset at period 5.">&lt;/p>
&lt;p>The two groups move in lockstep during periods 0 through 4, confirming that the parallel trends assumption holds in this synthetic dataset. Both lines fluctuate around similar values with no visible divergence before period 5. After treatment onset, the treated group (warm orange) jumps upward while the control group (steel blue) continues its prior trajectory. The gap between the two lines in the post-treatment period visually represents the treatment effect &amp;mdash; roughly 5 units, consistent with the true effect built into the data.&lt;/p>
&lt;h3 id="estimating-the-treatment-effect">Estimating the treatment effect&lt;/h3>
&lt;p>With parallel trends confirmed visually, we apply the classic DiD estimator. The &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>DifferenceInDifferences()&lt;/code>&lt;/a> class implements the 2x2 design with analytical standard errors. The &lt;code>.fit()&lt;/code> method takes the data along with column names for the outcome, treatment indicator, and time indicator (pre/post).&lt;/p>
&lt;pre>&lt;code class="language-python">did = DifferenceInDifferences()
results_2x2 = did.fit(data_2x2, outcome=&amp;quot;outcome&amp;quot;,
treatment=&amp;quot;treated&amp;quot;, time=&amp;quot;post&amp;quot;)
results_2x2.print_summary()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>======================================================================
Difference-in-Differences Estimation Results
======================================================================
Observations: 1000
Treated units: 500
Control units: 500
R-squared: 0.7332
----------------------------------------------------------------------
Parameter Estimate Std. Err. t-stat P&amp;gt;|t|
----------------------------------------------------------------------
ATT 5.1216 0.2455 20.863 0.0000 ***
----------------------------------------------------------------------
95% Confidence Interval: [4.6399, 5.6034]
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
======================================================================
&lt;/code>&lt;/pre>
&lt;p>The estimated ATT is 5.12, close to the true effect of 5.0, with a standard error of 0.25. The t-statistic of 20.86 and p-value near zero confirm that the effect is highly statistically significant. The 95% confidence interval [4.64, 5.60] comfortably contains the true value of 5.0, demonstrating that the classic DiD estimator successfully recovers the known treatment effect. The small deviation from 5.0 (an overestimate of 0.12) reflects sampling variability, not estimator bias &amp;mdash; with 100 units and 10 periods, some random noise is expected.&lt;/p>
&lt;h3 id="visualizing-the-counterfactual">Visualizing the counterfactual&lt;/h3>
&lt;p>DiD&amp;rsquo;s power lies in constructing a &lt;strong>counterfactual&lt;/strong> &amp;mdash; what would have happened to the treated group without treatment. We build this by projecting the control group&amp;rsquo;s post-treatment trajectory, shifted up by the pre-treatment gap between the groups. The shaded area between the actual treated outcomes and this counterfactual line represents the estimated causal effect.&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
ax.plot(control_means.index, control_means.values, &amp;quot;o-&amp;quot;,
color=STEEL_BLUE, linewidth=2, markersize=7, label=&amp;quot;Control group&amp;quot;)
ax.plot(treated_means.index, treated_means.values, &amp;quot;s-&amp;quot;,
color=WARM_ORANGE, linewidth=2, markersize=7, label=&amp;quot;Treated group&amp;quot;)
# Counterfactual: treated group without treatment
pre_diff = treated_means.loc[:4].mean() - control_means.loc[:4].mean()
counterfactual = control_means.loc[5:] + pre_diff
ax.plot(counterfactual.index, counterfactual.values, &amp;quot;s--&amp;quot;,
color=TEAL, linewidth=2, markersize=7,
label=&amp;quot;Counterfactual (no treatment)&amp;quot;)
ax.fill_between(counterfactual.index, counterfactual.values,
treated_means.loc[5:].values, alpha=0.2, color=TEAL,
label=f&amp;quot;Treatment effect (ATT ≈ {results_2x2.att:.1f})&amp;quot;)
ax.axvline(x=4.5, color=LIGHT_TEXT, linestyle=&amp;quot;--&amp;quot;, linewidth=1.5, alpha=0.7)
ax.set_xlabel(&amp;quot;Period&amp;quot;)
ax.set_ylabel(&amp;quot;Average Outcome&amp;quot;)
ax.set_title(&amp;quot;DiD Treatment Effect: Observed vs Counterfactual&amp;quot;)
ax.legend(loc=&amp;quot;upper left&amp;quot;)
ax.set_xticks(range(10))
plt.savefig(&amp;quot;did_treatment_effect.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_treatment_effect.png" alt="Counterfactual plot showing the treated group diverging from its projected path after treatment. The teal shaded area between the actual and counterfactual lines represents the causal effect.">&lt;/p>
&lt;p>The teal dashed line traces where the treated group would have been without the intervention, constructed by shifting the control group&amp;rsquo;s post-treatment path to match the treated group&amp;rsquo;s pre-treatment level. The shaded gap between the actual treated outcomes (warm orange) and this counterfactual (teal) is the estimated causal effect &amp;mdash; approximately 5.1 units per period. This visualization makes the DiD logic tangible: the control group&amp;rsquo;s trajectory serves as the mirror image of the treated group&amp;rsquo;s no-treatment path, and the extra gain above that mirror is what the policy caused.&lt;/p>
&lt;h2 id="testing-parallel-trends">Testing parallel trends&lt;/h2>
&lt;p>The visual check suggested parallel trends hold, but a formal statistical test provides more rigorous evidence. The &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>check_parallel_trends()&lt;/code>&lt;/a> function compares the pre-treatment time trends of the treated and control groups by estimating a linear slope for each group across the pre-treatment periods, then testing whether the two slopes are statistically different.&lt;/p>
&lt;pre>&lt;code class="language-python">pt_result = check_parallel_trends(
data_2x2,
outcome=&amp;quot;outcome&amp;quot;,
time=&amp;quot;period&amp;quot;,
treatment_group=&amp;quot;treated&amp;quot;,
pre_periods=[0, 1, 2, 3, 4],
)
print(f&amp;quot;Treated group pre-trend slope: {pt_result['treated_trend']:.4f}&amp;quot;
f&amp;quot; (SE = {pt_result['treated_trend_se']:.4f})&amp;quot;)
print(f&amp;quot;Control group pre-trend slope: {pt_result['control_trend']:.4f}&amp;quot;
f&amp;quot; (SE = {pt_result['control_trend_se']:.4f})&amp;quot;)
print(f&amp;quot;Trend difference: {pt_result['trend_difference']:.4f}&amp;quot;
f&amp;quot; (SE = {pt_result['trend_difference_se']:.4f})&amp;quot;)
print(f&amp;quot;t-statistic: {pt_result['t_statistic']:.4f}&amp;quot;)
print(f&amp;quot;p-value: {pt_result['p_value']:.4f}&amp;quot;)
print(f&amp;quot;Parallel trends plausible: {pt_result['parallel_trends_plausible']}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>Treated group pre-trend slope: 0.5262 (SE = 0.0839)
Control group pre-trend slope: 0.4047 (SE = 0.0798)
Trend difference: 0.1216 (SE = 0.1158)
t-statistic: 1.0497
p-value: 0.2938
Parallel trends plausible: True
&lt;/code>&lt;/pre>
&lt;p>The pre-treatment trend slopes are 0.53 for the treated group and 0.40 for the control group &amp;mdash; a difference of 0.12 with a p-value of 0.29. Since p &amp;gt; 0.05, we fail to reject the null hypothesis that the trends are equal, supporting the parallel trends assumption. However, a critical caveat: &lt;em>failing to reject is not the same as confirming&lt;/em>. The test has limited power, especially with only 5 pre-treatment periods. Even if the trends differed slightly, this test might not detect it. Moreover, &lt;a href="https://doi.org/10.1257/aeri.20210236" target="_blank" rel="noopener">Roth (2022)&lt;/a> shows that conditioning on passing a pre-test can distort subsequent inference &amp;mdash; estimated effects may be biased toward zero and confidence intervals may have incorrect coverage. This is why Section 11 introduces HonestDiD, which asks: &amp;ldquo;How wrong could parallel trends be before our conclusion changes?&amp;rdquo; That question is more informative than a binary pass/fail test.&lt;/p>
&lt;h2 id="event-study-dynamic-treatment-effects">Event study: Dynamic treatment effects&lt;/h2>
&lt;p>The 2x2 estimator produces a single ATT that averages across all post-treatment periods. But treatment effects often change over time &amp;mdash; they might build up gradually, appear immediately, or fade out. An &lt;strong>event study&lt;/strong> (also called dynamic DiD) estimates separate effects for each period relative to treatment, revealing the full trajectory.&lt;/p>
&lt;p>The event study extends the basic DiD regression by replacing the single treatment effect $\delta$ with a set of period-specific coefficients &amp;mdash; one for each period before and after treatment:&lt;/p>
&lt;p>$$Y_{it} = \gamma_i + \lambda_t + \sum_{k=-K+1}^{-2} \beta_k^{lead} D_{it}^k + \sum_{k=0}^{L} \beta_k^{lag} D_{it}^k + \varepsilon_{it}$$&lt;/p>
&lt;p>Let us unpack each component of this equation:&lt;/p>
&lt;ul>
&lt;li>$Y_{it}$ is the outcome for unit $i$ at time $t$ &amp;mdash; the variable we are trying to explain (our &lt;code>outcome&lt;/code> column).&lt;/li>
&lt;li>$\gamma_i$ are &lt;strong>unit fixed effects&lt;/strong> &amp;mdash; a separate intercept for each unit that absorbs all time-invariant characteristics. For example, if one city always has higher learning scores than another due to demographics or school funding levels, $\gamma_i$ captures that permanent difference. In practice, this is equivalent to demeaning each unit&amp;rsquo;s outcome by its own time-average.&lt;/li>
&lt;li>$\lambda_t$ are &lt;strong>time fixed effects&lt;/strong> &amp;mdash; a separate intercept for each period that absorbs shocks common to all units at a given time. If a national curriculum reform in period 3 raises learning outcomes for everyone equally, $\lambda_t$ captures that common shift. Together with unit fixed effects, this implements the &amp;ldquo;two-way&amp;rdquo; in TWFE.&lt;/li>
&lt;li>$D_{it}^k$ is a &lt;strong>relative-time indicator&lt;/strong> (also called an event-time dummy): it equals 1 when unit $i$ at time $t$ is exactly $k$ periods away from its treatment onset, and 0 otherwise. For a unit first treated at period 5, we have $D_{i,3}^{-2} = 1$ (two periods before treatment), $D_{i,5}^{0} = 1$ (the treatment period itself), $D_{i,7}^{2} = 1$ (two periods after treatment), and so on.&lt;/li>
&lt;li>$\beta_k^{lead}$ (for $k = -K+1, \ldots, -2$) are the &lt;strong>lead coefficients&lt;/strong> &amp;mdash; pre-treatment effects at each period before treatment. These serve as &lt;strong>placebo tests&lt;/strong>: if the treated and control groups were evolving similarly before the intervention, all lead coefficients should be close to zero and statistically insignificant. A significant lead coefficient signals a pre-existing divergence, which would undermine the parallel trends assumption. The summation starts at $k = -K+1$ (the earliest available lead) and stops at $k = -2$, because the period immediately before treatment ($k = -1$) is &lt;strong>omitted as the reference period&lt;/strong> and normalized to zero. All other coefficients are estimated relative to this baseline.&lt;/li>
&lt;li>$\beta_k^{lag}$ (for $k = 0, 1, \ldots, L$) are the &lt;strong>lag coefficients&lt;/strong> &amp;mdash; post-treatment effects at each period after treatment onset. The coefficient $\beta_0^{lag}$ captures the &lt;strong>instantaneous effect&lt;/strong> at the moment treatment begins, $\beta_1^{lag}$ captures the effect one period later, and so on through $\beta_L^{lag}$ at $L$ periods after treatment. These coefficients trace out the &lt;strong>dynamic treatment effect trajectory&lt;/strong>: they reveal whether the effect appears immediately or builds up gradually, whether it persists or fades out, and whether it stabilizes at a constant level or continues to grow.&lt;/li>
&lt;li>$\varepsilon_{it}$ is the error term, capturing all unobserved factors not absorbed by the fixed effects or treatment indicators.&lt;/li>
&lt;/ul>
&lt;p>The key insight is that this single equation simultaneously tests the identifying assumption &lt;em>and&lt;/em> estimates the treatment effect. The leads ($\beta_k^{lead}$) test parallel trends period by period, while the lags ($\beta_k^{lag}$) reveal how the treatment effect evolves over time. In our tutorial, treatment begins at period 5 and the reference period is 4 ($k = -1$), so we have 4 lead coefficients at $k = -5, -4, -3, -2$ (corresponding to periods 0&amp;ndash;3) and $L = 4$ lag coefficients at $k = 0, 1, 2, 3, 4$ (corresponding to periods 5&amp;ndash;9).&lt;/p>
&lt;p>The &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>MultiPeriodDiD()&lt;/code>&lt;/a> estimator fits this specification, using one pre-treatment period as the reference point.&lt;/p>
&lt;pre>&lt;code class="language-python">event = MultiPeriodDiD()
results_event = event.fit(
data_2x2,
outcome=&amp;quot;outcome&amp;quot;,
treatment=&amp;quot;treated&amp;quot;,
time=&amp;quot;period&amp;quot;,
post_periods=[5, 6, 7, 8, 9],
reference_period=4,
)
results_event.print_summary()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>================================================================================
Multi-Period Difference-in-Differences Estimation Results
================================================================================
Observations: 1000
Treated observations: 500
Control observations: 500
Pre-treatment periods: 5
Post-treatment periods: 5
R-squared: 0.7648
--------------------------------------------------------------------------------
Pre-Period Effects (Parallel Trends Test)
--------------------------------------------------------------------------------
Period Estimate Std. Err. t-stat P&amp;gt;|t| Sig.
--------------------------------------------------------------------------------
0 -0.5167 0.5121 -1.009 0.3132
1 -0.5050 0.5031 -1.004 0.3157
2 -0.2804 0.5228 -0.536 0.5919
3 -0.3227 0.5187 -0.622 0.5340
[ref: 4] 0.0000 --- --- ---
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Post-Period Treatment Effects
--------------------------------------------------------------------------------
Period Estimate Std. Err. t-stat P&amp;gt;|t| Sig.
--------------------------------------------------------------------------------
5 4.6509 0.5162 9.011 0.0000 ***
6 4.8285 0.5227 9.238 0.0000 ***
7 4.6907 0.5068 9.255 0.0000 ***
8 4.7888 0.4908 9.757 0.0000 ***
9 5.0244 0.5203 9.657 0.0000 ***
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Average Treatment Effect (across post-periods)
--------------------------------------------------------------------------------
Parameter Estimate Std. Err. t-stat P&amp;gt;|t| Sig.
--------------------------------------------------------------------------------
Avg ATT 4.7967 0.3923 12.227 0.0000 ***
--------------------------------------------------------------------------------
95% Confidence Interval: [4.0269, 5.5665]
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
================================================================================
&lt;/code>&lt;/pre>
&lt;p>The pre-treatment coefficients (periods 0&amp;ndash;3) are all small and statistically insignificant, ranging from -0.52 to -0.28 with p-values well above 0.05. This confirms that the treated and control groups were evolving similarly before the intervention &amp;mdash; the period-by-period placebo test passes. In contrast, all five post-treatment effects (periods 5&amp;ndash;9) are large and highly significant, ranging from 4.65 to 5.02 with t-statistics above 9.0. The average ATT across post periods is 4.80 with a 95% CI of [4.03, 5.57], consistent with the true effect of 5.0. The effects are remarkably stable over time, indicating no fade-out or build-up &amp;mdash; the treatment shifts outcomes by roughly 5 units immediately and maintains that shift.&lt;/p>
&lt;p>The event study plot below makes these dynamics visible:&lt;/p>
&lt;pre>&lt;code class="language-python">es_df = results_event.to_dataframe()
fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
pre = es_df[~es_df[&amp;quot;is_post&amp;quot;]]
post = es_df[es_df[&amp;quot;is_post&amp;quot;]]
ax.errorbar(pre[&amp;quot;period&amp;quot;], pre[&amp;quot;effect&amp;quot;], yerr=1.96 * pre[&amp;quot;se&amp;quot;],
fmt=&amp;quot;o&amp;quot;, color=STEEL_BLUE, capsize=4, linewidth=2,
markersize=8, label=&amp;quot;Pre-treatment&amp;quot;)
ax.errorbar(post[&amp;quot;period&amp;quot;], post[&amp;quot;effect&amp;quot;], yerr=1.96 * post[&amp;quot;se&amp;quot;],
fmt=&amp;quot;s&amp;quot;, color=WARM_ORANGE, capsize=4, linewidth=2,
markersize=8, label=&amp;quot;Post-treatment&amp;quot;)
# Reference period
ax.plot(4, 0, &amp;quot;D&amp;quot;, color=WHITE_TEXT, markersize=10, zorder=5,
label=&amp;quot;Reference period&amp;quot;)
ax.axhline(y=0, color=LIGHT_TEXT, linewidth=1, alpha=0.5)
ax.axvline(x=4.5, color=LIGHT_TEXT, linestyle=&amp;quot;--&amp;quot;, linewidth=1.5, alpha=0.5)
ax.axhline(y=5.0, color=TEAL, linestyle=&amp;quot;:&amp;quot;, linewidth=1.5, alpha=0.7,
label=&amp;quot;True effect (5.0)&amp;quot;)
ax.set_xlabel(&amp;quot;Period&amp;quot;)
ax.set_ylabel(&amp;quot;Estimated Effect&amp;quot;)
ax.set_title(&amp;quot;Event Study: Dynamic Treatment Effects&amp;quot;)
ax.legend(loc=&amp;quot;upper left&amp;quot;)
ax.set_xticks(range(10))
plt.savefig(&amp;quot;did_event_study.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_event_study.png" alt="Event study plot with pre-treatment coefficients clustered near zero and post-treatment coefficients jumping to approximately 5.0. Confidence intervals shown for each period.">&lt;/p>
&lt;p>The event study plot tells the DiD story at a glance. Pre-treatment coefficients (steel blue circles) hover near the zero line, their confidence intervals all crossing zero &amp;mdash; this is the visual signature of valid parallel trends. At the treatment cutoff (dashed vertical line), the estimates jump sharply to around 5.0 (warm orange squares), and the teal dotted line at 5.0 shows that every post-treatment estimate is close to the true effect. The confidence intervals in the post-treatment period are narrow and well above zero, confirming both statistical significance and accuracy.&lt;/p>
&lt;p>With the classic 2x2 case established, the next question is: what happens when different units adopt treatment at different times?&lt;/p>
&lt;h2 id="staggered-adoption-why-twfe-fails">Staggered adoption: Why TWFE fails&lt;/h2>
&lt;p>In many real-world policies, treatment does not begin simultaneously for all units. AI tutoring platforms roll out city by city, digital infrastructure investments phase in over years, and educational technology grants expand district by district. This is &lt;strong>staggered adoption&lt;/strong> &amp;mdash; different units start treatment at different times.&lt;/p>
&lt;p>The traditional approach is &lt;strong>Two-Way Fixed Effects (TWFE)&lt;/strong> regression, which estimates a single treatment coefficient using unit and time fixed effects:&lt;/p>
&lt;p>$$Y_{it} = \gamma_i + \lambda_t + \delta \cdot D_{it} + \varepsilon_{it}$$&lt;/p>
&lt;p>Here $\gamma_i$ absorbs all time-invariant unit characteristics (unit fixed effects), $\lambda_t$ absorbs all common time shocks (time fixed effects), $D_{it}$ is a treatment indicator that equals 1 when unit $i$ is treated at time $t$, and $\delta$ is the single treatment effect that TWFE estimates. With a single treatment period, $\delta$ correctly recovers the ATT. But with staggered timing, the single coefficient $\delta$ is a weighted average of many underlying 2x2 comparisons &amp;mdash; and some of those comparisons are problematic.&lt;/p>
&lt;p>The problem is that TWFE makes &lt;strong>forbidden comparisons&lt;/strong>: it implicitly uses already-treated units as controls for newly-treated units. If treatment effects grow over time, these forbidden comparisons produce negative bias, pulling the overall estimate downward. Think of it this way: if early adopters have been benefiting from treatment for three years and their outcomes have grown substantially, TWFE compares newly-treated units to these high-performing early adopters. The newly-treated units look &lt;em>worse&lt;/em> by comparison, even though they are genuinely benefiting from treatment. In extreme cases with heterogeneous treatment effects across cohorts, TWFE can even assign &lt;strong>negative weights&lt;/strong> to some 2x2 comparisons, potentially flipping the sign of the estimate opposite to every unit&amp;rsquo;s true treatment effect (this does not occur in our example, but is documented in &lt;a href="https://doi.org/10.1257/aer.20181169" target="_blank" rel="noopener">de Chaisemartin &amp;amp; D&amp;rsquo;Haultfoeuille, 2020&lt;/a>).&lt;/p>
&lt;h3 id="generating-staggered-adoption-data">Generating staggered adoption data&lt;/h3>
&lt;p>The &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>generate_staggered_data()&lt;/code>&lt;/a> function creates a panel with multiple treatment cohorts &amp;mdash; groups of units that begin treatment in different periods &amp;mdash; plus a never-treated group.&lt;/p>
&lt;pre>&lt;code class="language-python">data_stag = generate_staggered_data(
n_units=300,
n_periods=10,
seed=RANDOM_SEED,
)
print(f&amp;quot;Dataset shape: {data_stag.shape}&amp;quot;)
cohorts = data_stag.groupby(&amp;quot;first_treat&amp;quot;)[&amp;quot;unit&amp;quot;].nunique()
print(f&amp;quot;\nCohort sizes:&amp;quot;)
for ft, n in cohorts.items():
label = &amp;quot;Never-treated&amp;quot; if ft == 0 else f&amp;quot;First treated in period {ft}&amp;quot;
print(f&amp;quot; {label}: {n} units&amp;quot;)
print(f&amp;quot;\nTotal units: {cohorts.sum()}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>Dataset shape: (3000, 7)
Cohort sizes:
Never-treated: 90 units
First treated in period 3: 60 units
First treated in period 5: 75 units
First treated in period 7: 75 units
Total units: 300
&lt;/code>&lt;/pre>
&lt;p>The staggered panel has 3,000 observations (300 units across 10 periods). Three treatment cohorts adopt at different times: 60 units start treatment in period 3, 75 in period 5, and 75 in period 7. Another 90 units are never treated, serving as a clean control group. The &lt;code>first_treat&lt;/code> column records when each unit first received treatment (0 for never-treated). This staggered structure is where naive TWFE breaks down, as the next section demonstrates.&lt;/p>
&lt;h3 id="exploring-the-staggered-dataset">Exploring the staggered dataset&lt;/h3>
&lt;p>The staggered dataset has a richer structure than the 2x2 case. Inspecting the first rows reveals additional columns:&lt;/p>
&lt;pre>&lt;code class="language-python">data_stag.head(10)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> unit period outcome first_treat treated treat true_effect
0 0 11.278161 0 0 0 0.0
0 1 11.835615 0 0 0 0.0
0 2 11.542112 0 0 0 0.0
0 3 11.716260 0 0 0 0.0
0 4 12.289791 0 0 0 0.0
0 5 10.978501 0 0 0 0.0
0 6 11.426795 0 0 0 0.0
0 7 11.433938 0 0 0 0.0
0 8 11.108223 0 0 0 0.0
0 9 12.035899 0 0 0 0.0
&lt;/code>&lt;/pre>
&lt;p>Unit 0 is never-treated, so all indicators stay at zero across all 10 periods. To understand the staggered structure, we need to see what happens to treated units. The columns have distinct roles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>first_treat&lt;/code>&lt;/strong>: the period when a unit first receives treatment (0 = never treated)&lt;/li>
&lt;li>&lt;strong>&lt;code>treat&lt;/code>&lt;/strong>: &lt;strong>time-invariant&lt;/strong> group membership &amp;mdash; equals 1 for any unit &lt;em>ever&lt;/em> assigned to treatment, 0 for never-treated&lt;/li>
&lt;li>&lt;strong>&lt;code>treated&lt;/code>&lt;/strong>: &lt;strong>time-varying&lt;/strong> post-treatment indicator &amp;mdash; equals 0 before treatment onset and switches to 1 at &lt;code>first_treat&lt;/code>&lt;/li>
&lt;li>&lt;strong>&lt;code>true_effect&lt;/code>&lt;/strong>: the known ground-truth treatment effect at each period, used for verification&lt;/li>
&lt;/ul>
&lt;p>The distinction between &lt;code>treat&lt;/code> and &lt;code>treated&lt;/code> is crucial: &lt;code>treat&lt;/code> tells you &lt;em>who&lt;/em> is in the treatment group (a permanent label), while &lt;code>treated&lt;/code> tells you &lt;em>when&lt;/em> they are actually under treatment (a dynamic state). For never-treated units, both are always 0. For treated units, &lt;code>treat&lt;/code> is always 1, but &lt;code>treated&lt;/code> flips from 0 to 1 at the unit&amp;rsquo;s treatment onset.&lt;/p>
&lt;p>An early-treated unit from cohort 3 illustrates this structure:&lt;/p>
&lt;pre>&lt;code class="language-python">early_unit = data_stag[data_stag[&amp;quot;first_treat&amp;quot;] == 3][&amp;quot;unit&amp;quot;].iloc[0]
data_stag[data_stag[&amp;quot;unit&amp;quot;] == early_unit]
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> unit period outcome first_treat treated treat true_effect
90 0 13.299816 3 0 1 0.0
90 1 12.897337 3 0 1 0.0
90 2 11.882534 3 0 1 0.0
90 3 14.724679 3 1 1 2.0
90 4 16.139340 3 1 1 2.2
90 5 14.433891 3 1 1 2.4
90 6 15.949127 3 1 1 2.6
90 7 15.832888 3 1 1 2.8
90 8 17.125174 3 1 1 3.0
90 9 16.685332 3 1 1 3.2
&lt;/code>&lt;/pre>
&lt;p>Unit 90 has &lt;code>treat=1&lt;/code> throughout (it belongs to the treatment group), but &lt;code>treated&lt;/code> flips from 0 to 1 at period 3 &amp;mdash; the moment it enters the post-treatment state. The &lt;code>true_effect&lt;/code> is 0 in the pre-treatment periods, then starts at 2.0 and grows by 0.2 each period, reaching 3.2 by period 9. This growing effect pattern is what makes staggered DiD challenging: the treatment effect for cohort 3 at period 7 (2.8) is very different from the effect at period 3 (2.0).&lt;/p>
&lt;p>Now compare with a late-treated unit from cohort 7:&lt;/p>
&lt;pre>&lt;code class="language-python">late_unit = data_stag[data_stag[&amp;quot;first_treat&amp;quot;] == 7][&amp;quot;unit&amp;quot;].iloc[0]
data_stag[data_stag[&amp;quot;unit&amp;quot;] == late_unit]
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> unit period outcome first_treat treated treat true_effect
91 0 7.987886 7 0 1 0.0
91 1 8.168639 7 0 1 0.0
91 2 8.904022 7 0 1 0.0
91 3 7.984438 7 0 1 0.0
91 4 8.373931 7 0 1 0.0
91 5 7.543381 7 0 1 0.0
91 6 8.981115 7 0 1 0.0
91 7 10.105654 7 1 1 2.0
91 8 10.505532 7 1 1 2.2
91 9 11.074785 7 1 1 2.4
&lt;/code>&lt;/pre>
&lt;p>Unit 91 also has &lt;code>treat=1&lt;/code> throughout, but &lt;code>treated&lt;/code> does not flip until period 7 &amp;mdash; giving it a much longer pre-treatment phase (7 periods vs 3 for cohort 3) and only 3 post-treatment periods. Its &lt;code>true_effect&lt;/code> starts at 2.0 at period 7 and reaches only 2.4 by period 9, compared to cohort 3&amp;rsquo;s 3.2. This asymmetry &amp;mdash; early cohorts accumulating larger effects over more post-treatment periods &amp;mdash; is precisely what causes TWFE to produce biased estimates when it uses already-treated cohort 3 units as &amp;ldquo;controls&amp;rdquo; for cohort 7.&lt;/p>
&lt;p>Let us examine how the staggered structure differs from the 2x2 case in scale and treatment coverage. With multiple cohorts adopting at different times, the fraction of observations in post-treatment state is no longer 50%:&lt;/p>
&lt;pre>&lt;code class="language-python">data_stag.describe()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code> unit period outcome first_treat treated treat true_effect
count 3000.000000 3000.00000 3000.000000 3000.000000 3000.000000 3000.000000 3000.000000
mean 149.500000 4.50000 11.287067 3.600000 0.340000 0.700000 0.829000
std 86.616497 2.87276 2.528589 2.709695 0.473788 0.458334 1.173464
min 0.000000 0.00000 4.521385 0.000000 0.000000 0.000000 0.000000
25% 74.750000 2.00000 9.461867 0.000000 0.000000 0.000000 0.000000
50% 149.500000 4.50000 11.107083 4.000000 0.000000 1.000000 0.000000
75% 224.250000 7.00000 13.078036 5.500000 1.000000 1.000000 2.200000
max 299.000000 9.00000 20.616391 7.000000 1.000000 1.000000 3.200000
&lt;/code>&lt;/pre>
&lt;p>With 3,000 observations and 300 units, this panel is three times larger than the 2x2 case. The &lt;code>first_treat&lt;/code> variable has a mean of 3.60, reflecting the mix of never-treated (0) and cohorts treated at periods 3, 5, and 7. The &lt;code>treated&lt;/code> mean of 0.34 tells us that 34% of all unit-period observations are in a post-treatment state &amp;mdash; less than half because late cohorts contribute fewer treated periods than early cohorts.&lt;/p>
&lt;p>A crosstab of the number of &lt;strong>treated&lt;/strong> (post-treatment) units by cohort and period reveals the staggered rollout:&lt;/p>
&lt;pre>&lt;code class="language-python">pd.crosstab(data_stag[&amp;quot;first_treat&amp;quot;], data_stag[&amp;quot;period&amp;quot;],
values=data_stag[&amp;quot;treated&amp;quot;], aggfunc=&amp;quot;sum&amp;quot;).fillna(0).astype(int)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>period 0 1 2 3 4 5 6 7 8 9
first_treat
0 0 0 0 0 0 0 0 0 0 0
3 0 0 0 60 60 60 60 60 60 60
5 0 0 0 0 0 75 75 75 75 75
7 0 0 0 0 0 0 0 75 75 75
&lt;/code>&lt;/pre>
&lt;p>The staggered structure is immediately visible: zeros cascade to treatment counts as each cohort enters the post-treatment state. At period 2, no units are yet treated. At period 3, 60 units from cohort 3 enter treatment. At period 5, cohort 5 adds 75 more, bringing the total to 135. By period 7, all 210 treated units are in post-treatment. The never-treated group (row 0) remains at zero throughout. This growing treated population &amp;mdash; and the fact that cohort 3 has been treated for 4 periods by the time cohort 7 starts &amp;mdash; is the asymmetry that makes TWFE unreliable. When TWFE uses cohort 3 as a &amp;ldquo;control&amp;rdquo; for cohort 7, it compares against units whose outcomes already incorporate a treatment effect of 2.8, not the untreated counterfactual.&lt;/p>
&lt;p>The pivoted outcome means by cohort and period reveal the staggered treatment pattern:&lt;/p>
&lt;pre>&lt;code class="language-python">data_stag.groupby([&amp;quot;first_treat&amp;quot;, &amp;quot;period&amp;quot;])[&amp;quot;outcome&amp;quot;].mean().unstack()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>period 0 1 2 3 4 5 6 7 8 9
first_treat
0 9.92 9.95 10.17 10.28 10.40 10.46 10.53 10.68 10.78 10.88
3 10.39 10.51 10.59 12.82 13.07 13.33 13.60 13.99 14.22 14.56
5 10.08 10.17 10.33 10.32 10.58 12.70 12.90 13.11 13.64 13.77
7 9.61 9.76 9.73 10.04 10.00 10.10 10.35 12.25 12.59 12.91
&lt;/code>&lt;/pre>
&lt;p>All four cohorts track closely in their pre-treatment periods (values near 9.6&amp;ndash;10.6 in periods 0&amp;ndash;2), confirming parallel pre-trends. The divergence is sharp and cohort-specific: cohort 3 jumps at period 3 (from 10.59 to 12.82), cohort 5 jumps at period 5 (from 10.58 to 12.70), and cohort 7 jumps at period 7 (from 10.35 to 12.25). The never-treated group follows a smooth, gentle upward trend throughout. By period 9, all treated cohorts have outcomes around 12.9&amp;ndash;14.6, substantially above the never-treated group&amp;rsquo;s 10.88 &amp;mdash; but they arrived at those levels at different times.&lt;/p>
&lt;p>The line plot below visualizes these divergent trajectories:&lt;/p>
&lt;pre>&lt;code class="language-python">cohort_means = data_stag.groupby([&amp;quot;first_treat&amp;quot;, &amp;quot;period&amp;quot;])[&amp;quot;outcome&amp;quot;].mean().unstack(level=0)
cohort_colors = {0: STEEL_BLUE, 3: WARM_ORANGE, 5: TEAL, 7: WHITE_TEXT}
cohort_labels = {0: &amp;quot;Never-treated&amp;quot;, 3: &amp;quot;Cohort 3&amp;quot;, 5: &amp;quot;Cohort 5&amp;quot;, 7: &amp;quot;Cohort 7&amp;quot;}
fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
for ft in sorted(cohort_means.columns):
ax.plot(cohort_means.index, cohort_means[ft], &amp;quot;o-&amp;quot;,
color=cohort_colors[ft], linewidth=2, markersize=6,
label=cohort_labels[ft])
# Vertical lines at treatment onsets
for ft in [3, 5, 7]:
ax.axvline(x=ft - 0.5, color=cohort_colors[ft], linestyle=&amp;quot;--&amp;quot;,
linewidth=1.2, alpha=0.5)
ax.set_xlabel(&amp;quot;Period&amp;quot;)
ax.set_ylabel(&amp;quot;Mean Outcome&amp;quot;)
ax.set_title(&amp;quot;Staggered Adoption: Cohort Mean Outcomes Over Time&amp;quot;)
ax.legend(loc=&amp;quot;upper left&amp;quot;)
ax.set_xticks(range(10))
plt.savefig(&amp;quot;did_staggered_trends.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_staggered_trends.png" alt="Line plot showing four cohorts tracking together before treatment, then diverging upward at their respective treatment onset periods. Dashed vertical lines mark each cohort&amp;rsquo;s treatment timing.">&lt;/p>
&lt;p>The plot makes the staggered adoption pattern unmistakable. All four lines run in parallel during the early pre-treatment periods, then each treated cohort jumps upward at its treatment onset (marked by a dashed vertical line in the corresponding color). Cohort 3 (warm orange) diverges first at period 3, followed by cohort 5 (teal) at period 5, and cohort 7 (near black) at period 7. The never-treated group (steel blue) continues its steady, gentle upward trend without any jump. This visualization explains &lt;em>why TWFE fails&lt;/em>: between periods 3 and 7, TWFE uses cohort 3 (already treated and elevated) as a comparison for cohort 7 (not yet treated). Since cohort 3&amp;rsquo;s outcomes are inflated by treatment, the comparison underestimates cohort 7&amp;rsquo;s true effect when it eventually adopts.&lt;/p>
&lt;h3 id="bacon-decomposition-diagnosing-twfe">Bacon decomposition: Diagnosing TWFE&lt;/h3>
&lt;p>The &lt;strong>Goodman-Bacon decomposition&lt;/strong> (&lt;a href="https://doi.org/10.1016/j.jeconom.2021.03.014" target="_blank" rel="noopener">Goodman-Bacon, 2021&lt;/a>) reveals exactly how TWFE constructs its estimate. The key insight is that the TWFE coefficient $\hat{\delta}$ is a weighted average of all possible 2x2 DiD comparisons between pairs of treatment cohorts:&lt;/p>
&lt;p>$$\hat{\delta}^{TWFE} = \sum_{k} s_{kU} \hat{\delta}_{kU} + \sum_{e \neq U} \sum_{l &amp;gt; e} \big( s_{el} \hat{\delta}_{el} + s_{le} \hat{\delta}_{le} \big)$$&lt;/p>
&lt;p>The first sum covers &lt;strong>clean comparisons&lt;/strong> between each treated cohort $k$ and the never-treated group $U$, weighted by $s_{kU}$. The double sum covers comparisons between pairs of treated cohorts: $\hat{\delta}_{el}$ compares earlier-treated ($e$) against later-treated ($l$) units, and $\hat{\delta}_{le}$ compares later-treated against earlier-treated units. The weights $s$ are proportional to each subsample&amp;rsquo;s size and the variance of the treatment indicator within each pair &amp;mdash; groups treated in the middle of the panel receive the most weight. Crucially, the weights sum to one, so the TWFE estimate is a proper weighted average.&lt;/p>
&lt;p>The three types of comparisons have very different reliability:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Treated vs never-treated&lt;/strong> ($\hat{\delta}_{kU}$): Clean comparisons using permanently untreated units as controls. These are the gold standard.&lt;/li>
&lt;li>&lt;strong>Earlier vs later treated&lt;/strong> ($\hat{\delta}_{el}$): Uses not-yet-treated units as controls. Valid as long as treatment has not yet affected the later cohort.&lt;/li>
&lt;li>&lt;strong>Later vs earlier treated&lt;/strong> ($\hat{\delta}_{le}$): The &lt;strong>forbidden comparisons&lt;/strong>. Uses already-treated units as controls. If treatment effects evolve over time, these comparisons are contaminated because the &amp;ldquo;controls&amp;rdquo; are themselves experiencing treatment effects.&lt;/li>
&lt;/ol>
&lt;pre>&lt;code class="language-python">bacon = BaconDecomposition()
bacon_results = bacon.fit(
data_stag, outcome=&amp;quot;outcome&amp;quot;, unit=&amp;quot;unit&amp;quot;,
time=&amp;quot;period&amp;quot;, first_treat=&amp;quot;first_treat&amp;quot;,
)
bacon_results.print_summary()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>=====================================================================================
Goodman-Bacon Decomposition of Two-Way Fixed Effects
=====================================================================================
Total observations: 3000
Treatment timing groups: 3
Never-treated units: 90
Total 2x2 comparisons: 9
-------------------------------------------------------------------------------------
TWFE Decomposition
-------------------------------------------------------------------------------------
TWFE Estimate: 2.1822
Weighted Sum of 2x2 Estimates: 2.1052
Decomposition Error: 0.076977
-------------------------------------------------------------------------------------
Weight Breakdown by Comparison Type
-------------------------------------------------------------------------------------
Comparison Type Weight Avg Effect Contribution
-------------------------------------------------------------------------------------
Treated vs Never-treated 0.4331 2.3745 1.0284
Earlier vs Later treated 0.2836 2.1999 0.6238
Later vs Earlier (forbidden) 0.2834 1.5989 0.4531
-------------------------------------------------------------------------------------
Total 1.0000 2.1052
-------------------------------------------------------------------------------------
WARNING: 28.3% of weight is on 'forbidden' comparisons where
already-treated units serve as controls. This can bias TWFE
when treatment effects are heterogeneous over time.
Consider using Callaway-Sant'Anna or other robust estimators.
=====================================================================================
&lt;/code>&lt;/pre>
&lt;p>The decomposition reveals that 28.3% of TWFE&amp;rsquo;s weight falls on forbidden comparisons &amp;mdash; cases where already-treated units serve as controls. These forbidden comparisons produce an average effect of only 1.60, substantially lower than the 2.37 from clean treated-vs-never-treated comparisons. This downward pull drags the TWFE estimate to 2.18, below the true treatment effect. The clean comparisons (treated vs never-treated) account for 43.3% of the weight and produce the most reliable estimates, while the earlier-vs-later comparisons (28.4% weight) sit in between. The decomposition error of 0.08 reflects higher-order interaction terms that the 2x2 decomposition does not fully capture.&lt;/p>
&lt;p>The following plot visualizes the decomposition:&lt;/p>
&lt;pre>&lt;code class="language-python">bacon_df = bacon_results.to_dataframe()
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.patch.set_linewidth(0)
# Left panel: scatter by comparison type
type_colors = {
&amp;quot;Treated vs Never-treated&amp;quot;: STEEL_BLUE,
&amp;quot;Earlier vs Later treated&amp;quot;: WARM_ORANGE,
&amp;quot;Later vs Earlier (forbidden)&amp;quot;: &amp;quot;#e8856c&amp;quot;,
&amp;quot;treated_vs_never&amp;quot;: STEEL_BLUE,
&amp;quot;earlier_vs_later&amp;quot;: WARM_ORANGE,
&amp;quot;later_vs_earlier&amp;quot;: &amp;quot;#e8856c&amp;quot;,
}
for comp_type in bacon_df[&amp;quot;comparison_type&amp;quot;].unique():
subset = bacon_df[bacon_df[&amp;quot;comparison_type&amp;quot;] == comp_type]
color = type_colors.get(comp_type, LIGHT_TEXT)
axes[0].scatter(subset[&amp;quot;weight&amp;quot;], subset[&amp;quot;estimate&amp;quot;],
s=80, color=color, alpha=0.7, edgecolors=DARK_NAVY,
label=comp_type)
axes[0].axhline(y=bacon_results.twfe_estimate, color=WHITE_TEXT,
linestyle=&amp;quot;--&amp;quot;, linewidth=1.5, alpha=0.7,
label=f&amp;quot;TWFE = {bacon_results.twfe_estimate:.2f}&amp;quot;)
axes[0].set_xlabel(&amp;quot;Weight&amp;quot;)
axes[0].set_ylabel(&amp;quot;2×2 DiD Estimate&amp;quot;)
axes[0].set_title(&amp;quot;Bacon Decomposition: Individual Comparisons&amp;quot;)
axes[0].legend(fontsize=9, loc=&amp;quot;lower right&amp;quot;)
# Right panel: bar chart of weights by type
type_summary = bacon_df.groupby(&amp;quot;comparison_type&amp;quot;).agg(
weight=(&amp;quot;weight&amp;quot;, &amp;quot;sum&amp;quot;),
avg_effect=(&amp;quot;estimate&amp;quot;, lambda x: np.average(
x, weights=bacon_df.loc[x.index, &amp;quot;weight&amp;quot;])),
).reset_index()
bar_colors = [type_colors.get(t, LIGHT_TEXT)
for t in type_summary[&amp;quot;comparison_type&amp;quot;]]
axes[1].barh(range(len(type_summary)), type_summary[&amp;quot;weight&amp;quot;],
color=bar_colors, edgecolor=DARK_NAVY, height=0.6)
axes[1].set_yticks(range(len(type_summary)))
axes[1].set_yticklabels(type_summary[&amp;quot;comparison_type&amp;quot;], fontsize=10)
axes[1].set_xlabel(&amp;quot;Total Weight&amp;quot;)
axes[1].set_title(&amp;quot;Weight Distribution by Comparison Type&amp;quot;)
for i, (w, e) in enumerate(zip(type_summary[&amp;quot;weight&amp;quot;],
type_summary[&amp;quot;avg_effect&amp;quot;])):
axes[1].text(w + 0.01, i, f&amp;quot;{w:.1%} (avg = {e:.2f})&amp;quot;,
va=&amp;quot;center&amp;quot;, fontsize=10)
plt.tight_layout()
plt.savefig(&amp;quot;did_bacon_decomposition.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_bacon_decomposition.png" alt="Two-panel Bacon decomposition plot. Left: scatter of individual 2x2 estimates colored by comparison type with TWFE reference line. Right: horizontal bars showing total weight by comparison type.">&lt;/p>
&lt;p>The left panel shows each individual 2x2 comparison as a point, colored by type. The forbidden comparisons (dark orange) cluster at lower effect estimates than the clean comparisons (steel blue), visually demonstrating how they pull TWFE downward. The right panel makes the weight problem stark: nearly a third of the total weight goes to comparisons where already-treated units masquerade as controls. For a policymaker relying on the TWFE estimate of 2.18, this contamination means the reported effect underestimates the true treatment impact.&lt;/p>
&lt;h2 id="callaway-santanna-the-modern-solution">Callaway-Sant&amp;rsquo;Anna: The modern solution&lt;/h2>
&lt;p>The &lt;strong>Callaway-Sant&amp;rsquo;Anna (CS) estimator&lt;/strong> (&lt;a href="https://doi.org/10.1016/j.jeconom.2020.12.001" target="_blank" rel="noopener">Callaway &amp;amp; Sant&amp;rsquo;Anna, 2021&lt;/a>) avoids forbidden comparisons entirely. Instead of a single pooled regression, CS starts from a fundamental building block &amp;mdash; the &lt;strong>group-time ATT&lt;/strong>:&lt;/p>
&lt;p>$$ATT(g, t) = E[Y_t(g) - Y_t(\infty) \mid G = g], \quad \text{for } t \geq g$$&lt;/p>
&lt;p>Here $g$ denotes the cohort (the period when a unit first becomes treated), $t$ is the current calendar period, $Y_t(g)$ is the potential outcome at time $t$ if first treated in period $g$, and $Y_t(\infty)$ is the potential outcome under perpetual non-treatment. The conditioning on $G = g$ restricts attention to units in cohort $g$. This yields a separate treatment effect estimate for each combination of cohort and calendar period, using only clean comparisons.&lt;/p>
&lt;p>With never-treated controls, the group-time ATT is identified as:&lt;/p>
&lt;p>$$ATT(g, t) = E[Y_t - Y_{g-1} \mid G = g] - E[Y_t - Y_{g-1} \mid G = \infty]$$&lt;/p>
&lt;p>In words: take the change in outcomes from the period just before treatment ($g - 1$) to the current period ($t$) for cohort $g$ units, and subtract the same change for never-treated units ($G = \infty$). This is a 2x2 DiD comparison that uses only the never-treated group as controls, eliminating all forbidden comparisons by construction.&lt;/p>
&lt;h3 id="the-doubly-robust-estimator">The doubly robust estimator&lt;/h3>
&lt;p>In practice, Callaway and Sant&amp;rsquo;Anna implement a &lt;strong>doubly robust&lt;/strong> version of this estimator. Before diving into the formal equation, here is the core idea: the doubly robust estimator adjusts the comparison between treated and control units in &lt;em>two&lt;/em> ways simultaneously &amp;mdash; by reweighting the control group to look more similar to the treated group (inverse-probability weighting), and by directly modeling and subtracting the expected outcome change for controls (outcome regression). Think of it as wearing both a belt &lt;em>and&lt;/em> suspenders: if either adjustment is correctly specified, the estimate is valid, even if the other one is wrong. This double protection makes the estimator more reliable than methods that rely on a single modeling assumption.&lt;/p>
&lt;p>The formal equation combines inverse-probability weighting with an outcome regression adjustment:&lt;/p>
&lt;p>$$ATT(g, t) = \mathbb{E}\left[\left(\frac{G_g}{\mathbb{E}[G_g]} - \frac{\frac{p_g(X)}{1-p_g(X)}}{\mathbb{E}\left[\frac{p_g(X)}{1-p_g(X)}\right]}\right)\left(Y_t - Y_{g-1} - m_{g,t}^{nev}(X)\right)\right]$$&lt;/p>
&lt;p>This equation multiplies two terms inside the expectation &amp;mdash; a &lt;strong>weighting term&lt;/strong> (first parentheses) and an &lt;strong>outcome term&lt;/strong> (second parentheses). Let us unpack each one.&lt;/p>
&lt;p>&lt;strong>The weighting term:&lt;/strong> $\frac{G_g}{\mathbb{E}[G_g]} - \frac{\frac{p_g(X)}{1-p_g(X)}}{\mathbb{E}\left[\frac{p_g(X)}{1-p_g(X)}\right]}$&lt;/p>
&lt;p>This term determines &lt;em>how much each observation contributes&lt;/em> to the ATT estimate. It works differently for treated and control units:&lt;/p>
&lt;ul>
&lt;li>$G_g$ is a &lt;strong>group indicator&lt;/strong> that equals 1 if the unit belongs to cohort $g$ and 0 otherwise. Dividing by $\mathbb{E}[G_g]$ (the share of units in cohort $g$) normalizes so that treated units receive equal weight on average. For a treated unit in cohort $g$, the first fraction contributes a positive value; for never-treated units, $G_g = 0$ so the first fraction is zero.&lt;/li>
&lt;li>$p_g(X)$ is the &lt;strong>generalized propensity score&lt;/strong> &amp;mdash; the probability of being in cohort $g$ (rather than the never-treated group) given covariates $X$. This is estimated via logit regression of cohort membership on covariates. The ratio $\frac{p_g(X)}{1-p_g(X)}$ are the odds of being in cohort $g$, and dividing by its expectation normalizes the weights. For never-treated units, this second fraction creates a &lt;strong>negative weight&lt;/strong> that is larger for control units whose covariates resemble the treated cohort &amp;mdash; effectively selecting the most comparable controls. For treated units, the two fractions partially cancel, leaving a net positive weight.&lt;/li>
&lt;/ul>
&lt;p>The intuition is similar to propensity score matching: if a never-treated city has covariates (population, per-student spending, teacher-student ratio) that look very much like a treated city, it receives a larger (more negative) weight, making it contribute more as a counterfactual. Cities with covariates far from the treated group receive near-zero weight. This &lt;strong>rebalances&lt;/strong> the control group so that the covariate distribution of the weighted controls matches that of the treated cohort.&lt;/p>
&lt;p>&lt;strong>The outcome term:&lt;/strong> $Y_t - Y_{g-1} - m_{g,t}^{nev}(X)$&lt;/p>
&lt;p>This term measures the &lt;strong>adjusted outcome change&lt;/strong> for each unit:&lt;/p>
&lt;ul>
&lt;li>$Y_t - Y_{g-1}$ is the raw change in outcomes from the baseline period ($g - 1$, the period just before cohort $g$ starts treatment) to the current period $t$. This is the same first difference used in any DiD estimator.&lt;/li>
&lt;li>$m_{g,t}^{nev}(X)$ is the &lt;strong>outcome regression adjustment&lt;/strong> &amp;mdash; the expected change $E[Y_t - Y_{g-1} \mid X, G = \infty]$ for never-treated units with covariates $X$. In practice, this is estimated by regressing the outcome change $\Delta Y = Y_t - Y_{g-1}$ on covariates $X$ using only the never-treated group. Subtracting $m_{g,t}^{nev}(X)$ removes the portion of the outcome change that would have occurred &lt;em>anyway&lt;/em> based on observable characteristics &amp;mdash; even without treatment. What remains is the treatment-induced change that cannot be explained by covariates alone.&lt;/li>
&lt;/ul>
&lt;p>Think of it this way: if cities with higher per-student spending tend to improve learning scores faster regardless of AI adoption, $m_{g,t}^{nev}(X)$ captures that covariate-driven growth trajectory. Subtracting it ensures that the estimated treatment effect is not confounded by differential growth rates across different types of cities.&lt;/p>
&lt;p>&lt;strong>Why &amp;ldquo;doubly robust&amp;rdquo;?&lt;/strong> The estimator combines &lt;em>both&lt;/em> adjustment strategies &amp;mdash; inverse-probability weighting (through the weighting term) and outcome regression (through $m_{g,t}^{nev}(X)$). The key advantage is that the ATT estimate is consistent if &lt;em>either&lt;/em> the propensity score model or the outcome regression model is correctly specified &amp;mdash; both do not need to be right simultaneously. If the propensity score model is wrong but the outcome regression is correct, the $m_{g,t}^{nev}(X)$ adjustment still removes confounding. If the outcome regression is wrong but the propensity score is correct, the reweighting still produces a valid comparison group. This double layer of protection makes the estimator more reliable in practice than methods relying on a single modeling assumption.&lt;/p>
&lt;p>&lt;strong>Note on the no-covariate case:&lt;/strong> In this tutorial, we do not pass covariates to &lt;code>CallawaySantAnna()&lt;/code>. Without covariates, the propensity score $p_g(X)$ reduces to the unconditional probability of being in cohort $g$ (simply the group share), and $m_{g,t}^{nev}(X)$ reduces to the simple mean outcome change among never-treated units. The doubly robust estimator then collapses to the basic difference-in-means formula shown earlier. The full equation is presented here because it is the general form that practitioners encounter when working with real data and covariates.&lt;/p>
&lt;p>The group-time ATTs are then &lt;strong>aggregated&lt;/strong> into summary parameters. Any summary is a weighted average of the building blocks:&lt;/p>
&lt;p>$$\theta = \sum_{g} \sum_{t \geq g} w_{g,t} \cdot ATT(g, t), \quad \sum_{g,t} w_{g,t} = 1$$&lt;/p>
&lt;p>Two aggregations are especially useful. The &lt;strong>overall ATT&lt;/strong> weights by cohort size:&lt;/p>
&lt;p>$$\theta^{O} = \sum_{g} \theta(g) \cdot P(G = g), \quad \text{where } \theta(g) = \frac{1}{T - g + 1} \sum_{t=g}^{T} ATT(g, t)$$&lt;/p>
&lt;p>The &lt;strong>event study aggregation&lt;/strong> averages across cohorts at each relative time $e$ (periods since treatment onset):&lt;/p>
&lt;p>$$\theta_D(e) = \sum_{g} ATT(g, g + e) \cdot P(G = g \mid g + e \leq T)$$&lt;/p>
&lt;p>This event study aggregation is the CS analogue of the leads-and-lags event study, but free from the forbidden comparison contamination that plagues TWFE-based event studies.&lt;/p>
&lt;p>The &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">&lt;code>CallawaySantAnna()&lt;/code>&lt;/a> class takes &lt;code>control_group&lt;/code> to specify which units serve as controls. Using &lt;code>&amp;quot;never_treated&amp;quot;&lt;/code> restricts comparisons to units that never received treatment, the cleanest possible counterfactual. The &lt;code>base_period=&amp;quot;universal&amp;quot;&lt;/code> option uses a single reference period ($g - 1$) for all relative time comparisons within each cohort, rather than letting each relative period use its own baseline. This ensures that the pre-treatment coefficients are proper placebo tests: each one measures the outcome change from $g - 1$ to an earlier period, so a coefficient near zero means the treated and control groups were evolving similarly over that specific interval. With a universal base period, the period immediately before treatment ($e = -1$) is normalized to zero by construction.&lt;/p>
&lt;pre>&lt;code class="language-python">cs = CallawaySantAnna(control_group=&amp;quot;never_treated&amp;quot;, base_period=&amp;quot;universal&amp;quot;)
results_cs = cs.fit(
data_stag, outcome=&amp;quot;outcome&amp;quot;, unit=&amp;quot;unit&amp;quot;,
time=&amp;quot;period&amp;quot;, first_treat=&amp;quot;first_treat&amp;quot;,
aggregate=&amp;quot;event_study&amp;quot;,
)
results_cs.print_summary()
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>=====================================================================================
Callaway-Sant'Anna Staggered Difference-in-Differences Results
=====================================================================================
Total observations: 3000
Treated units: 210
Never-treated units: 90
Treatment cohorts: 3
Time periods: 10
Control group: never_treated
Base period: universal
-------------------------------------------------------------------------------------
Overall Average Treatment Effect on the Treated
-------------------------------------------------------------------------------------
Parameter Estimate Std. Err. t-stat P&amp;gt;|t| Sig.
-------------------------------------------------------------------------------------
ATT 2.4136 0.0552 43.753 0.0000 ***
-------------------------------------------------------------------------------------
95% Confidence Interval: [2.3055, 2.5217]
-------------------------------------------------------------------------------------
Event Study (Dynamic) Effects
-------------------------------------------------------------------------------------
Rel. Period Estimate Std. Err. t-stat P&amp;gt;|t| Sig.
-------------------------------------------------------------------------------------
-7 -0.1344 0.1171 -1.148 0.2510
-6 -0.0188 0.1126 -0.167 0.8671
-5 -0.1435 0.0813 -1.766 0.0774 .
-4 -0.0091 0.0744 -0.122 0.9028
-3 -0.0697 0.0560 -1.244 0.2134
-2 -0.0709 0.0631 -1.124 0.2610
-1 0.0000 nan nan nan
0 1.9713 0.0645 30.551 0.0000 ***
1 2.1416 0.0577 37.124 0.0000 ***
2 2.2969 0.0644 35.644 0.0000 ***
3 2.6763 0.0796 33.642 0.0000 ***
4 2.7925 0.0800 34.898 0.0000 ***
5 3.0259 0.1227 24.669 0.0000 ***
6 3.2663 0.1090 29.961 0.0000 ***
-------------------------------------------------------------------------------------
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
=====================================================================================
&lt;/code>&lt;/pre>
&lt;p>The overall CS estimate of the ATT is 2.41 (SE = 0.06, p &amp;lt; 0.001), with a 95% CI of [2.31, 2.52]. This is higher than the TWFE estimate of 2.18, confirming that TWFE was biased downward by the forbidden comparisons. The event study reveals dynamic effects that grow over time: the effect starts at 1.97 in the first period after treatment and increases to 3.27 by six periods post-treatment. This pattern of growing effects is exactly the scenario where TWFE fails most dramatically &amp;mdash; the forbidden comparisons use units with large accumulated effects as controls for newly-treated units, producing a downward-biased average.&lt;/p>
&lt;p>With the universal base period, relative period -1 is the reference and is normalized to zero by construction. The remaining pre-treatment estimates all hover near zero &amp;mdash; the largest in magnitude is -0.14 at relative period -5 (p = 0.08), which does not reach significance at the 5% level. None of the seven pre-treatment coefficients are individually significant, providing clean support for the parallel trends assumption. This contrasts with the varying base period specification, where each pre-treatment coefficient uses a different baseline, making the placebo tests harder to interpret collectively.&lt;/p>
&lt;p>The event study plot visualizes these dynamics, showing how the treatment effect builds over time relative to treatment onset:&lt;/p>
&lt;pre>&lt;code class="language-python">cs_df = results_cs.to_dataframe(&amp;quot;event_study&amp;quot;)
fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
pre_cs = cs_df[cs_df[&amp;quot;relative_period&amp;quot;] &amp;lt; 0]
post_cs = cs_df[cs_df[&amp;quot;relative_period&amp;quot;] &amp;gt;= 0]
ax.errorbar(pre_cs[&amp;quot;relative_period&amp;quot;], pre_cs[&amp;quot;effect&amp;quot;],
yerr=1.96 * pre_cs[&amp;quot;se&amp;quot;], fmt=&amp;quot;o&amp;quot;, color=STEEL_BLUE,
capsize=4, linewidth=2, markersize=8, label=&amp;quot;Pre-treatment&amp;quot;)
ax.errorbar(post_cs[&amp;quot;relative_period&amp;quot;], post_cs[&amp;quot;effect&amp;quot;],
yerr=1.96 * post_cs[&amp;quot;se&amp;quot;], fmt=&amp;quot;s&amp;quot;, color=TEAL,
capsize=4, linewidth=2, markersize=8, label=&amp;quot;Post-treatment&amp;quot;)
ax.axhline(y=0, color=LIGHT_TEXT, linewidth=1, alpha=0.5)
ax.axvline(x=-0.5, color=LIGHT_TEXT, linestyle=&amp;quot;--&amp;quot;, linewidth=1.5, alpha=0.5)
ax.set_xlabel(&amp;quot;Periods Relative to Treatment&amp;quot;)
ax.set_ylabel(&amp;quot;Estimated ATT&amp;quot;)
ax.set_title(&amp;quot;Callaway-Sant'Anna: Event Study for Staggered Adoption&amp;quot;)
ax.legend(loc=&amp;quot;upper left&amp;quot;)
plt.savefig(&amp;quot;did_staggered_att.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_staggered_att.png" alt="Callaway-Sant&amp;rsquo;Anna event study plot showing pre-treatment effects near zero (with period -1 normalized to zero) and post-treatment effects growing steadily from about 2.0 to 3.3.">&lt;/p>
&lt;p>The CS event study plot shows the hallmark pattern of a valid DiD analysis: pre-treatment coefficients (steel blue) cluster tightly around zero &amp;mdash; with relative period -1 pinned at exactly zero as the universal base period &amp;mdash; then post-treatment coefficients (teal) rise sharply and progressively. The upward slope in the post-treatment period reveals that the treatment effect accumulates over time, growing from roughly 2.0 immediately after treatment to 3.3 six periods later. This dynamic pattern would have been obscured by TWFE&amp;rsquo;s single pooled estimate and further distorted by its forbidden comparisons.&lt;/p>
&lt;h2 id="choosing-the-right-estimator">Choosing the right estimator&lt;/h2>
&lt;p>With multiple DiD estimators available, the choice depends on the data structure. The following decision flowchart guides the selection:&lt;/p>
&lt;pre>&lt;code class="language-mermaid">graph TD
A[&amp;quot;&amp;lt;b&amp;gt;Panel data with&amp;lt;br/&amp;gt;treatment &amp;amp; control&amp;lt;/b&amp;gt;&amp;quot;] --&amp;gt; B{&amp;quot;Single treatment&amp;lt;br/&amp;gt;period?&amp;quot;}
B --&amp;gt;|Yes| C[&amp;quot;&amp;lt;b&amp;gt;Classic 2×2 DiD&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;DifferenceInDifferences()&amp;quot;]
B --&amp;gt;|No| D{&amp;quot;Staggered&amp;lt;br/&amp;gt;adoption?&amp;quot;}
D --&amp;gt;|&amp;quot;No&amp;lt;br/&amp;gt;(same timing)&amp;quot;| E[&amp;quot;&amp;lt;b&amp;gt;Multi-Period DiD&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;MultiPeriodDiD()&amp;quot;]
D --&amp;gt;|Yes| F{&amp;quot;Never-treated&amp;lt;br/&amp;gt;group available?&amp;quot;}
F --&amp;gt;|Yes| G[&amp;quot;&amp;lt;b&amp;gt;Callaway-Sant'Anna&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;CallawaySantAnna()&amp;quot;]
F --&amp;gt;|No| H[&amp;quot;&amp;lt;b&amp;gt;Sun-Abraham / Stacked DiD&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;SunAbraham() / StackedDiD()&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;(not covered here)&amp;lt;/i&amp;gt;&amp;quot;]
style A fill:#141413,stroke:#141413,color:#fff
style B fill:#6a9bcc,stroke:#141413,color:#fff
style C fill:#00d4c8,stroke:#141413,color:#fff
style D fill:#6a9bcc,stroke:#141413,color:#fff
style E fill:#00d4c8,stroke:#141413,color:#fff
style F fill:#6a9bcc,stroke:#141413,color:#fff
style G fill:#00d4c8,stroke:#141413,color:#fff
style H fill:#d97757,stroke:#141413,color:#fff
&lt;/code>&lt;/pre>
&lt;p>The following table summarizes when to use each estimator:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scenario&lt;/th>
&lt;th>Estimator&lt;/th>
&lt;th>Advantage&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Single treatment time, 2 groups&lt;/td>
&lt;td>&lt;code>DifferenceInDifferences()&lt;/code>&lt;/td>
&lt;td>Simplest, most transparent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Single treatment time, many periods&lt;/td>
&lt;td>&lt;code>MultiPeriodDiD()&lt;/code>&lt;/td>
&lt;td>Period-by-period effects, pre-trend test&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Staggered, never-treated available&lt;/td>
&lt;td>&lt;code>CallawaySantAnna()&lt;/code>&lt;/td>
&lt;td>Clean comparisons, flexible aggregation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Staggered, no never-treated group&lt;/td>
&lt;td>&lt;code>SunAbraham()&lt;/code>&lt;/td>
&lt;td>Interaction-weighted, uses not-yet-treated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Diagnosing TWFE bias&lt;/td>
&lt;td>&lt;code>BaconDecomposition()&lt;/code>&lt;/td>
&lt;td>Reveals forbidden comparison weights&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The decision logic is straightforward: if all treated units start at the same time, use the classic estimator or the multi-period event study. If treatment timing varies, use Callaway-Sant&amp;rsquo;Anna (or Sun-Abraham if no never-treated group exists). Always run Bacon decomposition on TWFE results to check for contamination from forbidden comparisons. The &lt;code>diff-diff&lt;/code> package also offers &lt;code>SyntheticDiD()&lt;/code>, &lt;code>ImputationDiD()&lt;/code>, and &lt;code>ContinuousDiD()&lt;/code> for specialized settings, but the estimators above cover the vast majority of applied research.&lt;/p>
&lt;h2 id="sensitivity-analysis-honestdid">Sensitivity analysis: HonestDiD&lt;/h2>
&lt;p>Every DiD analysis rests on parallel trends &amp;mdash; but this assumption is fundamentally &lt;strong>untestable&lt;/strong> for the post-treatment period. Pre-treatment trend tests (Section 6) check whether trends were parallel &lt;em>before&lt;/em> treatment, but they cannot guarantee that trends would have remained parallel &lt;em>after&lt;/em> treatment in the absence of the intervention. A new regulation might coincide with an economic downturn that affects treated regions differently, violating parallel trends even though pre-trends looked clean.&lt;/p>
&lt;p>&lt;strong>HonestDiD&lt;/strong> (&lt;a href="https://doi.org/10.1093/restud/rdad018" target="_blank" rel="noopener">Rambachan &amp;amp; Roth, 2023&lt;/a>) addresses this problem directly. Instead of assuming parallel trends hold exactly, it bounds the degree of violation using a &lt;strong>relative magnitudes restriction&lt;/strong>. Let $\delta_t = E[Y^0_t - Y^0_{t-1} \mid G = g] - E[Y^0_t - Y^0_{t-1} \mid G = \infty]$ denote the parallel trends violation at period $t$ &amp;mdash; the difference in untreated outcome trends between the treated cohort and the never-treated group. HonestDiD constrains the post-treatment violations relative to the largest pre-treatment violation:&lt;/p>
&lt;p>$$|\delta_t| \leq M \cdot \max_{t' &amp;lt; g} |\delta_{t'}|, \quad \text{for all } t \geq g$$&lt;/p>
&lt;p>The parameter $M$ controls the degree of allowed departure. At $M = 0$, the method assumes perfect parallel trends ($\delta_t = 0$ for all post-treatment periods) and recovers the standard CI. As $M$ increases, it allows for progressively larger post-treatment violations, widening the robust CI. The &lt;strong>breakdown value&lt;/strong> of $M$ is where the CI first includes zero &amp;mdash; the point at which the treatment conclusion becomes fragile.&lt;/p>
&lt;p>Think of $M$ as a stress test dial. Turning it up to $M = 1$ says: &amp;ldquo;The worst post-treatment violation could be as large as the worst thing we saw pre-treatment.&amp;rdquo; Turning it to $M = 5$ says: &amp;ldquo;The violation could be five times worse.&amp;rdquo; If the effect remains significant even at high $M$, the finding is genuinely robust.&lt;/p>
&lt;pre>&lt;code class="language-python">M_values = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 7.0, 10.0, 12.0, 15.0]
sensitivity = []
for M in M_values:
honest = HonestDiD(method=&amp;quot;relative_magnitude&amp;quot;, M=M)
hres = honest.fit(results_cs)
sensitivity.append({
&amp;quot;M&amp;quot;: M,
&amp;quot;ci_lb&amp;quot;: hres.ci_lb,
&amp;quot;ci_ub&amp;quot;: hres.ci_ub,
&amp;quot;significant&amp;quot;: hres.ci_lb &amp;gt; 0,
})
print(f&amp;quot;M = {M:.1f}: CI = [{hres.ci_lb:.4f}, {hres.ci_ub:.4f}]&amp;quot;
f&amp;quot; {'significant' if hres.ci_lb &amp;gt; 0 else 'includes zero'}&amp;quot;)
sens_df = pd.DataFrame(sensitivity)
# Find breakdown point
breakdown_M = (sens_df[~sens_df[&amp;quot;significant&amp;quot;]][&amp;quot;M&amp;quot;].min()
if not sens_df[&amp;quot;significant&amp;quot;].all()
else sens_df[&amp;quot;M&amp;quot;].max())
print(f&amp;quot;\nBreakdown value of M: {breakdown_M:.1f}&amp;quot;)
&lt;/code>&lt;/pre>
&lt;pre>&lt;code>M = 0.0: CI = [2.5324, 2.6592] significant
M = 0.5: CI = [2.4606, 2.7310] significant
M = 1.0: CI = [2.3889, 2.8028] significant
M = 1.5: CI = [2.3171, 2.8745] significant
M = 2.0: CI = [2.2453, 2.9463] significant
M = 3.0: CI = [2.1018, 3.0898] significant
M = 4.0: CI = [1.9583, 3.2334] significant
M = 5.0: CI = [1.8148, 3.3769] significant
M = 7.0: CI = [1.5277, 3.6639] significant
M = 10.0: CI = [1.0971, 4.0945] significant
M = 12.0: CI = [0.8101, 4.3816] significant
M = 15.0: CI = [0.3795, 4.8122] significant
Breakdown value of M: 15.0
&lt;/code>&lt;/pre>
&lt;p>At $M = 0$ (perfect parallel trends), the CI is narrow: [2.53, 2.66]. As $M$ increases, the CI widens symmetrically. At $M = 10$, the lower bound remains comfortably positive (1.10), and even at $M = 15$, it barely stays above zero (0.38). The breakdown value exceeds $M = 15$ &amp;mdash; the treatment effect remains statistically significant even if post-treatment violations of parallel trends are more than 15 times larger than the worst pre-treatment deviation. This is exceptionally robust &amp;mdash; in practice, a breakdown value above $M = 3$ is considered strong evidence that the finding is not driven by parallel trends violations. The improvement over the varying base period specification (which had a breakdown of $M = 12$) reflects the universal base period&amp;rsquo;s tighter pre-treatment estimates, which give HonestDiD a smaller &amp;ldquo;worst pre-treatment deviation&amp;rdquo; to scale against.&lt;/p>
&lt;p>The sensitivity plot maps the robust CI as a function of $M$, making the breakdown point visually apparent:&lt;/p>
&lt;pre>&lt;code class="language-python">fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_linewidth(0)
ax.fill_between(sens_df[&amp;quot;M&amp;quot;], sens_df[&amp;quot;ci_lb&amp;quot;], sens_df[&amp;quot;ci_ub&amp;quot;],
alpha=0.25, color=STEEL_BLUE, label=&amp;quot;95% Robust CI&amp;quot;)
ax.plot(sens_df[&amp;quot;M&amp;quot;], sens_df[&amp;quot;ci_lb&amp;quot;], &amp;quot;-&amp;quot;, color=STEEL_BLUE, linewidth=2)
ax.plot(sens_df[&amp;quot;M&amp;quot;], sens_df[&amp;quot;ci_ub&amp;quot;], &amp;quot;-&amp;quot;, color=STEEL_BLUE, linewidth=2)
ax.axhline(y=0, color=LIGHT_TEXT, linewidth=1.5, alpha=0.7)
att_val = results_cs.overall_att
ax.axhline(y=att_val, color=TEAL, linestyle=&amp;quot;:&amp;quot;, linewidth=1.5,
alpha=0.7, label=f&amp;quot;Overall ATT = {att_val:.2f}&amp;quot;)
ax.axvline(x=breakdown_M, color=WARM_ORANGE, linestyle=&amp;quot;--&amp;quot;,
linewidth=2, alpha=0.8,
label=f&amp;quot;Breakdown (M = {breakdown_M:.1f})&amp;quot;)
ax.set_xlabel(&amp;quot;Sensitivity Parameter M\n&amp;quot;
&amp;quot;(maximum post-treatment violation relative to &amp;quot;
&amp;quot;largest pre-treatment violation)&amp;quot;)
ax.set_ylabel(&amp;quot;Treatment Effect (ATT)&amp;quot;)
ax.set_title(&amp;quot;HonestDiD Sensitivity Analysis: Robustness of the ATT&amp;quot;)
ax.legend(loc=&amp;quot;upper left&amp;quot;)
plt.savefig(&amp;quot;did_honest_sensitivity.png&amp;quot;, dpi=300, bbox_inches=&amp;quot;tight&amp;quot;,
facecolor=DARK_NAVY, edgecolor=DARK_NAVY, pad_inches=0)
plt.show()
&lt;/code>&lt;/pre>
&lt;p>&lt;img src="did_honest_sensitivity.png" alt="HonestDiD sensitivity plot showing the 95% robust CI widening as M increases. The CI band is steel blue, the ATT is a teal dotted line, and the breakdown point at M=15 is marked with an orange dashed line.">&lt;/p>
&lt;p>The sensitivity plot tells the robustness story at a glance. The steel blue band shows the 95% robust CI expanding as $M$ grows &amp;mdash; allowing for larger violations of parallel trends. The teal dotted line marks the overall ATT of 2.41, which sits comfortably within the CI for all values of $M$. The warm orange dashed line at $M = 15$ marks the boundary of our grid, with the lower CI bound still positive (0.38) at that point &amp;mdash; the true breakdown lies even further out. In practical terms, the treatment conclusion would only be overturned if post-treatment parallel trend violations were more than 15 times worse than anything observed in the pre-treatment data &amp;mdash; an extreme scenario that would require a dramatic structural break coinciding precisely with the treatment timing.&lt;/p>
&lt;p>Best practice is to always report the breakdown value alongside the point estimate. A finding with a breakdown at $M = 0.5$ is fragile &amp;mdash; even mild violations destroy the conclusion. A finding with a breakdown at $M = 15$ or above, as in this example, provides strong evidence that the effect is genuine regardless of moderate parallel trends violations.&lt;/p>
&lt;h2 id="discussion">Discussion&lt;/h2>
&lt;p>Returning to the motivating question &amp;mdash; did AI tutoring actually improve learning? &amp;mdash; the evidence from both the classic and modern DiD estimators is clear: treatment produced a genuine, statistically significant positive effect. In the 2x2 setting, the estimated ATT of 5.12 (95% CI: [4.64, 5.60]) closely matches the true effect of 5.0, confirming that the classic estimator works well when all units start treatment simultaneously. The event study further validates this finding by showing near-zero pre-treatment coefficients (the largest is -0.52 with p = 0.31) and stable post-treatment effects around 4.7&amp;ndash;5.0.&lt;/p>
&lt;p>The staggered adoption setting reveals a more nuanced picture. Naive TWFE estimation produces a biased estimate of 2.18, pulled downward by the 28.3% weight on forbidden comparisons where already-treated units serve as controls. The Callaway-Sant&amp;rsquo;Anna estimator corrects this bias, finding an overall ATT of 2.41 &amp;mdash; and the event study shows that the effect is not constant but grows over time, from 1.97 immediately after treatment to 3.27 six periods later. For an education policymaker, this dynamic pattern means the AI initiative&amp;rsquo;s full benefits take time to materialize: evaluating the program too early would underestimate its long-run impact.&lt;/p>
&lt;p>The HonestDiD sensitivity analysis provides the final piece of evidence. With a breakdown value exceeding $M = 15$, the treatment conclusion is robust to post-treatment parallel trends violations more than 15 times larger than anything observed pre-treatment. This level of robustness far exceeds the $M = 3$ threshold typically considered strong in applied research. Even a skeptic who doubts the parallel trends assumption would find it difficult to argue that the treatment had no effect.&lt;/p>
&lt;p>Two important caveats apply. First, these results use synthetic data with known true effects, so the estimators are guaranteed to work under their assumptions. Real-world applications face additional challenges &amp;mdash; measurement error in learning assessments, spillover effects between treated and control cities (e.g., students in control cities accessing AI tools on their own), and the possibility that AI adoption depends on unobserved factors correlated with learning outcomes. Second, the treatment effects in the staggered dataset grow linearly over time by construction. In practice, effects may follow more complex trajectories &amp;mdash; plateauing, fading out, or accelerating &amp;mdash; which would require careful specification of the event study window and aggregation weights.&lt;/p>
&lt;h2 id="summary-and-key-takeaways">Summary and key takeaways&lt;/h2>
&lt;p>This tutorial walked through the DiD toolkit from its simplest form to its most robust modern extensions. Four key takeaways emerge:&lt;/p>
&lt;p>&lt;strong>Method insight:&lt;/strong> DiD targets the &lt;strong>ATT&lt;/strong> by using untreated units as a counterfactual for how treated units would have evolved without intervention. The classic 2x2 estimator (ATT = 5.12, SE = 0.25) works well when all units start treatment simultaneously, but staggered adoption requires modern estimators like Callaway-Sant&amp;rsquo;Anna to avoid TWFE&amp;rsquo;s forbidden comparison bias.&lt;/p>
&lt;p>&lt;strong>Data insight:&lt;/strong> The classic DiD recovered the true effect of 5.0 within sampling error (95% CI: [4.64, 5.60]). In the staggered setting, TWFE estimated 2.18 while the cleaner CS estimator found 2.41 &amp;mdash; a 10% upward correction driven by eliminating the 28.3% weight on forbidden comparisons that dragged TWFE down. The CS event study further revealed that treatment effects grow over time, from 1.97 immediately after treatment to 3.27 six periods later.&lt;/p>
&lt;p>&lt;strong>Practical limitation:&lt;/strong> Parallel trends is untestable for the post-treatment period. Pre-treatment tests (p = 0.29 in our example) can only fail to reject, not confirm. HonestDiD provides a principled solution by computing robust confidence intervals under bounded violations. Our breakdown value exceeding $M = 15$ means the conclusion survives violations more than 15 times the worst pre-treatment departure &amp;mdash; exceptionally strong robustness.&lt;/p>
&lt;p>&lt;strong>Next steps:&lt;/strong> This tutorial used synthetic data &amp;mdash; the 2x2 dataset with a constant treatment effect and the staggered dataset with effects that grow over time. Real-world applications should consider adding covariates to the CS estimator (via the &lt;code>covariates&lt;/code> argument), exploring continuous treatment intensity with &lt;code>ContinuousDiD()&lt;/code>, and comparing CS results against &lt;code>SunAbraham()&lt;/code> or &lt;code>ImputationDiD()&lt;/code> as robustness checks. The &lt;code>diff-diff&lt;/code> package supports all of these within the same API.&lt;/p>
&lt;h2 id="exercises">Exercises&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Null effect test.&lt;/strong> Modify the &lt;code>generate_did_data()&lt;/code> call to set &lt;code>treatment_effect=0.0&lt;/code>. Run the full 2x2 analysis and event study. Does the estimator correctly find a zero effect? What do the pre- and post-treatment event study coefficients look like?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Covariates in Callaway-Sant&amp;rsquo;Anna.&lt;/strong> Add covariates to the staggered data (e.g., unit-level characteristics) and pass them via the &lt;code>covariates&lt;/code> argument in &lt;code>CallawaySantAnna().fit()&lt;/code>. Compare the ATT with and without covariate adjustment. When does covariate adjustment matter most?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Sun-Abraham comparison.&lt;/strong> Estimate the staggered treatment effect using &lt;code>SunAbraham(control_group=&amp;quot;never_treated&amp;quot;)&lt;/code> instead of &lt;code>CallawaySantAnna()&lt;/code>. Compare the overall ATT and event study coefficients. Under what conditions do the two estimators differ?&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>HonestDiD with finer M grid.&lt;/strong> Run the sensitivity analysis with &lt;code>M_values = np.arange(0, 15, 0.5)&lt;/code> to find the exact breakdown point. How does the breakdown change if you use &lt;code>method=&amp;quot;smoothness&amp;quot;&lt;/code> instead of &lt;code>&amp;quot;relative_magnitude&amp;quot;&lt;/code>?&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jeconom.2020.12.001" target="_blank" rel="noopener">Callaway, B. &amp;amp; Sant&amp;rsquo;Anna, P. H. C. (2021). Difference-in-Differences with Multiple Time Periods. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 200&amp;ndash;230.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/igerber/diff-diff" target="_blank" rel="noopener">Gerber, I. (2026). diff-diff: Difference-in-Differences Causal Inference for Python. GitHub repository.&lt;/a> &amp;mdash; &lt;a href="https://diff-diff.readthedocs.io/en/stable/" target="_blank" rel="noopener">Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jeconom.2021.03.014" target="_blank" rel="noopener">Goodman-Bacon, A. (2021). Difference-in-Differences with Variation in Treatment Timing. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 254&amp;ndash;277.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1093/restud/rdad018" target="_blank" rel="noopener">Rambachan, A. &amp;amp; Roth, J. (2023). A More Credible Approach to Parallel Trends. &lt;em>Review of Economic Studies&lt;/em>, 90(5), 2555&amp;ndash;2591.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1257/aeri.20210236" target="_blank" rel="noopener">Roth, J. (2022). Pretest with Caution: Event-Study Estimates after Testing for Parallel Trends. &lt;em>American Economic Review: Insights&lt;/em>, 4(3), 305&amp;ndash;322.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1016/j.jeconom.2020.09.006" target="_blank" rel="noopener">Sun, L. &amp;amp; Abraham, S. (2021). Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects. &lt;em>Journal of Econometrics&lt;/em>, 225(2), 175&amp;ndash;199.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.jstor.org/stable/2118030" target="_blank" rel="noopener">Card, D. &amp;amp; Krueger, A. B. (1994). Minimum Wages and Employment: A Case Study of the Fast-Food Industry in New Jersey and Pennsylvania. &lt;em>American Economic Review&lt;/em>, 84(4), 772&amp;ndash;793.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://mixtape.scunning.com/09-difference_in_differences" target="_blank" rel="noopener">Cunningham, S. (2021). &lt;em>Causal Inference: The Mixtape&lt;/em>. Yale University Press. Chapter 9: Difference-in-Differences.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1257/aer.20181169" target="_blank" rel="noopener">de Chaisemartin, C. &amp;amp; D&amp;rsquo;Haultfoeuille, X. (2020). Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects. &lt;em>American Economic Review&lt;/em>, 110(9), 2964&amp;ndash;2996.&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://doi.org/10.1037/h0037350" target="_blank" rel="noopener">Rubin, D. B. (1974). Estimating Causal Effects of Treatments in Randomized and Nonrandomized Studies. &lt;em>Journal of Educational Psychology&lt;/em>, 66(5), 688&amp;ndash;701.&lt;/a>&lt;/li>
&lt;/ol>
&lt;h4 id="acknowledgements">Acknowledgements&lt;/h4>
&lt;p>AI tools (Claude Code, Gemini, NotebookLM) were used to make the contents of this post more accessible to students. Nevertheless, the content in this post may still have errors. Caution is needed when applying the contents of this post to true research projects.&lt;/p></description></item><item><title>Heterogeneous treatment effects via two-stage DID</title><link>https://carlos-mendez.org/post/r_two_stage_did/</link><pubDate>Mon, 29 Jul 2024 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_two_stage_did/</guid><description>&lt;h2 id="homogeneous-treatment-effects">Homogeneous Treatment Effects&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>🎯 &lt;strong>Purpose&lt;/strong>:
Estimate treatment effects when the treatment is not randomly assigned.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>📉 &lt;strong>Parallel Trends Assumption&lt;/strong>:
In the absence of treatment, the treated and untreated groups would have followed parallel paths over time.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔄 &lt;strong>Two-Way Fixed-Effects (TWFE) Model&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Static Model&lt;/strong>:&lt;/li>
&lt;/ul>
&lt;p>$$
y_{igt} = \mu_g + \eta_t + \tau D_{gt} + \epsilon_{igt}
$$&lt;/p>
&lt;ul>
&lt;li>$ y_{igt} $: Outcome variable.&lt;/li>
&lt;li>$ i $: Individual.&lt;/li>
&lt;li>$ t $: Time.&lt;/li>
&lt;li>$ g $: Group.&lt;/li>
&lt;li>$ \mu_g $: Group fixed-effects.&lt;/li>
&lt;li>$ \eta_t $: Time fixed-effects.&lt;/li>
&lt;li>$ D_{gt} $: Indicator for treatment status.&lt;/li>
&lt;li>$ \tau $: Average treatment effect on the treated (ATT).&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>❗ &lt;strong>Limitations&lt;/strong>:
Assumes constant treatment effects across groups and time, which is often unrealistic.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="heterogeneous-treatment-effects">Heterogeneous Treatment Effects&lt;/h2>
&lt;ul>
&lt;li>🔄 &lt;strong>Enhanced TWFE Model&lt;/strong>:
$$
y_{igt} = \mu_g + \eta_t + \tau_{gt} D_{gt} + \epsilon_{igt}
$$
&lt;ul>
&lt;li>Allows treatment effects ($ \tau_{gt} $) to vary by group and time.&lt;/li>
&lt;li>Aggregates group-time average treatment effects into an overall average treatment effect ($ \tau $).&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="dynamic-event-study-twfe-model">Dynamic Event-Study TWFE Model&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>🔄 &lt;strong>Model&lt;/strong>:
$$
y_{igt} = \mu_g + \eta_t + \sum_{k=-L}^{-2} \tau_k D_{gt}^k + \sum_{k=0}^{K} \tau_k D_{gt}^k + \epsilon_{igt}
$$&lt;/p>
&lt;ul>
&lt;li>Allows for treatment effects to change over time.&lt;/li>
&lt;li>$ D_{gt}^k $: Lags and leads of treatment status.&lt;/li>
&lt;li>Coefficients ($ \tau_k $) represent the average effect of being treated for $ k $ periods.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>🎯 &lt;strong>Estimation Goals&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Objective&lt;/strong>: Estimate the average treatment effect of being exposed for $ k $ periods.&lt;/li>
&lt;li>&lt;strong>Average Treatment Effect&lt;/strong>:
$$
\tau_k = \sum_{g,t : t-g=k} \frac{N_{gt}}{N_k} \tau_{gt}
$$
&lt;ul>
&lt;li>$ N_{gt} $: Number of observations in group $ g $ and time $ t $.&lt;/li>
&lt;li>$ N_k $: Total number of observations with $ t - g = k $.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="negative-weighting-problem">Negative Weighting Problem&lt;/h2>
&lt;ul>
&lt;li>❗ &lt;strong>Issue&lt;/strong>: Traditional TWFE models can produce estimates with negative weights, leading to biased overall treatment effect estimates.&lt;/li>
&lt;li>🛠 &lt;strong>Solution by Gardner (2021)&lt;/strong>:
&lt;ul>
&lt;li>Use a two-stage approach to estimate group and time fixed-effects from untreated/not-yet-treated observations and then estimate treatment effects using residualized outcomes.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="two-stage-differences-in-differences">Two-stage differences in differences&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>🌱 &lt;strong>Gardner (2021) Approach&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>🔍 &lt;strong>Key Insight&lt;/strong>: Under parallel trends, group and time effects are identified from the untreated/not-yet-treated observations.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>📜 &lt;strong>Procedure&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>🥇 &lt;strong>First Stage&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Estimate the model:&lt;/p>
&lt;p>\begin{equation}
y_{igt} = \mu_g + \eta_t + \epsilon_{igt}
\end{equation}&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Using only untreated/not-yet-treated observations ($D_{gt} = 0$).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Obtain estimates for group and time effects ($\mu_g$ and $\eta_t$).&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>🥈 &lt;strong>Second Stage&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Regress adjusted outcomes ($y_{igt} - \mu_g - \eta_t$) on treatment status ($D_{gt}$) in the full sample to estimate treatment effects ($\tau$).&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>
&lt;p>🎯 &lt;strong>Rationale&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>The parallel trends assumption implies that residuals ($\epsilon_{igt}$) are uncorrelated with the treatment dummy, leading to a consistent estimator for the average treatment effect.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;center>
&lt;div class="alert alert-note">
&lt;div>
Learn by coding using this &lt;a href="https://colab.research.google.com/drive/1A5zxj9SU8phTTCHBkt1fQkFX1xhFbycI?usp=sharing">Google Colab notebook&lt;/a>.
&lt;/div>
&lt;/div>
&lt;/center></description></item><item><title>Staggered DiD (Ex1)</title><link>https://carlos-mendez.org/post/r_staggered_did/</link><pubDate>Sun, 03 Sep 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_staggered_did/</guid><description>&lt;p>An introduction to difference in differences with multiple time periods and staggered treatment adoption. This tutorial is based on &lt;a href="https://github.com/Mixtape-Sessions/Advanced-DID/tree/main/Exercises/Exercise-1" target="_blank" rel="noopener">Exercise 1&lt;/a> of the Advanced DiD mixed tape session of Jonathan Roth. You can run and extend the analysis of this case study using &lt;a href="https://colab.research.google.com/drive/14LJEYHZTlw5wtIK0bR0lOza7lQiO0krc?usp=sharing" target="_blank" rel="noopener">Google Colab&lt;/a>.&lt;/p></description></item><item><title>Staggered DiD</title><link>https://carlos-mendez.org/post/r_staggered_did1/</link><pubDate>Sat, 02 Sep 2023 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_staggered_did1/</guid><description>&lt;p>An introduction to difference in differences with multiple time periods and staggered treatment adoption. You can run and extend the analysis of this case study using &lt;a href="https://colab.research.google.com/drive/1ucJmhyvb7pn01zyQji0xVZy_nZbo3_jB?usp=sharing" target="_blank" rel="noopener">Google Colab&lt;/a>.&lt;/p></description></item><item><title>Basic DiD</title><link>https://carlos-mendez.org/post/r_basic_did/</link><pubDate>Mon, 01 Apr 2019 00:00:00 +0000</pubDate><guid>https://carlos-mendez.org/post/r_basic_did/</guid><description>&lt;p>In this case study, we use the Differences in Differences (DiD) method to analyze the effect of a garbage incinerator&amp;rsquo;s location on housing prices. This method is a statistical technique used in econometrics that calculates the effect of a treatment (in this case, the placement of a garbage incinerator) on an outcome (here, housing prices) by comparing the average change over time in the outcome variable for the treatment group to the average change over time for the control group. You can run and extend the analysis of this case study using &lt;a href="https://posit.cloud/content/6182152" target="_blank" rel="noopener">Posit cloud&lt;/a> or &lt;a href="https://colab.research.google.com/drive/14LJEYHZTlw5wtIK0bR0lOza7lQiO0krc?usp=sharing" target="_blank" rel="noopener">Google Colab&lt;/a>.&lt;/p></description></item></channel></rss>