Skip to content

Commit 68e383b

Browse files
authored
docs(v4): PR 4b of the 4.0 cut - rebuild tutorials 02/04/05/07/23 on the TWFE event-study mode (#867)
1 parent 6ae1d36 commit 68e383b

14 files changed

Lines changed: 560 additions & 461 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
### Documentation
2+
- **Tutorials 02/04/05/07/23 are rebuilt on `TwoWayFixedEffects().fit(..., event_study=True)`**
3+
(`spec="within"`: unit + period fixed effects, unit-clustered SEs) ahead of the
4+
MultiPeriodDiD removal (`M-010`, 4.0 cut PR 4c). Point estimates match the former
5+
MultiPeriodDiD fits on these balanced panels; the SEs change because the within spec
6+
clusters by unit. Tutorial 23's naive headline is the equal-weight average of the two
7+
post-period coefficients computed in the cell (ATT unchanged at -4.29; the SE is now the
8+
unit-clustered one), and tutorial 02's period-effects loop reads `to_dataframe()`.
9+
- The static `TwoWayFixedEffects().fit(..., time=...)` calls in tutorials 01/02/09 (the
10+
`M-082` alias, misused there as the post dummy) are replaced: 01 passes `post="post"`;
11+
02 and 09 report `TwoWayFixedEffects().decompose(...).twfe_estimate`, the unit + period
12+
fixed-effects coefficient their Goodman-Bacon sections decompose (the old cells printed
13+
`treated x period` artefacts: 0.6309 and 0.0001). Tutorial 02 cell 36's covariate draw is
14+
seeded. Tutorials 02 and 09 are re-executed on the Rust backend, which produced the
15+
committed outputs, so the seeded bootstrap cells are unchanged.
16+
- Tutorials 08 and 16 describe `StaggeredTripleDifference` as the removed class (4.0 cut,
17+
PR 4a). `diff_diff/guides/llms-full.txt`'s event-study snippet calls `print(es.summary())`
18+
(`EventStudyResults` has no `print_summary`).
19+
20+
### Testing
21+
- `tests/test_t23_spillover_tva_drift.py`'s `naive_fit` fixture mirrors the notebook's TWFE
22+
event-study headline cell, runs the fit with warnings escalated to errors, and pins the
23+
post-period-average identity and the unit-clustered vcov-block SE alongside the existing
24+
-4.29 endpoint.
25+
26+
### Internal
27+
- `docs/v4-design.md` cut-sequence item 4b marked shipped; item 4c lists the two remaining
28+
markdown-only MultiPeriodDiD mentions (06 cell 34, 16 cell 40). `docs/doc-deps.yaml` maps
29+
the TWFE module to tutorials 01/02/04/05/07/09/23. No ledger row flips.

‎diff_diff/guides/llms-full.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ results.print_summary()
146146
# auto-cluster applies with the static carve-outs)
147147
es = twfe.fit(data, outcome='y', treatment='treated', unit='unit_id',
148148
event_study=True, time='period', post_periods=[4, 5, 6, 7])
149-
es.print_summary()
149+
print(es.summary())
150150
```
151151

152152
**Note:** TWFE can be biased with staggered treatment timing and heterogeneous effects. Consider CallawaySantAnna, SunAbraham, or ImputationDiD for staggered designs.

‎docs/doc-deps.yaml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,27 @@ sources:
186186
note: "TWFE block documents the full fit signature incl. event-study params; the Conley section's panel examples use post=/event_study (rows M-010/M-082)"
187187
- path: docs/migration-4.0.md
188188
type: user_guide
189+
- path: docs/tutorials/01_basic_did.ipynb
190+
type: tutorial
191+
note: "Static TWFE example (post= dummy, row M-082)"
192+
- path: docs/tutorials/02_staggered_did.ipynb
193+
type: tutorial
194+
note: "decompose() TWFE estimate + the TWFE event-study comparison section (event_study=True, spec within)"
195+
- path: docs/tutorials/04_parallel_trends.ipynb
196+
type: tutorial
197+
note: "Event-study parallel-trends check uses event_study=True"
198+
- path: docs/tutorials/05_honest_did.ipynb
199+
type: tutorial
200+
note: "HonestDiD inputs come from TWFE event_study=True"
201+
- path: docs/tutorials/07_pretrends_power.ipynb
202+
type: tutorial
203+
note: "PreTrendsPower inputs come from TWFE event_study=True"
204+
- path: docs/tutorials/09_real_world_examples.ipynb
205+
type: tutorial
206+
note: "decompose() TWFE estimate in the castle-doctrine section"
207+
- path: docs/tutorials/23_spillover_tva.ipynb
208+
type: tutorial
209+
note: "Naive headline = TWFE event_study=True post-period average"
189210

190211
# ── CallawaySantAnna (staggered group) ─────��────────────────────────
191212

‎docs/tutorials/01_basic_did.ipynb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@
306306
"source": [
307307
"## Two-Way Fixed Effects (TWFE)\n",
308308
"\n",
309-
"For panel data, the `TwoWayFixedEffects` estimator automatically includes both unit and time fixed effects using within-transformation."
309+
"For panel data, the `TwoWayFixedEffects` estimator absorbs unit fixed effects and the pre/post period effect by within-transformation: the regressor is `treated x post`, and `post=` names the 0/1 post-treatment column. For period-by-period effects with calendar-time fixed effects use the event-study mode, `fit(..., unit=, time=, event_study=True, post_periods=)` (tutorials 04 and 05)."
310310
]
311311
},
312312
{
@@ -321,7 +321,7 @@
321321
" panel_df,\n",
322322
" outcome=\"outcome\",\n",
323323
" treatment=\"treated\",\n",
324-
" time=\"period\", # Use actual time periods\n",
324+
" post=\"post\", # 0/1 post-treatment indicator\n",
325325
" unit=\"unit\"\n",
326326
")\n",
327327
"\n",

‎docs/tutorials/02_staggered_did.ipynb‎

Lines changed: 178 additions & 232 deletions
Large diffs are not rendered by default.

‎docs/tutorials/04_parallel_trends.ipynb‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"source": [
2626
"import numpy as np\n",
2727
"import pandas as pd\n",
28-
"from diff_diff import DifferenceInDifferences, MultiPeriodDiD\n",
28+
"from diff_diff import DifferenceInDifferences, TwoWayFixedEffects\n",
2929
"from diff_diff.utils import (\n",
3030
" check_parallel_trends,\n",
3131
" check_parallel_trends_robust,\n",
@@ -543,7 +543,7 @@
543543
"source": [
544544
"## Event Study as a Parallel Trends Check\n",
545545
"\n",
546-
"An **event study** shows period-by-period effects. Pre-treatment coefficients should be close to zero if parallel trends holds."
546+
"An **event study** shows period-by-period effects. Pre-treatment coefficients should be close to zero if parallel trends holds. We fit a TWFE event study (`TwoWayFixedEffects().fit(..., event_study=True)`): unit and period fixed effects, one coefficient per period relative to the reference period 3."
547547
]
548548
},
549549
{
@@ -552,13 +552,15 @@
552552
"metadata": {},
553553
"outputs": [],
554554
"source": [
555-
"# Event study\n",
556-
"mp_did = MultiPeriodDiD()\n",
557-
"event_results = mp_did.fit(\n",
555+
"# Event study: unit + period fixed effects, one coefficient per period\n",
556+
"twfe = TwoWayFixedEffects()\n",
557+
"event_results = twfe.fit(\n",
558558
" df_parallel,\n",
559559
" outcome='outcome',\n",
560560
" treatment='treated',\n",
561+
" unit='unit',\n",
561562
" time='period',\n",
563+
" event_study=True,\n",
562564
" post_periods=[4, 5, 6, 7],\n",
563565
" reference_period=3 # Use period 3 as reference\n",
564566
")\n",

‎docs/tutorials/05_honest_did.ipynb‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"source": [
3333
"import numpy as np\n",
3434
"import pandas as pd\n",
35-
"from diff_diff import MultiPeriodDiD\n",
35+
"from diff_diff import TwoWayFixedEffects\n",
3636
"from diff_diff.honest_did import (\n",
3737
" HonestDiD,\n",
3838
" compute_honest_did,\n",
@@ -148,7 +148,7 @@
148148
"source": [
149149
"## Fit Standard Event Study\n",
150150
"\n",
151-
"First, let's estimate a standard event study using `MultiPeriodDiD`."
151+
"First, let's estimate a standard event study with `TwoWayFixedEffects().fit(..., event_study=True)` (unit and period fixed effects; the last pre-period, 4, is the reference)."
152152
]
153153
},
154154
{
@@ -158,13 +158,15 @@
158158
"metadata": {},
159159
"outputs": [],
160160
"source": [
161-
"# Fit event study\n",
162-
"mp_did = MultiPeriodDiD()\n",
163-
"event_results = mp_did.fit(\n",
161+
"# Fit event study (unit + period fixed effects)\n",
162+
"twfe = TwoWayFixedEffects()\n",
163+
"event_results = twfe.fit(\n",
164164
" df,\n",
165165
" outcome='outcome',\n",
166166
" treatment='treated',\n",
167+
" unit='unit',\n",
167168
" time='period',\n",
169+
" event_study=True,\n",
168170
" post_periods=[5, 6, 7, 8, 9]\n",
169171
")\n",
170172
"\n",

‎docs/tutorials/07_pretrends_power.ipynb‎

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"import pandas as pd\n",
3434
"\n",
3535
"from diff_diff import (\n",
36-
" MultiPeriodDiD,\n",
36+
" TwoWayFixedEffects,\n",
3737
" PreTrendsPower,\n",
3838
" compute_pretrends_power,\n",
3939
" compute_mdv,\n",
@@ -131,7 +131,26 @@
131131
"id": "cell-6",
132132
"metadata": {},
133133
"outputs": [],
134-
"source": "# Fit event study with ALL periods (pre and post) relative to reference period\n# For pre-trends power analysis, we need coefficients for pre-periods too\nmp_did = MultiPeriodDiD()\n\n# Use period 4 as the reference period (last pre-period, excluded from estimation)\n# Specify post_periods as the actual post-treatment periods; MultiPeriodDiD\n# automatically estimates pre-period coefficients for the event study.\nevent_results = mp_did.fit(\n df,\n outcome='outcome',\n treatment='treated',\n time='period',\n post_periods=[5, 6, 7, 8, 9]\n)\n\nprint(event_results.summary())"
134+
"source": [
135+
"# Fit event study with ALL periods (pre and post) relative to reference period\n",
136+
"# For pre-trends power analysis, we need coefficients for pre-periods too\n",
137+
"twfe = TwoWayFixedEffects()\n",
138+
"\n",
139+
"# Use period 4 as the reference period (last pre-period, excluded from estimation)\n",
140+
"# Specify post_periods as the actual post-treatment periods; the event-study mode\n",
141+
"# estimates one coefficient per other period, pre-periods included.\n",
142+
"event_results = twfe.fit(\n",
143+
" df,\n",
144+
" outcome='outcome',\n",
145+
" treatment='treated',\n",
146+
" unit='unit',\n",
147+
" time='period',\n",
148+
" event_study=True,\n",
149+
" post_periods=[5, 6, 7, 8, 9]\n",
150+
")\n",
151+
"\n",
152+
"print(event_results.summary())"
153+
]
135154
},
136155
{
137156
"cell_type": "code",
@@ -520,7 +539,46 @@
520539
"id": "cell-30",
521540
"metadata": {},
522541
"outputs": [],
523-
"source": "# Typical workflow for pre-trends power analysis\n\n# Step 1: Estimate event study with proper pre/post period classification\nmp_did = MultiPeriodDiD()\n\n# Specify actual post-treatment periods; pre-period coefficients are\n# estimated automatically by MultiPeriodDiD for the event study\npre_treatment_periods = [0, 1, 2, 3] # Define which are pre-treatment\n\nresults = mp_did.fit(\n df, \n outcome='outcome',\n treatment='treated', \n time='period',\n post_periods=[5, 6, 7, 8, 9]\n)\n\n# Step 2: Assess power of the pre-trends test \nprint(\"Step 2: Pre-Trends Power Analysis\")\npt = PreTrendsPower(alpha=0.05, power=0.80, violation_type='linear')\npt_results = pt.fit(results, pre_periods=pre_treatment_periods)\nprint(f\"MDV (80% power): {pt_results.mdv:.3f}\")\nprint(\"\")\n\n# Step 3: Interpret\nprint(\"Step 3: Interpretation\")\nprint(f\"Your pre-trends test could only detect violations >= {pt_results.mdv:.3f}\")\nprint(f\"Violations smaller than this would likely go undetected.\")\nprint(\"\")\n\n# Step 4: Connect to Honest DiD for robust inference\nprint(\"Step 4: Robust Inference with Honest DiD\")\nhonest = HonestDiD(method='smoothness', M=pt_results.mdv)\nhonest_results = honest.fit(results)\nprint(f\"Robust 95% CI (M=MDV): [{honest_results.ci_lb:.3f}, {honest_results.ci_ub:.3f}]\")\nprint(f\"Conclusion: {'Effect is robust' if honest_results.is_significant else 'Effect may not be robust'}\")"
542+
"source": [
543+
"# Typical workflow for pre-trends power analysis\n",
544+
"\n",
545+
"# Step 1: Estimate event study with proper pre/post period classification\n",
546+
"twfe = TwoWayFixedEffects()\n",
547+
"\n",
548+
"# Specify actual post-treatment periods; the event-study mode estimates\n",
549+
"# the pre-period coefficients automatically\n",
550+
"pre_treatment_periods = [0, 1, 2, 3] # Define which are pre-treatment\n",
551+
"\n",
552+
"results = twfe.fit(\n",
553+
" df, \n",
554+
" outcome='outcome',\n",
555+
" treatment='treated', \n",
556+
" unit='unit',\n",
557+
" time='period',\n",
558+
" event_study=True,\n",
559+
" post_periods=[5, 6, 7, 8, 9]\n",
560+
")\n",
561+
"\n",
562+
"# Step 2: Assess power of the pre-trends test \n",
563+
"print(\"Step 2: Pre-Trends Power Analysis\")\n",
564+
"pt = PreTrendsPower(alpha=0.05, power=0.80, violation_type='linear')\n",
565+
"pt_results = pt.fit(results, pre_periods=pre_treatment_periods)\n",
566+
"print(f\"MDV (80% power): {pt_results.mdv:.3f}\")\n",
567+
"print(\"\")\n",
568+
"\n",
569+
"# Step 3: Interpret\n",
570+
"print(\"Step 3: Interpretation\")\n",
571+
"print(f\"Your pre-trends test could only detect violations >= {pt_results.mdv:.3f}\")\n",
572+
"print(f\"Violations smaller than this would likely go undetected.\")\n",
573+
"print(\"\")\n",
574+
"\n",
575+
"# Step 4: Connect to Honest DiD for robust inference\n",
576+
"print(\"Step 4: Robust Inference with Honest DiD\")\n",
577+
"honest = HonestDiD(method='smoothness', M=pt_results.mdv)\n",
578+
"honest_results = honest.fit(results)\n",
579+
"print(f\"Robust 95% CI (M=MDV): [{honest_results.ci_lb:.3f}, {honest_results.ci_ub:.3f}]\")\n",
580+
"print(f\"Conclusion: {'Effect is robust' if honest_results.is_significant else 'Effect may not be robust'}\")"
581+
]
524582
},
525583
{
526584
"cell_type": "markdown",

‎docs/tutorials/08_triple_diff.ipynb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -673,8 +673,8 @@
673673
"The staggered parameters are keyword-only, and mixing the two designs' parameters\n",
674674
"raises rather than guessing which one you meant.\n",
675675
"\n",
676-
"> This replaces `StaggeredTripleDifference`, which is deprecated in 3.9 and\n",
677-
"> removed in 4.0. It runs the same engine, so the numbers are unchanged; on the\n",
676+
"> This replaces the former `StaggeredTripleDifference` class (deprecated in 3.9,\n",
677+
"> removed at the 4.0 cut). It runs the same engine, so the numbers are unchanged; on the\n",
678678
"> merged surface the third dimension is named `partition=` (not `eligibility=`)\n",
679679
"> and `control_group` takes `\"not_yet_treated\"` / `\"never_treated\"`."
680680
]

‎docs/tutorials/09_real_world_examples.ipynb‎

Lines changed: 122 additions & 150 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)