diff-diff 1.0.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-1.0.0 → diff_diff-1.1.0}/PKG-INFO +172 -6
- {diff_diff-1.0.0 → diff_diff-1.1.0}/README.md +171 -5
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/__init__.py +9 -1
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/diagnostics.py +3 -15
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/estimators.py +24 -860
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/honest_did.py +4 -2
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/power.py +1 -1
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/staggered.py +50 -19
- 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-1.0.0 → diff_diff-1.1.0}/diff_diff/visualization.py +4 -1
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff.egg-info/PKG-INFO +172 -6
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff.egg-info/SOURCES.txt +4 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/pyproject.toml +1 -1
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_staggered.py +9 -3
- diff_diff-1.1.0/tests/test_sun_abraham.py +732 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/bacon.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/prep.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/results.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff/utils.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/setup.cfg +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_bacon.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_diagnostics.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_estimators.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_honest_did.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_power.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_prep.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_utils.py +0 -0
- {diff_diff-1.0.0 → diff_diff-1.1.0}/tests/test_visualization.py +0 -0
- {diff_diff-1.0.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: 1.
|
|
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
|
|
@@ -105,7 +105,7 @@ 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
|
|
@@ -122,7 +122,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
122
122
|
| Notebook | Description |
|
|
123
123
|
|----------|-------------|
|
|
124
124
|
| `01_basic_did.ipynb` | Basic 2x2 DiD, formula interface, covariates, fixed effects, cluster-robust SE, wild bootstrap |
|
|
125
|
-
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna, group-time effects, aggregation methods, Bacon decomposition |
|
|
125
|
+
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna and Sun-Abraham, group-time effects, aggregation methods, Bacon decomposition |
|
|
126
126
|
| `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
|
|
127
127
|
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
128
128
|
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
@@ -574,7 +574,7 @@ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
|
|
|
574
574
|
### Two-Way Fixed Effects (Panel Data)
|
|
575
575
|
|
|
576
576
|
```python
|
|
577
|
-
from diff_diff
|
|
577
|
+
from diff_diff import TwoWayFixedEffects
|
|
578
578
|
|
|
579
579
|
twfe = TwoWayFixedEffects()
|
|
580
580
|
results = twfe.fit(
|
|
@@ -797,12 +797,115 @@ results = cs.fit(
|
|
|
797
797
|
)
|
|
798
798
|
```
|
|
799
799
|
|
|
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
|
+
```
|
|
902
|
+
|
|
800
903
|
### Event Study Visualization
|
|
801
904
|
|
|
802
905
|
Create publication-ready event study plots:
|
|
803
906
|
|
|
804
907
|
```python
|
|
805
|
-
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
908
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
|
|
806
909
|
|
|
807
910
|
# From MultiPeriodDiD
|
|
808
911
|
did = MultiPeriodDiD()
|
|
@@ -814,7 +917,13 @@ plot_event_study(results, title="Treatment Effects Over Time")
|
|
|
814
917
|
cs = CallawaySantAnna()
|
|
815
918
|
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
816
919
|
first_treat='first_treat', aggregate='event_study')
|
|
817
|
-
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)")
|
|
818
927
|
|
|
819
928
|
# From a DataFrame
|
|
820
929
|
df = pd.DataFrame({
|
|
@@ -1445,6 +1554,63 @@ SyntheticDiD(
|
|
|
1445
1554
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1446
1555
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1447
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
|
+
|
|
1448
1614
|
### HonestDiD
|
|
1449
1615
|
|
|
1450
1616
|
```python
|
|
@@ -70,7 +70,7 @@ 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
|
|
@@ -87,7 +87,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
87
87
|
| Notebook | Description |
|
|
88
88
|
|----------|-------------|
|
|
89
89
|
| `01_basic_did.ipynb` | Basic 2x2 DiD, formula interface, covariates, fixed effects, cluster-robust SE, wild bootstrap |
|
|
90
|
-
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna, group-time effects, aggregation methods, Bacon decomposition |
|
|
90
|
+
| `02_staggered_did.ipynb` | Staggered adoption with Callaway-Sant'Anna and Sun-Abraham, group-time effects, aggregation methods, Bacon decomposition |
|
|
91
91
|
| `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
|
|
92
92
|
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
93
93
|
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
@@ -539,7 +539,7 @@ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
|
|
|
539
539
|
### Two-Way Fixed Effects (Panel Data)
|
|
540
540
|
|
|
541
541
|
```python
|
|
542
|
-
from diff_diff
|
|
542
|
+
from diff_diff import TwoWayFixedEffects
|
|
543
543
|
|
|
544
544
|
twfe = TwoWayFixedEffects()
|
|
545
545
|
results = twfe.fit(
|
|
@@ -762,12 +762,115 @@ results = cs.fit(
|
|
|
762
762
|
)
|
|
763
763
|
```
|
|
764
764
|
|
|
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
|
+
```
|
|
867
|
+
|
|
765
868
|
### Event Study Visualization
|
|
766
869
|
|
|
767
870
|
Create publication-ready event study plots:
|
|
768
871
|
|
|
769
872
|
```python
|
|
770
|
-
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
873
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
|
|
771
874
|
|
|
772
875
|
# From MultiPeriodDiD
|
|
773
876
|
did = MultiPeriodDiD()
|
|
@@ -779,7 +882,13 @@ plot_event_study(results, title="Treatment Effects Over Time")
|
|
|
779
882
|
cs = CallawaySantAnna()
|
|
780
883
|
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
781
884
|
first_treat='first_treat', aggregate='event_study')
|
|
782
|
-
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)")
|
|
783
892
|
|
|
784
893
|
# From a DataFrame
|
|
785
894
|
df = pd.DataFrame({
|
|
@@ -1410,6 +1519,63 @@ SyntheticDiD(
|
|
|
1410
1519
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1411
1520
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1412
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
|
+
|
|
1413
1579
|
### HonestDiD
|
|
1414
1580
|
|
|
1415
1581
|
```python
|
|
@@ -69,6 +69,11 @@ from diff_diff.staggered import (
|
|
|
69
69
|
CSBootstrapResults,
|
|
70
70
|
GroupTimeEffect,
|
|
71
71
|
)
|
|
72
|
+
from diff_diff.sun_abraham import (
|
|
73
|
+
SABootstrapResults,
|
|
74
|
+
SunAbraham,
|
|
75
|
+
SunAbrahamResults,
|
|
76
|
+
)
|
|
72
77
|
from diff_diff.utils import (
|
|
73
78
|
WildBootstrapResults,
|
|
74
79
|
check_parallel_trends,
|
|
@@ -85,7 +90,7 @@ from diff_diff.visualization import (
|
|
|
85
90
|
plot_sensitivity,
|
|
86
91
|
)
|
|
87
92
|
|
|
88
|
-
__version__ = "1.
|
|
93
|
+
__version__ = "1.1.0"
|
|
89
94
|
__all__ = [
|
|
90
95
|
# Estimators
|
|
91
96
|
"DifferenceInDifferences",
|
|
@@ -93,6 +98,7 @@ __all__ = [
|
|
|
93
98
|
"MultiPeriodDiD",
|
|
94
99
|
"SyntheticDiD",
|
|
95
100
|
"CallawaySantAnna",
|
|
101
|
+
"SunAbraham",
|
|
96
102
|
# Bacon Decomposition
|
|
97
103
|
"BaconDecomposition",
|
|
98
104
|
"BaconDecompositionResults",
|
|
@@ -107,6 +113,8 @@ __all__ = [
|
|
|
107
113
|
"CallawaySantAnnaResults",
|
|
108
114
|
"CSBootstrapResults",
|
|
109
115
|
"GroupTimeEffect",
|
|
116
|
+
"SunAbrahamResults",
|
|
117
|
+
"SABootstrapResults",
|
|
110
118
|
# Visualization
|
|
111
119
|
"plot_event_study",
|
|
112
120
|
"plot_group_effects",
|
|
@@ -18,22 +18,10 @@ import numpy as np
|
|
|
18
18
|
import pandas as pd
|
|
19
19
|
|
|
20
20
|
from diff_diff.estimators import DifferenceInDifferences
|
|
21
|
+
from diff_diff.results import _get_significance_stars
|
|
21
22
|
from diff_diff.utils import compute_confidence_interval, compute_p_value
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def _get_significance_stars(p_value: float) -> str:
|
|
25
|
-
"""Return significance stars based on p-value."""
|
|
26
|
-
if p_value < 0.001:
|
|
27
|
-
return "***"
|
|
28
|
-
elif p_value < 0.01:
|
|
29
|
-
return "**"
|
|
30
|
-
elif p_value < 0.05:
|
|
31
|
-
return "*"
|
|
32
|
-
elif p_value < 0.1:
|
|
33
|
-
return "."
|
|
34
|
-
return ""
|
|
35
|
-
|
|
36
|
-
|
|
37
25
|
@dataclass
|
|
38
26
|
class PlaceboTestResults:
|
|
39
27
|
"""
|
|
@@ -633,7 +621,7 @@ def permutation_test(
|
|
|
633
621
|
time=time
|
|
634
622
|
)
|
|
635
623
|
permuted_effects[i] = perm_results.att
|
|
636
|
-
except
|
|
624
|
+
except (ValueError, KeyError, np.linalg.LinAlgError):
|
|
637
625
|
# Handle edge cases where fitting fails
|
|
638
626
|
permuted_effects[i] = np.nan
|
|
639
627
|
|
|
@@ -744,7 +732,7 @@ def leave_one_out_test(
|
|
|
744
732
|
time=time
|
|
745
733
|
)
|
|
746
734
|
loo_effects[u] = loo_results.att
|
|
747
|
-
except
|
|
735
|
+
except (ValueError, KeyError, np.linalg.LinAlgError):
|
|
748
736
|
# Skip units that cause fitting issues
|
|
749
737
|
loo_effects[u] = np.nan
|
|
750
738
|
|