diff-diff 0.4.0__tar.gz → 0.5.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.4.0 → diff_diff-0.5.0}/PKG-INFO +168 -10
- {diff_diff-0.4.0 → diff_diff-0.5.0}/README.md +167 -9
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/__init__.py +23 -1
- diff_diff-0.5.0/diff_diff/diagnostics.py +885 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/estimators.py +146 -34
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/results.py +26 -7
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/utils.py +379 -3
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/PKG-INFO +168 -10
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/SOURCES.txt +4 -1
- {diff_diff-0.4.0 → diff_diff-0.5.0}/pyproject.toml +1 -1
- diff_diff-0.5.0/tests/test_diagnostics.py +677 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_estimators.py +4 -2
- diff_diff-0.5.0/tests/test_wild_bootstrap.py +624 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/prep.py +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/staggered.py +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/visualization.py +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/setup.cfg +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_prep.py +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_staggered.py +0 -0
- {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_visualization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
5
|
Author: diff-diff contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -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.
|
|
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
|
-
|
|
79
|
+
Difference-in-Differences Estimation Results
|
|
80
80
|
======================================================================
|
|
81
81
|
|
|
82
|
-
Observations:
|
|
83
|
-
Treated units:
|
|
84
|
-
Control units:
|
|
85
|
-
R-squared:
|
|
82
|
+
Observations: 8
|
|
83
|
+
Treated units: 4
|
|
84
|
+
Control units: 4
|
|
85
|
+
R-squared: 0.9055
|
|
86
86
|
|
|
87
87
|
----------------------------------------------------------------------
|
|
88
|
-
Parameter
|
|
88
|
+
Parameter Estimate Std. Err. t-stat P>|t|
|
|
89
89
|
----------------------------------------------------------------------
|
|
90
|
-
ATT
|
|
90
|
+
ATT 3.0000 1.7321 1.732 0.1583
|
|
91
91
|
----------------------------------------------------------------------
|
|
92
92
|
|
|
93
|
-
95% Confidence Interval: [
|
|
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,12 +102,14 @@ 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
|
|
111
113
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
112
114
|
|
|
113
115
|
## Data Preparation
|
|
@@ -523,6 +525,36 @@ results = did.fit(
|
|
|
523
525
|
)
|
|
524
526
|
```
|
|
525
527
|
|
|
528
|
+
### Wild Cluster Bootstrap
|
|
529
|
+
|
|
530
|
+
When you have few clusters (<50), standard cluster-robust SEs are biased. Wild cluster bootstrap provides valid inference even with 5-10 clusters.
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
# Use wild bootstrap for inference
|
|
534
|
+
did = DifferenceInDifferences(
|
|
535
|
+
cluster='state',
|
|
536
|
+
inference='wild_bootstrap',
|
|
537
|
+
n_bootstrap=999,
|
|
538
|
+
bootstrap_weights='rademacher', # or 'webb' for <10 clusters, 'mammen'
|
|
539
|
+
seed=42
|
|
540
|
+
)
|
|
541
|
+
results = did.fit(data, outcome='y', treatment='treated', time='post')
|
|
542
|
+
|
|
543
|
+
# Results include bootstrap-based SE and p-value
|
|
544
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
545
|
+
print(f"P-value: {results.p_value:.4f}")
|
|
546
|
+
print(f"95% CI: {results.conf_int}")
|
|
547
|
+
print(f"Inference method: {results.inference_method}")
|
|
548
|
+
print(f"Number of clusters: {results.n_clusters}")
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Weight types:**
|
|
552
|
+
- `'rademacher'` - Default, ±1 with p=0.5, good for most cases
|
|
553
|
+
- `'webb'` - 6-point distribution, recommended for <10 clusters
|
|
554
|
+
- `'mammen'` - Two-point distribution, alternative to Rademacher
|
|
555
|
+
|
|
556
|
+
Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
|
|
557
|
+
|
|
526
558
|
### Two-Way Fixed Effects (Panel Data)
|
|
527
559
|
|
|
528
560
|
```python
|
|
@@ -972,6 +1004,120 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
|
972
1004
|
print(f"Trends equivalent: {results['equivalent']}")
|
|
973
1005
|
```
|
|
974
1006
|
|
|
1007
|
+
### Placebo Tests
|
|
1008
|
+
|
|
1009
|
+
Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
|
|
1010
|
+
|
|
1011
|
+
**Fake timing test:**
|
|
1012
|
+
|
|
1013
|
+
```python
|
|
1014
|
+
from diff_diff import run_placebo_test
|
|
1015
|
+
|
|
1016
|
+
# Test: Is there an effect before treatment actually occurred?
|
|
1017
|
+
# Actual treatment is at period 3 (post_periods=[3, 4, 5])
|
|
1018
|
+
# We test if a "fake" treatment at period 1 shows an effect
|
|
1019
|
+
results = run_placebo_test(
|
|
1020
|
+
data,
|
|
1021
|
+
outcome='outcome',
|
|
1022
|
+
treatment='treated',
|
|
1023
|
+
time='period',
|
|
1024
|
+
test_type='fake_timing',
|
|
1025
|
+
fake_treatment_period=1, # Pretend treatment was in period 1
|
|
1026
|
+
post_periods=[3, 4, 5] # Actual post-treatment periods
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
print(results.summary())
|
|
1030
|
+
# If parallel trends hold, placebo_effect should be ~0 and not significant
|
|
1031
|
+
print(f"Placebo effect: {results.placebo_effect:.3f} (p={results.p_value:.3f})")
|
|
1032
|
+
print(f"Is significant (bad): {results.is_significant}")
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
**Fake group test:**
|
|
1036
|
+
|
|
1037
|
+
```python
|
|
1038
|
+
# Test: Is there an effect among never-treated units?
|
|
1039
|
+
# Get some control unit IDs to use as "fake treated"
|
|
1040
|
+
control_units = data[data['treated'] == 0]['firm_id'].unique()[:5]
|
|
1041
|
+
|
|
1042
|
+
results = run_placebo_test(
|
|
1043
|
+
data,
|
|
1044
|
+
outcome='outcome',
|
|
1045
|
+
treatment='treated',
|
|
1046
|
+
time='period',
|
|
1047
|
+
unit='firm_id',
|
|
1048
|
+
test_type='fake_group',
|
|
1049
|
+
fake_treatment_group=list(control_units), # List of control unit IDs
|
|
1050
|
+
post_periods=[3, 4, 5]
|
|
1051
|
+
)
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
**Permutation test:**
|
|
1055
|
+
|
|
1056
|
+
```python
|
|
1057
|
+
# Randomly reassign treatment and compute distribution of effects
|
|
1058
|
+
# Note: requires binary post indicator (use 'post' column, not 'period')
|
|
1059
|
+
results = run_placebo_test(
|
|
1060
|
+
data,
|
|
1061
|
+
outcome='outcome',
|
|
1062
|
+
treatment='treated',
|
|
1063
|
+
time='post', # Binary post-treatment indicator
|
|
1064
|
+
unit='firm_id',
|
|
1065
|
+
test_type='permutation',
|
|
1066
|
+
n_permutations=1000,
|
|
1067
|
+
seed=42
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
print(f"Original effect: {results.original_effect:.3f}")
|
|
1071
|
+
print(f"Permutation p-value: {results.p_value:.4f}")
|
|
1072
|
+
# Low p-value indicates the effect is unlikely to be due to chance
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
**Leave-one-out sensitivity:**
|
|
1076
|
+
|
|
1077
|
+
```python
|
|
1078
|
+
# Test sensitivity to individual treated units
|
|
1079
|
+
# Note: requires binary post indicator (use 'post' column, not 'period')
|
|
1080
|
+
results = run_placebo_test(
|
|
1081
|
+
data,
|
|
1082
|
+
outcome='outcome',
|
|
1083
|
+
treatment='treated',
|
|
1084
|
+
time='post', # Binary post-treatment indicator
|
|
1085
|
+
unit='firm_id',
|
|
1086
|
+
test_type='leave_one_out'
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# Check if any single unit drives the result
|
|
1090
|
+
print(results.leave_one_out_effects) # Effect when each unit is dropped
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
**Run all placebo tests:**
|
|
1094
|
+
|
|
1095
|
+
```python
|
|
1096
|
+
from diff_diff import run_all_placebo_tests
|
|
1097
|
+
|
|
1098
|
+
# Comprehensive diagnostic suite
|
|
1099
|
+
# Note: This function runs fake_timing tests on pre-treatment periods.
|
|
1100
|
+
# The permutation and leave_one_out tests require a binary post indicator,
|
|
1101
|
+
# so they may return errors if the data uses multi-period time column.
|
|
1102
|
+
all_results = run_all_placebo_tests(
|
|
1103
|
+
data,
|
|
1104
|
+
outcome='outcome',
|
|
1105
|
+
treatment='treated',
|
|
1106
|
+
time='period',
|
|
1107
|
+
unit='firm_id',
|
|
1108
|
+
pre_periods=[0, 1, 2],
|
|
1109
|
+
post_periods=[3, 4, 5],
|
|
1110
|
+
n_permutations=500,
|
|
1111
|
+
seed=42
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
for test_name, result in all_results.items():
|
|
1115
|
+
if hasattr(result, 'p_value'):
|
|
1116
|
+
print(f"{test_name}: p={result.p_value:.3f}, significant={result.is_significant}")
|
|
1117
|
+
elif isinstance(result, dict) and 'error' in result:
|
|
1118
|
+
print(f"{test_name}: Error - {result['error']}")
|
|
1119
|
+
```
|
|
1120
|
+
|
|
975
1121
|
## API Reference
|
|
976
1122
|
|
|
977
1123
|
### DifferenceInDifferences
|
|
@@ -1351,6 +1497,18 @@ This library implements methods from the following scholarly works:
|
|
|
1351
1497
|
|
|
1352
1498
|
- **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
1499
|
|
|
1500
|
+
### Wild Cluster Bootstrap
|
|
1501
|
+
|
|
1502
|
+
- **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)
|
|
1503
|
+
|
|
1504
|
+
- **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)
|
|
1505
|
+
|
|
1506
|
+
- **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)
|
|
1507
|
+
|
|
1508
|
+
### Placebo Tests and DiD Diagnostics
|
|
1509
|
+
|
|
1510
|
+
- **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)
|
|
1511
|
+
|
|
1354
1512
|
### Synthetic Control Method
|
|
1355
1513
|
|
|
1356
1514
|
- **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)
|
|
@@ -34,28 +34,28 @@ did = DifferenceInDifferences()
|
|
|
34
34
|
results = did.fit(data, outcome='outcome', treatment='treated', time='post')
|
|
35
35
|
|
|
36
36
|
# View results
|
|
37
|
-
print(results) # DiDResults(ATT=3.
|
|
37
|
+
print(results) # DiDResults(ATT=3.0000, SE=1.7321, p=0.1583)
|
|
38
38
|
results.print_summary()
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Output:
|
|
42
42
|
```
|
|
43
43
|
======================================================================
|
|
44
|
-
|
|
44
|
+
Difference-in-Differences Estimation Results
|
|
45
45
|
======================================================================
|
|
46
46
|
|
|
47
|
-
Observations:
|
|
48
|
-
Treated units:
|
|
49
|
-
Control units:
|
|
50
|
-
R-squared:
|
|
47
|
+
Observations: 8
|
|
48
|
+
Treated units: 4
|
|
49
|
+
Control units: 4
|
|
50
|
+
R-squared: 0.9055
|
|
51
51
|
|
|
52
52
|
----------------------------------------------------------------------
|
|
53
|
-
Parameter
|
|
53
|
+
Parameter Estimate Std. Err. t-stat P>|t|
|
|
54
54
|
----------------------------------------------------------------------
|
|
55
|
-
ATT
|
|
55
|
+
ATT 3.0000 1.7321 1.732 0.1583
|
|
56
56
|
----------------------------------------------------------------------
|
|
57
57
|
|
|
58
|
-
95% Confidence Interval: [
|
|
58
|
+
95% Confidence Interval: [-1.8089, 7.8089]
|
|
59
59
|
|
|
60
60
|
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
61
61
|
======================================================================
|
|
@@ -67,12 +67,14 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
67
67
|
- **Pythonic results**: Easy access to coefficients, standard errors, and confidence intervals
|
|
68
68
|
- **Multiple interfaces**: Column names or R-style formulas
|
|
69
69
|
- **Robust inference**: Heteroskedasticity-robust (HC1) and cluster-robust standard errors
|
|
70
|
+
- **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
|
|
70
71
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
71
72
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
72
73
|
- **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
|
|
73
74
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
74
75
|
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
75
76
|
- **Parallel trends testing**: Multiple methods including equivalence tests
|
|
77
|
+
- **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
|
|
76
78
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
77
79
|
|
|
78
80
|
## Data Preparation
|
|
@@ -488,6 +490,36 @@ results = did.fit(
|
|
|
488
490
|
)
|
|
489
491
|
```
|
|
490
492
|
|
|
493
|
+
### Wild Cluster Bootstrap
|
|
494
|
+
|
|
495
|
+
When you have few clusters (<50), standard cluster-robust SEs are biased. Wild cluster bootstrap provides valid inference even with 5-10 clusters.
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
# Use wild bootstrap for inference
|
|
499
|
+
did = DifferenceInDifferences(
|
|
500
|
+
cluster='state',
|
|
501
|
+
inference='wild_bootstrap',
|
|
502
|
+
n_bootstrap=999,
|
|
503
|
+
bootstrap_weights='rademacher', # or 'webb' for <10 clusters, 'mammen'
|
|
504
|
+
seed=42
|
|
505
|
+
)
|
|
506
|
+
results = did.fit(data, outcome='y', treatment='treated', time='post')
|
|
507
|
+
|
|
508
|
+
# Results include bootstrap-based SE and p-value
|
|
509
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
510
|
+
print(f"P-value: {results.p_value:.4f}")
|
|
511
|
+
print(f"95% CI: {results.conf_int}")
|
|
512
|
+
print(f"Inference method: {results.inference_method}")
|
|
513
|
+
print(f"Number of clusters: {results.n_clusters}")
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Weight types:**
|
|
517
|
+
- `'rademacher'` - Default, ±1 with p=0.5, good for most cases
|
|
518
|
+
- `'webb'` - 6-point distribution, recommended for <10 clusters
|
|
519
|
+
- `'mammen'` - Two-point distribution, alternative to Rademacher
|
|
520
|
+
|
|
521
|
+
Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
|
|
522
|
+
|
|
491
523
|
### Two-Way Fixed Effects (Panel Data)
|
|
492
524
|
|
|
493
525
|
```python
|
|
@@ -937,6 +969,120 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
|
937
969
|
print(f"Trends equivalent: {results['equivalent']}")
|
|
938
970
|
```
|
|
939
971
|
|
|
972
|
+
### Placebo Tests
|
|
973
|
+
|
|
974
|
+
Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
|
|
975
|
+
|
|
976
|
+
**Fake timing test:**
|
|
977
|
+
|
|
978
|
+
```python
|
|
979
|
+
from diff_diff import run_placebo_test
|
|
980
|
+
|
|
981
|
+
# Test: Is there an effect before treatment actually occurred?
|
|
982
|
+
# Actual treatment is at period 3 (post_periods=[3, 4, 5])
|
|
983
|
+
# We test if a "fake" treatment at period 1 shows an effect
|
|
984
|
+
results = run_placebo_test(
|
|
985
|
+
data,
|
|
986
|
+
outcome='outcome',
|
|
987
|
+
treatment='treated',
|
|
988
|
+
time='period',
|
|
989
|
+
test_type='fake_timing',
|
|
990
|
+
fake_treatment_period=1, # Pretend treatment was in period 1
|
|
991
|
+
post_periods=[3, 4, 5] # Actual post-treatment periods
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
print(results.summary())
|
|
995
|
+
# If parallel trends hold, placebo_effect should be ~0 and not significant
|
|
996
|
+
print(f"Placebo effect: {results.placebo_effect:.3f} (p={results.p_value:.3f})")
|
|
997
|
+
print(f"Is significant (bad): {results.is_significant}")
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
**Fake group test:**
|
|
1001
|
+
|
|
1002
|
+
```python
|
|
1003
|
+
# Test: Is there an effect among never-treated units?
|
|
1004
|
+
# Get some control unit IDs to use as "fake treated"
|
|
1005
|
+
control_units = data[data['treated'] == 0]['firm_id'].unique()[:5]
|
|
1006
|
+
|
|
1007
|
+
results = run_placebo_test(
|
|
1008
|
+
data,
|
|
1009
|
+
outcome='outcome',
|
|
1010
|
+
treatment='treated',
|
|
1011
|
+
time='period',
|
|
1012
|
+
unit='firm_id',
|
|
1013
|
+
test_type='fake_group',
|
|
1014
|
+
fake_treatment_group=list(control_units), # List of control unit IDs
|
|
1015
|
+
post_periods=[3, 4, 5]
|
|
1016
|
+
)
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
**Permutation test:**
|
|
1020
|
+
|
|
1021
|
+
```python
|
|
1022
|
+
# Randomly reassign treatment and compute distribution of effects
|
|
1023
|
+
# Note: requires binary post indicator (use 'post' column, not 'period')
|
|
1024
|
+
results = run_placebo_test(
|
|
1025
|
+
data,
|
|
1026
|
+
outcome='outcome',
|
|
1027
|
+
treatment='treated',
|
|
1028
|
+
time='post', # Binary post-treatment indicator
|
|
1029
|
+
unit='firm_id',
|
|
1030
|
+
test_type='permutation',
|
|
1031
|
+
n_permutations=1000,
|
|
1032
|
+
seed=42
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
print(f"Original effect: {results.original_effect:.3f}")
|
|
1036
|
+
print(f"Permutation p-value: {results.p_value:.4f}")
|
|
1037
|
+
# Low p-value indicates the effect is unlikely to be due to chance
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
**Leave-one-out sensitivity:**
|
|
1041
|
+
|
|
1042
|
+
```python
|
|
1043
|
+
# Test sensitivity to individual treated units
|
|
1044
|
+
# Note: requires binary post indicator (use 'post' column, not 'period')
|
|
1045
|
+
results = run_placebo_test(
|
|
1046
|
+
data,
|
|
1047
|
+
outcome='outcome',
|
|
1048
|
+
treatment='treated',
|
|
1049
|
+
time='post', # Binary post-treatment indicator
|
|
1050
|
+
unit='firm_id',
|
|
1051
|
+
test_type='leave_one_out'
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
# Check if any single unit drives the result
|
|
1055
|
+
print(results.leave_one_out_effects) # Effect when each unit is dropped
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
**Run all placebo tests:**
|
|
1059
|
+
|
|
1060
|
+
```python
|
|
1061
|
+
from diff_diff import run_all_placebo_tests
|
|
1062
|
+
|
|
1063
|
+
# Comprehensive diagnostic suite
|
|
1064
|
+
# Note: This function runs fake_timing tests on pre-treatment periods.
|
|
1065
|
+
# The permutation and leave_one_out tests require a binary post indicator,
|
|
1066
|
+
# so they may return errors if the data uses multi-period time column.
|
|
1067
|
+
all_results = run_all_placebo_tests(
|
|
1068
|
+
data,
|
|
1069
|
+
outcome='outcome',
|
|
1070
|
+
treatment='treated',
|
|
1071
|
+
time='period',
|
|
1072
|
+
unit='firm_id',
|
|
1073
|
+
pre_periods=[0, 1, 2],
|
|
1074
|
+
post_periods=[3, 4, 5],
|
|
1075
|
+
n_permutations=500,
|
|
1076
|
+
seed=42
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
for test_name, result in all_results.items():
|
|
1080
|
+
if hasattr(result, 'p_value'):
|
|
1081
|
+
print(f"{test_name}: p={result.p_value:.3f}, significant={result.is_significant}")
|
|
1082
|
+
elif isinstance(result, dict) and 'error' in result:
|
|
1083
|
+
print(f"{test_name}: Error - {result['error']}")
|
|
1084
|
+
```
|
|
1085
|
+
|
|
940
1086
|
## API Reference
|
|
941
1087
|
|
|
942
1088
|
### DifferenceInDifferences
|
|
@@ -1316,6 +1462,18 @@ This library implements methods from the following scholarly works:
|
|
|
1316
1462
|
|
|
1317
1463
|
- **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)
|
|
1318
1464
|
|
|
1465
|
+
### Wild Cluster Bootstrap
|
|
1466
|
+
|
|
1467
|
+
- **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)
|
|
1468
|
+
|
|
1469
|
+
- **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)
|
|
1470
|
+
|
|
1471
|
+
- **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)
|
|
1472
|
+
|
|
1473
|
+
### Placebo Tests and DiD Diagnostics
|
|
1474
|
+
|
|
1475
|
+
- **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)
|
|
1476
|
+
|
|
1319
1477
|
### Synthetic Control Method
|
|
1320
1478
|
|
|
1321
1479
|
- **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)
|
|
@@ -42,9 +42,20 @@ from diff_diff.utils import (
|
|
|
42
42
|
check_parallel_trends,
|
|
43
43
|
check_parallel_trends_robust,
|
|
44
44
|
equivalence_test_trends,
|
|
45
|
+
WildBootstrapResults,
|
|
46
|
+
wild_bootstrap_se,
|
|
47
|
+
)
|
|
48
|
+
from diff_diff.diagnostics import (
|
|
49
|
+
PlaceboTestResults,
|
|
50
|
+
run_placebo_test,
|
|
51
|
+
placebo_timing_test,
|
|
52
|
+
placebo_group_test,
|
|
53
|
+
permutation_test,
|
|
54
|
+
leave_one_out_test,
|
|
55
|
+
run_all_placebo_tests,
|
|
45
56
|
)
|
|
46
57
|
|
|
47
|
-
__version__ = "0.
|
|
58
|
+
__version__ = "0.5.0"
|
|
48
59
|
__all__ = [
|
|
49
60
|
# Estimators
|
|
50
61
|
"DifferenceInDifferences",
|
|
@@ -66,6 +77,17 @@ __all__ = [
|
|
|
66
77
|
"check_parallel_trends",
|
|
67
78
|
"check_parallel_trends_robust",
|
|
68
79
|
"equivalence_test_trends",
|
|
80
|
+
# Wild cluster bootstrap
|
|
81
|
+
"WildBootstrapResults",
|
|
82
|
+
"wild_bootstrap_se",
|
|
83
|
+
# Placebo tests / diagnostics
|
|
84
|
+
"PlaceboTestResults",
|
|
85
|
+
"run_placebo_test",
|
|
86
|
+
"placebo_timing_test",
|
|
87
|
+
"placebo_group_test",
|
|
88
|
+
"permutation_test",
|
|
89
|
+
"leave_one_out_test",
|
|
90
|
+
"run_all_placebo_tests",
|
|
69
91
|
# Data preparation utilities
|
|
70
92
|
"make_treatment_indicator",
|
|
71
93
|
"make_post_indicator",
|