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 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.0.0"
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 Exception:
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("All permutations failed - check your data")
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 Exception:
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("All leave-one-out estimates failed")
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}"] = {"error": str(e)}
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"] = {"error": str(e)}
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"] = {"error": str(e)}
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