diff-diff 0.4.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.
Files changed (27) hide show
  1. {diff_diff-0.4.0 → diff_diff-0.6.0}/PKG-INFO +361 -15
  2. {diff_diff-0.4.0 → diff_diff-0.6.0}/README.md +359 -13
  3. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/__init__.py +66 -21
  4. diff_diff-0.6.0/diff_diff/diagnostics.py +888 -0
  5. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/estimators.py +152 -38
  6. diff_diff-0.6.0/diff_diff/honest_did.py +1491 -0
  7. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/prep.py +1 -1
  8. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/results.py +26 -7
  9. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/staggered.py +341 -52
  10. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/utils.py +393 -17
  11. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff/visualization.py +350 -1
  12. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff.egg-info/PKG-INFO +361 -15
  13. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff.egg-info/SOURCES.txt +7 -1
  14. {diff_diff-0.4.0 → diff_diff-0.6.0}/pyproject.toml +11 -3
  15. diff_diff-0.6.0/tests/test_diagnostics.py +674 -0
  16. {diff_diff-0.4.0 → diff_diff-0.6.0}/tests/test_estimators.py +5 -3
  17. diff_diff-0.6.0/tests/test_honest_did.py +699 -0
  18. diff_diff-0.6.0/tests/test_staggered.py +752 -0
  19. diff_diff-0.6.0/tests/test_utils.py +1270 -0
  20. diff_diff-0.6.0/tests/test_wild_bootstrap.py +623 -0
  21. diff_diff-0.4.0/tests/test_staggered.py +0 -390
  22. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff.egg-info/dependency_links.txt +0 -0
  23. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff.egg-info/requires.txt +0 -0
  24. {diff_diff-0.4.0 → diff_diff-0.6.0}/diff_diff.egg-info/top_level.txt +0 -0
  25. {diff_diff-0.4.0 → diff_diff-0.6.0}/setup.cfg +0 -0
  26. {diff_diff-0.4.0 → diff_diff-0.6.0}/tests/test_prep.py +7 -7
  27. {diff_diff-0.4.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.4.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://github.com/igerber/diff-diff#readme
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
@@ -69,28 +69,28 @@ did = DifferenceInDifferences()
69
69
  results = did.fit(data, outcome='outcome', treatment='treated', time='post')
70
70
 
71
71
  # View results
72
- print(results) # DiDResults(ATT=3.5000*, SE=1.2583, p=0.0367)
72
+ print(results) # DiDResults(ATT=3.0000, SE=1.7321, p=0.1583)
73
73
  results.print_summary()
74
74
  ```
75
75
 
76
76
  Output:
77
77
  ```
78
78
  ======================================================================
79
- Difference-in-Differences Estimation Results
79
+ Difference-in-Differences Estimation Results
80
80
  ======================================================================
81
81
 
82
- Observations: 8
83
- Treated units: 4
84
- Control units: 4
85
- R-squared: 0.9123
82
+ Observations: 8
83
+ Treated units: 4
84
+ Control units: 4
85
+ R-squared: 0.9055
86
86
 
87
87
  ----------------------------------------------------------------------
88
- Parameter Estimate Std. Err. t-stat P>|t|
88
+ Parameter Estimate Std. Err. t-stat P>|t|
89
89
  ----------------------------------------------------------------------
90
- ATT 3.5000 1.2583 2.782 0.0367
90
+ ATT 3.0000 1.7321 1.732 0.1583
91
91
  ----------------------------------------------------------------------
92
92
 
93
- 95% Confidence Interval: [0.3912, 6.6088]
93
+ 95% Confidence Interval: [-1.8089, 7.8089]
94
94
 
95
95
  Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
96
96
  ======================================================================
@@ -102,14 +102,29 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
102
102
  - **Pythonic results**: Easy access to coefficients, standard errors, and confidence intervals
103
103
  - **Multiple interfaces**: Column names or R-style formulas
104
104
  - **Robust inference**: Heteroskedasticity-robust (HC1) and cluster-robust standard errors
105
+ - **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
105
106
  - **Panel data support**: Two-way fixed effects estimator for panel designs
106
107
  - **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
107
108
  - **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
108
109
  - **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
109
110
  - **Event study plots**: Publication-ready visualization of treatment effects
110
111
  - **Parallel trends testing**: Multiple methods including equivalence tests
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
111
114
  - **Data prep utilities**: Helper functions for common data preparation tasks
112
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
+
113
128
  ## Data Preparation
114
129
 
115
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.
@@ -523,6 +538,36 @@ results = did.fit(
523
538
  )
524
539
  ```
525
540
 
541
+ ### Wild Cluster Bootstrap
542
+
543
+ When you have few clusters (<50), standard cluster-robust SEs are biased. Wild cluster bootstrap provides valid inference even with 5-10 clusters.
544
+
545
+ ```python
546
+ # Use wild bootstrap for inference
547
+ did = DifferenceInDifferences(
548
+ cluster='state',
549
+ inference='wild_bootstrap',
550
+ n_bootstrap=999,
551
+ bootstrap_weights='rademacher', # or 'webb' for <10 clusters, 'mammen'
552
+ seed=42
553
+ )
554
+ results = did.fit(data, outcome='y', treatment='treated', time='post')
555
+
556
+ # Results include bootstrap-based SE and p-value
557
+ print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
558
+ print(f"P-value: {results.p_value:.4f}")
559
+ print(f"95% CI: {results.conf_int}")
560
+ print(f"Inference method: {results.inference_method}")
561
+ print(f"Number of clusters: {results.n_clusters}")
562
+ ```
563
+
564
+ **Weight types:**
565
+ - `'rademacher'` - Default, ±1 with p=0.5, good for most cases
566
+ - `'webb'` - 6-point distribution, recommended for <10 clusters
567
+ - `'mammen'` - Two-point distribution, alternative to Rademacher
568
+
569
+ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
570
+
526
571
  ### Two-Way Fixed Effects (Panel Data)
527
572
 
528
573
  ```python
@@ -688,14 +733,31 @@ CallawaySantAnna(
688
733
  estimation_method='dr', # 'dr', 'ipw', or 'reg'
689
734
  alpha=0.05, # Significance level
690
735
  cluster=None, # Column for cluster SEs
691
- n_bootstrap=0, # Must be 0 (bootstrap not yet implemented)
736
+ n_bootstrap=0, # Bootstrap iterations (0 = analytical SEs)
692
737
  seed=None # Random seed
693
738
  )
694
739
  ```
695
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
+
696
759
  **Current limitations:**
697
- - Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
698
- - Covariate adjustment for conditional parallel trends is not yet implemented
760
+ - Bootstrap inference (`n_bootstrap > 0`) is not yet fully implemented
699
761
 
700
762
  ### Event Study Visualization
701
763
 
@@ -972,6 +1034,195 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
972
1034
  print(f"Trends equivalent: {results['equivalent']}")
973
1035
  ```
974
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
+
1112
+ ### Placebo Tests
1113
+
1114
+ Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
1115
+
1116
+ **Fake timing test:**
1117
+
1118
+ ```python
1119
+ from diff_diff import run_placebo_test
1120
+
1121
+ # Test: Is there an effect before treatment actually occurred?
1122
+ # Actual treatment is at period 3 (post_periods=[3, 4, 5])
1123
+ # We test if a "fake" treatment at period 1 shows an effect
1124
+ results = run_placebo_test(
1125
+ data,
1126
+ outcome='outcome',
1127
+ treatment='treated',
1128
+ time='period',
1129
+ test_type='fake_timing',
1130
+ fake_treatment_period=1, # Pretend treatment was in period 1
1131
+ post_periods=[3, 4, 5] # Actual post-treatment periods
1132
+ )
1133
+
1134
+ print(results.summary())
1135
+ # If parallel trends hold, placebo_effect should be ~0 and not significant
1136
+ print(f"Placebo effect: {results.placebo_effect:.3f} (p={results.p_value:.3f})")
1137
+ print(f"Is significant (bad): {results.is_significant}")
1138
+ ```
1139
+
1140
+ **Fake group test:**
1141
+
1142
+ ```python
1143
+ # Test: Is there an effect among never-treated units?
1144
+ # Get some control unit IDs to use as "fake treated"
1145
+ control_units = data[data['treated'] == 0]['firm_id'].unique()[:5]
1146
+
1147
+ results = run_placebo_test(
1148
+ data,
1149
+ outcome='outcome',
1150
+ treatment='treated',
1151
+ time='period',
1152
+ unit='firm_id',
1153
+ test_type='fake_group',
1154
+ fake_treatment_group=list(control_units), # List of control unit IDs
1155
+ post_periods=[3, 4, 5]
1156
+ )
1157
+ ```
1158
+
1159
+ **Permutation test:**
1160
+
1161
+ ```python
1162
+ # Randomly reassign treatment and compute distribution of effects
1163
+ # Note: requires binary post indicator (use 'post' column, not 'period')
1164
+ results = run_placebo_test(
1165
+ data,
1166
+ outcome='outcome',
1167
+ treatment='treated',
1168
+ time='post', # Binary post-treatment indicator
1169
+ unit='firm_id',
1170
+ test_type='permutation',
1171
+ n_permutations=1000,
1172
+ seed=42
1173
+ )
1174
+
1175
+ print(f"Original effect: {results.original_effect:.3f}")
1176
+ print(f"Permutation p-value: {results.p_value:.4f}")
1177
+ # Low p-value indicates the effect is unlikely to be due to chance
1178
+ ```
1179
+
1180
+ **Leave-one-out sensitivity:**
1181
+
1182
+ ```python
1183
+ # Test sensitivity to individual treated units
1184
+ # Note: requires binary post indicator (use 'post' column, not 'period')
1185
+ results = run_placebo_test(
1186
+ data,
1187
+ outcome='outcome',
1188
+ treatment='treated',
1189
+ time='post', # Binary post-treatment indicator
1190
+ unit='firm_id',
1191
+ test_type='leave_one_out'
1192
+ )
1193
+
1194
+ # Check if any single unit drives the result
1195
+ print(results.leave_one_out_effects) # Effect when each unit is dropped
1196
+ ```
1197
+
1198
+ **Run all placebo tests:**
1199
+
1200
+ ```python
1201
+ from diff_diff import run_all_placebo_tests
1202
+
1203
+ # Comprehensive diagnostic suite
1204
+ # Note: This function runs fake_timing tests on pre-treatment periods.
1205
+ # The permutation and leave_one_out tests require a binary post indicator,
1206
+ # so they may return errors if the data uses multi-period time column.
1207
+ all_results = run_all_placebo_tests(
1208
+ data,
1209
+ outcome='outcome',
1210
+ treatment='treated',
1211
+ time='period',
1212
+ unit='firm_id',
1213
+ pre_periods=[0, 1, 2],
1214
+ post_periods=[3, 4, 5],
1215
+ n_permutations=500,
1216
+ seed=42
1217
+ )
1218
+
1219
+ for test_name, result in all_results.items():
1220
+ if hasattr(result, 'p_value'):
1221
+ print(f"{test_name}: p={result.p_value:.3f}, significant={result.is_significant}")
1222
+ elif isinstance(result, dict) and 'error' in result:
1223
+ print(f"{test_name}: Error - {result['error']}")
1224
+ ```
1225
+
975
1226
  ## API Reference
976
1227
 
977
1228
  ### DifferenceInDifferences
@@ -1156,6 +1407,75 @@ SyntheticDiD(
1156
1407
  | `get_unit_weights_df()` | Get unit weights as DataFrame |
1157
1408
  | `get_time_weights_df()` | Get time weights as DataFrame |
1158
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
+
1159
1479
  ### Data Preparation Functions
1160
1480
 
1161
1481
  #### generate_did_data
@@ -1351,6 +1671,18 @@ This library implements methods from the following scholarly works:
1351
1671
 
1352
1672
  - **Cameron, A. C., Gelbach, J. B., & Miller, D. L. (2011).** "Robust Inference With Multiway Clustering." *Journal of Business & Economic Statistics*, 29(2), 238-249. [https://doi.org/10.1198/jbes.2010.07136](https://doi.org/10.1198/jbes.2010.07136)
1353
1673
 
1674
+ ### Wild Cluster Bootstrap
1675
+
1676
+ - **Cameron, A. C., Gelbach, J. B., & Miller, D. L. (2008).** "Bootstrap-Based Improvements for Inference with Clustered Errors." *The Review of Economics and Statistics*, 90(3), 414-427. [https://doi.org/10.1162/rest.90.3.414](https://doi.org/10.1162/rest.90.3.414)
1677
+
1678
+ - **Webb, M. D. (2014).** "Reworking Wild Bootstrap Based Inference for Clustered Errors." Queen's Economics Department Working Paper No. 1315. [https://www.econ.queensu.ca/sites/econ.queensu.ca/files/qed_wp_1315.pdf](https://www.econ.queensu.ca/sites/econ.queensu.ca/files/qed_wp_1315.pdf)
1679
+
1680
+ - **MacKinnon, J. G., & Webb, M. D. (2018).** "The Wild Bootstrap for Few (Treated) Clusters." *The Econometrics Journal*, 21(2), 114-135. [https://doi.org/10.1111/ectj.12107](https://doi.org/10.1111/ectj.12107)
1681
+
1682
+ ### Placebo Tests and DiD Diagnostics
1683
+
1684
+ - **Bertrand, M., Duflo, E., & Mullainathan, S. (2004).** "How Much Should We Trust Differences-in-Differences Estimates?" *The Quarterly Journal of Economics*, 119(1), 249-275. [https://doi.org/10.1162/003355304772839588](https://doi.org/10.1162/003355304772839588)
1685
+
1354
1686
  ### Synthetic Control Method
1355
1687
 
1356
1688
  - **Abadie, A., & Gardeazabal, J. (2003).** "The Economic Costs of Conflict: A Case Study of the Basque Country." *The American Economic Review*, 93(1), 113-132. [https://doi.org/10.1257/000282803321455188](https://doi.org/10.1257/000282803321455188)
@@ -1367,9 +1699,23 @@ This library implements methods from the following scholarly works:
1367
1699
 
1368
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)
1369
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
+
1370
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)
1371
1709
 
1372
- - **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)
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.
1373
1719
 
1374
1720
  ### Multi-Period and Staggered Adoption
1375
1721