diff-diff 0.5.0__tar.gz → 0.6.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.5.0 → diff_diff-0.6.0}/PKG-INFO +194 -6
- {diff_diff-0.5.0 → diff_diff-0.6.0}/README.md +192 -4
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/__init__.py +53 -30
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/diagnostics.py +3 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/estimators.py +6 -4
- diff_diff-0.6.0/diff_diff/honest_did.py +1491 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/prep.py +1 -1
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/staggered.py +341 -52
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/utils.py +21 -21
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/visualization.py +350 -1
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff.egg-info/PKG-INFO +194 -6
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff.egg-info/SOURCES.txt +3 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/pyproject.toml +11 -3
- {diff_diff-0.5.0 → diff_diff-0.6.0}/tests/test_diagnostics.py +5 -8
- diff_diff-0.6.0/tests/test_honest_did.py +699 -0
- diff_diff-0.6.0/tests/test_staggered.py +752 -0
- diff_diff-0.6.0/tests/test_utils.py +1270 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/tests/test_wild_bootstrap.py +3 -4
- diff_diff-0.5.0/tests/test_staggered.py +0 -390
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff/results.py +0 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/setup.cfg +0 -0
- {diff_diff-0.5.0 → diff_diff-0.6.0}/tests/test_estimators.py +1 -1
- {diff_diff-0.5.0 → diff_diff-0.6.0}/tests/test_prep.py +7 -7
- {diff_diff-0.5.0 → diff_diff-0.6.0}/tests/test_visualization.py +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
5
|
Author: diff-diff contributors
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/igerber/diff-diff
|
|
8
|
-
Project-URL: Documentation, https://
|
|
8
|
+
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
|
|
@@ -110,8 +110,21 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
110
110
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
111
111
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
112
112
|
- **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
|
|
113
|
+
- **Honest DiD sensitivity analysis**: Rambachan-Roth (2023) bounds and breakdown analysis for parallel trends violations
|
|
113
114
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
114
115
|
|
|
116
|
+
## Tutorials
|
|
117
|
+
|
|
118
|
+
We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
119
|
+
|
|
120
|
+
| Notebook | Description |
|
|
121
|
+
|----------|-------------|
|
|
122
|
+
| `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 |
|
|
124
|
+
| `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
|
|
125
|
+
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
126
|
+
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
127
|
+
|
|
115
128
|
## Data Preparation
|
|
116
129
|
|
|
117
130
|
diff-diff provides utility functions to help prepare your data for DiD analysis. These functions handle common data transformation tasks like creating treatment indicators, reshaping panel data, and validating data formats.
|
|
@@ -720,14 +733,31 @@ CallawaySantAnna(
|
|
|
720
733
|
estimation_method='dr', # 'dr', 'ipw', or 'reg'
|
|
721
734
|
alpha=0.05, # Significance level
|
|
722
735
|
cluster=None, # Column for cluster SEs
|
|
723
|
-
n_bootstrap=0, #
|
|
736
|
+
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
724
737
|
seed=None # Random seed
|
|
725
738
|
)
|
|
726
739
|
```
|
|
727
740
|
|
|
741
|
+
**Covariate adjustment for conditional parallel trends:**
|
|
742
|
+
|
|
743
|
+
When parallel trends only holds conditional on covariates, use the `covariates` parameter:
|
|
744
|
+
|
|
745
|
+
```python
|
|
746
|
+
# Doubly robust estimation with covariates
|
|
747
|
+
cs = CallawaySantAnna(estimation_method='dr') # 'dr', 'ipw', or 'reg'
|
|
748
|
+
results = cs.fit(
|
|
749
|
+
data,
|
|
750
|
+
outcome='sales',
|
|
751
|
+
unit='firm_id',
|
|
752
|
+
time='year',
|
|
753
|
+
first_treat='first_treat',
|
|
754
|
+
covariates=['size', 'age', 'industry'], # Covariates for conditional PT
|
|
755
|
+
aggregate='event_study'
|
|
756
|
+
)
|
|
757
|
+
```
|
|
758
|
+
|
|
728
759
|
**Current limitations:**
|
|
729
|
-
- Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
|
|
730
|
-
- Covariate adjustment for conditional parallel trends is not yet implemented
|
|
760
|
+
- Bootstrap inference (`n_bootstrap > 0`) is not yet fully implemented
|
|
731
761
|
|
|
732
762
|
### Event Study Visualization
|
|
733
763
|
|
|
@@ -1004,6 +1034,81 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
|
1004
1034
|
print(f"Trends equivalent: {results['equivalent']}")
|
|
1005
1035
|
```
|
|
1006
1036
|
|
|
1037
|
+
### Honest DiD Sensitivity Analysis (Rambachan-Roth)
|
|
1038
|
+
|
|
1039
|
+
Pre-trends tests have low power and can exacerbate bias. **Honest DiD** (Rambachan & Roth 2023) provides sensitivity analysis showing how robust your results are to violations of parallel trends.
|
|
1040
|
+
|
|
1041
|
+
```python
|
|
1042
|
+
from diff_diff import HonestDiD, MultiPeriodDiD
|
|
1043
|
+
|
|
1044
|
+
# First, fit a standard event study
|
|
1045
|
+
did = MultiPeriodDiD()
|
|
1046
|
+
event_results = did.fit(
|
|
1047
|
+
data,
|
|
1048
|
+
outcome='outcome',
|
|
1049
|
+
treatment='treated',
|
|
1050
|
+
time='period',
|
|
1051
|
+
post_periods=[5, 6, 7, 8, 9]
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
# Compute honest bounds with relative magnitudes restriction
|
|
1055
|
+
# M=1 means post-treatment violations can be up to 1x the worst pre-treatment violation
|
|
1056
|
+
honest = HonestDiD(method='relative_magnitude', M=1.0)
|
|
1057
|
+
honest_results = honest.fit(event_results)
|
|
1058
|
+
|
|
1059
|
+
print(honest_results.summary())
|
|
1060
|
+
print(f"Original estimate: {honest_results.original_estimate:.4f}")
|
|
1061
|
+
print(f"Robust 95% CI: [{honest_results.ci_lb:.4f}, {honest_results.ci_ub:.4f}]")
|
|
1062
|
+
print(f"Effect robust to violations: {honest_results.is_significant}")
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Sensitivity analysis over M values:**
|
|
1066
|
+
|
|
1067
|
+
```python
|
|
1068
|
+
# How do results change as we allow larger violations?
|
|
1069
|
+
sensitivity = honest.sensitivity_analysis(
|
|
1070
|
+
event_results,
|
|
1071
|
+
M_grid=[0, 0.5, 1.0, 1.5, 2.0]
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
print(sensitivity.summary())
|
|
1075
|
+
print(f"Breakdown value: M = {sensitivity.breakdown_M}")
|
|
1076
|
+
# Breakdown = smallest M where the robust CI includes zero
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
**Breakdown value:**
|
|
1080
|
+
|
|
1081
|
+
The breakdown value tells you how robust your conclusion is:
|
|
1082
|
+
|
|
1083
|
+
```python
|
|
1084
|
+
breakdown = honest.breakdown_value(event_results)
|
|
1085
|
+
if breakdown >= 1.0:
|
|
1086
|
+
print("Result holds even if post-treatment violations are as bad as pre-treatment")
|
|
1087
|
+
else:
|
|
1088
|
+
print(f"Result requires violations smaller than {breakdown:.1f}x pre-treatment")
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
**Smoothness restriction (alternative approach):**
|
|
1092
|
+
|
|
1093
|
+
```python
|
|
1094
|
+
# Bounds second differences of trend violations
|
|
1095
|
+
# M=0 means linear extrapolation of pre-trends
|
|
1096
|
+
honest_smooth = HonestDiD(method='smoothness', M=0.5)
|
|
1097
|
+
smooth_results = honest_smooth.fit(event_results)
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
**Visualization:**
|
|
1101
|
+
|
|
1102
|
+
```python
|
|
1103
|
+
from diff_diff import plot_sensitivity, plot_honest_event_study
|
|
1104
|
+
|
|
1105
|
+
# Plot sensitivity analysis
|
|
1106
|
+
plot_sensitivity(sensitivity, title="Sensitivity to Parallel Trends Violations")
|
|
1107
|
+
|
|
1108
|
+
# Event study with honest confidence intervals
|
|
1109
|
+
plot_honest_event_study(event_results, honest_results)
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1007
1112
|
### Placebo Tests
|
|
1008
1113
|
|
|
1009
1114
|
Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
|
|
@@ -1302,6 +1407,75 @@ SyntheticDiD(
|
|
|
1302
1407
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1303
1408
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1304
1409
|
|
|
1410
|
+
### HonestDiD
|
|
1411
|
+
|
|
1412
|
+
```python
|
|
1413
|
+
HonestDiD(
|
|
1414
|
+
method='relative_magnitude', # 'relative_magnitude' or 'smoothness'
|
|
1415
|
+
M=None, # Restriction parameter (default: 1.0 for RM, 0.0 for SD)
|
|
1416
|
+
alpha=0.05, # Significance level for CIs
|
|
1417
|
+
l_vec=None # Linear combination vector for target parameter
|
|
1418
|
+
)
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
**fit() Parameters:**
|
|
1422
|
+
|
|
1423
|
+
| Parameter | Type | Description |
|
|
1424
|
+
|-----------|------|-------------|
|
|
1425
|
+
| `results` | MultiPeriodDiDResults | Results from MultiPeriodDiD.fit() |
|
|
1426
|
+
| `M` | float | Restriction parameter (overrides constructor value) |
|
|
1427
|
+
|
|
1428
|
+
**Methods:**
|
|
1429
|
+
|
|
1430
|
+
| Method | Description |
|
|
1431
|
+
|--------|-------------|
|
|
1432
|
+
| `fit(results, M)` | Compute bounds for given event study results |
|
|
1433
|
+
| `sensitivity_analysis(results, M_grid)` | Compute bounds over grid of M values |
|
|
1434
|
+
| `breakdown_value(results, tol)` | Find smallest M where CI includes zero |
|
|
1435
|
+
|
|
1436
|
+
### HonestDiDResults
|
|
1437
|
+
|
|
1438
|
+
**Attributes:**
|
|
1439
|
+
|
|
1440
|
+
| Attribute | Description |
|
|
1441
|
+
|-----------|-------------|
|
|
1442
|
+
| `original_estimate` | Point estimate under parallel trends |
|
|
1443
|
+
| `lb` | Lower bound of identified set |
|
|
1444
|
+
| `ub` | Upper bound of identified set |
|
|
1445
|
+
| `ci_lb` | Lower bound of robust confidence interval |
|
|
1446
|
+
| `ci_ub` | Upper bound of robust confidence interval |
|
|
1447
|
+
| `ci_width` | Width of robust CI |
|
|
1448
|
+
| `M` | Restriction parameter used |
|
|
1449
|
+
| `method` | Restriction method ('relative_magnitude' or 'smoothness') |
|
|
1450
|
+
| `alpha` | Significance level |
|
|
1451
|
+
| `is_significant` | True if robust CI excludes zero |
|
|
1452
|
+
|
|
1453
|
+
**Methods:**
|
|
1454
|
+
|
|
1455
|
+
| Method | Description |
|
|
1456
|
+
|--------|-------------|
|
|
1457
|
+
| `summary()` | Get formatted summary string |
|
|
1458
|
+
| `to_dict()` | Convert to dictionary |
|
|
1459
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1460
|
+
|
|
1461
|
+
### SensitivityResults
|
|
1462
|
+
|
|
1463
|
+
**Attributes:**
|
|
1464
|
+
|
|
1465
|
+
| Attribute | Description |
|
|
1466
|
+
|-----------|-------------|
|
|
1467
|
+
| `M_grid` | Array of M values analyzed |
|
|
1468
|
+
| `results` | List of HonestDiDResults for each M |
|
|
1469
|
+
| `breakdown_M` | Smallest M where CI includes zero (None if always significant) |
|
|
1470
|
+
|
|
1471
|
+
**Methods:**
|
|
1472
|
+
|
|
1473
|
+
| Method | Description |
|
|
1474
|
+
|--------|-------------|
|
|
1475
|
+
| `summary()` | Get formatted summary string |
|
|
1476
|
+
| `plot(ax)` | Plot sensitivity analysis |
|
|
1477
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1478
|
+
|
|
1305
1479
|
### Data Preparation Functions
|
|
1306
1480
|
|
|
1307
1481
|
#### generate_did_data
|
|
@@ -1525,9 +1699,23 @@ This library implements methods from the following scholarly works:
|
|
|
1525
1699
|
|
|
1526
1700
|
- **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)
|
|
1527
1701
|
|
|
1702
|
+
- **Lakens, D. (2017).** "Equivalence Tests: A Practical Primer for t Tests, Correlations, and Meta-Analyses." *Social Psychological and Personality Science*, 8(4), 355-362. [https://doi.org/10.1177/1948550617697177](https://doi.org/10.1177/1948550617697177)
|
|
1703
|
+
|
|
1704
|
+
### Honest DiD / Sensitivity Analysis
|
|
1705
|
+
|
|
1706
|
+
The `HonestDiD` module implements sensitivity analysis methods for relaxing the parallel trends assumption:
|
|
1707
|
+
|
|
1528
1708
|
- **Rambachan, A., & Roth, J. (2023).** "A More Credible Approach to Parallel Trends." *The Review of Economic Studies*, 90(5), 2555-2591. [https://doi.org/10.1093/restud/rdad018](https://doi.org/10.1093/restud/rdad018)
|
|
1529
1709
|
|
|
1530
|
-
|
|
1710
|
+
This paper introduces the "Honest DiD" framework implemented in our `HonestDiD` class:
|
|
1711
|
+
- **Relative Magnitudes (ΔRM)**: Bounds post-treatment violations by a multiple of observed pre-treatment violations
|
|
1712
|
+
- **Smoothness (ΔSD)**: Bounds on second differences of trend violations, allowing for linear extrapolation of pre-trends
|
|
1713
|
+
- **Breakdown Analysis**: Finding the smallest violation magnitude that would overturn conclusions
|
|
1714
|
+
- **Robust Confidence Intervals**: Valid inference under partial identification
|
|
1715
|
+
|
|
1716
|
+
- **Roth, J., & Sant'Anna, P. H. C. (2023).** "When Is Parallel Trends Sensitive to Functional Form?" *Econometrica*, 91(2), 737-747. [https://doi.org/10.3982/ECTA19402](https://doi.org/10.3982/ECTA19402)
|
|
1717
|
+
|
|
1718
|
+
Discusses functional form sensitivity in parallel trends assumptions, relevant to understanding when smoothness restrictions are appropriate.
|
|
1531
1719
|
|
|
1532
1720
|
### Multi-Period and Staggered Adoption
|
|
1533
1721
|
|
|
@@ -75,8 +75,21 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
75
75
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
76
76
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
77
77
|
- **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
|
|
78
|
+
- **Honest DiD sensitivity analysis**: Rambachan-Roth (2023) bounds and breakdown analysis for parallel trends violations
|
|
78
79
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
79
80
|
|
|
81
|
+
## Tutorials
|
|
82
|
+
|
|
83
|
+
We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
84
|
+
|
|
85
|
+
| Notebook | Description |
|
|
86
|
+
|----------|-------------|
|
|
87
|
+
| `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 |
|
|
89
|
+
| `03_synthetic_did.ipynb` | Synthetic DiD, unit/time weights, inference methods, regularization |
|
|
90
|
+
| `04_parallel_trends.ipynb` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
|
|
91
|
+
| `05_honest_did.ipynb` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
|
|
92
|
+
|
|
80
93
|
## Data Preparation
|
|
81
94
|
|
|
82
95
|
diff-diff provides utility functions to help prepare your data for DiD analysis. These functions handle common data transformation tasks like creating treatment indicators, reshaping panel data, and validating data formats.
|
|
@@ -685,14 +698,31 @@ CallawaySantAnna(
|
|
|
685
698
|
estimation_method='dr', # 'dr', 'ipw', or 'reg'
|
|
686
699
|
alpha=0.05, # Significance level
|
|
687
700
|
cluster=None, # Column for cluster SEs
|
|
688
|
-
n_bootstrap=0, #
|
|
701
|
+
n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
|
|
689
702
|
seed=None # Random seed
|
|
690
703
|
)
|
|
691
704
|
```
|
|
692
705
|
|
|
706
|
+
**Covariate adjustment for conditional parallel trends:**
|
|
707
|
+
|
|
708
|
+
When parallel trends only holds conditional on covariates, use the `covariates` parameter:
|
|
709
|
+
|
|
710
|
+
```python
|
|
711
|
+
# Doubly robust estimation with covariates
|
|
712
|
+
cs = CallawaySantAnna(estimation_method='dr') # 'dr', 'ipw', or 'reg'
|
|
713
|
+
results = cs.fit(
|
|
714
|
+
data,
|
|
715
|
+
outcome='sales',
|
|
716
|
+
unit='firm_id',
|
|
717
|
+
time='year',
|
|
718
|
+
first_treat='first_treat',
|
|
719
|
+
covariates=['size', 'age', 'industry'], # Covariates for conditional PT
|
|
720
|
+
aggregate='event_study'
|
|
721
|
+
)
|
|
722
|
+
```
|
|
723
|
+
|
|
693
724
|
**Current limitations:**
|
|
694
|
-
- Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
|
|
695
|
-
- Covariate adjustment for conditional parallel trends is not yet implemented
|
|
725
|
+
- Bootstrap inference (`n_bootstrap > 0`) is not yet fully implemented
|
|
696
726
|
|
|
697
727
|
### Event Study Visualization
|
|
698
728
|
|
|
@@ -969,6 +999,81 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
|
969
999
|
print(f"Trends equivalent: {results['equivalent']}")
|
|
970
1000
|
```
|
|
971
1001
|
|
|
1002
|
+
### Honest DiD Sensitivity Analysis (Rambachan-Roth)
|
|
1003
|
+
|
|
1004
|
+
Pre-trends tests have low power and can exacerbate bias. **Honest DiD** (Rambachan & Roth 2023) provides sensitivity analysis showing how robust your results are to violations of parallel trends.
|
|
1005
|
+
|
|
1006
|
+
```python
|
|
1007
|
+
from diff_diff import HonestDiD, MultiPeriodDiD
|
|
1008
|
+
|
|
1009
|
+
# First, fit a standard event study
|
|
1010
|
+
did = MultiPeriodDiD()
|
|
1011
|
+
event_results = did.fit(
|
|
1012
|
+
data,
|
|
1013
|
+
outcome='outcome',
|
|
1014
|
+
treatment='treated',
|
|
1015
|
+
time='period',
|
|
1016
|
+
post_periods=[5, 6, 7, 8, 9]
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
# Compute honest bounds with relative magnitudes restriction
|
|
1020
|
+
# M=1 means post-treatment violations can be up to 1x the worst pre-treatment violation
|
|
1021
|
+
honest = HonestDiD(method='relative_magnitude', M=1.0)
|
|
1022
|
+
honest_results = honest.fit(event_results)
|
|
1023
|
+
|
|
1024
|
+
print(honest_results.summary())
|
|
1025
|
+
print(f"Original estimate: {honest_results.original_estimate:.4f}")
|
|
1026
|
+
print(f"Robust 95% CI: [{honest_results.ci_lb:.4f}, {honest_results.ci_ub:.4f}]")
|
|
1027
|
+
print(f"Effect robust to violations: {honest_results.is_significant}")
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
**Sensitivity analysis over M values:**
|
|
1031
|
+
|
|
1032
|
+
```python
|
|
1033
|
+
# How do results change as we allow larger violations?
|
|
1034
|
+
sensitivity = honest.sensitivity_analysis(
|
|
1035
|
+
event_results,
|
|
1036
|
+
M_grid=[0, 0.5, 1.0, 1.5, 2.0]
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
print(sensitivity.summary())
|
|
1040
|
+
print(f"Breakdown value: M = {sensitivity.breakdown_M}")
|
|
1041
|
+
# Breakdown = smallest M where the robust CI includes zero
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
**Breakdown value:**
|
|
1045
|
+
|
|
1046
|
+
The breakdown value tells you how robust your conclusion is:
|
|
1047
|
+
|
|
1048
|
+
```python
|
|
1049
|
+
breakdown = honest.breakdown_value(event_results)
|
|
1050
|
+
if breakdown >= 1.0:
|
|
1051
|
+
print("Result holds even if post-treatment violations are as bad as pre-treatment")
|
|
1052
|
+
else:
|
|
1053
|
+
print(f"Result requires violations smaller than {breakdown:.1f}x pre-treatment")
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
**Smoothness restriction (alternative approach):**
|
|
1057
|
+
|
|
1058
|
+
```python
|
|
1059
|
+
# Bounds second differences of trend violations
|
|
1060
|
+
# M=0 means linear extrapolation of pre-trends
|
|
1061
|
+
honest_smooth = HonestDiD(method='smoothness', M=0.5)
|
|
1062
|
+
smooth_results = honest_smooth.fit(event_results)
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Visualization:**
|
|
1066
|
+
|
|
1067
|
+
```python
|
|
1068
|
+
from diff_diff import plot_sensitivity, plot_honest_event_study
|
|
1069
|
+
|
|
1070
|
+
# Plot sensitivity analysis
|
|
1071
|
+
plot_sensitivity(sensitivity, title="Sensitivity to Parallel Trends Violations")
|
|
1072
|
+
|
|
1073
|
+
# Event study with honest confidence intervals
|
|
1074
|
+
plot_honest_event_study(event_results, honest_results)
|
|
1075
|
+
```
|
|
1076
|
+
|
|
972
1077
|
### Placebo Tests
|
|
973
1078
|
|
|
974
1079
|
Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
|
|
@@ -1267,6 +1372,75 @@ SyntheticDiD(
|
|
|
1267
1372
|
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
1268
1373
|
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
1269
1374
|
|
|
1375
|
+
### HonestDiD
|
|
1376
|
+
|
|
1377
|
+
```python
|
|
1378
|
+
HonestDiD(
|
|
1379
|
+
method='relative_magnitude', # 'relative_magnitude' or 'smoothness'
|
|
1380
|
+
M=None, # Restriction parameter (default: 1.0 for RM, 0.0 for SD)
|
|
1381
|
+
alpha=0.05, # Significance level for CIs
|
|
1382
|
+
l_vec=None # Linear combination vector for target parameter
|
|
1383
|
+
)
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
**fit() Parameters:**
|
|
1387
|
+
|
|
1388
|
+
| Parameter | Type | Description |
|
|
1389
|
+
|-----------|------|-------------|
|
|
1390
|
+
| `results` | MultiPeriodDiDResults | Results from MultiPeriodDiD.fit() |
|
|
1391
|
+
| `M` | float | Restriction parameter (overrides constructor value) |
|
|
1392
|
+
|
|
1393
|
+
**Methods:**
|
|
1394
|
+
|
|
1395
|
+
| Method | Description |
|
|
1396
|
+
|--------|-------------|
|
|
1397
|
+
| `fit(results, M)` | Compute bounds for given event study results |
|
|
1398
|
+
| `sensitivity_analysis(results, M_grid)` | Compute bounds over grid of M values |
|
|
1399
|
+
| `breakdown_value(results, tol)` | Find smallest M where CI includes zero |
|
|
1400
|
+
|
|
1401
|
+
### HonestDiDResults
|
|
1402
|
+
|
|
1403
|
+
**Attributes:**
|
|
1404
|
+
|
|
1405
|
+
| Attribute | Description |
|
|
1406
|
+
|-----------|-------------|
|
|
1407
|
+
| `original_estimate` | Point estimate under parallel trends |
|
|
1408
|
+
| `lb` | Lower bound of identified set |
|
|
1409
|
+
| `ub` | Upper bound of identified set |
|
|
1410
|
+
| `ci_lb` | Lower bound of robust confidence interval |
|
|
1411
|
+
| `ci_ub` | Upper bound of robust confidence interval |
|
|
1412
|
+
| `ci_width` | Width of robust CI |
|
|
1413
|
+
| `M` | Restriction parameter used |
|
|
1414
|
+
| `method` | Restriction method ('relative_magnitude' or 'smoothness') |
|
|
1415
|
+
| `alpha` | Significance level |
|
|
1416
|
+
| `is_significant` | True if robust CI excludes zero |
|
|
1417
|
+
|
|
1418
|
+
**Methods:**
|
|
1419
|
+
|
|
1420
|
+
| Method | Description |
|
|
1421
|
+
|--------|-------------|
|
|
1422
|
+
| `summary()` | Get formatted summary string |
|
|
1423
|
+
| `to_dict()` | Convert to dictionary |
|
|
1424
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1425
|
+
|
|
1426
|
+
### SensitivityResults
|
|
1427
|
+
|
|
1428
|
+
**Attributes:**
|
|
1429
|
+
|
|
1430
|
+
| Attribute | Description |
|
|
1431
|
+
|-----------|-------------|
|
|
1432
|
+
| `M_grid` | Array of M values analyzed |
|
|
1433
|
+
| `results` | List of HonestDiDResults for each M |
|
|
1434
|
+
| `breakdown_M` | Smallest M where CI includes zero (None if always significant) |
|
|
1435
|
+
|
|
1436
|
+
**Methods:**
|
|
1437
|
+
|
|
1438
|
+
| Method | Description |
|
|
1439
|
+
|--------|-------------|
|
|
1440
|
+
| `summary()` | Get formatted summary string |
|
|
1441
|
+
| `plot(ax)` | Plot sensitivity analysis |
|
|
1442
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
1443
|
+
|
|
1270
1444
|
### Data Preparation Functions
|
|
1271
1445
|
|
|
1272
1446
|
#### generate_did_data
|
|
@@ -1490,9 +1664,23 @@ This library implements methods from the following scholarly works:
|
|
|
1490
1664
|
|
|
1491
1665
|
- **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)
|
|
1492
1666
|
|
|
1667
|
+
- **Lakens, D. (2017).** "Equivalence Tests: A Practical Primer for t Tests, Correlations, and Meta-Analyses." *Social Psychological and Personality Science*, 8(4), 355-362. [https://doi.org/10.1177/1948550617697177](https://doi.org/10.1177/1948550617697177)
|
|
1668
|
+
|
|
1669
|
+
### Honest DiD / Sensitivity Analysis
|
|
1670
|
+
|
|
1671
|
+
The `HonestDiD` module implements sensitivity analysis methods for relaxing the parallel trends assumption:
|
|
1672
|
+
|
|
1493
1673
|
- **Rambachan, A., & Roth, J. (2023).** "A More Credible Approach to Parallel Trends." *The Review of Economic Studies*, 90(5), 2555-2591. [https://doi.org/10.1093/restud/rdad018](https://doi.org/10.1093/restud/rdad018)
|
|
1494
1674
|
|
|
1495
|
-
|
|
1675
|
+
This paper introduces the "Honest DiD" framework implemented in our `HonestDiD` class:
|
|
1676
|
+
- **Relative Magnitudes (ΔRM)**: Bounds post-treatment violations by a multiple of observed pre-treatment violations
|
|
1677
|
+
- **Smoothness (ΔSD)**: Bounds on second differences of trend violations, allowing for linear extrapolation of pre-trends
|
|
1678
|
+
- **Breakdown Analysis**: Finding the smallest violation magnitude that would overturn conclusions
|
|
1679
|
+
- **Robust Confidence Intervals**: Valid inference under partial identification
|
|
1680
|
+
|
|
1681
|
+
- **Roth, J., & Sant'Anna, P. H. C. (2023).** "When Is Parallel Trends Sensitive to Functional Form?" *Econometrica*, 91(2), 737-747. [https://doi.org/10.3982/ECTA19402](https://doi.org/10.3982/ECTA19402)
|
|
1682
|
+
|
|
1683
|
+
Discusses functional form sensitivity in parallel trends assumptions, relevant to understanding when smoothness restrictions are appropriate.
|
|
1496
1684
|
|
|
1497
1685
|
### Multi-Period and Staggered Adoption
|
|
1498
1686
|
|
|
@@ -5,16 +5,42 @@ This library provides sklearn-like estimators for causal inference
|
|
|
5
5
|
using the difference-in-differences methodology.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from diff_diff.diagnostics import (
|
|
9
|
+
PlaceboTestResults,
|
|
10
|
+
leave_one_out_test,
|
|
11
|
+
permutation_test,
|
|
12
|
+
placebo_group_test,
|
|
13
|
+
placebo_timing_test,
|
|
14
|
+
run_all_placebo_tests,
|
|
15
|
+
run_placebo_test,
|
|
16
|
+
)
|
|
8
17
|
from diff_diff.estimators import (
|
|
9
18
|
DifferenceInDifferences,
|
|
10
|
-
TwoWayFixedEffects,
|
|
11
19
|
MultiPeriodDiD,
|
|
12
20
|
SyntheticDiD,
|
|
21
|
+
TwoWayFixedEffects,
|
|
13
22
|
)
|
|
14
|
-
from diff_diff.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
from diff_diff.honest_did import (
|
|
24
|
+
DeltaRM,
|
|
25
|
+
DeltaSD,
|
|
26
|
+
DeltaSDRM,
|
|
27
|
+
HonestDiD,
|
|
28
|
+
HonestDiDResults,
|
|
29
|
+
SensitivityResults,
|
|
30
|
+
compute_honest_did,
|
|
31
|
+
sensitivity_plot,
|
|
32
|
+
)
|
|
33
|
+
from diff_diff.prep import (
|
|
34
|
+
aggregate_to_cohorts,
|
|
35
|
+
balance_panel,
|
|
36
|
+
create_event_time,
|
|
37
|
+
generate_did_data,
|
|
38
|
+
make_post_indicator,
|
|
39
|
+
make_treatment_indicator,
|
|
40
|
+
rank_control_units,
|
|
41
|
+
summarize_did_data,
|
|
42
|
+
validate_did_data,
|
|
43
|
+
wide_to_long,
|
|
18
44
|
)
|
|
19
45
|
from diff_diff.results import (
|
|
20
46
|
DiDResults,
|
|
@@ -22,40 +48,26 @@ from diff_diff.results import (
|
|
|
22
48
|
PeriodEffect,
|
|
23
49
|
SyntheticDiDResults,
|
|
24
50
|
)
|
|
25
|
-
from diff_diff.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
from diff_diff.prep import (
|
|
30
|
-
make_treatment_indicator,
|
|
31
|
-
make_post_indicator,
|
|
32
|
-
wide_to_long,
|
|
33
|
-
balance_panel,
|
|
34
|
-
validate_did_data,
|
|
35
|
-
summarize_did_data,
|
|
36
|
-
generate_did_data,
|
|
37
|
-
create_event_time,
|
|
38
|
-
aggregate_to_cohorts,
|
|
39
|
-
rank_control_units,
|
|
51
|
+
from diff_diff.staggered import (
|
|
52
|
+
CallawaySantAnna,
|
|
53
|
+
CallawaySantAnnaResults,
|
|
54
|
+
GroupTimeEffect,
|
|
40
55
|
)
|
|
41
56
|
from diff_diff.utils import (
|
|
57
|
+
WildBootstrapResults,
|
|
42
58
|
check_parallel_trends,
|
|
43
59
|
check_parallel_trends_robust,
|
|
44
60
|
equivalence_test_trends,
|
|
45
|
-
WildBootstrapResults,
|
|
46
61
|
wild_bootstrap_se,
|
|
47
62
|
)
|
|
48
|
-
from diff_diff.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
permutation_test,
|
|
54
|
-
leave_one_out_test,
|
|
55
|
-
run_all_placebo_tests,
|
|
63
|
+
from diff_diff.visualization import (
|
|
64
|
+
plot_event_study,
|
|
65
|
+
plot_group_effects,
|
|
66
|
+
plot_honest_event_study,
|
|
67
|
+
plot_sensitivity,
|
|
56
68
|
)
|
|
57
69
|
|
|
58
|
-
__version__ = "0.
|
|
70
|
+
__version__ = "0.6.0"
|
|
59
71
|
__all__ = [
|
|
60
72
|
# Estimators
|
|
61
73
|
"DifferenceInDifferences",
|
|
@@ -73,6 +85,8 @@ __all__ = [
|
|
|
73
85
|
# Visualization
|
|
74
86
|
"plot_event_study",
|
|
75
87
|
"plot_group_effects",
|
|
88
|
+
"plot_sensitivity",
|
|
89
|
+
"plot_honest_event_study",
|
|
76
90
|
# Parallel trends testing
|
|
77
91
|
"check_parallel_trends",
|
|
78
92
|
"check_parallel_trends_robust",
|
|
@@ -99,4 +113,13 @@ __all__ = [
|
|
|
99
113
|
"create_event_time",
|
|
100
114
|
"aggregate_to_cohorts",
|
|
101
115
|
"rank_control_units",
|
|
116
|
+
# Honest DiD sensitivity analysis
|
|
117
|
+
"HonestDiD",
|
|
118
|
+
"HonestDiDResults",
|
|
119
|
+
"SensitivityResults",
|
|
120
|
+
"DeltaSD",
|
|
121
|
+
"DeltaRM",
|
|
122
|
+
"DeltaSDRM",
|
|
123
|
+
"compute_honest_did",
|
|
124
|
+
"sensitivity_plot",
|
|
102
125
|
]
|
|
@@ -603,6 +603,7 @@ class DifferenceInDifferences:
|
|
|
603
603
|
"""
|
|
604
604
|
if not self.is_fitted_:
|
|
605
605
|
raise RuntimeError("Model must be fitted before calling summary()")
|
|
606
|
+
assert self.results_ is not None
|
|
606
607
|
return self.results_.summary()
|
|
607
608
|
|
|
608
609
|
def print_summary(self) -> None:
|
|
@@ -641,7 +642,7 @@ class TwoWayFixedEffects(DifferenceInDifferences):
|
|
|
641
642
|
staggered designs.
|
|
642
643
|
"""
|
|
643
644
|
|
|
644
|
-
def fit(
|
|
645
|
+
def fit( # type: ignore[override]
|
|
645
646
|
self,
|
|
646
647
|
data: pd.DataFrame,
|
|
647
648
|
outcome: str,
|
|
@@ -763,7 +764,7 @@ class TwoWayFixedEffects(DifferenceInDifferences):
|
|
|
763
764
|
n_treated=n_treated,
|
|
764
765
|
n_control=n_control,
|
|
765
766
|
alpha=self.alpha,
|
|
766
|
-
coefficients={"ATT": att},
|
|
767
|
+
coefficients={"ATT": float(att)},
|
|
767
768
|
vcov=vcov,
|
|
768
769
|
residuals=residuals,
|
|
769
770
|
fitted_values=fitted,
|
|
@@ -892,7 +893,7 @@ class MultiPeriodDiD(DifferenceInDifferences):
|
|
|
892
893
|
The average ATT is computed as the mean of the δ_t coefficients.
|
|
893
894
|
"""
|
|
894
895
|
|
|
895
|
-
def fit(
|
|
896
|
+
def fit( # type: ignore[override]
|
|
896
897
|
self,
|
|
897
898
|
data: pd.DataFrame,
|
|
898
899
|
outcome: str,
|
|
@@ -1166,6 +1167,7 @@ class MultiPeriodDiD(DifferenceInDifferences):
|
|
|
1166
1167
|
"""
|
|
1167
1168
|
if not self.is_fitted_:
|
|
1168
1169
|
raise RuntimeError("Model must be fitted before calling summary()")
|
|
1170
|
+
assert self.results_ is not None
|
|
1169
1171
|
return self.results_.summary()
|
|
1170
1172
|
|
|
1171
1173
|
|
|
@@ -1284,7 +1286,7 @@ class SyntheticDiD(DifferenceInDifferences):
|
|
|
1284
1286
|
self._unit_weights = None
|
|
1285
1287
|
self._time_weights = None
|
|
1286
1288
|
|
|
1287
|
-
def fit(
|
|
1289
|
+
def fit( # type: ignore[override]
|
|
1288
1290
|
self,
|
|
1289
1291
|
data: pd.DataFrame,
|
|
1290
1292
|
outcome: str,
|