diff-diff 0.3.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.3.0 → diff_diff-0.5.0}/PKG-INFO +417 -10
- {diff_diff-0.3.0 → diff_diff-0.5.0}/README.md +416 -9
- diff_diff-0.5.0/diff_diff/__init__.py +102 -0
- diff_diff-0.5.0/diff_diff/diagnostics.py +885 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff/estimators.py +146 -34
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff/prep.py +532 -38
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff/results.py +26 -7
- diff_diff-0.5.0/diff_diff/staggered.py +982 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff/utils.py +379 -3
- diff_diff-0.5.0/diff_diff/visualization.py +471 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff.egg-info/PKG-INFO +417 -10
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff.egg-info/SOURCES.txt +8 -1
- {diff_diff-0.3.0 → diff_diff-0.5.0}/pyproject.toml +1 -1
- diff_diff-0.5.0/tests/test_diagnostics.py +677 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/tests/test_estimators.py +4 -2
- {diff_diff-0.3.0 → diff_diff-0.5.0}/tests/test_prep.py +343 -0
- diff_diff-0.5.0/tests/test_staggered.py +390 -0
- diff_diff-0.5.0/tests/test_visualization.py +284 -0
- diff_diff-0.5.0/tests/test_wild_bootstrap.py +624 -0
- diff_diff-0.3.0/diff_diff/__init__.py +0 -43
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-0.3.0 → diff_diff-0.5.0}/setup.cfg +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,9 +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
|
|
108
|
+
- **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
|
|
107
109
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
110
|
+
- **Event study plots**: Publication-ready visualization of treatment effects
|
|
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
|
|
108
113
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
109
114
|
|
|
110
115
|
## Data Preparation
|
|
@@ -342,6 +347,79 @@ cohort_data = aggregate_to_cohorts(
|
|
|
342
347
|
# Result: mean outcome by treatment group and period
|
|
343
348
|
```
|
|
344
349
|
|
|
350
|
+
### Rank Control Units
|
|
351
|
+
|
|
352
|
+
Select the best control units for DiD or Synthetic DiD analysis by ranking them based on pre-treatment outcome similarity:
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
from diff_diff import rank_control_units, generate_did_data
|
|
356
|
+
|
|
357
|
+
# Generate sample data
|
|
358
|
+
data = generate_did_data(n_units=50, n_periods=6, seed=42)
|
|
359
|
+
|
|
360
|
+
# Rank control units by their similarity to treated units
|
|
361
|
+
ranking = rank_control_units(
|
|
362
|
+
data,
|
|
363
|
+
unit_column='unit',
|
|
364
|
+
time_column='period',
|
|
365
|
+
outcome_column='outcome',
|
|
366
|
+
treatment_column='treated',
|
|
367
|
+
n_top=10 # Return top 10 controls
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
print(ranking[['unit', 'quality_score', 'pre_trend_rmse']])
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Output:
|
|
374
|
+
```
|
|
375
|
+
unit quality_score pre_trend_rmse
|
|
376
|
+
0 35 1.0000 0.4521
|
|
377
|
+
1 42 0.9234 0.5123
|
|
378
|
+
2 28 0.8876 0.5892
|
|
379
|
+
...
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
With covariates for matching:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
# Add covariate-based matching
|
|
386
|
+
ranking = rank_control_units(
|
|
387
|
+
data,
|
|
388
|
+
unit_column='unit',
|
|
389
|
+
time_column='period',
|
|
390
|
+
outcome_column='outcome',
|
|
391
|
+
treatment_column='treated',
|
|
392
|
+
covariates=['size', 'age'], # Match on these too
|
|
393
|
+
outcome_weight=0.7, # 70% weight on outcome trends
|
|
394
|
+
covariate_weight=0.3 # 30% weight on covariate similarity
|
|
395
|
+
)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Filter data for SyntheticDiD using top controls:
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
from diff_diff import SyntheticDiD
|
|
402
|
+
|
|
403
|
+
# Get top control units
|
|
404
|
+
top_controls = ranking['unit'].tolist()
|
|
405
|
+
|
|
406
|
+
# Filter data to treated + top controls
|
|
407
|
+
filtered_data = data[
|
|
408
|
+
(data['treated'] == 1) | (data['unit'].isin(top_controls))
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
# Fit SyntheticDiD with selected controls
|
|
412
|
+
sdid = SyntheticDiD()
|
|
413
|
+
results = sdid.fit(
|
|
414
|
+
filtered_data,
|
|
415
|
+
outcome='outcome',
|
|
416
|
+
treatment='treated',
|
|
417
|
+
unit='unit',
|
|
418
|
+
time='period',
|
|
419
|
+
post_periods=[3, 4, 5]
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
|
|
345
423
|
## Usage
|
|
346
424
|
|
|
347
425
|
### Basic DiD with Column Names
|
|
@@ -447,6 +525,36 @@ results = did.fit(
|
|
|
447
525
|
)
|
|
448
526
|
```
|
|
449
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
|
+
|
|
450
558
|
### Two-Way Fixed Effects (Panel Data)
|
|
451
559
|
|
|
452
560
|
```python
|
|
@@ -522,6 +630,148 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
|
522
630
|
================================================================================
|
|
523
631
|
```
|
|
524
632
|
|
|
633
|
+
### Staggered Difference-in-Differences (Callaway-Sant'Anna)
|
|
634
|
+
|
|
635
|
+
When treatment is adopted at different times by different units, traditional TWFE estimators can be biased. The Callaway-Sant'Anna estimator provides unbiased estimates with staggered adoption.
|
|
636
|
+
|
|
637
|
+
```python
|
|
638
|
+
from diff_diff import CallawaySantAnna
|
|
639
|
+
|
|
640
|
+
# Panel data with staggered treatment
|
|
641
|
+
# 'first_treat' = period when unit was first treated (0 if never treated)
|
|
642
|
+
cs = CallawaySantAnna()
|
|
643
|
+
results = cs.fit(
|
|
644
|
+
panel_data,
|
|
645
|
+
outcome='sales',
|
|
646
|
+
unit='firm_id',
|
|
647
|
+
time='year',
|
|
648
|
+
first_treat='first_treat', # 0 for never-treated, else first treatment year
|
|
649
|
+
aggregate='event_study' # Compute event study effects
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# View results
|
|
653
|
+
results.print_summary()
|
|
654
|
+
|
|
655
|
+
# Access group-time effects ATT(g,t)
|
|
656
|
+
for (group, time), effect in results.group_time_effects.items():
|
|
657
|
+
print(f"Cohort {group}, Period {time}: {effect['effect']:.3f}")
|
|
658
|
+
|
|
659
|
+
# Event study effects (averaged by relative time)
|
|
660
|
+
for rel_time, effect in results.event_study_effects.items():
|
|
661
|
+
print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
|
|
662
|
+
|
|
663
|
+
# Convert to DataFrame
|
|
664
|
+
df = results.to_dataframe(level='event_study')
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Output:
|
|
668
|
+
```
|
|
669
|
+
=====================================================================================
|
|
670
|
+
Callaway-Sant'Anna Staggered Difference-in-Differences Results
|
|
671
|
+
=====================================================================================
|
|
672
|
+
|
|
673
|
+
Total observations: 600
|
|
674
|
+
Treated units: 35
|
|
675
|
+
Control units: 15
|
|
676
|
+
Treatment cohorts: 3
|
|
677
|
+
Time periods: 8
|
|
678
|
+
Control group: never_treated
|
|
679
|
+
|
|
680
|
+
-------------------------------------------------------------------------------------
|
|
681
|
+
Overall Average Treatment Effect on the Treated
|
|
682
|
+
-------------------------------------------------------------------------------------
|
|
683
|
+
Parameter Estimate Std. Err. t-stat P>|t| Sig.
|
|
684
|
+
-------------------------------------------------------------------------------------
|
|
685
|
+
ATT 2.5000 0.3521 7.101 0.0000 ***
|
|
686
|
+
-------------------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
95% Confidence Interval: [1.8099, 3.1901]
|
|
689
|
+
|
|
690
|
+
-------------------------------------------------------------------------------------
|
|
691
|
+
Event Study (Dynamic) Effects
|
|
692
|
+
-------------------------------------------------------------------------------------
|
|
693
|
+
Rel. Period Estimate Std. Err. t-stat P>|t| Sig.
|
|
694
|
+
-------------------------------------------------------------------------------------
|
|
695
|
+
0 2.1000 0.4521 4.645 0.0000 ***
|
|
696
|
+
1 2.5000 0.4123 6.064 0.0000 ***
|
|
697
|
+
2 2.8000 0.5234 5.349 0.0000 ***
|
|
698
|
+
-------------------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
701
|
+
=====================================================================================
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**When to use Callaway-Sant'Anna vs TWFE:**
|
|
705
|
+
|
|
706
|
+
| Scenario | Use TWFE | Use Callaway-Sant'Anna |
|
|
707
|
+
|----------|----------|------------------------|
|
|
708
|
+
| All units treated at same time | ✓ | ✓ |
|
|
709
|
+
| Staggered adoption, homogeneous effects | ✓ | ✓ |
|
|
710
|
+
| Staggered adoption, heterogeneous effects | ✗ | ✓ |
|
|
711
|
+
| Need event study with staggered timing | ✗ | ✓ |
|
|
712
|
+
| Fewer than ~20 treated units | ✓ | Depends on design |
|
|
713
|
+
|
|
714
|
+
**Parameters:**
|
|
715
|
+
|
|
716
|
+
```python
|
|
717
|
+
CallawaySantAnna(
|
|
718
|
+
control_group='never_treated', # or 'not_yet_treated'
|
|
719
|
+
anticipation=0, # Periods before treatment with effects
|
|
720
|
+
estimation_method='dr', # 'dr', 'ipw', or 'reg'
|
|
721
|
+
alpha=0.05, # Significance level
|
|
722
|
+
cluster=None, # Column for cluster SEs
|
|
723
|
+
n_bootstrap=0, # Must be 0 (bootstrap not yet implemented)
|
|
724
|
+
seed=None # Random seed
|
|
725
|
+
)
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**Current limitations:**
|
|
729
|
+
- Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
|
|
730
|
+
- Covariate adjustment for conditional parallel trends is not yet implemented
|
|
731
|
+
|
|
732
|
+
### Event Study Visualization
|
|
733
|
+
|
|
734
|
+
Create publication-ready event study plots:
|
|
735
|
+
|
|
736
|
+
```python
|
|
737
|
+
from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
|
|
738
|
+
|
|
739
|
+
# From MultiPeriodDiD
|
|
740
|
+
did = MultiPeriodDiD()
|
|
741
|
+
results = did.fit(data, outcome='y', treatment='treated',
|
|
742
|
+
time='period', post_periods=[3, 4, 5])
|
|
743
|
+
plot_event_study(results, title="Treatment Effects Over Time")
|
|
744
|
+
|
|
745
|
+
# From CallawaySantAnna (with event study aggregation)
|
|
746
|
+
cs = CallawaySantAnna()
|
|
747
|
+
results = cs.fit(data, outcome='y', unit='unit', time='period',
|
|
748
|
+
first_treat='first_treat', aggregate='event_study')
|
|
749
|
+
plot_event_study(results, title="Staggered DiD Event Study")
|
|
750
|
+
|
|
751
|
+
# From a DataFrame
|
|
752
|
+
df = pd.DataFrame({
|
|
753
|
+
'period': [-2, -1, 0, 1, 2],
|
|
754
|
+
'effect': [0.1, 0.05, 0.0, 2.5, 2.8],
|
|
755
|
+
'se': [0.3, 0.25, 0.0, 0.4, 0.45]
|
|
756
|
+
})
|
|
757
|
+
plot_event_study(df, reference_period=0)
|
|
758
|
+
|
|
759
|
+
# With customization
|
|
760
|
+
ax = plot_event_study(
|
|
761
|
+
results,
|
|
762
|
+
title="Dynamic Treatment Effects",
|
|
763
|
+
xlabel="Years Relative to Treatment",
|
|
764
|
+
ylabel="Effect on Sales ($1000s)",
|
|
765
|
+
color="#2563eb",
|
|
766
|
+
marker="o",
|
|
767
|
+
shade_pre=True, # Shade pre-treatment region
|
|
768
|
+
show_zero_line=True, # Horizontal line at y=0
|
|
769
|
+
show_reference_line=True, # Vertical line at reference period
|
|
770
|
+
figsize=(10, 6),
|
|
771
|
+
show=False # Don't call plt.show(), return axes
|
|
772
|
+
)
|
|
773
|
+
```
|
|
774
|
+
|
|
525
775
|
### Synthetic Difference-in-Differences
|
|
526
776
|
|
|
527
777
|
Synthetic DiD combines the strengths of Difference-in-Differences and Synthetic Control methods by re-weighting control units to better match treated units' pre-treatment outcomes.
|
|
@@ -754,6 +1004,120 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
|
754
1004
|
print(f"Trends equivalent: {results['equivalent']}")
|
|
755
1005
|
```
|
|
756
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
|
+
|
|
757
1121
|
## API Reference
|
|
758
1122
|
|
|
759
1123
|
### DifferenceInDifferences
|
|
@@ -1061,6 +1425,31 @@ aggregate_to_cohorts(
|
|
|
1061
1425
|
)
|
|
1062
1426
|
```
|
|
1063
1427
|
|
|
1428
|
+
#### rank_control_units
|
|
1429
|
+
|
|
1430
|
+
```python
|
|
1431
|
+
rank_control_units(
|
|
1432
|
+
data, # Panel data in long format
|
|
1433
|
+
unit_column, # Unit identifier column
|
|
1434
|
+
time_column, # Time period column
|
|
1435
|
+
outcome_column, # Outcome variable column
|
|
1436
|
+
treatment_column=None, # Treatment indicator column (0/1)
|
|
1437
|
+
treated_units=None, # Explicit list of treated unit IDs
|
|
1438
|
+
pre_periods=None, # Pre-treatment periods (default: first half)
|
|
1439
|
+
covariates=None, # Covariate columns for matching
|
|
1440
|
+
outcome_weight=0.7, # Weight for outcome trend similarity (0-1)
|
|
1441
|
+
covariate_weight=0.3, # Weight for covariate distance (0-1)
|
|
1442
|
+
exclude_units=None, # Units to exclude from control pool
|
|
1443
|
+
require_units=None, # Units that must appear in output
|
|
1444
|
+
n_top=None, # Return only top N controls
|
|
1445
|
+
suggest_treatment_candidates=False, # Identify treatment candidates
|
|
1446
|
+
n_treatment_candidates=5, # Number of treatment candidates
|
|
1447
|
+
lambda_reg=0.0 # Regularization for synthetic weights
|
|
1448
|
+
)
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`, `covariate_score`, `synthetic_weight`, `pre_trend_rmse`, `is_required`.
|
|
1452
|
+
|
|
1064
1453
|
## Requirements
|
|
1065
1454
|
|
|
1066
1455
|
- Python >= 3.9
|
|
@@ -1108,6 +1497,18 @@ This library implements methods from the following scholarly works:
|
|
|
1108
1497
|
|
|
1109
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)
|
|
1110
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
|
+
|
|
1111
1512
|
### Synthetic Control Method
|
|
1112
1513
|
|
|
1113
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)
|
|
@@ -1126,14 +1527,20 @@ This library implements methods from the following scholarly works:
|
|
|
1126
1527
|
|
|
1127
1528
|
- **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)
|
|
1128
1529
|
|
|
1530
|
+
- **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)
|
|
1531
|
+
|
|
1129
1532
|
### Multi-Period and Staggered Adoption
|
|
1130
1533
|
|
|
1131
1534
|
- **Callaway, B., & Sant'Anna, P. H. C. (2021).** "Difference-in-Differences with Multiple Time Periods." *Journal of Econometrics*, 225(2), 200-230. [https://doi.org/10.1016/j.jeconom.2020.12.001](https://doi.org/10.1016/j.jeconom.2020.12.001)
|
|
1132
1535
|
|
|
1536
|
+
- **Sant'Anna, P. H. C., & Zhao, J. (2020).** "Doubly Robust Difference-in-Differences Estimators." *Journal of Econometrics*, 219(1), 101-122. [https://doi.org/10.1016/j.jeconom.2020.06.003](https://doi.org/10.1016/j.jeconom.2020.06.003)
|
|
1537
|
+
|
|
1133
1538
|
- **Sun, L., & Abraham, S. (2021).** "Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects." *Journal of Econometrics*, 225(2), 175-199. [https://doi.org/10.1016/j.jeconom.2020.09.006](https://doi.org/10.1016/j.jeconom.2020.09.006)
|
|
1134
1539
|
|
|
1135
1540
|
- **de Chaisemartin, C., & D'Haultfœuille, X. (2020).** "Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects." *American Economic Review*, 110(9), 2964-2996. [https://doi.org/10.1257/aer.20181169](https://doi.org/10.1257/aer.20181169)
|
|
1136
1541
|
|
|
1542
|
+
- **Goodman-Bacon, A. (2021).** "Difference-in-Differences with Variation in Treatment Timing." *Journal of Econometrics*, 225(2), 254-277. [https://doi.org/10.1016/j.jeconom.2021.03.014](https://doi.org/10.1016/j.jeconom.2021.03.014)
|
|
1543
|
+
|
|
1137
1544
|
### General Causal Inference
|
|
1138
1545
|
|
|
1139
1546
|
- **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
|