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.
Files changed (23) hide show
  1. {diff_diff-0.4.0 → diff_diff-0.5.0}/PKG-INFO +168 -10
  2. {diff_diff-0.4.0 → diff_diff-0.5.0}/README.md +167 -9
  3. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/__init__.py +23 -1
  4. diff_diff-0.5.0/diff_diff/diagnostics.py +885 -0
  5. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/estimators.py +146 -34
  6. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/results.py +26 -7
  7. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/utils.py +379 -3
  8. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/PKG-INFO +168 -10
  9. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/SOURCES.txt +4 -1
  10. {diff_diff-0.4.0 → diff_diff-0.5.0}/pyproject.toml +1 -1
  11. diff_diff-0.5.0/tests/test_diagnostics.py +677 -0
  12. {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_estimators.py +4 -2
  13. diff_diff-0.5.0/tests/test_wild_bootstrap.py +624 -0
  14. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/prep.py +0 -0
  15. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/staggered.py +0 -0
  16. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff/visualization.py +0 -0
  17. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/dependency_links.txt +0 -0
  18. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/requires.txt +0 -0
  19. {diff_diff-0.4.0 → diff_diff-0.5.0}/diff_diff.egg-info/top_level.txt +0 -0
  20. {diff_diff-0.4.0 → diff_diff-0.5.0}/setup.cfg +0 -0
  21. {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_prep.py +0 -0
  22. {diff_diff-0.4.0 → diff_diff-0.5.0}/tests/test_staggered.py +0 -0
  23. {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.4.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.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,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.5000*, SE=1.2583, p=0.0367)
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
- Difference-in-Differences Estimation Results
44
+ Difference-in-Differences Estimation Results
45
45
  ======================================================================
46
46
 
47
- Observations: 8
48
- Treated units: 4
49
- Control units: 4
50
- R-squared: 0.9123
47
+ Observations: 8
48
+ Treated units: 4
49
+ Control units: 4
50
+ R-squared: 0.9055
51
51
 
52
52
  ----------------------------------------------------------------------
53
- Parameter Estimate Std. Err. t-stat P>|t|
53
+ Parameter Estimate Std. Err. t-stat P>|t|
54
54
  ----------------------------------------------------------------------
55
- ATT 3.5000 1.2583 2.782 0.0367
55
+ ATT 3.0000 1.7321 1.732 0.1583
56
56
  ----------------------------------------------------------------------
57
57
 
58
- 95% Confidence Interval: [0.3912, 6.6088]
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.4.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",