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.
Files changed (34) hide show
  1. {diff_diff-0.6.0 → diff_diff-1.1.0}/PKG-INFO +223 -9
  2. {diff_diff-0.6.0 → diff_diff-1.1.0}/README.md +221 -7
  3. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/__init__.py +43 -1
  4. diff_diff-1.1.0/diff_diff/bacon.py +1027 -0
  5. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/diagnostics.py +3 -15
  6. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/estimators.py +23 -722
  7. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/honest_did.py +4 -2
  8. diff_diff-1.1.0/diff_diff/power.py +1350 -0
  9. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/staggered.py +595 -13
  10. diff_diff-1.1.0/diff_diff/sun_abraham.py +1198 -0
  11. diff_diff-1.1.0/diff_diff/synthetic_did.py +540 -0
  12. diff_diff-1.1.0/diff_diff/twfe.py +357 -0
  13. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/visualization.py +572 -1
  14. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/PKG-INFO +223 -9
  15. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/SOURCES.txt +8 -0
  16. {diff_diff-0.6.0 → diff_diff-1.1.0}/pyproject.toml +2 -2
  17. diff_diff-1.1.0/tests/test_bacon.py +679 -0
  18. diff_diff-1.1.0/tests/test_power.py +691 -0
  19. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_staggered.py +375 -0
  20. diff_diff-1.1.0/tests/test_sun_abraham.py +732 -0
  21. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/prep.py +0 -0
  22. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/results.py +0 -0
  23. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff/utils.py +0 -0
  24. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/dependency_links.txt +0 -0
  25. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/requires.txt +0 -0
  26. {diff_diff-0.6.0 → diff_diff-1.1.0}/diff_diff.egg-info/top_level.txt +0 -0
  27. {diff_diff-0.6.0 → diff_diff-1.1.0}/setup.cfg +0 -0
  28. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_diagnostics.py +0 -0
  29. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_estimators.py +0 -0
  30. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_honest_did.py +0 -0
  31. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_prep.py +0 -0
  32. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_utils.py +0 -0
  33. {diff_diff-0.6.0 → diff_diff-1.1.0}/tests/test_visualization.py +0 -0
  34. {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: 0.6.0
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 :: 3 - Alpha
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) estimator for heterogeneous treatment timing
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.estimators import TwoWayFixedEffects
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
- **Current limitations:**
760
- - Bootstrap inference (`n_bootstrap > 0`) is not yet fully implemented
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) estimator for heterogeneous treatment timing
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.estimators import TwoWayFixedEffects
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
- **Current limitations:**
725
- - Bootstrap inference (`n_bootstrap > 0`) is not yet fully implemented
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.