Skip to content

Commit 9e5b790

Browse files
committed
Add check and warning for staggered adoption in EventStudy
Added a warning in the EventStudy class and documentation that the implementation only supports simultaneous treatment timing and does not support staggered adoption. Introduced a validation to raise a DataException if treated units have different treatment times. Added a corresponding test to ensure staggered adoption raises an error, and updated the notebook to clarify estimator limitations.
1 parent ff61e55 commit 9e5b790

File tree

3 files changed

+57
-7
lines changed

3 files changed

+57
-7
lines changed

causalpy/experiments/event_study.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ class EventStudy(BaseExperiment):
5555
- :math:`\\beta_k` are the dynamic treatment effects at event time k
5656
- :math:`k_0` is the reference (omitted) event time
5757
58+
.. warning::
59+
60+
This implementation uses a standard two-way fixed effects (TWFE) estimator,
61+
which requires **simultaneous treatment timing** (all treated units receive
62+
treatment at the same time). Staggered adoption designs, where different units
63+
are treated at different times, can produce biased estimates when treatment
64+
effects vary across cohorts. See Sun & Abraham (2021) for details.
65+
5866
Parameters
5967
----------
6068
data : pd.DataFrame
@@ -197,6 +205,19 @@ def input_validation(self) -> None:
197205
"Each unit should have at most one observation per time period."
198206
)
199207

208+
# Check that all treated units have the same treatment time
209+
# (staggered adoption is not currently supported)
210+
treated_times = self.data.loc[
211+
~self.data[self.treat_time_col].isna(), self.treat_time_col
212+
].unique()
213+
if len(treated_times) > 1:
214+
raise DataException(
215+
f"All treated units must have the same treatment time. "
216+
f"Found {len(treated_times)} different treatment times: "
217+
f"{sorted(treated_times)}. "
218+
f"Staggered adoption designs are not currently supported."
219+
)
220+
200221
def _compute_event_time(self) -> None:
201222
"""Compute event time (time relative to treatment) for each observation."""
202223
self.data["_event_time"] = np.nan

causalpy/tests/test_event_study.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,35 @@ def test_event_study_duplicate_observations():
176176
)
177177

178178

179+
def test_event_study_staggered_adoption_not_supported():
180+
"""Test that staggered adoption (different treatment times) raises DataException."""
181+
df = pd.DataFrame(
182+
{
183+
"unit": [0, 0, 1, 1, 2, 2],
184+
"time": [0, 1, 0, 1, 0, 1],
185+
"y": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
186+
"treat_time": [
187+
5.0,
188+
5.0,
189+
10.0,
190+
10.0,
191+
np.nan,
192+
np.nan,
193+
], # Different treat times
194+
}
195+
)
196+
197+
with pytest.raises(DataException, match="same treatment time"):
198+
cp.EventStudy(
199+
df,
200+
formula="y ~ C(unit) + C(time)",
201+
unit_col="unit",
202+
time_col="time",
203+
treat_time_col="treat_time",
204+
model=cp.pymc_models.LinearRegression(sample_kwargs=sample_kwargs),
205+
)
206+
207+
179208
# ============================================================================
180209
# Integration Tests with PyMC
181210
# ============================================================================

docs/source/notebooks/event_study_pymc.ipynb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
"source": [
77
"# Event Study (Dynamic DiD) with `pymc` models\n",
88
"\n",
9-
"This notebook demonstrates how to use CausalPy's `EventStudy` class to estimate **dynamic treatment effects** over event time. This is also known as a \"dynamic {term}`difference in differences`\" analysis.\n",
10-
"\n",
11-
":::{note}\n",
12-
"{term}`Event study` is a powerful tool for:\n",
9+
"This notebook demonstrates how to use CausalPy's `EventStudy` class to estimate **dynamic treatment effects** over event time. This is also known as a \"dynamic {term}`difference in differences`\" analysis. The {term}`event study` is a powerful tool for:\n",
1310
"1. Examining **pre-treatment trends** (placebo checks for {term}`parallel trends assumption`)\n",
1411
"2. Estimating how **treatment effects evolve over time** after treatment\n",
15-
"3. Visualizing the full **time path of causal effects**\n",
16-
":::"
12+
"3. Visualizing the full **time path of causal effects**"
1713
]
1814
},
1915
{
@@ -42,7 +38,11 @@
4238
"\n",
4339
"**Interpretation:**\n",
4440
"- $\\beta_k$ for $k < 0$ (pre-treatment): Should be near zero if parallel trends hold\n",
45-
"- $\\beta_k$ for $k \\geq 0$ (post-treatment): Measure the causal effect at each period after treatment\n"
41+
"- $\\beta_k$ for $k \\geq 0$ (post-treatment): Measure the causal effect at each period after treatment\n",
42+
"\n",
43+
":::{warning}\n",
44+
"This implementation uses a standard two-way fixed effects (TWFE) estimator, which requires **simultaneous treatment timing** - all treated units must receive treatment at the same time. Staggered adoption designs (where different units are treated at different times) can produce biased estimates when treatment effects vary across cohorts {footcite:t}`sun2021estimating`.\n",
45+
":::\n"
4646
]
4747
},
4848
{

0 commit comments

Comments
 (0)