diff-diff 0.6.0__tar.gz → 1.1.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.6.0 → diff_diff-1.1.0}/PKG-INFO +223 -9
- {diff_diff-0.6.0 → diff_diff-1.1.0}/README.md +221 -7
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/__init__.py +43 -1
- diff_diff-1.1.0/diff_diff/bacon.py +1027 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/diagnostics.py +3 -15
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/estimators.py +23 -722
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/honest_did.py +4 -2
- diff_diff-1.1.0/diff_diff/power.py +1350 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/staggered.py +595 -13
- diff_diff-1.1.0/diff_diff/sun_abraham.py +1198 -0
- diff_diff-1.1.0/diff_diff/synthetic_did.py +540 -0
- diff_diff-1.1.0/diff_diff/twfe.py +357 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/visualization.py +572 -1
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/PKG-INFO +223 -9
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/SOURCES.txt +8 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/pyproject.toml +2 -2
- diff_diff-1.1.0/tests/test_bacon.py +679 -0
- diff_diff-1.1.0/tests/test_power.py +691 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_staggered.py +375 -0
- diff_diff-1.1.0/tests/test_sun_abraham.py +732 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/prep.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/results.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/utils.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/setup.cfg +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_diagnostics.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_estimators.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_honest_did.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_prep.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_utils.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_visualization.py +0 -0
- {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_wild_bootstrap.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
5
|
Author: diff-diff contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://diff-diff.readthedocs.io
|
|
|
9
9
|
Project-URL: Repository, https://github.com/igerber/diff-diff
|
|
10
10
|
Project-URL: Issues, https://github.com/igerber/diff-diff/issues
|
|
11
11
|
Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -105,12 +105,14 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
105
105
|
- **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
|
|
106
106
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
107
107
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
108
|
-
- **Staggered adoption**: Callaway-Sant'Anna (2021)
|
|
108
|
+
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
|
|
109
109
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
110
110
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
111
111
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
112
|
+
- **Goodman-Bacon decomposition**: Diagnose TWFE bias by decomposing into 2x2 comparisons
|
|
112
113
|
- **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
|
|
113
114
|
- **Honest DiD sensitivity analysis**: Rambachan-Roth (2023) bounds and breakdown analysis for parallel trends violations
|
|
115
|
+
- **Power analysis**: MDE, sample size, and power calculations for study design; simulation-based power for any estimator
|
|
114
116
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
115
117
|
|
|
116
118
|
## Tutorials
|
|
@@ -120,10 +122,11 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
120
122
|
| Notebook | Description |
|
|
121
123
|
|----------|-------------|
|
|
122
124
|
| `01_basic_did.ipynb` | Basic 2x2 DiD, formula interface, covariates, fixed effects, cluster-robust SE, wild bootstrap |
|
|
123
|
-
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna, group-time effects, aggregation methods |
|
|
125
|
+
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna and Sun-Abraham, group-time effects, aggregation methods, Bacon decomposition |
|
|
124
126
|
| `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
|
|
125
127
|
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
126
128
|
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
129
|
+
| `06_power_analysis.ipynb` | Power analysis, MDE, sample size calculations, simulation-based power |
|
|
127
130
|
|
|
128
131
|
## Data Preparation
|
|
129
132
|
|
|
@@ -571,7 +574,7 @@ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
|
|
|
571
574
|
### Two-Way Fixed Effects (Panel Data)
|
|
572
575
|
|
|
573
576
|
```python
|
|
574
|
-
from diff_diff
|
|
577
|
+
from diff_diff import TwoWayFixedEffects
|
|
575
578
|
|
|
576
579
|
twfe = TwoWayFixedEffects()
|
|
577
580
|
results = twfe.fit(
|
|
@@ -734,10 +737,48 @@ CallawaySantAnna(
|
|
|
734
737
|
alpha=0.05, # Significance level
|
|
735
738
|
cluster=None, # Column for cluster SEs
|
|
736
739
|
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
740
|
+
bootstrap_weight_type='rademacher', # 'rademacher', 'mammen', or 'webb'
|
|
737
741
|
seed=None # Random seed
|
|
738
742
|
)
|
|
739
743
|
```
|
|
740
744
|
|
|
745
|
+
**Multiplier bootstrap for inference:**
|
|
746
|
+
|
|
747
|
+
With few clusters or when analytical standard errors may be unreliable, use the multiplier bootstrap for valid inference. This implements the approach from Callaway & Sant'Anna (2021).
|
|
748
|
+
|
|
749
|
+
```python
|
|
750
|
+
# Bootstrap inference with 999 iterations
|
|
751
|
+
cs = CallawaySantAnna(
|
|
752
|
+
n_bootstrap=999,
|
|
753
|
+
bootstrap_weight_type='rademacher', # or 'mammen', 'webb'
|
|
754
|
+
seed=42
|
|
755
|
+
)
|
|
756
|
+
results = cs.fit(
|
|
757
|
+
data,
|
|
758
|
+
outcome='sales',
|
|
759
|
+
unit='firm_id',
|
|
760
|
+
time='year',
|
|
761
|
+
first_treat='first_treat',
|
|
762
|
+
aggregate='event_study'
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
# Access bootstrap results
|
|
766
|
+
print(f"Overall ATT: {results.overall_att:.3f}")
|
|
767
|
+
print(f"Bootstrap SE: {results.bootstrap_results.overall_att_se:.3f}")
|
|
768
|
+
print(f"Bootstrap 95% CI: {results.bootstrap_results.overall_att_ci}")
|
|
769
|
+
print(f"Bootstrap p-value: {results.bootstrap_results.overall_att_p_value:.4f}")
|
|
770
|
+
|
|
771
|
+
# Event study bootstrap inference
|
|
772
|
+
for rel_time, se in results.bootstrap_results.event_study_ses.items():
|
|
773
|
+
ci = results.bootstrap_results.event_study_cis[rel_time]
|
|
774
|
+
print(f"e={rel_time}: SE={se:.3f}, 95% CI=[{ci[0]:.3f}, {ci[1]:.3f}]")
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**Bootstrap weight types:**
|
|
778
|
+
- `'rademacher'` - Default, ±1 with p=0.5, good for most cases
|
|
779
|
+
- `'mammen'` - Two-point distribution matching first 3 moments
|
|
780
|
+
- `'webb'` - Six-point distribution, recommended for very few clusters (<10)
|
|
781
|
+
|
|
741
782
|
**Covariate adjustment for conditional parallel trends:**
|
|
742
783
|
|
|
743
784
|
When parallel trends only holds conditional on covariates, use the `covariates` parameter:
|
|
@@ -756,15 +797,115 @@ results = cs.fit(
|
|
|
756
797
|
)
|
|
757
798
|
```
|
|
758
799
|
|
|
759
|
-
|
|
760
|
-
|
|
800
|
+
### Sun-Abraham Interaction-Weighted Estimator
|
|
801
|
+
|
|
802
|
+
The Sun-Abraham (2021) estimator provides an alternative to Callaway-Sant'Anna using an interaction-weighted (IW) regression approach. Running both estimators serves as a useful robustness check—when they agree, results are more credible.
|
|
803
|
+
|
|
804
|
+
```python
|
|
805
|
+
from diff_diff import SunAbraham
|
|
806
|
+
|
|
807
|
+
# Basic usage
|
|
808
|
+
sa = SunAbraham()
|
|
809
|
+
results = sa.fit(
|
|
810
|
+
panel_data,
|
|
811
|
+
outcome='sales',
|
|
812
|
+
unit='firm_id',
|
|
813
|
+
time='year',
|
|
814
|
+
first_treat='first_treat' # 0 for never-treated, else first treatment year
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# View results
|
|
818
|
+
results.print_summary()
|
|
819
|
+
|
|
820
|
+
# Event study effects (by relative time to treatment)
|
|
821
|
+
for rel_time, effect in results.event_study_effects.items():
|
|
822
|
+
print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
|
|
823
|
+
|
|
824
|
+
# Overall ATT
|
|
825
|
+
print(f"Overall ATT: {results.overall_att:.3f} (SE: {results.overall_se:.3f})")
|
|
826
|
+
|
|
827
|
+
# Cohort weights (how each cohort contributes to each event-time estimate)
|
|
828
|
+
for rel_time, weights in results.cohort_weights.items():
|
|
829
|
+
print(f"e={rel_time}: {weights}")
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**Parameters:**
|
|
833
|
+
|
|
834
|
+
```python
|
|
835
|
+
SunAbraham(
|
|
836
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
837
|
+
anticipation=0, # Periods before treatment with effects
|
|
838
|
+
alpha=0.05, # Significance level
|
|
839
|
+
cluster=None, # Column for cluster SEs
|
|
840
|
+
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
841
|
+
bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
|
|
842
|
+
seed=None # Random seed
|
|
843
|
+
)
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**Bootstrap inference:**
|
|
847
|
+
|
|
848
|
+
```python
|
|
849
|
+
# Bootstrap inference with 999 iterations
|
|
850
|
+
sa = SunAbraham(
|
|
851
|
+
n_bootstrap=999,
|
|
852
|
+
bootstrap_weights='rademacher',
|
|
853
|
+
seed=42
|
|
854
|
+
)
|
|
855
|
+
results = sa.fit(
|
|
856
|
+
data,
|
|
857
|
+
outcome='sales',
|
|
858
|
+
unit='firm_id',
|
|
859
|
+
time='year',
|
|
860
|
+
first_treat='first_treat'
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Access bootstrap results
|
|
864
|
+
print(f"Overall ATT: {results.overall_att:.3f}")
|
|
865
|
+
print(f"Bootstrap SE: {results.bootstrap_results.overall_att_se:.3f}")
|
|
866
|
+
print(f"Bootstrap 95% CI: {results.bootstrap_results.overall_att_ci}")
|
|
867
|
+
print(f"Bootstrap p-value: {results.bootstrap_results.overall_att_p_value:.4f}")
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
**When to use Sun-Abraham vs Callaway-Sant'Anna:**
|
|
871
|
+
|
|
872
|
+
| Aspect | Sun-Abraham | Callaway-Sant'Anna |
|
|
873
|
+
|--------|-------------|-------------------|
|
|
874
|
+
| Approach | Interaction-weighted regression | 2x2 DiD aggregation |
|
|
875
|
+
| Efficiency | More efficient under homogeneous effects | More robust to heterogeneity |
|
|
876
|
+
| Weighting | Weights by cohort share at each relative time | Weights by sample size |
|
|
877
|
+
| Use case | Robustness check, regression-based inference | Primary staggered DiD estimator |
|
|
878
|
+
|
|
879
|
+
**Both estimators should give similar results when:**
|
|
880
|
+
- Treatment effects are relatively homogeneous across cohorts
|
|
881
|
+
- Parallel trends holds
|
|
882
|
+
|
|
883
|
+
**Running both as robustness check:**
|
|
884
|
+
|
|
885
|
+
```python
|
|
886
|
+
from diff_diff import CallawaySantAnna, SunAbraham
|
|
887
|
+
|
|
888
|
+
# Callaway-Sant'Anna
|
|
889
|
+
cs = CallawaySantAnna()
|
|
890
|
+
cs_results = cs.fit(data, outcome='y', unit='unit', time='time', first_treat='first_treat')
|
|
891
|
+
|
|
892
|
+
# Sun-Abraham
|
|
893
|
+
sa = SunAbraham()
|
|
894
|
+
sa_results = sa.fit(data, outcome='y', unit='unit', time='time', first_treat='first_treat')
|
|
895
|
+
|
|
896
|
+
# Compare
|
|
897
|
+
print(f"Callaway-Sant'Anna ATT: {cs_results.overall_att:.3f}")
|
|
898
|
+
print(f"Sun-Abraham ATT: {sa_results.overall_att:.3f}")
|
|
899
|
+
|
|
900
|
+
# If results differ substantially, investigate heterogeneity
|
|
901
|
+
```
|
|
761
902
|
|
|
762
903
|
### Event Study Visualization
|
|
763
904
|
|
|
764
905
|
Create publication-ready event study plots:
|
|
765
906
|
|
|
766
907
|
```python
|
|
767
|
-
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
908
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
|
|
768
909
|
|
|
769
910
|
# From MultiPeriodDiD
|
|
770
911
|
did = MultiPeriodDiD()
|
|
@@ -776,7 +917,13 @@ plot_event_study(results, title="Treatment Effects Over Time")
|
|
|
776
917
|
cs = CallawaySantAnna()
|
|
777
918
|
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
778
919
|
first_treat='first_treat', aggregate='event_study')
|
|
779
|
-
plot_event_study(results, title="Staggered DiD Event Study")
|
|
920
|
+
plot_event_study(results, title="Staggered DiD Event Study (CS)")
|
|
921
|
+
|
|
922
|
+
# From SunAbraham
|
|
923
|
+
sa = SunAbraham()
|
|
924
|
+
results = sa.fit(data, outcome='y', unit='unit', time='period',
|
|
925
|
+
first_treat='first_treat')
|
|
926
|
+
plot_event_study(results, title="Staggered DiD Event Study (SA)")
|
|
780
927
|
|
|
781
928
|
# From a DataFrame
|
|
782
929
|
df = pd.DataFrame({
|
|
@@ -1407,6 +1554,63 @@ SyntheticDiD(
|
|
|
1407
1554
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1408
1555
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1409
1556
|
|
|
1557
|
+
### SunAbraham
|
|
1558
|
+
|
|
1559
|
+
```python
|
|
1560
|
+
SunAbraham(
|
|
1561
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
1562
|
+
anticipation=0, # Periods of anticipation effects
|
|
1563
|
+
alpha=0.05, # Significance level for CIs
|
|
1564
|
+
cluster=None, # Column for cluster-robust SEs
|
|
1565
|
+
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
1566
|
+
bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
|
|
1567
|
+
seed=None # Random seed
|
|
1568
|
+
)
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
**fit() Parameters:**
|
|
1572
|
+
|
|
1573
|
+
| Parameter | Type | Description |
|
|
1574
|
+
|-----------|------|-------------|
|
|
1575
|
+
| `data` | DataFrame | Panel data |
|
|
1576
|
+
| `outcome` | str | Outcome variable column name |
|
|
1577
|
+
| `unit` | str | Unit identifier column |
|
|
1578
|
+
| `time` | str | Time period column |
|
|
1579
|
+
| `first_treat` | str | Column with first treatment period (0 for never-treated) |
|
|
1580
|
+
| `covariates` | list | Covariate column names |
|
|
1581
|
+
| `min_pre_periods` | int | Minimum pre-treatment periods to include |
|
|
1582
|
+
| `min_post_periods` | int | Minimum post-treatment periods to include |
|
|
1583
|
+
|
|
1584
|
+
### SunAbrahamResults
|
|
1585
|
+
|
|
1586
|
+
**Attributes:**
|
|
1587
|
+
|
|
1588
|
+
| Attribute | Description |
|
|
1589
|
+
|-----------|-------------|
|
|
1590
|
+
| `event_study_effects` | Dict mapping relative time to effect info |
|
|
1591
|
+
| `overall_att` | Overall average treatment effect |
|
|
1592
|
+
| `overall_se` | Standard error of overall ATT |
|
|
1593
|
+
| `overall_t_stat` | T-statistic for overall ATT |
|
|
1594
|
+
| `overall_p_value` | P-value for overall ATT |
|
|
1595
|
+
| `overall_conf_int` | Confidence interval for overall ATT |
|
|
1596
|
+
| `cohort_weights` | Dict mapping relative time to cohort weights |
|
|
1597
|
+
| `groups` | List of treatment cohorts |
|
|
1598
|
+
| `time_periods` | List of all time periods |
|
|
1599
|
+
| `n_obs` | Total number of observations |
|
|
1600
|
+
| `n_treated_units` | Number of ever-treated units |
|
|
1601
|
+
| `n_control_units` | Number of never-treated units |
|
|
1602
|
+
| `is_significant` | Boolean for significance at alpha |
|
|
1603
|
+
| `significance_stars` | String of significance stars |
|
|
1604
|
+
| `bootstrap_results` | SABootstrapResults (if bootstrap enabled) |
|
|
1605
|
+
|
|
1606
|
+
**Methods:**
|
|
1607
|
+
|
|
1608
|
+
| Method | Description |
|
|
1609
|
+
|--------|-------------|
|
|
1610
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
1611
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
1612
|
+
| `to_dataframe(level)` | Convert to DataFrame ('event_study' or 'cohort') |
|
|
1613
|
+
|
|
1410
1614
|
### HonestDiD
|
|
1411
1615
|
|
|
1412
1616
|
```python
|
|
@@ -1729,6 +1933,16 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the
|
|
|
1729
1933
|
|
|
1730
1934
|
- **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)
|
|
1731
1935
|
|
|
1936
|
+
### Power Analysis
|
|
1937
|
+
|
|
1938
|
+
- **Bloom, H. S. (1995).** "Minimum Detectable Effects: A Simple Way to Report the Statistical Power of Experimental Designs." *Evaluation Review*, 19(5), 547-556. [https://doi.org/10.1177/0193841X9501900504](https://doi.org/10.1177/0193841X9501900504)
|
|
1939
|
+
|
|
1940
|
+
- **Burlig, F., Preonas, L., & Woerman, M. (2020).** "Panel Data and Experimental Design." *Journal of Development Economics*, 144, 102458. [https://doi.org/10.1016/j.jdeveco.2020.102458](https://doi.org/10.1016/j.jdeveco.2020.102458)
|
|
1941
|
+
|
|
1942
|
+
Essential reference for power analysis in panel DiD designs. Discusses how serial correlation (ICC) affects power and provides formulas for panel data settings.
|
|
1943
|
+
|
|
1944
|
+
- **Djimeu, E. W., & Houndolo, D.-G. (2016).** "Power Calculation for Causal Inference in Social Science: Sample Size and Minimum Detectable Effect Determination." *Journal of Development Effectiveness*, 8(4), 508-527. [https://doi.org/10.1080/19439342.2016.1244555](https://doi.org/10.1080/19439342.2016.1244555)
|
|
1945
|
+
|
|
1732
1946
|
### General Causal Inference
|
|
1733
1947
|
|
|
1734
1948
|
- **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
|
|
@@ -70,12 +70,14 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
70
70
|
- **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
|
|
71
71
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
72
72
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
73
|
-
- **Staggered adoption**: Callaway-Sant'Anna (2021)
|
|
73
|
+
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
|
|
74
74
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
75
75
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
76
76
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
77
|
+
- **Goodman-Bacon decomposition**: Diagnose TWFE bias by decomposing into 2x2 comparisons
|
|
77
78
|
- **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
|
|
78
79
|
- **Honest DiD sensitivity analysis**: Rambachan-Roth (2023) bounds and breakdown analysis for parallel trends violations
|
|
80
|
+
- **Power analysis**: MDE, sample size, and power calculations for study design; simulation-based power for any estimator
|
|
79
81
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
80
82
|
|
|
81
83
|
## Tutorials
|
|
@@ -85,10 +87,11 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
85
87
|
| Notebook | Description |
|
|
86
88
|
|----------|-------------|
|
|
87
89
|
| `01_basic_did.ipynb` | Basic 2x2 DiD, formula interface, covariates, fixed effects, cluster-robust SE, wild bootstrap |
|
|
88
|
-
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna, group-time effects, aggregation methods |
|
|
90
|
+
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna and Sun-Abraham, group-time effects, aggregation methods, Bacon decomposition |
|
|
89
91
|
| `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
|
|
90
92
|
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
91
93
|
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
94
|
+
| `06_power_analysis.ipynb` | Power analysis, MDE, sample size calculations, simulation-based power |
|
|
92
95
|
|
|
93
96
|
## Data Preparation
|
|
94
97
|
|
|
@@ -536,7 +539,7 @@ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
|
|
|
536
539
|
### Two-Way Fixed Effects (Panel Data)
|
|
537
540
|
|
|
538
541
|
```python
|
|
539
|
-
from diff_diff
|
|
542
|
+
from diff_diff import TwoWayFixedEffects
|
|
540
543
|
|
|
541
544
|
twfe = TwoWayFixedEffects()
|
|
542
545
|
results = twfe.fit(
|
|
@@ -699,10 +702,48 @@ CallawaySantAnna(
|
|
|
699
702
|
alpha=0.05, # Significance level
|
|
700
703
|
cluster=None, # Column for cluster SEs
|
|
701
704
|
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
705
|
+
bootstrap_weight_type='rademacher', # 'rademacher', 'mammen', or 'webb'
|
|
702
706
|
seed=None # Random seed
|
|
703
707
|
)
|
|
704
708
|
```
|
|
705
709
|
|
|
710
|
+
**Multiplier bootstrap for inference:**
|
|
711
|
+
|
|
712
|
+
With few clusters or when analytical standard errors may be unreliable, use the multiplier bootstrap for valid inference. This implements the approach from Callaway & Sant'Anna (2021).
|
|
713
|
+
|
|
714
|
+
```python
|
|
715
|
+
# Bootstrap inference with 999 iterations
|
|
716
|
+
cs = CallawaySantAnna(
|
|
717
|
+
n_bootstrap=999,
|
|
718
|
+
bootstrap_weight_type='rademacher', # or 'mammen', 'webb'
|
|
719
|
+
seed=42
|
|
720
|
+
)
|
|
721
|
+
results = cs.fit(
|
|
722
|
+
data,
|
|
723
|
+
outcome='sales',
|
|
724
|
+
unit='firm_id',
|
|
725
|
+
time='year',
|
|
726
|
+
first_treat='first_treat',
|
|
727
|
+
aggregate='event_study'
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Access bootstrap results
|
|
731
|
+
print(f"Overall ATT: {results.overall_att:.3f}")
|
|
732
|
+
print(f"Bootstrap SE: {results.bootstrap_results.overall_att_se:.3f}")
|
|
733
|
+
print(f"Bootstrap 95% CI: {results.bootstrap_results.overall_att_ci}")
|
|
734
|
+
print(f"Bootstrap p-value: {results.bootstrap_results.overall_att_p_value:.4f}")
|
|
735
|
+
|
|
736
|
+
# Event study bootstrap inference
|
|
737
|
+
for rel_time, se in results.bootstrap_results.event_study_ses.items():
|
|
738
|
+
ci = results.bootstrap_results.event_study_cis[rel_time]
|
|
739
|
+
print(f"e={rel_time}: SE={se:.3f}, 95% CI=[{ci[0]:.3f}, {ci[1]:.3f}]")
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Bootstrap weight types:**
|
|
743
|
+
- `'rademacher'` - Default, ±1 with p=0.5, good for most cases
|
|
744
|
+
- `'mammen'` - Two-point distribution matching first 3 moments
|
|
745
|
+
- `'webb'` - Six-point distribution, recommended for very few clusters (<10)
|
|
746
|
+
|
|
706
747
|
**Covariate adjustment for conditional parallel trends:**
|
|
707
748
|
|
|
708
749
|
When parallel trends only holds conditional on covariates, use the `covariates` parameter:
|
|
@@ -721,15 +762,115 @@ results = cs.fit(
|
|
|
721
762
|
)
|
|
722
763
|
```
|
|
723
764
|
|
|
724
|
-
|
|
725
|
-
|
|
765
|
+
### Sun-Abraham Interaction-Weighted Estimator
|
|
766
|
+
|
|
767
|
+
The Sun-Abraham (2021) estimator provides an alternative to Callaway-Sant'Anna using an interaction-weighted (IW) regression approach. Running both estimators serves as a useful robustness check—when they agree, results are more credible.
|
|
768
|
+
|
|
769
|
+
```python
|
|
770
|
+
from diff_diff import SunAbraham
|
|
771
|
+
|
|
772
|
+
# Basic usage
|
|
773
|
+
sa = SunAbraham()
|
|
774
|
+
results = sa.fit(
|
|
775
|
+
panel_data,
|
|
776
|
+
outcome='sales',
|
|
777
|
+
unit='firm_id',
|
|
778
|
+
time='year',
|
|
779
|
+
first_treat='first_treat' # 0 for never-treated, else first treatment year
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# View results
|
|
783
|
+
results.print_summary()
|
|
784
|
+
|
|
785
|
+
# Event study effects (by relative time to treatment)
|
|
786
|
+
for rel_time, effect in results.event_study_effects.items():
|
|
787
|
+
print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
|
|
788
|
+
|
|
789
|
+
# Overall ATT
|
|
790
|
+
print(f"Overall ATT: {results.overall_att:.3f} (SE: {results.overall_se:.3f})")
|
|
791
|
+
|
|
792
|
+
# Cohort weights (how each cohort contributes to each event-time estimate)
|
|
793
|
+
for rel_time, weights in results.cohort_weights.items():
|
|
794
|
+
print(f"e={rel_time}: {weights}")
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
**Parameters:**
|
|
798
|
+
|
|
799
|
+
```python
|
|
800
|
+
SunAbraham(
|
|
801
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
802
|
+
anticipation=0, # Periods before treatment with effects
|
|
803
|
+
alpha=0.05, # Significance level
|
|
804
|
+
cluster=None, # Column for cluster SEs
|
|
805
|
+
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
806
|
+
bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
|
|
807
|
+
seed=None # Random seed
|
|
808
|
+
)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Bootstrap inference:**
|
|
812
|
+
|
|
813
|
+
```python
|
|
814
|
+
# Bootstrap inference with 999 iterations
|
|
815
|
+
sa = SunAbraham(
|
|
816
|
+
n_bootstrap=999,
|
|
817
|
+
bootstrap_weights='rademacher',
|
|
818
|
+
seed=42
|
|
819
|
+
)
|
|
820
|
+
results = sa.fit(
|
|
821
|
+
data,
|
|
822
|
+
outcome='sales',
|
|
823
|
+
unit='firm_id',
|
|
824
|
+
time='year',
|
|
825
|
+
first_treat='first_treat'
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Access bootstrap results
|
|
829
|
+
print(f"Overall ATT: {results.overall_att:.3f}")
|
|
830
|
+
print(f"Bootstrap SE: {results.bootstrap_results.overall_att_se:.3f}")
|
|
831
|
+
print(f"Bootstrap 95% CI: {results.bootstrap_results.overall_att_ci}")
|
|
832
|
+
print(f"Bootstrap p-value: {results.bootstrap_results.overall_att_p_value:.4f}")
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
**When to use Sun-Abraham vs Callaway-Sant'Anna:**
|
|
836
|
+
|
|
837
|
+
| Aspect | Sun-Abraham | Callaway-Sant'Anna |
|
|
838
|
+
|--------|-------------|-------------------|
|
|
839
|
+
| Approach | Interaction-weighted regression | 2x2 DiD aggregation |
|
|
840
|
+
| Efficiency | More efficient under homogeneous effects | More robust to heterogeneity |
|
|
841
|
+
| Weighting | Weights by cohort share at each relative time | Weights by sample size |
|
|
842
|
+
| Use case | Robustness check, regression-based inference | Primary staggered DiD estimator |
|
|
843
|
+
|
|
844
|
+
**Both estimators should give similar results when:**
|
|
845
|
+
- Treatment effects are relatively homogeneous across cohorts
|
|
846
|
+
- Parallel trends holds
|
|
847
|
+
|
|
848
|
+
**Running both as robustness check:**
|
|
849
|
+
|
|
850
|
+
```python
|
|
851
|
+
from diff_diff import CallawaySantAnna, SunAbraham
|
|
852
|
+
|
|
853
|
+
# Callaway-Sant'Anna
|
|
854
|
+
cs = CallawaySantAnna()
|
|
855
|
+
cs_results = cs.fit(data, outcome='y', unit='unit', time='time', first_treat='first_treat')
|
|
856
|
+
|
|
857
|
+
# Sun-Abraham
|
|
858
|
+
sa = SunAbraham()
|
|
859
|
+
sa_results = sa.fit(data, outcome='y', unit='unit', time='time', first_treat='first_treat')
|
|
860
|
+
|
|
861
|
+
# Compare
|
|
862
|
+
print(f"Callaway-Sant'Anna ATT: {cs_results.overall_att:.3f}")
|
|
863
|
+
print(f"Sun-Abraham ATT: {sa_results.overall_att:.3f}")
|
|
864
|
+
|
|
865
|
+
# If results differ substantially, investigate heterogeneity
|
|
866
|
+
```
|
|
726
867
|
|
|
727
868
|
### Event Study Visualization
|
|
728
869
|
|
|
729
870
|
Create publication-ready event study plots:
|
|
730
871
|
|
|
731
872
|
```python
|
|
732
|
-
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
873
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
|
|
733
874
|
|
|
734
875
|
# From MultiPeriodDiD
|
|
735
876
|
did = MultiPeriodDiD()
|
|
@@ -741,7 +882,13 @@ plot_event_study(results, title="Treatment Effects Over Time")
|
|
|
741
882
|
cs = CallawaySantAnna()
|
|
742
883
|
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
743
884
|
first_treat='first_treat', aggregate='event_study')
|
|
744
|
-
plot_event_study(results, title="Staggered DiD Event Study")
|
|
885
|
+
plot_event_study(results, title="Staggered DiD Event Study (CS)")
|
|
886
|
+
|
|
887
|
+
# From SunAbraham
|
|
888
|
+
sa = SunAbraham()
|
|
889
|
+
results = sa.fit(data, outcome='y', unit='unit', time='period',
|
|
890
|
+
first_treat='first_treat')
|
|
891
|
+
plot_event_study(results, title="Staggered DiD Event Study (SA)")
|
|
745
892
|
|
|
746
893
|
# From a DataFrame
|
|
747
894
|
df = pd.DataFrame({
|
|
@@ -1372,6 +1519,63 @@ SyntheticDiD(
|
|
|
1372
1519
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1373
1520
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1374
1521
|
|
|
1522
|
+
### SunAbraham
|
|
1523
|
+
|
|
1524
|
+
```python
|
|
1525
|
+
SunAbraham(
|
|
1526
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
1527
|
+
anticipation=0, # Periods of anticipation effects
|
|
1528
|
+
alpha=0.05, # Significance level for CIs
|
|
1529
|
+
cluster=None, # Column for cluster-robust SEs
|
|
1530
|
+
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
1531
|
+
bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
|
|
1532
|
+
seed=None # Random seed
|
|
1533
|
+
)
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
**fit() Parameters:**
|
|
1537
|
+
|
|
1538
|
+
| Parameter | Type | Description |
|
|
1539
|
+
|-----------|------|-------------|
|
|
1540
|
+
| `data` | DataFrame | Panel data |
|
|
1541
|
+
| `outcome` | str | Outcome variable column name |
|
|
1542
|
+
| `unit` | str | Unit identifier column |
|
|
1543
|
+
| `time` | str | Time period column |
|
|
1544
|
+
| `first_treat` | str | Column with first treatment period (0 for never-treated) |
|
|
1545
|
+
| `covariates` | list | Covariate column names |
|
|
1546
|
+
| `min_pre_periods` | int | Minimum pre-treatment periods to include |
|
|
1547
|
+
| `min_post_periods` | int | Minimum post-treatment periods to include |
|
|
1548
|
+
|
|
1549
|
+
### SunAbrahamResults
|
|
1550
|
+
|
|
1551
|
+
**Attributes:**
|
|
1552
|
+
|
|
1553
|
+
| Attribute | Description |
|
|
1554
|
+
|-----------|-------------|
|
|
1555
|
+
| `event_study_effects` | Dict mapping relative time to effect info |
|
|
1556
|
+
| `overall_att` | Overall average treatment effect |
|
|
1557
|
+
| `overall_se` | Standard error of overall ATT |
|
|
1558
|
+
| `overall_t_stat` | T-statistic for overall ATT |
|
|
1559
|
+
| `overall_p_value` | P-value for overall ATT |
|
|
1560
|
+
| `overall_conf_int` | Confidence interval for overall ATT |
|
|
1561
|
+
| `cohort_weights` | Dict mapping relative time to cohort weights |
|
|
1562
|
+
| `groups` | List of treatment cohorts |
|
|
1563
|
+
| `time_periods` | List of all time periods |
|
|
1564
|
+
| `n_obs` | Total number of observations |
|
|
1565
|
+
| `n_treated_units` | Number of ever-treated units |
|
|
1566
|
+
| `n_control_units` | Number of never-treated units |
|
|
1567
|
+
| `is_significant` | Boolean for significance at alpha |
|
|
1568
|
+
| `significance_stars` | String of significance stars |
|
|
1569
|
+
| `bootstrap_results` | SABootstrapResults (if bootstrap enabled) |
|
|
1570
|
+
|
|
1571
|
+
**Methods:**
|
|
1572
|
+
|
|
1573
|
+
| Method | Description |
|
|
1574
|
+
|--------|-------------|
|
|
1575
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
1576
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
1577
|
+
| `to_dataframe(level)` | Convert to DataFrame ('event_study' or 'cohort') |
|
|
1578
|
+
|
|
1375
1579
|
### HonestDiD
|
|
1376
1580
|
|
|
1377
1581
|
```python
|
|
@@ -1694,6 +1898,16 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the
|
|
|
1694
1898
|
|
|
1695
1899
|
- **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)
|
|
1696
1900
|
|
|
1901
|
+
### Power Analysis
|
|
1902
|
+
|
|
1903
|
+
- **Bloom, H. S. (1995).** "Minimum Detectable Effects: A Simple Way to Report the Statistical Power of Experimental Designs." *Evaluation Review*, 19(5), 547-556. [https://doi.org/10.1177/0193841X9501900504](https://doi.org/10.1177/0193841X9501900504)
|
|
1904
|
+
|
|
1905
|
+
- **Burlig, F., Preonas, L., & Woerman, M. (2020).** "Panel Data and Experimental Design." *Journal of Development Economics*, 144, 102458. [https://doi.org/10.1016/j.jdeveco.2020.102458](https://doi.org/10.1016/j.jdeveco.2020.102458)
|
|
1906
|
+
|
|
1907
|
+
Essential reference for power analysis in panel DiD designs. Discusses how serial correlation (ICC) affects power and provides formulas for panel data settings.
|
|
1908
|
+
|
|
1909
|
+
- **Djimeu, E. W., & Houndolo, D.-G. (2016).** "Power Calculation for Causal Inference in Social Science: Sample Size and Minimum Detectable Effect Determination." *Journal of Development Effectiveness*, 8(4), 508-527. [https://doi.org/10.1080/19439342.2016.1244555](https://doi.org/10.1080/19439342.2016.1244555)
|
|
1910
|
+
|
|
1697
1911
|
### General Causal Inference
|
|
1698
1912
|
|
|
1699
1913
|
- **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
|