diff-diff 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diff_diff-0.3.0 → diff_diff-0.4.0}/PKG-INFO +250 -1
- {diff_diff-0.3.0 → diff_diff-0.4.0}/README.md +249 -0
- diff_diff-0.4.0/diff_diff/__init__.py +80 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff/prep.py +532 -38
- diff_diff-0.4.0/diff_diff/staggered.py +982 -0
- diff_diff-0.4.0/diff_diff/visualization.py +471 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff.egg-info/PKG-INFO +250 -1
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff.egg-info/SOURCES.txt +5 -1
- {diff_diff-0.3.0 → diff_diff-0.4.0}/pyproject.toml +1 -1
- {diff_diff-0.3.0 → diff_diff-0.4.0}/tests/test_prep.py +343 -0
- diff_diff-0.4.0/tests/test_staggered.py +390 -0
- diff_diff-0.4.0/tests/test_visualization.py +284 -0
- diff_diff-0.3.0/diff_diff/__init__.py +0 -43
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff/estimators.py +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff/results.py +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff/utils.py +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/setup.cfg +0 -0
- {diff_diff-0.3.0 → diff_diff-0.4.0}/tests/test_estimators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
5
|
Author: diff-diff contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -104,7 +104,10 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
104
104
|
- **Robust inference**: Heteroskedasticity-robust (HC1) and cluster-robust standard errors
|
|
105
105
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
106
106
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
107
|
+
- **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
|
|
107
108
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
109
|
+
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
110
|
+
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
108
111
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
109
112
|
|
|
110
113
|
## Data Preparation
|
|
@@ -342,6 +345,79 @@ cohort_data = aggregate_to_cohorts(
|
|
|
342
345
|
# Result: mean outcome by treatment group and period
|
|
343
346
|
```
|
|
344
347
|
|
|
348
|
+
### Rank Control Units
|
|
349
|
+
|
|
350
|
+
Select the best control units for DiD or Synthetic DiD analysis by ranking them based on pre-treatment outcome similarity:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from diff_diff import rank_control_units, generate_did_data
|
|
354
|
+
|
|
355
|
+
# Generate sample data
|
|
356
|
+
data = generate_did_data(n_units=50, n_periods=6, seed=42)
|
|
357
|
+
|
|
358
|
+
# Rank control units by their similarity to treated units
|
|
359
|
+
ranking = rank_control_units(
|
|
360
|
+
data,
|
|
361
|
+
unit_column='unit',
|
|
362
|
+
time_column='period',
|
|
363
|
+
outcome_column='outcome',
|
|
364
|
+
treatment_column='treated',
|
|
365
|
+
n_top=10 # Return top 10 controls
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
print(ranking[['unit', 'quality_score', 'pre_trend_rmse']])
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Output:
|
|
372
|
+
```
|
|
373
|
+
unit quality_score pre_trend_rmse
|
|
374
|
+
0 35 1.0000 0.4521
|
|
375
|
+
1 42 0.9234 0.5123
|
|
376
|
+
2 28 0.8876 0.5892
|
|
377
|
+
...
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
With covariates for matching:
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
# Add covariate-based matching
|
|
384
|
+
ranking = rank_control_units(
|
|
385
|
+
data,
|
|
386
|
+
unit_column='unit',
|
|
387
|
+
time_column='period',
|
|
388
|
+
outcome_column='outcome',
|
|
389
|
+
treatment_column='treated',
|
|
390
|
+
covariates=['size', 'age'], # Match on these too
|
|
391
|
+
outcome_weight=0.7, # 70% weight on outcome trends
|
|
392
|
+
covariate_weight=0.3 # 30% weight on covariate similarity
|
|
393
|
+
)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Filter data for SyntheticDiD using top controls:
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
from diff_diff import SyntheticDiD
|
|
400
|
+
|
|
401
|
+
# Get top control units
|
|
402
|
+
top_controls = ranking['unit'].tolist()
|
|
403
|
+
|
|
404
|
+
# Filter data to treated + top controls
|
|
405
|
+
filtered_data = data[
|
|
406
|
+
(data['treated'] == 1) | (data['unit'].isin(top_controls))
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
# Fit SyntheticDiD with selected controls
|
|
410
|
+
sdid = SyntheticDiD()
|
|
411
|
+
results = sdid.fit(
|
|
412
|
+
filtered_data,
|
|
413
|
+
outcome='outcome',
|
|
414
|
+
treatment='treated',
|
|
415
|
+
unit='unit',
|
|
416
|
+
time='period',
|
|
417
|
+
post_periods=[3, 4, 5]
|
|
418
|
+
)
|
|
419
|
+
```
|
|
420
|
+
|
|
345
421
|
## Usage
|
|
346
422
|
|
|
347
423
|
### Basic DiD with Column Names
|
|
@@ -522,6 +598,148 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
522
598
|
================================================================================
|
|
523
599
|
```
|
|
524
600
|
|
|
601
|
+
### Staggered Difference-in-Differences (Callaway-Sant'Anna)
|
|
602
|
+
|
|
603
|
+
When treatment is adopted at different times by different units, traditional TWFE estimators can be biased. The Callaway-Sant'Anna estimator provides unbiased estimates with staggered adoption.
|
|
604
|
+
|
|
605
|
+
```python
|
|
606
|
+
from diff_diff import CallawaySantAnna
|
|
607
|
+
|
|
608
|
+
# Panel data with staggered treatment
|
|
609
|
+
# 'first_treat' = period when unit was first treated (0 if never treated)
|
|
610
|
+
cs = CallawaySantAnna()
|
|
611
|
+
results = cs.fit(
|
|
612
|
+
panel_data,
|
|
613
|
+
outcome='sales',
|
|
614
|
+
unit='firm_id',
|
|
615
|
+
time='year',
|
|
616
|
+
first_treat='first_treat', # 0 for never-treated, else first treatment year
|
|
617
|
+
aggregate='event_study' # Compute event study effects
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# View results
|
|
621
|
+
results.print_summary()
|
|
622
|
+
|
|
623
|
+
# Access group-time effects ATT(g,t)
|
|
624
|
+
for (group, time), effect in results.group_time_effects.items():
|
|
625
|
+
print(f"Cohort {group}, Period {time}: {effect['effect']:.3f}")
|
|
626
|
+
|
|
627
|
+
# Event study effects (averaged by relative time)
|
|
628
|
+
for rel_time, effect in results.event_study_effects.items():
|
|
629
|
+
print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
|
|
630
|
+
|
|
631
|
+
# Convert to DataFrame
|
|
632
|
+
df = results.to_dataframe(level='event_study')
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Output:
|
|
636
|
+
```
|
|
637
|
+
=====================================================================================
|
|
638
|
+
Callaway-Sant'Anna Staggered Difference-in-Differences Results
|
|
639
|
+
=====================================================================================
|
|
640
|
+
|
|
641
|
+
Total observations: 600
|
|
642
|
+
Treated units: 35
|
|
643
|
+
Control units: 15
|
|
644
|
+
Treatment cohorts: 3
|
|
645
|
+
Time periods: 8
|
|
646
|
+
Control group: never_treated
|
|
647
|
+
|
|
648
|
+
-------------------------------------------------------------------------------------
|
|
649
|
+
Overall Average Treatment Effect on the Treated
|
|
650
|
+
-------------------------------------------------------------------------------------
|
|
651
|
+
Parameter Estimate Std. Err. t-stat P>|t| Sig.
|
|
652
|
+
-------------------------------------------------------------------------------------
|
|
653
|
+
ATT 2.5000 0.3521 7.101 0.0000 ***
|
|
654
|
+
-------------------------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
95% Confidence Interval: [1.8099, 3.1901]
|
|
657
|
+
|
|
658
|
+
-------------------------------------------------------------------------------------
|
|
659
|
+
Event Study (Dynamic) Effects
|
|
660
|
+
-------------------------------------------------------------------------------------
|
|
661
|
+
Rel. Period Estimate Std. Err. t-stat P>|t| Sig.
|
|
662
|
+
-------------------------------------------------------------------------------------
|
|
663
|
+
0 2.1000 0.4521 4.645 0.0000 ***
|
|
664
|
+
1 2.5000 0.4123 6.064 0.0000 ***
|
|
665
|
+
2 2.8000 0.5234 5.349 0.0000 ***
|
|
666
|
+
-------------------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
669
|
+
=====================================================================================
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**When to use Callaway-Sant'Anna vs TWFE:**
|
|
673
|
+
|
|
674
|
+
| Scenario | Use TWFE | Use Callaway-Sant'Anna |
|
|
675
|
+
|----------|----------|------------------------|
|
|
676
|
+
| All units treated at same time | ✓ | ✓ |
|
|
677
|
+
| Staggered adoption, homogeneous effects | ✓ | ✓ |
|
|
678
|
+
| Staggered adoption, heterogeneous effects | ✗ | ✓ |
|
|
679
|
+
| Need event study with staggered timing | ✗ | ✓ |
|
|
680
|
+
| Fewer than ~20 treated units | ✓ | Depends on design |
|
|
681
|
+
|
|
682
|
+
**Parameters:**
|
|
683
|
+
|
|
684
|
+
```python
|
|
685
|
+
CallawaySantAnna(
|
|
686
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
687
|
+
anticipation=0, # Periods before treatment with effects
|
|
688
|
+
estimation_method='dr', # 'dr', 'ipw', or 'reg'
|
|
689
|
+
alpha=0.05, # Significance level
|
|
690
|
+
cluster=None, # Column for cluster SEs
|
|
691
|
+
n_bootstrap=0, # Must be 0 (bootstrap not yet implemented)
|
|
692
|
+
seed=None # Random seed
|
|
693
|
+
)
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Current limitations:**
|
|
697
|
+
- Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
|
|
698
|
+
- Covariate adjustment for conditional parallel trends is not yet implemented
|
|
699
|
+
|
|
700
|
+
### Event Study Visualization
|
|
701
|
+
|
|
702
|
+
Create publication-ready event study plots:
|
|
703
|
+
|
|
704
|
+
```python
|
|
705
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
706
|
+
|
|
707
|
+
# From MultiPeriodDiD
|
|
708
|
+
did = MultiPeriodDiD()
|
|
709
|
+
results = did.fit(data, outcome='y', treatment='treated',
|
|
710
|
+
time='period', post_periods=[3, 4, 5])
|
|
711
|
+
plot_event_study(results, title="Treatment Effects Over Time")
|
|
712
|
+
|
|
713
|
+
# From CallawaySantAnna (with event study aggregation)
|
|
714
|
+
cs = CallawaySantAnna()
|
|
715
|
+
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
716
|
+
first_treat='first_treat', aggregate='event_study')
|
|
717
|
+
plot_event_study(results, title="Staggered DiD Event Study")
|
|
718
|
+
|
|
719
|
+
# From a DataFrame
|
|
720
|
+
df = pd.DataFrame({
|
|
721
|
+
'period': [-2, -1, 0, 1, 2],
|
|
722
|
+
'effect': [0.1, 0.05, 0.0, 2.5, 2.8],
|
|
723
|
+
'se': [0.3, 0.25, 0.0, 0.4, 0.45]
|
|
724
|
+
})
|
|
725
|
+
plot_event_study(df, reference_period=0)
|
|
726
|
+
|
|
727
|
+
# With customization
|
|
728
|
+
ax = plot_event_study(
|
|
729
|
+
results,
|
|
730
|
+
title="Dynamic Treatment Effects",
|
|
731
|
+
xlabel="Years Relative to Treatment",
|
|
732
|
+
ylabel="Effect on Sales ($1000s)",
|
|
733
|
+
color="#2563eb",
|
|
734
|
+
marker="o",
|
|
735
|
+
shade_pre=True, # Shade pre-treatment region
|
|
736
|
+
show_zero_line=True, # Horizontal line at y=0
|
|
737
|
+
show_reference_line=True, # Vertical line at reference period
|
|
738
|
+
figsize=(10, 6),
|
|
739
|
+
show=False # Don't call plt.show(), return axes
|
|
740
|
+
)
|
|
741
|
+
```
|
|
742
|
+
|
|
525
743
|
### Synthetic Difference-in-Differences
|
|
526
744
|
|
|
527
745
|
Synthetic DiD combines the strengths of Difference-in-Differences and Synthetic Control methods by re-weighting control units to better match treated units' pre-treatment outcomes.
|
|
@@ -1061,6 +1279,31 @@ aggregate_to_cohorts(
|
|
|
1061
1279
|
)
|
|
1062
1280
|
```
|
|
1063
1281
|
|
|
1282
|
+
#### rank_control_units
|
|
1283
|
+
|
|
1284
|
+
```python
|
|
1285
|
+
rank_control_units(
|
|
1286
|
+
data, # Panel data in long format
|
|
1287
|
+
unit_column, # Unit identifier column
|
|
1288
|
+
time_column, # Time period column
|
|
1289
|
+
outcome_column, # Outcome variable column
|
|
1290
|
+
treatment_column=None, # Treatment indicator column (0/1)
|
|
1291
|
+
treated_units=None, # Explicit list of treated unit IDs
|
|
1292
|
+
pre_periods=None, # Pre-treatment periods (default: first half)
|
|
1293
|
+
covariates=None, # Covariate columns for matching
|
|
1294
|
+
outcome_weight=0.7, # Weight for outcome trend similarity (0-1)
|
|
1295
|
+
covariate_weight=0.3, # Weight for covariate distance (0-1)
|
|
1296
|
+
exclude_units=None, # Units to exclude from control pool
|
|
1297
|
+
require_units=None, # Units that must appear in output
|
|
1298
|
+
n_top=None, # Return only top N controls
|
|
1299
|
+
suggest_treatment_candidates=False, # Identify treatment candidates
|
|
1300
|
+
n_treatment_candidates=5, # Number of treatment candidates
|
|
1301
|
+
lambda_reg=0.0 # Regularization for synthetic weights
|
|
1302
|
+
)
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`, `covariate_score`, `synthetic_weight`, `pre_trend_rmse`, `is_required`.
|
|
1306
|
+
|
|
1064
1307
|
## Requirements
|
|
1065
1308
|
|
|
1066
1309
|
- Python >= 3.9
|
|
@@ -1126,14 +1369,20 @@ This library implements methods from the following scholarly works:
|
|
|
1126
1369
|
|
|
1127
1370
|
- **Rambachan, A., & Roth, J. (2023).** "A More Credible Approach to Parallel Trends." *The Review of Economic Studies*, 90(5), 2555-2591. [https://doi.org/10.1093/restud/rdad018](https://doi.org/10.1093/restud/rdad018)
|
|
1128
1371
|
|
|
1372
|
+
- **Lakens, D. (2017).** "Equivalence Tests: A Practical Primer for t Tests, Correlations, and Meta-Analyses." *Social Psychological and Personality Science*, 8(4), 355-362. [https://doi.org/10.1177/1948550617697177](https://doi.org/10.1177/1948550617697177)
|
|
1373
|
+
|
|
1129
1374
|
### Multi-Period and Staggered Adoption
|
|
1130
1375
|
|
|
1131
1376
|
- **Callaway, B., & Sant'Anna, P. H. C. (2021).** "Difference-in-Differences with Multiple Time Periods." *Journal of Econometrics*, 225(2), 200-230. [https://doi.org/10.1016/j.jeconom.2020.12.001](https://doi.org/10.1016/j.jeconom.2020.12.001)
|
|
1132
1377
|
|
|
1378
|
+
- **Sant'Anna, P. H. C., & Zhao, J. (2020).** "Doubly Robust Difference-in-Differences Estimators." *Journal of Econometrics*, 219(1), 101-122. [https://doi.org/10.1016/j.jeconom.2020.06.003](https://doi.org/10.1016/j.jeconom.2020.06.003)
|
|
1379
|
+
|
|
1133
1380
|
- **Sun, L., & Abraham, S. (2021).** "Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects." *Journal of Econometrics*, 225(2), 175-199. [https://doi.org/10.1016/j.jeconom.2020.09.006](https://doi.org/10.1016/j.jeconom.2020.09.006)
|
|
1134
1381
|
|
|
1135
1382
|
- **de Chaisemartin, C., & D'Haultfœuille, X. (2020).** "Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects." *American Economic Review*, 110(9), 2964-2996. [https://doi.org/10.1257/aer.20181169](https://doi.org/10.1257/aer.20181169)
|
|
1136
1383
|
|
|
1384
|
+
- **Goodman-Bacon, A. (2021).** "Difference-in-Differences with Variation in Treatment Timing." *Journal of Econometrics*, 225(2), 254-277. [https://doi.org/10.1016/j.jeconom.2021.03.014](https://doi.org/10.1016/j.jeconom.2021.03.014)
|
|
1385
|
+
|
|
1137
1386
|
### General Causal Inference
|
|
1138
1387
|
|
|
1139
1388
|
- **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
|
|
@@ -69,7 +69,10 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
69
69
|
- **Robust inference**: Heteroskedasticity-robust (HC1) and cluster-robust standard errors
|
|
70
70
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
71
71
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
72
|
+
- **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
|
|
72
73
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
74
|
+
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
75
|
+
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
73
76
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
74
77
|
|
|
75
78
|
## Data Preparation
|
|
@@ -307,6 +310,79 @@ cohort_data = aggregate_to_cohorts(
|
|
|
307
310
|
# Result: mean outcome by treatment group and period
|
|
308
311
|
```
|
|
309
312
|
|
|
313
|
+
### Rank Control Units
|
|
314
|
+
|
|
315
|
+
Select the best control units for DiD or Synthetic DiD analysis by ranking them based on pre-treatment outcome similarity:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from diff_diff import rank_control_units, generate_did_data
|
|
319
|
+
|
|
320
|
+
# Generate sample data
|
|
321
|
+
data = generate_did_data(n_units=50, n_periods=6, seed=42)
|
|
322
|
+
|
|
323
|
+
# Rank control units by their similarity to treated units
|
|
324
|
+
ranking = rank_control_units(
|
|
325
|
+
data,
|
|
326
|
+
unit_column='unit',
|
|
327
|
+
time_column='period',
|
|
328
|
+
outcome_column='outcome',
|
|
329
|
+
treatment_column='treated',
|
|
330
|
+
n_top=10 # Return top 10 controls
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
print(ranking[['unit', 'quality_score', 'pre_trend_rmse']])
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Output:
|
|
337
|
+
```
|
|
338
|
+
unit quality_score pre_trend_rmse
|
|
339
|
+
0 35 1.0000 0.4521
|
|
340
|
+
1 42 0.9234 0.5123
|
|
341
|
+
2 28 0.8876 0.5892
|
|
342
|
+
...
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
With covariates for matching:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
# Add covariate-based matching
|
|
349
|
+
ranking = rank_control_units(
|
|
350
|
+
data,
|
|
351
|
+
unit_column='unit',
|
|
352
|
+
time_column='period',
|
|
353
|
+
outcome_column='outcome',
|
|
354
|
+
treatment_column='treated',
|
|
355
|
+
covariates=['size', 'age'], # Match on these too
|
|
356
|
+
outcome_weight=0.7, # 70% weight on outcome trends
|
|
357
|
+
covariate_weight=0.3 # 30% weight on covariate similarity
|
|
358
|
+
)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Filter data for SyntheticDiD using top controls:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
from diff_diff import SyntheticDiD
|
|
365
|
+
|
|
366
|
+
# Get top control units
|
|
367
|
+
top_controls = ranking['unit'].tolist()
|
|
368
|
+
|
|
369
|
+
# Filter data to treated + top controls
|
|
370
|
+
filtered_data = data[
|
|
371
|
+
(data['treated'] == 1) | (data['unit'].isin(top_controls))
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
# Fit SyntheticDiD with selected controls
|
|
375
|
+
sdid = SyntheticDiD()
|
|
376
|
+
results = sdid.fit(
|
|
377
|
+
filtered_data,
|
|
378
|
+
outcome='outcome',
|
|
379
|
+
treatment='treated',
|
|
380
|
+
unit='unit',
|
|
381
|
+
time='period',
|
|
382
|
+
post_periods=[3, 4, 5]
|
|
383
|
+
)
|
|
384
|
+
```
|
|
385
|
+
|
|
310
386
|
## Usage
|
|
311
387
|
|
|
312
388
|
### Basic DiD with Column Names
|
|
@@ -487,6 +563,148 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
487
563
|
================================================================================
|
|
488
564
|
```
|
|
489
565
|
|
|
566
|
+
### Staggered Difference-in-Differences (Callaway-Sant'Anna)
|
|
567
|
+
|
|
568
|
+
When treatment is adopted at different times by different units, traditional TWFE estimators can be biased. The Callaway-Sant'Anna estimator provides unbiased estimates with staggered adoption.
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
from diff_diff import CallawaySantAnna
|
|
572
|
+
|
|
573
|
+
# Panel data with staggered treatment
|
|
574
|
+
# 'first_treat' = period when unit was first treated (0 if never treated)
|
|
575
|
+
cs = CallawaySantAnna()
|
|
576
|
+
results = cs.fit(
|
|
577
|
+
panel_data,
|
|
578
|
+
outcome='sales',
|
|
579
|
+
unit='firm_id',
|
|
580
|
+
time='year',
|
|
581
|
+
first_treat='first_treat', # 0 for never-treated, else first treatment year
|
|
582
|
+
aggregate='event_study' # Compute event study effects
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# View results
|
|
586
|
+
results.print_summary()
|
|
587
|
+
|
|
588
|
+
# Access group-time effects ATT(g,t)
|
|
589
|
+
for (group, time), effect in results.group_time_effects.items():
|
|
590
|
+
print(f"Cohort {group}, Period {time}: {effect['effect']:.3f}")
|
|
591
|
+
|
|
592
|
+
# Event study effects (averaged by relative time)
|
|
593
|
+
for rel_time, effect in results.event_study_effects.items():
|
|
594
|
+
print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
|
|
595
|
+
|
|
596
|
+
# Convert to DataFrame
|
|
597
|
+
df = results.to_dataframe(level='event_study')
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Output:
|
|
601
|
+
```
|
|
602
|
+
=====================================================================================
|
|
603
|
+
Callaway-Sant'Anna Staggered Difference-in-Differences Results
|
|
604
|
+
=====================================================================================
|
|
605
|
+
|
|
606
|
+
Total observations: 600
|
|
607
|
+
Treated units: 35
|
|
608
|
+
Control units: 15
|
|
609
|
+
Treatment cohorts: 3
|
|
610
|
+
Time periods: 8
|
|
611
|
+
Control group: never_treated
|
|
612
|
+
|
|
613
|
+
-------------------------------------------------------------------------------------
|
|
614
|
+
Overall Average Treatment Effect on the Treated
|
|
615
|
+
-------------------------------------------------------------------------------------
|
|
616
|
+
Parameter Estimate Std. Err. t-stat P>|t| Sig.
|
|
617
|
+
-------------------------------------------------------------------------------------
|
|
618
|
+
ATT 2.5000 0.3521 7.101 0.0000 ***
|
|
619
|
+
-------------------------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
95% Confidence Interval: [1.8099, 3.1901]
|
|
622
|
+
|
|
623
|
+
-------------------------------------------------------------------------------------
|
|
624
|
+
Event Study (Dynamic) Effects
|
|
625
|
+
-------------------------------------------------------------------------------------
|
|
626
|
+
Rel. Period Estimate Std. Err. t-stat P>|t| Sig.
|
|
627
|
+
-------------------------------------------------------------------------------------
|
|
628
|
+
0 2.1000 0.4521 4.645 0.0000 ***
|
|
629
|
+
1 2.5000 0.4123 6.064 0.0000 ***
|
|
630
|
+
2 2.8000 0.5234 5.349 0.0000 ***
|
|
631
|
+
-------------------------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
634
|
+
=====================================================================================
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**When to use Callaway-Sant'Anna vs TWFE:**
|
|
638
|
+
|
|
639
|
+
| Scenario | Use TWFE | Use Callaway-Sant'Anna |
|
|
640
|
+
|----------|----------|------------------------|
|
|
641
|
+
| All units treated at same time | ✓ | ✓ |
|
|
642
|
+
| Staggered adoption, homogeneous effects | ✓ | ✓ |
|
|
643
|
+
| Staggered adoption, heterogeneous effects | ✗ | ✓ |
|
|
644
|
+
| Need event study with staggered timing | ✗ | ✓ |
|
|
645
|
+
| Fewer than ~20 treated units | ✓ | Depends on design |
|
|
646
|
+
|
|
647
|
+
**Parameters:**
|
|
648
|
+
|
|
649
|
+
```python
|
|
650
|
+
CallawaySantAnna(
|
|
651
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
652
|
+
anticipation=0, # Periods before treatment with effects
|
|
653
|
+
estimation_method='dr', # 'dr', 'ipw', or 'reg'
|
|
654
|
+
alpha=0.05, # Significance level
|
|
655
|
+
cluster=None, # Column for cluster SEs
|
|
656
|
+
n_bootstrap=0, # Must be 0 (bootstrap not yet implemented)
|
|
657
|
+
seed=None # Random seed
|
|
658
|
+
)
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Current limitations:**
|
|
662
|
+
- Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
|
|
663
|
+
- Covariate adjustment for conditional parallel trends is not yet implemented
|
|
664
|
+
|
|
665
|
+
### Event Study Visualization
|
|
666
|
+
|
|
667
|
+
Create publication-ready event study plots:
|
|
668
|
+
|
|
669
|
+
```python
|
|
670
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
671
|
+
|
|
672
|
+
# From MultiPeriodDiD
|
|
673
|
+
did = MultiPeriodDiD()
|
|
674
|
+
results = did.fit(data, outcome='y', treatment='treated',
|
|
675
|
+
time='period', post_periods=[3, 4, 5])
|
|
676
|
+
plot_event_study(results, title="Treatment Effects Over Time")
|
|
677
|
+
|
|
678
|
+
# From CallawaySantAnna (with event study aggregation)
|
|
679
|
+
cs = CallawaySantAnna()
|
|
680
|
+
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
681
|
+
first_treat='first_treat', aggregate='event_study')
|
|
682
|
+
plot_event_study(results, title="Staggered DiD Event Study")
|
|
683
|
+
|
|
684
|
+
# From a DataFrame
|
|
685
|
+
df = pd.DataFrame({
|
|
686
|
+
'period': [-2, -1, 0, 1, 2],
|
|
687
|
+
'effect': [0.1, 0.05, 0.0, 2.5, 2.8],
|
|
688
|
+
'se': [0.3, 0.25, 0.0, 0.4, 0.45]
|
|
689
|
+
})
|
|
690
|
+
plot_event_study(df, reference_period=0)
|
|
691
|
+
|
|
692
|
+
# With customization
|
|
693
|
+
ax = plot_event_study(
|
|
694
|
+
results,
|
|
695
|
+
title="Dynamic Treatment Effects",
|
|
696
|
+
xlabel="Years Relative to Treatment",
|
|
697
|
+
ylabel="Effect on Sales ($1000s)",
|
|
698
|
+
color="#2563eb",
|
|
699
|
+
marker="o",
|
|
700
|
+
shade_pre=True, # Shade pre-treatment region
|
|
701
|
+
show_zero_line=True, # Horizontal line at y=0
|
|
702
|
+
show_reference_line=True, # Vertical line at reference period
|
|
703
|
+
figsize=(10, 6),
|
|
704
|
+
show=False # Don't call plt.show(), return axes
|
|
705
|
+
)
|
|
706
|
+
```
|
|
707
|
+
|
|
490
708
|
### Synthetic Difference-in-Differences
|
|
491
709
|
|
|
492
710
|
Synthetic DiD combines the strengths of Difference-in-Differences and Synthetic Control methods by re-weighting control units to better match treated units' pre-treatment outcomes.
|
|
@@ -1026,6 +1244,31 @@ aggregate_to_cohorts(
|
|
|
1026
1244
|
)
|
|
1027
1245
|
```
|
|
1028
1246
|
|
|
1247
|
+
#### rank_control_units
|
|
1248
|
+
|
|
1249
|
+
```python
|
|
1250
|
+
rank_control_units(
|
|
1251
|
+
data, # Panel data in long format
|
|
1252
|
+
unit_column, # Unit identifier column
|
|
1253
|
+
time_column, # Time period column
|
|
1254
|
+
outcome_column, # Outcome variable column
|
|
1255
|
+
treatment_column=None, # Treatment indicator column (0/1)
|
|
1256
|
+
treated_units=None, # Explicit list of treated unit IDs
|
|
1257
|
+
pre_periods=None, # Pre-treatment periods (default: first half)
|
|
1258
|
+
covariates=None, # Covariate columns for matching
|
|
1259
|
+
outcome_weight=0.7, # Weight for outcome trend similarity (0-1)
|
|
1260
|
+
covariate_weight=0.3, # Weight for covariate distance (0-1)
|
|
1261
|
+
exclude_units=None, # Units to exclude from control pool
|
|
1262
|
+
require_units=None, # Units that must appear in output
|
|
1263
|
+
n_top=None, # Return only top N controls
|
|
1264
|
+
suggest_treatment_candidates=False, # Identify treatment candidates
|
|
1265
|
+
n_treatment_candidates=5, # Number of treatment candidates
|
|
1266
|
+
lambda_reg=0.0 # Regularization for synthetic weights
|
|
1267
|
+
)
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`, `covariate_score`, `synthetic_weight`, `pre_trend_rmse`, `is_required`.
|
|
1271
|
+
|
|
1029
1272
|
## Requirements
|
|
1030
1273
|
|
|
1031
1274
|
- Python >= 3.9
|
|
@@ -1091,14 +1334,20 @@ This library implements methods from the following scholarly works:
|
|
|
1091
1334
|
|
|
1092
1335
|
- **Rambachan, A., & Roth, J. (2023).** "A More Credible Approach to Parallel Trends." *The Review of Economic Studies*, 90(5), 2555-2591. [https://doi.org/10.1093/restud/rdad018](https://doi.org/10.1093/restud/rdad018)
|
|
1093
1336
|
|
|
1337
|
+
- **Lakens, D. (2017).** "Equivalence Tests: A Practical Primer for t Tests, Correlations, and Meta-Analyses." *Social Psychological and Personality Science*, 8(4), 355-362. [https://doi.org/10.1177/1948550617697177](https://doi.org/10.1177/1948550617697177)
|
|
1338
|
+
|
|
1094
1339
|
### Multi-Period and Staggered Adoption
|
|
1095
1340
|
|
|
1096
1341
|
- **Callaway, B., & Sant'Anna, P. H. C. (2021).** "Difference-in-Differences with Multiple Time Periods." *Journal of Econometrics*, 225(2), 200-230. [https://doi.org/10.1016/j.jeconom.2020.12.001](https://doi.org/10.1016/j.jeconom.2020.12.001)
|
|
1097
1342
|
|
|
1343
|
+
- **Sant'Anna, P. H. C., & Zhao, J. (2020).** "Doubly Robust Difference-in-Differences Estimators." *Journal of Econometrics*, 219(1), 101-122. [https://doi.org/10.1016/j.jeconom.2020.06.003](https://doi.org/10.1016/j.jeconom.2020.06.003)
|
|
1344
|
+
|
|
1098
1345
|
- **Sun, L., & Abraham, S. (2021).** "Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects." *Journal of Econometrics*, 225(2), 175-199. [https://doi.org/10.1016/j.jeconom.2020.09.006](https://doi.org/10.1016/j.jeconom.2020.09.006)
|
|
1099
1346
|
|
|
1100
1347
|
- **de Chaisemartin, C., & D'Haultfœuille, X. (2020).** "Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects." *American Economic Review*, 110(9), 2964-2996. [https://doi.org/10.1257/aer.20181169](https://doi.org/10.1257/aer.20181169)
|
|
1101
1348
|
|
|
1349
|
+
- **Goodman-Bacon, A. (2021).** "Difference-in-Differences with Variation in Treatment Timing." *Journal of Econometrics*, 225(2), 254-277. [https://doi.org/10.1016/j.jeconom.2021.03.014](https://doi.org/10.1016/j.jeconom.2021.03.014)
|
|
1350
|
+
|
|
1102
1351
|
### General Causal Inference
|
|
1103
1352
|
|
|
1104
1353
|
- **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
diff-diff: A library for Difference-in-Differences analysis.
|
|
3
|
+
|
|
4
|
+
This library provides sklearn-like estimators for causal inference
|
|
5
|
+
using the difference-in-differences methodology.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from diff_diff.estimators import (
|
|
9
|
+
DifferenceInDifferences,
|
|
10
|
+
TwoWayFixedEffects,
|
|
11
|
+
MultiPeriodDiD,
|
|
12
|
+
SyntheticDiD,
|
|
13
|
+
)
|
|
14
|
+
from diff_diff.staggered import (
|
|
15
|
+
CallawaySantAnna,
|
|
16
|
+
CallawaySantAnnaResults,
|
|
17
|
+
GroupTimeEffect,
|
|
18
|
+
)
|
|
19
|
+
from diff_diff.results import (
|
|
20
|
+
DiDResults,
|
|
21
|
+
MultiPeriodDiDResults,
|
|
22
|
+
PeriodEffect,
|
|
23
|
+
SyntheticDiDResults,
|
|
24
|
+
)
|
|
25
|
+
from diff_diff.visualization import (
|
|
26
|
+
plot_event_study,
|
|
27
|
+
plot_group_effects,
|
|
28
|
+
)
|
|
29
|
+
from diff_diff.prep import (
|
|
30
|
+
make_treatment_indicator,
|
|
31
|
+
make_post_indicator,
|
|
32
|
+
wide_to_long,
|
|
33
|
+
balance_panel,
|
|
34
|
+
validate_did_data,
|
|
35
|
+
summarize_did_data,
|
|
36
|
+
generate_did_data,
|
|
37
|
+
create_event_time,
|
|
38
|
+
aggregate_to_cohorts,
|
|
39
|
+
rank_control_units,
|
|
40
|
+
)
|
|
41
|
+
from diff_diff.utils import (
|
|
42
|
+
check_parallel_trends,
|
|
43
|
+
check_parallel_trends_robust,
|
|
44
|
+
equivalence_test_trends,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__version__ = "0.4.0"
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Estimators
|
|
50
|
+
"DifferenceInDifferences",
|
|
51
|
+
"TwoWayFixedEffects",
|
|
52
|
+
"MultiPeriodDiD",
|
|
53
|
+
"SyntheticDiD",
|
|
54
|
+
"CallawaySantAnna",
|
|
55
|
+
# Results
|
|
56
|
+
"DiDResults",
|
|
57
|
+
"MultiPeriodDiDResults",
|
|
58
|
+
"SyntheticDiDResults",
|
|
59
|
+
"PeriodEffect",
|
|
60
|
+
"CallawaySantAnnaResults",
|
|
61
|
+
"GroupTimeEffect",
|
|
62
|
+
# Visualization
|
|
63
|
+
"plot_event_study",
|
|
64
|
+
"plot_group_effects",
|
|
65
|
+
# Parallel trends testing
|
|
66
|
+
"check_parallel_trends",
|
|
67
|
+
"check_parallel_trends_robust",
|
|
68
|
+
"equivalence_test_trends",
|
|
69
|
+
# Data preparation utilities
|
|
70
|
+
"make_treatment_indicator",
|
|
71
|
+
"make_post_indicator",
|
|
72
|
+
"wide_to_long",
|
|
73
|
+
"balance_panel",
|
|
74
|
+
"validate_did_data",
|
|
75
|
+
"summarize_did_data",
|
|
76
|
+
"generate_did_data",
|
|
77
|
+
"create_event_time",
|
|
78
|
+
"aggregate_to_cohorts",
|
|
79
|
+
"rank_control_units",
|
|
80
|
+
]
|