diff-diff 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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/__init__.py +24 -1
- diff_diff/diagnostics.py +62 -23
- diff_diff/estimators.py +77 -875
- diff_diff/honest_did.py +4 -2
- diff_diff/power.py +1 -1
- diff_diff/prep.py +1 -1
- diff_diff/pretrends.py +1067 -0
- diff_diff/staggered.py +52 -21
- diff_diff/sun_abraham.py +1198 -0
- diff_diff/synthetic_did.py +564 -0
- diff_diff/twfe.py +344 -0
- diff_diff/utils.py +1 -1
- diff_diff/visualization.py +240 -1
- {diff_diff-1.0.0.dist-info → diff_diff-1.2.0.dist-info}/METADATA +332 -6
- diff_diff-1.2.0.dist-info/RECORD +19 -0
- diff_diff-1.0.0.dist-info/RECORD +0 -15
- {diff_diff-1.0.0.dist-info → diff_diff-1.2.0.dist-info}/WHEEL +0 -0
- {diff_diff-1.0.0.dist-info → diff_diff-1.2.0.dist-info}/top_level.txt +0 -0
diff_diff/__init__.py
CHANGED
|
@@ -45,6 +45,13 @@ from diff_diff.power import (
|
|
|
45
45
|
compute_sample_size,
|
|
46
46
|
simulate_power,
|
|
47
47
|
)
|
|
48
|
+
from diff_diff.pretrends import (
|
|
49
|
+
PreTrendsPower,
|
|
50
|
+
PreTrendsPowerCurve,
|
|
51
|
+
PreTrendsPowerResults,
|
|
52
|
+
compute_mdv,
|
|
53
|
+
compute_pretrends_power,
|
|
54
|
+
)
|
|
48
55
|
from diff_diff.prep import (
|
|
49
56
|
aggregate_to_cohorts,
|
|
50
57
|
balance_panel,
|
|
@@ -69,6 +76,11 @@ from diff_diff.staggered import (
|
|
|
69
76
|
CSBootstrapResults,
|
|
70
77
|
GroupTimeEffect,
|
|
71
78
|
)
|
|
79
|
+
from diff_diff.sun_abraham import (
|
|
80
|
+
SABootstrapResults,
|
|
81
|
+
SunAbraham,
|
|
82
|
+
SunAbrahamResults,
|
|
83
|
+
)
|
|
72
84
|
from diff_diff.utils import (
|
|
73
85
|
WildBootstrapResults,
|
|
74
86
|
check_parallel_trends,
|
|
@@ -82,10 +94,11 @@ from diff_diff.visualization import (
|
|
|
82
94
|
plot_group_effects,
|
|
83
95
|
plot_honest_event_study,
|
|
84
96
|
plot_power_curve,
|
|
97
|
+
plot_pretrends_power,
|
|
85
98
|
plot_sensitivity,
|
|
86
99
|
)
|
|
87
100
|
|
|
88
|
-
__version__ = "1.
|
|
101
|
+
__version__ = "1.2.0"
|
|
89
102
|
__all__ = [
|
|
90
103
|
# Estimators
|
|
91
104
|
"DifferenceInDifferences",
|
|
@@ -93,6 +106,7 @@ __all__ = [
|
|
|
93
106
|
"MultiPeriodDiD",
|
|
94
107
|
"SyntheticDiD",
|
|
95
108
|
"CallawaySantAnna",
|
|
109
|
+
"SunAbraham",
|
|
96
110
|
# Bacon Decomposition
|
|
97
111
|
"BaconDecomposition",
|
|
98
112
|
"BaconDecompositionResults",
|
|
@@ -107,6 +121,8 @@ __all__ = [
|
|
|
107
121
|
"CallawaySantAnnaResults",
|
|
108
122
|
"CSBootstrapResults",
|
|
109
123
|
"GroupTimeEffect",
|
|
124
|
+
"SunAbrahamResults",
|
|
125
|
+
"SABootstrapResults",
|
|
110
126
|
# Visualization
|
|
111
127
|
"plot_event_study",
|
|
112
128
|
"plot_group_effects",
|
|
@@ -156,4 +172,11 @@ __all__ = [
|
|
|
156
172
|
"compute_sample_size",
|
|
157
173
|
"simulate_power",
|
|
158
174
|
"plot_power_curve",
|
|
175
|
+
# Pre-trends power analysis
|
|
176
|
+
"PreTrendsPower",
|
|
177
|
+
"PreTrendsPowerResults",
|
|
178
|
+
"PreTrendsPowerCurve",
|
|
179
|
+
"compute_pretrends_power",
|
|
180
|
+
"compute_mdv",
|
|
181
|
+
"plot_pretrends_power",
|
|
159
182
|
]
|
diff_diff/diagnostics.py
CHANGED
|
@@ -18,22 +18,10 @@ import numpy as np
|
|
|
18
18
|
import pandas as pd
|
|
19
19
|
|
|
20
20
|
from diff_diff.estimators import DifferenceInDifferences
|
|
21
|
+
from diff_diff.results import _get_significance_stars
|
|
21
22
|
from diff_diff.utils import compute_confidence_interval, compute_p_value
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def _get_significance_stars(p_value: float) -> str:
|
|
25
|
-
"""Return significance stars based on p-value."""
|
|
26
|
-
if p_value < 0.001:
|
|
27
|
-
return "***"
|
|
28
|
-
elif p_value < 0.01:
|
|
29
|
-
return "**"
|
|
30
|
-
elif p_value < 0.05:
|
|
31
|
-
return "*"
|
|
32
|
-
elif p_value < 0.1:
|
|
33
|
-
return "."
|
|
34
|
-
return ""
|
|
35
|
-
|
|
36
|
-
|
|
37
25
|
@dataclass
|
|
38
26
|
class PlaceboTestResults:
|
|
39
27
|
"""
|
|
@@ -633,15 +621,34 @@ def permutation_test(
|
|
|
633
621
|
time=time
|
|
634
622
|
)
|
|
635
623
|
permuted_effects[i] = perm_results.att
|
|
636
|
-
except
|
|
624
|
+
except (ValueError, KeyError, np.linalg.LinAlgError):
|
|
637
625
|
# Handle edge cases where fitting fails
|
|
638
626
|
permuted_effects[i] = np.nan
|
|
639
627
|
|
|
640
|
-
# Remove any NaN values
|
|
628
|
+
# Remove any NaN values and track failure rate
|
|
641
629
|
valid_effects = permuted_effects[~np.isnan(permuted_effects)]
|
|
630
|
+
n_failed = n_permutations - len(valid_effects)
|
|
642
631
|
|
|
643
632
|
if len(valid_effects) == 0:
|
|
644
|
-
raise RuntimeError(
|
|
633
|
+
raise RuntimeError(
|
|
634
|
+
f"All {n_permutations} permutations failed. This typically occurs when:\n"
|
|
635
|
+
f" - Treatment/control groups are too small for valid permutation\n"
|
|
636
|
+
f" - Data contains collinearity or singular matrices after permutation\n"
|
|
637
|
+
f" - There are too few observations per time period\n"
|
|
638
|
+
f"Consider checking data quality with validate_did_data() from diff_diff.prep."
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Warn if significant number of permutations failed
|
|
642
|
+
if n_failed > 0:
|
|
643
|
+
failure_rate = n_failed / n_permutations
|
|
644
|
+
if failure_rate > 0.1:
|
|
645
|
+
import warnings
|
|
646
|
+
warnings.warn(
|
|
647
|
+
f"{n_failed}/{n_permutations} permutations failed ({failure_rate:.1%}). "
|
|
648
|
+
f"Results based on {len(valid_effects)} successful permutations.",
|
|
649
|
+
UserWarning,
|
|
650
|
+
stacklevel=2
|
|
651
|
+
)
|
|
645
652
|
|
|
646
653
|
# Compute p-value: proportion of |permuted| >= |original|
|
|
647
654
|
p_value = np.mean(np.abs(valid_effects) >= np.abs(original_att))
|
|
@@ -744,15 +751,34 @@ def leave_one_out_test(
|
|
|
744
751
|
time=time
|
|
745
752
|
)
|
|
746
753
|
loo_effects[u] = loo_results.att
|
|
747
|
-
except
|
|
754
|
+
except (ValueError, KeyError, np.linalg.LinAlgError):
|
|
748
755
|
# Skip units that cause fitting issues
|
|
749
756
|
loo_effects[u] = np.nan
|
|
750
757
|
|
|
751
|
-
# Remove NaN values for statistics
|
|
758
|
+
# Remove NaN values for statistics and track failures
|
|
752
759
|
valid_effects = [v for v in loo_effects.values() if not np.isnan(v)]
|
|
760
|
+
n_total = len(loo_effects)
|
|
761
|
+
n_failed = n_total - len(valid_effects)
|
|
753
762
|
|
|
754
763
|
if len(valid_effects) == 0:
|
|
755
|
-
raise RuntimeError(
|
|
764
|
+
raise RuntimeError(
|
|
765
|
+
f"All {n_total} leave-one-out estimates failed. This typically occurs when:\n"
|
|
766
|
+
f" - Removing any single treated unit causes model fitting to fail\n"
|
|
767
|
+
f" - Very few treated units (need at least 2 for LOO)\n"
|
|
768
|
+
f" - Data has collinearity issues that manifest when units are removed\n"
|
|
769
|
+
f"Consider checking data quality and ensuring sufficient treated units."
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Warn if significant number of LOO iterations failed
|
|
773
|
+
if n_failed > 0:
|
|
774
|
+
import warnings
|
|
775
|
+
failed_units = [u for u, v in loo_effects.items() if np.isnan(v)]
|
|
776
|
+
warnings.warn(
|
|
777
|
+
f"{n_failed}/{n_total} leave-one-out estimates failed for units: {failed_units}. "
|
|
778
|
+
f"Results based on {len(valid_effects)} successful iterations.",
|
|
779
|
+
UserWarning,
|
|
780
|
+
stacklevel=2
|
|
781
|
+
)
|
|
756
782
|
|
|
757
783
|
# Statistics of LOO distribution
|
|
758
784
|
mean_effect = np.mean(valid_effects)
|
|
@@ -850,8 +876,13 @@ def run_all_placebo_tests(
|
|
|
850
876
|
)
|
|
851
877
|
results[f"fake_timing_{period}"] = test_result
|
|
852
878
|
except Exception as e:
|
|
853
|
-
# Store error info
|
|
854
|
-
results[f"fake_timing_{period}"] = {
|
|
879
|
+
# Store structured error info for debugging
|
|
880
|
+
results[f"fake_timing_{period}"] = {
|
|
881
|
+
"error": str(e),
|
|
882
|
+
"error_type": type(e).__name__,
|
|
883
|
+
"test_type": "fake_timing",
|
|
884
|
+
"period": period
|
|
885
|
+
}
|
|
855
886
|
|
|
856
887
|
# Permutation test
|
|
857
888
|
try:
|
|
@@ -868,7 +899,11 @@ def run_all_placebo_tests(
|
|
|
868
899
|
)
|
|
869
900
|
results["permutation"] = perm_result
|
|
870
901
|
except Exception as e:
|
|
871
|
-
results["permutation"] = {
|
|
902
|
+
results["permutation"] = {
|
|
903
|
+
"error": str(e),
|
|
904
|
+
"error_type": type(e).__name__,
|
|
905
|
+
"test_type": "permutation"
|
|
906
|
+
}
|
|
872
907
|
|
|
873
908
|
# Leave-one-out test
|
|
874
909
|
try:
|
|
@@ -883,6 +918,10 @@ def run_all_placebo_tests(
|
|
|
883
918
|
)
|
|
884
919
|
results["leave_one_out"] = loo_result
|
|
885
920
|
except Exception as e:
|
|
886
|
-
results["leave_one_out"] = {
|
|
921
|
+
results["leave_one_out"] = {
|
|
922
|
+
"error": str(e),
|
|
923
|
+
"error_type": type(e).__name__,
|
|
924
|
+
"test_type": "leave_one_out"
|
|
925
|
+
}
|
|
887
926
|
|
|
888
927
|
return results
|