diff-diff 1.2.0__tar.gz → 1.3.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.2.0 → diff_diff-1.3.0}/PKG-INFO +141 -1
- {diff_diff-1.2.0 → diff_diff-1.3.0}/README.md +140 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/__init__.py +9 -1
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/staggered.py +49 -29
- diff_diff-1.3.0/diff_diff/triple_diff.py +1300 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff.egg-info/PKG-INFO +141 -1
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff.egg-info/SOURCES.txt +2 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/pyproject.toml +1 -1
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_estimators.py +518 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_staggered.py +231 -0
- diff_diff-1.3.0/tests/test_triple_diff.py +869 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_wild_bootstrap.py +181 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/bacon.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/diagnostics.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/estimators.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/honest_did.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/power.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/prep.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/pretrends.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/results.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/twfe.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/utils.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff/visualization.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/setup.cfg +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_bacon.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_diagnostics.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_honest_did.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_power.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_prep.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_pretrends.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_sun_abraham.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_utils.py +0 -0
- {diff_diff-1.2.0 → diff_diff-1.3.0}/tests/test_visualization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
5
|
Author: diff-diff contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -106,6 +106,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
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
108
|
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
|
|
109
|
+
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
|
|
109
110
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
110
111
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
111
112
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
@@ -128,6 +129,8 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
128
129
|
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
129
130
|
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
130
131
|
| `06_power_analysis.ipynb` | Power analysis, MDE, sample size calculations, simulation-based power |
|
|
132
|
+
| `07_pretrends_power.ipynb` | Pre-trends power analysis (Roth 2022), MDV, power curves |
|
|
133
|
+
| `08_triple_diff.ipynb` | Triple Difference (DDD) estimation with proper covariate handling |
|
|
131
134
|
|
|
132
135
|
## Data Preparation
|
|
133
136
|
|
|
@@ -901,6 +904,77 @@ print(f"Sun-Abraham ATT: {sa_results.overall_att:.3f}")
|
|
|
901
904
|
# If results differ substantially, investigate heterogeneity
|
|
902
905
|
```
|
|
903
906
|
|
|
907
|
+
### Triple Difference (DDD)
|
|
908
|
+
|
|
909
|
+
Triple Difference (DDD) is used when treatment requires satisfying two criteria: belonging to a treated **group** AND being in an eligible **partition**. The `TripleDifference` class implements the methodology from Ortiz-Villavicencio & Sant'Anna (2025), which correctly handles covariate adjustment (unlike naive implementations).
|
|
910
|
+
|
|
911
|
+
```python
|
|
912
|
+
from diff_diff import TripleDifference, triple_difference
|
|
913
|
+
|
|
914
|
+
# Basic usage
|
|
915
|
+
ddd = TripleDifference(estimation_method='dr') # doubly robust (recommended)
|
|
916
|
+
results = ddd.fit(
|
|
917
|
+
data,
|
|
918
|
+
outcome='wages',
|
|
919
|
+
group='policy_state', # 1=state enacted policy, 0=control state
|
|
920
|
+
partition='female', # 1=women (affected by policy), 0=men
|
|
921
|
+
time='post' # 1=post-policy, 0=pre-policy
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# View results
|
|
925
|
+
results.print_summary()
|
|
926
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
927
|
+
|
|
928
|
+
# With covariates (properly incorporated, unlike naive DDD)
|
|
929
|
+
results = ddd.fit(
|
|
930
|
+
data,
|
|
931
|
+
outcome='wages',
|
|
932
|
+
group='policy_state',
|
|
933
|
+
partition='female',
|
|
934
|
+
time='post',
|
|
935
|
+
covariates=['age', 'education', 'experience']
|
|
936
|
+
)
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
**Estimation methods:**
|
|
940
|
+
|
|
941
|
+
| Method | Description | When to use |
|
|
942
|
+
|--------|-------------|-------------|
|
|
943
|
+
| `"dr"` | Doubly robust | Recommended. Consistent if either outcome or propensity model is correct |
|
|
944
|
+
| `"reg"` | Regression adjustment | Simple outcome regression with full interactions |
|
|
945
|
+
| `"ipw"` | Inverse probability weighting | When propensity score model is well-specified |
|
|
946
|
+
|
|
947
|
+
```python
|
|
948
|
+
# Compare estimation methods
|
|
949
|
+
for method in ['reg', 'ipw', 'dr']:
|
|
950
|
+
est = TripleDifference(estimation_method=method)
|
|
951
|
+
res = est.fit(data, outcome='y', group='g', partition='p', time='t')
|
|
952
|
+
print(f"{method}: ATT={res.att:.3f} (SE={res.se:.3f})")
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
**Convenience function:**
|
|
956
|
+
|
|
957
|
+
```python
|
|
958
|
+
# One-liner estimation
|
|
959
|
+
results = triple_difference(
|
|
960
|
+
data,
|
|
961
|
+
outcome='wages',
|
|
962
|
+
group='policy_state',
|
|
963
|
+
partition='female',
|
|
964
|
+
time='post',
|
|
965
|
+
covariates=['age', 'education'],
|
|
966
|
+
estimation_method='dr'
|
|
967
|
+
)
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
**Why use DDD instead of DiD?**
|
|
971
|
+
|
|
972
|
+
DDD allows for violations of parallel trends that are:
|
|
973
|
+
- Group-specific (e.g., economic shocks in treatment states)
|
|
974
|
+
- Partition-specific (e.g., trends affecting women everywhere)
|
|
975
|
+
|
|
976
|
+
As long as these biases are additive, DDD differences them out. The key assumption is that the *differential* trend between eligible and ineligible units would be the same across groups.
|
|
977
|
+
|
|
904
978
|
### Event Study Visualization
|
|
905
979
|
|
|
906
980
|
Create publication-ready event study plots:
|
|
@@ -1696,6 +1770,60 @@ SunAbraham(
|
|
|
1696
1770
|
| `print_summary(alpha)` | Print summary to stdout |
|
|
1697
1771
|
| `to_dataframe(level)` | Convert to DataFrame ('event_study' or 'cohort') |
|
|
1698
1772
|
|
|
1773
|
+
### TripleDifference
|
|
1774
|
+
|
|
1775
|
+
```python
|
|
1776
|
+
TripleDifference(
|
|
1777
|
+
estimation_method='dr', # 'dr' (doubly robust), 'reg', or 'ipw'
|
|
1778
|
+
robust=True, # Use HC1 robust standard errors
|
|
1779
|
+
cluster=None, # Column for cluster-robust SEs
|
|
1780
|
+
alpha=0.05, # Significance level for CIs
|
|
1781
|
+
pscore_trim=0.01 # Propensity score trimming threshold
|
|
1782
|
+
)
|
|
1783
|
+
```
|
|
1784
|
+
|
|
1785
|
+
**fit() Parameters:**
|
|
1786
|
+
|
|
1787
|
+
| Parameter | Type | Description |
|
|
1788
|
+
|-----------|------|-------------|
|
|
1789
|
+
| `data` | DataFrame | Input data |
|
|
1790
|
+
| `outcome` | str | Outcome variable column name |
|
|
1791
|
+
| `group` | str | Group indicator column (0/1): 1=treated group |
|
|
1792
|
+
| `partition` | str | Partition/eligibility indicator column (0/1): 1=eligible |
|
|
1793
|
+
| `time` | str | Time indicator column (0/1): 1=post-treatment |
|
|
1794
|
+
| `covariates` | list | Covariate column names for adjustment |
|
|
1795
|
+
|
|
1796
|
+
### TripleDifferenceResults
|
|
1797
|
+
|
|
1798
|
+
**Attributes:**
|
|
1799
|
+
|
|
1800
|
+
| Attribute | Description |
|
|
1801
|
+
|-----------|-------------|
|
|
1802
|
+
| `att` | Average Treatment effect on the Treated |
|
|
1803
|
+
| `se` | Standard error of ATT |
|
|
1804
|
+
| `t_stat` | T-statistic |
|
|
1805
|
+
| `p_value` | P-value for H0: ATT = 0 |
|
|
1806
|
+
| `conf_int` | Tuple of (lower, upper) confidence bounds |
|
|
1807
|
+
| `n_obs` | Total number of observations |
|
|
1808
|
+
| `n_treated_eligible` | Obs in treated group & eligible partition |
|
|
1809
|
+
| `n_treated_ineligible` | Obs in treated group & ineligible partition |
|
|
1810
|
+
| `n_control_eligible` | Obs in control group & eligible partition |
|
|
1811
|
+
| `n_control_ineligible` | Obs in control group & ineligible partition |
|
|
1812
|
+
| `estimation_method` | Method used ('dr', 'reg', or 'ipw') |
|
|
1813
|
+
| `group_means` | Dict of cell means for diagnostics |
|
|
1814
|
+
| `pscore_stats` | Propensity score statistics (IPW/DR only) |
|
|
1815
|
+
| `is_significant` | Boolean for significance at alpha |
|
|
1816
|
+
| `significance_stars` | String of significance stars |
|
|
1817
|
+
|
|
1818
|
+
**Methods:**
|
|
1819
|
+
|
|
1820
|
+
| Method | Description |
|
|
1821
|
+
|--------|-------------|
|
|
1822
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
1823
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
1824
|
+
| `to_dict()` | Convert to dictionary |
|
|
1825
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1826
|
+
|
|
1699
1827
|
### HonestDiD
|
|
1700
1828
|
|
|
1701
1829
|
```python
|
|
@@ -2059,6 +2187,18 @@ This library implements methods from the following scholarly works:
|
|
|
2059
2187
|
|
|
2060
2188
|
- **Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021).** "Synthetic Difference-in-Differences." *American Economic Review*, 111(12), 4088-4118. [https://doi.org/10.1257/aer.20190159](https://doi.org/10.1257/aer.20190159)
|
|
2061
2189
|
|
|
2190
|
+
### Triple Difference (DDD)
|
|
2191
|
+
|
|
2192
|
+
- **Ortiz-Villavicencio, M., & Sant'Anna, P. H. C. (2025).** "Better Understanding Triple Differences Estimators." *Working Paper*. [https://arxiv.org/abs/2505.09942](https://arxiv.org/abs/2505.09942)
|
|
2193
|
+
|
|
2194
|
+
This paper shows that common DDD implementations (taking the difference between two DiDs, or applying three-way fixed effects regressions) are generally invalid when identification requires conditioning on covariates. The `TripleDifference` class implements their regression adjustment, inverse probability weighting, and doubly robust estimators.
|
|
2195
|
+
|
|
2196
|
+
- **Gruber, J. (1994).** "The Incidence of Mandated Maternity Benefits." *American Economic Review*, 84(3), 622-641. [https://www.jstor.org/stable/2118071](https://www.jstor.org/stable/2118071)
|
|
2197
|
+
|
|
2198
|
+
Classic paper introducing the Triple Difference design for policy evaluation.
|
|
2199
|
+
|
|
2200
|
+
- **Olden, A., & Møen, J. (2022).** "The Triple Difference Estimator." *The Econometrics Journal*, 25(3), 531-553. [https://doi.org/10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
|
|
2201
|
+
|
|
2062
2202
|
### Parallel Trends and Pre-Trend Testing
|
|
2063
2203
|
|
|
2064
2204
|
- **Roth, J. (2022).** "Pretest with Caution: Event-Study Estimates after Testing for Parallel Trends." *American Economic Review: Insights*, 4(3), 305-322. [https://doi.org/10.1257/aeri.20210236](https://doi.org/10.1257/aeri.20210236)
|
|
@@ -71,6 +71,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
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
73
|
- **Staggered adoption**: Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
|
|
74
|
+
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
|
|
74
75
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
75
76
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
76
77
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
@@ -93,6 +94,8 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
93
94
|
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
94
95
|
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
95
96
|
| `06_power_analysis.ipynb` | Power analysis, MDE, sample size calculations, simulation-based power |
|
|
97
|
+
| `07_pretrends_power.ipynb` | Pre-trends power analysis (Roth 2022), MDV, power curves |
|
|
98
|
+
| `08_triple_diff.ipynb` | Triple Difference (DDD) estimation with proper covariate handling |
|
|
96
99
|
|
|
97
100
|
## Data Preparation
|
|
98
101
|
|
|
@@ -866,6 +869,77 @@ print(f"Sun-Abraham ATT: {sa_results.overall_att:.3f}")
|
|
|
866
869
|
# If results differ substantially, investigate heterogeneity
|
|
867
870
|
```
|
|
868
871
|
|
|
872
|
+
### Triple Difference (DDD)
|
|
873
|
+
|
|
874
|
+
Triple Difference (DDD) is used when treatment requires satisfying two criteria: belonging to a treated **group** AND being in an eligible **partition**. The `TripleDifference` class implements the methodology from Ortiz-Villavicencio & Sant'Anna (2025), which correctly handles covariate adjustment (unlike naive implementations).
|
|
875
|
+
|
|
876
|
+
```python
|
|
877
|
+
from diff_diff import TripleDifference, triple_difference
|
|
878
|
+
|
|
879
|
+
# Basic usage
|
|
880
|
+
ddd = TripleDifference(estimation_method='dr') # doubly robust (recommended)
|
|
881
|
+
results = ddd.fit(
|
|
882
|
+
data,
|
|
883
|
+
outcome='wages',
|
|
884
|
+
group='policy_state', # 1=state enacted policy, 0=control state
|
|
885
|
+
partition='female', # 1=women (affected by policy), 0=men
|
|
886
|
+
time='post' # 1=post-policy, 0=pre-policy
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# View results
|
|
890
|
+
results.print_summary()
|
|
891
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
892
|
+
|
|
893
|
+
# With covariates (properly incorporated, unlike naive DDD)
|
|
894
|
+
results = ddd.fit(
|
|
895
|
+
data,
|
|
896
|
+
outcome='wages',
|
|
897
|
+
group='policy_state',
|
|
898
|
+
partition='female',
|
|
899
|
+
time='post',
|
|
900
|
+
covariates=['age', 'education', 'experience']
|
|
901
|
+
)
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
**Estimation methods:**
|
|
905
|
+
|
|
906
|
+
| Method | Description | When to use |
|
|
907
|
+
|--------|-------------|-------------|
|
|
908
|
+
| `"dr"` | Doubly robust | Recommended. Consistent if either outcome or propensity model is correct |
|
|
909
|
+
| `"reg"` | Regression adjustment | Simple outcome regression with full interactions |
|
|
910
|
+
| `"ipw"` | Inverse probability weighting | When propensity score model is well-specified |
|
|
911
|
+
|
|
912
|
+
```python
|
|
913
|
+
# Compare estimation methods
|
|
914
|
+
for method in ['reg', 'ipw', 'dr']:
|
|
915
|
+
est = TripleDifference(estimation_method=method)
|
|
916
|
+
res = est.fit(data, outcome='y', group='g', partition='p', time='t')
|
|
917
|
+
print(f"{method}: ATT={res.att:.3f} (SE={res.se:.3f})")
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Convenience function:**
|
|
921
|
+
|
|
922
|
+
```python
|
|
923
|
+
# One-liner estimation
|
|
924
|
+
results = triple_difference(
|
|
925
|
+
data,
|
|
926
|
+
outcome='wages',
|
|
927
|
+
group='policy_state',
|
|
928
|
+
partition='female',
|
|
929
|
+
time='post',
|
|
930
|
+
covariates=['age', 'education'],
|
|
931
|
+
estimation_method='dr'
|
|
932
|
+
)
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
**Why use DDD instead of DiD?**
|
|
936
|
+
|
|
937
|
+
DDD allows for violations of parallel trends that are:
|
|
938
|
+
- Group-specific (e.g., economic shocks in treatment states)
|
|
939
|
+
- Partition-specific (e.g., trends affecting women everywhere)
|
|
940
|
+
|
|
941
|
+
As long as these biases are additive, DDD differences them out. The key assumption is that the *differential* trend between eligible and ineligible units would be the same across groups.
|
|
942
|
+
|
|
869
943
|
### Event Study Visualization
|
|
870
944
|
|
|
871
945
|
Create publication-ready event study plots:
|
|
@@ -1661,6 +1735,60 @@ SunAbraham(
|
|
|
1661
1735
|
| `print_summary(alpha)` | Print summary to stdout |
|
|
1662
1736
|
| `to_dataframe(level)` | Convert to DataFrame ('event_study' or 'cohort') |
|
|
1663
1737
|
|
|
1738
|
+
### TripleDifference
|
|
1739
|
+
|
|
1740
|
+
```python
|
|
1741
|
+
TripleDifference(
|
|
1742
|
+
estimation_method='dr', # 'dr' (doubly robust), 'reg', or 'ipw'
|
|
1743
|
+
robust=True, # Use HC1 robust standard errors
|
|
1744
|
+
cluster=None, # Column for cluster-robust SEs
|
|
1745
|
+
alpha=0.05, # Significance level for CIs
|
|
1746
|
+
pscore_trim=0.01 # Propensity score trimming threshold
|
|
1747
|
+
)
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
**fit() Parameters:**
|
|
1751
|
+
|
|
1752
|
+
| Parameter | Type | Description |
|
|
1753
|
+
|-----------|------|-------------|
|
|
1754
|
+
| `data` | DataFrame | Input data |
|
|
1755
|
+
| `outcome` | str | Outcome variable column name |
|
|
1756
|
+
| `group` | str | Group indicator column (0/1): 1=treated group |
|
|
1757
|
+
| `partition` | str | Partition/eligibility indicator column (0/1): 1=eligible |
|
|
1758
|
+
| `time` | str | Time indicator column (0/1): 1=post-treatment |
|
|
1759
|
+
| `covariates` | list | Covariate column names for adjustment |
|
|
1760
|
+
|
|
1761
|
+
### TripleDifferenceResults
|
|
1762
|
+
|
|
1763
|
+
**Attributes:**
|
|
1764
|
+
|
|
1765
|
+
| Attribute | Description |
|
|
1766
|
+
|-----------|-------------|
|
|
1767
|
+
| `att` | Average Treatment effect on the Treated |
|
|
1768
|
+
| `se` | Standard error of ATT |
|
|
1769
|
+
| `t_stat` | T-statistic |
|
|
1770
|
+
| `p_value` | P-value for H0: ATT = 0 |
|
|
1771
|
+
| `conf_int` | Tuple of (lower, upper) confidence bounds |
|
|
1772
|
+
| `n_obs` | Total number of observations |
|
|
1773
|
+
| `n_treated_eligible` | Obs in treated group & eligible partition |
|
|
1774
|
+
| `n_treated_ineligible` | Obs in treated group & ineligible partition |
|
|
1775
|
+
| `n_control_eligible` | Obs in control group & eligible partition |
|
|
1776
|
+
| `n_control_ineligible` | Obs in control group & ineligible partition |
|
|
1777
|
+
| `estimation_method` | Method used ('dr', 'reg', or 'ipw') |
|
|
1778
|
+
| `group_means` | Dict of cell means for diagnostics |
|
|
1779
|
+
| `pscore_stats` | Propensity score statistics (IPW/DR only) |
|
|
1780
|
+
| `is_significant` | Boolean for significance at alpha |
|
|
1781
|
+
| `significance_stars` | String of significance stars |
|
|
1782
|
+
|
|
1783
|
+
**Methods:**
|
|
1784
|
+
|
|
1785
|
+
| Method | Description |
|
|
1786
|
+
|--------|-------------|
|
|
1787
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
1788
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
1789
|
+
| `to_dict()` | Convert to dictionary |
|
|
1790
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1791
|
+
|
|
1664
1792
|
### HonestDiD
|
|
1665
1793
|
|
|
1666
1794
|
```python
|
|
@@ -2024,6 +2152,18 @@ This library implements methods from the following scholarly works:
|
|
|
2024
2152
|
|
|
2025
2153
|
- **Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021).** "Synthetic Difference-in-Differences." *American Economic Review*, 111(12), 4088-4118. [https://doi.org/10.1257/aer.20190159](https://doi.org/10.1257/aer.20190159)
|
|
2026
2154
|
|
|
2155
|
+
### Triple Difference (DDD)
|
|
2156
|
+
|
|
2157
|
+
- **Ortiz-Villavicencio, M., & Sant'Anna, P. H. C. (2025).** "Better Understanding Triple Differences Estimators." *Working Paper*. [https://arxiv.org/abs/2505.09942](https://arxiv.org/abs/2505.09942)
|
|
2158
|
+
|
|
2159
|
+
This paper shows that common DDD implementations (taking the difference between two DiDs, or applying three-way fixed effects regressions) are generally invalid when identification requires conditioning on covariates. The `TripleDifference` class implements their regression adjustment, inverse probability weighting, and doubly robust estimators.
|
|
2160
|
+
|
|
2161
|
+
- **Gruber, J. (1994).** "The Incidence of Mandated Maternity Benefits." *American Economic Review*, 84(3), 622-641. [https://www.jstor.org/stable/2118071](https://www.jstor.org/stable/2118071)
|
|
2162
|
+
|
|
2163
|
+
Classic paper introducing the Triple Difference design for policy evaluation.
|
|
2164
|
+
|
|
2165
|
+
- **Olden, A., & Møen, J. (2022).** "The Triple Difference Estimator." *The Econometrics Journal*, 25(3), 531-553. [https://doi.org/10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
|
|
2166
|
+
|
|
2027
2167
|
### Parallel Trends and Pre-Trend Testing
|
|
2028
2168
|
|
|
2029
2169
|
- **Roth, J. (2022).** "Pretest with Caution: Event-Study Estimates after Testing for Parallel Trends." *American Economic Review: Insights*, 4(3), 305-322. [https://doi.org/10.1257/aeri.20210236](https://doi.org/10.1257/aeri.20210236)
|
|
@@ -81,6 +81,11 @@ from diff_diff.sun_abraham import (
|
|
|
81
81
|
SunAbraham,
|
|
82
82
|
SunAbrahamResults,
|
|
83
83
|
)
|
|
84
|
+
from diff_diff.triple_diff import (
|
|
85
|
+
TripleDifference,
|
|
86
|
+
TripleDifferenceResults,
|
|
87
|
+
triple_difference,
|
|
88
|
+
)
|
|
84
89
|
from diff_diff.utils import (
|
|
85
90
|
WildBootstrapResults,
|
|
86
91
|
check_parallel_trends,
|
|
@@ -98,7 +103,7 @@ from diff_diff.visualization import (
|
|
|
98
103
|
plot_sensitivity,
|
|
99
104
|
)
|
|
100
105
|
|
|
101
|
-
__version__ = "1.2.
|
|
106
|
+
__version__ = "1.2.1"
|
|
102
107
|
__all__ = [
|
|
103
108
|
# Estimators
|
|
104
109
|
"DifferenceInDifferences",
|
|
@@ -107,6 +112,7 @@ __all__ = [
|
|
|
107
112
|
"SyntheticDiD",
|
|
108
113
|
"CallawaySantAnna",
|
|
109
114
|
"SunAbraham",
|
|
115
|
+
"TripleDifference",
|
|
110
116
|
# Bacon Decomposition
|
|
111
117
|
"BaconDecomposition",
|
|
112
118
|
"BaconDecompositionResults",
|
|
@@ -123,6 +129,8 @@ __all__ = [
|
|
|
123
129
|
"GroupTimeEffect",
|
|
124
130
|
"SunAbrahamResults",
|
|
125
131
|
"SABootstrapResults",
|
|
132
|
+
"TripleDifferenceResults",
|
|
133
|
+
"triple_difference",
|
|
126
134
|
# Visualization
|
|
127
135
|
"plot_event_study",
|
|
128
136
|
"plot_group_effects",
|
|
@@ -1603,30 +1603,25 @@ class CallawaySantAnna:
|
|
|
1603
1603
|
weights * bootstrap_atts_gt[b, gt_indices]
|
|
1604
1604
|
)
|
|
1605
1605
|
|
|
1606
|
-
# Compute bootstrap statistics
|
|
1607
|
-
# ATT(g,t) statistics
|
|
1606
|
+
# Compute bootstrap statistics for ATT(g,t)
|
|
1608
1607
|
gt_ses = {}
|
|
1609
1608
|
gt_cis = {}
|
|
1610
1609
|
gt_p_values = {}
|
|
1611
1610
|
|
|
1612
1611
|
for j, gt in enumerate(gt_pairs):
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
se = float(np.std(boot_dist, ddof=1))
|
|
1617
|
-
ci = self._compute_percentile_ci(boot_dist, self.alpha)
|
|
1618
|
-
p_value = self._compute_bootstrap_pvalue(original_effect, boot_dist)
|
|
1619
|
-
|
|
1612
|
+
se, ci, p_value = self._compute_effect_bootstrap_stats(
|
|
1613
|
+
original_atts[j], bootstrap_atts_gt[:, j]
|
|
1614
|
+
)
|
|
1620
1615
|
gt_ses[gt] = se
|
|
1621
1616
|
gt_cis[gt] = ci
|
|
1622
1617
|
gt_p_values[gt] = p_value
|
|
1623
1618
|
|
|
1624
|
-
#
|
|
1625
|
-
overall_se =
|
|
1626
|
-
|
|
1627
|
-
|
|
1619
|
+
# Compute bootstrap statistics for overall ATT
|
|
1620
|
+
overall_se, overall_ci, overall_p_value = self._compute_effect_bootstrap_stats(
|
|
1621
|
+
original_overall, bootstrap_overall
|
|
1622
|
+
)
|
|
1628
1623
|
|
|
1629
|
-
#
|
|
1624
|
+
# Compute bootstrap statistics for event study effects
|
|
1630
1625
|
event_study_ses = None
|
|
1631
1626
|
event_study_cis = None
|
|
1632
1627
|
event_study_p_values = None
|
|
@@ -1637,16 +1632,14 @@ class CallawaySantAnna:
|
|
|
1637
1632
|
event_study_p_values = {}
|
|
1638
1633
|
|
|
1639
1634
|
for e in rel_periods:
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
event_study_ses[e] = float(np.std(boot_dist, ddof=1))
|
|
1644
|
-
event_study_cis[e] = self._compute_percentile_ci(boot_dist, self.alpha)
|
|
1645
|
-
event_study_p_values[e] = self._compute_bootstrap_pvalue(
|
|
1646
|
-
original_effect, boot_dist
|
|
1635
|
+
se, ci, p_value = self._compute_effect_bootstrap_stats(
|
|
1636
|
+
event_study_info[e]['effect'], bootstrap_event_study[e]
|
|
1647
1637
|
)
|
|
1638
|
+
event_study_ses[e] = se
|
|
1639
|
+
event_study_cis[e] = ci
|
|
1640
|
+
event_study_p_values[e] = p_value
|
|
1648
1641
|
|
|
1649
|
-
#
|
|
1642
|
+
# Compute bootstrap statistics for group effects
|
|
1650
1643
|
group_effect_ses = None
|
|
1651
1644
|
group_effect_cis = None
|
|
1652
1645
|
group_effect_p_values = None
|
|
@@ -1657,14 +1650,12 @@ class CallawaySantAnna:
|
|
|
1657
1650
|
group_effect_p_values = {}
|
|
1658
1651
|
|
|
1659
1652
|
for g in groups:
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
group_effect_ses[g] = float(np.std(boot_dist, ddof=1))
|
|
1664
|
-
group_effect_cis[g] = self._compute_percentile_ci(boot_dist, self.alpha)
|
|
1665
|
-
group_effect_p_values[g] = self._compute_bootstrap_pvalue(
|
|
1666
|
-
original_effect, boot_dist
|
|
1653
|
+
se, ci, p_value = self._compute_effect_bootstrap_stats(
|
|
1654
|
+
group_agg_info[g]['effect'], bootstrap_group[g]
|
|
1667
1655
|
)
|
|
1656
|
+
group_effect_ses[g] = se
|
|
1657
|
+
group_effect_cis[g] = ci
|
|
1658
|
+
group_effect_p_values[g] = p_value
|
|
1668
1659
|
|
|
1669
1660
|
return CSBootstrapResults(
|
|
1670
1661
|
n_bootstrap=self.n_bootstrap,
|
|
@@ -1817,6 +1808,35 @@ class CallawaySantAnna:
|
|
|
1817
1808
|
|
|
1818
1809
|
return float(p_value)
|
|
1819
1810
|
|
|
1811
|
+
def _compute_effect_bootstrap_stats(
|
|
1812
|
+
self,
|
|
1813
|
+
original_effect: float,
|
|
1814
|
+
boot_dist: np.ndarray,
|
|
1815
|
+
) -> Tuple[float, Tuple[float, float], float]:
|
|
1816
|
+
"""
|
|
1817
|
+
Compute bootstrap statistics for a single effect.
|
|
1818
|
+
|
|
1819
|
+
Parameters
|
|
1820
|
+
----------
|
|
1821
|
+
original_effect : float
|
|
1822
|
+
Original point estimate.
|
|
1823
|
+
boot_dist : np.ndarray
|
|
1824
|
+
Bootstrap distribution of the effect.
|
|
1825
|
+
|
|
1826
|
+
Returns
|
|
1827
|
+
-------
|
|
1828
|
+
se : float
|
|
1829
|
+
Bootstrap standard error.
|
|
1830
|
+
ci : Tuple[float, float]
|
|
1831
|
+
Percentile confidence interval.
|
|
1832
|
+
p_value : float
|
|
1833
|
+
Bootstrap p-value.
|
|
1834
|
+
"""
|
|
1835
|
+
se = float(np.std(boot_dist, ddof=1))
|
|
1836
|
+
ci = self._compute_percentile_ci(boot_dist, self.alpha)
|
|
1837
|
+
p_value = self._compute_bootstrap_pvalue(original_effect, boot_dist)
|
|
1838
|
+
return se, ci, p_value
|
|
1839
|
+
|
|
1820
1840
|
def get_params(self) -> Dict[str, Any]:
|
|
1821
1841
|
"""Get estimator parameters (sklearn-compatible)."""
|
|
1822
1842
|
return {
|