diff-diff 2.4.0__tar.gz → 2.4.2__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 (42) hide show
  1. {diff_diff-2.4.0 → diff_diff-2.4.2}/PKG-INFO +4 -4
  2. {diff_diff-2.4.0 → diff_diff-2.4.2}/README.md +3 -3
  3. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/__init__.py +1 -1
  4. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/_backend.py +21 -0
  5. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/diagnostics.py +4 -10
  6. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/estimators.py +5 -19
  7. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/imputation.py +30 -754
  8. diff_diff-2.4.2/diff_diff/imputation_bootstrap.py +333 -0
  9. diff_diff-2.4.2/diff_diff/imputation_results.py +426 -0
  10. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/linalg.py +6 -6
  11. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/staggered.py +16 -30
  12. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/staggered_aggregation.py +3 -10
  13. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/sun_abraham.py +7 -34
  14. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/synthetic_did.py +8 -19
  15. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/triple_diff.py +6 -11
  16. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/trop.py +19 -326
  17. diff_diff-2.4.2/diff_diff/trop_results.py +322 -0
  18. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/two_stage.py +85 -843
  19. diff_diff-2.4.2/diff_diff/two_stage_bootstrap.py +459 -0
  20. diff_diff-2.4.2/diff_diff/two_stage_results.py +379 -0
  21. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/utils.py +2 -101
  22. {diff_diff-2.4.0 → diff_diff-2.4.2}/pyproject.toml +1 -1
  23. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/Cargo.lock +28 -1
  24. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/Cargo.toml +11 -3
  25. diff_diff-2.4.2/rust/build.rs +12 -0
  26. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/src/lib.rs +25 -0
  27. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/src/weights.rs +410 -45
  28. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/bacon.py +0 -0
  29. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/datasets.py +0 -0
  30. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/honest_did.py +0 -0
  31. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/power.py +0 -0
  32. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/prep.py +0 -0
  33. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/prep_dgp.py +0 -0
  34. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/pretrends.py +0 -0
  35. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/results.py +0 -0
  36. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/staggered_bootstrap.py +0 -0
  37. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/staggered_results.py +0 -0
  38. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/twfe.py +0 -0
  39. {diff_diff-2.4.0 → diff_diff-2.4.2}/diff_diff/visualization.py +0 -0
  40. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/src/bootstrap.rs +0 -0
  41. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/src/linalg.rs +0 -0
  42. {diff_diff-2.4.0 → diff_diff-2.4.2}/rust/src/trop.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -2021,7 +2021,7 @@ TROP(
2021
2021
  max_iter=100, # Max iterations for factor estimation
2022
2022
  tol=1e-6, # Convergence tolerance
2023
2023
  alpha=0.05, # Significance level for CIs
2024
- n_bootstrap=200, # Bootstrap replications
2024
+ n_bootstrap=200, # Bootstrap replications (minimum 2; TROP requires bootstrap for SEs)
2025
2025
  seed=None # Random seed
2026
2026
  )
2027
2027
  ```
@@ -2102,8 +2102,6 @@ SunAbraham(
2102
2102
  | `time` | str | Time period column |
2103
2103
  | `first_treat` | str | Column with first treatment period (0 for never-treated) |
2104
2104
  | `covariates` | list | Covariate column names |
2105
- | `min_pre_periods` | int | Minimum pre-treatment periods to include |
2106
- | `min_post_periods` | int | Minimum post-treatment periods to include |
2107
2105
 
2108
2106
  ### SunAbrahamResults
2109
2107
 
@@ -2143,6 +2141,7 @@ ImputationDiD(
2143
2141
  alpha=0.05, # Significance level for CIs
2144
2142
  cluster=None, # Column for cluster-robust SEs
2145
2143
  n_bootstrap=0, # Bootstrap iterations (0 = analytical)
2144
+ bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
2146
2145
  seed=None, # Random seed
2147
2146
  rank_deficient_action='warn', # 'warn', 'error', or 'silent'
2148
2147
  horizon_max=None, # Max event-study horizon
@@ -2197,6 +2196,7 @@ TwoStageDiD(
2197
2196
  alpha=0.05, # Significance level for CIs
2198
2197
  cluster=None, # Column for cluster-robust SEs (defaults to unit)
2199
2198
  n_bootstrap=0, # Bootstrap iterations (0 = analytical GMM SEs)
2199
+ bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
2200
2200
  seed=None, # Random seed
2201
2201
  rank_deficient_action='warn', # 'warn', 'error', or 'silent'
2202
2202
  horizon_max=None, # Max event-study horizon
@@ -1983,7 +1983,7 @@ TROP(
1983
1983
  max_iter=100, # Max iterations for factor estimation
1984
1984
  tol=1e-6, # Convergence tolerance
1985
1985
  alpha=0.05, # Significance level for CIs
1986
- n_bootstrap=200, # Bootstrap replications
1986
+ n_bootstrap=200, # Bootstrap replications (minimum 2; TROP requires bootstrap for SEs)
1987
1987
  seed=None # Random seed
1988
1988
  )
1989
1989
  ```
@@ -2064,8 +2064,6 @@ SunAbraham(
2064
2064
  | `time` | str | Time period column |
2065
2065
  | `first_treat` | str | Column with first treatment period (0 for never-treated) |
2066
2066
  | `covariates` | list | Covariate column names |
2067
- | `min_pre_periods` | int | Minimum pre-treatment periods to include |
2068
- | `min_post_periods` | int | Minimum post-treatment periods to include |
2069
2067
 
2070
2068
  ### SunAbrahamResults
2071
2069
 
@@ -2105,6 +2103,7 @@ ImputationDiD(
2105
2103
  alpha=0.05, # Significance level for CIs
2106
2104
  cluster=None, # Column for cluster-robust SEs
2107
2105
  n_bootstrap=0, # Bootstrap iterations (0 = analytical)
2106
+ bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
2108
2107
  seed=None, # Random seed
2109
2108
  rank_deficient_action='warn', # 'warn', 'error', or 'silent'
2110
2109
  horizon_max=None, # Max event-study horizon
@@ -2159,6 +2158,7 @@ TwoStageDiD(
2159
2158
  alpha=0.05, # Significance level for CIs
2160
2159
  cluster=None, # Column for cluster-robust SEs (defaults to unit)
2161
2160
  n_bootstrap=0, # Bootstrap iterations (0 = analytical GMM SEs)
2161
+ bootstrap_weights='rademacher', # 'rademacher', 'mammen', or 'webb'
2162
2162
  seed=None, # Random seed
2163
2163
  rank_deficient_action='warn', # 'warn', 'error', or 'silent'
2164
2164
  horizon_max=None, # Max event-study horizon
@@ -148,7 +148,7 @@ from diff_diff.datasets import (
148
148
  load_mpdta,
149
149
  )
150
150
 
151
- __version__ = "2.4.0"
151
+ __version__ = "2.4.2"
152
152
  __all__ = [
153
153
  # Estimators
154
154
  "DifferenceInDifferences",
@@ -35,6 +35,8 @@ try:
35
35
  compute_time_weights as _rust_compute_time_weights,
36
36
  compute_noise_level as _rust_compute_noise_level,
37
37
  sc_weight_fw as _rust_sc_weight_fw,
38
+ # Diagnostics
39
+ rust_backend_info as _rust_backend_info,
38
40
  )
39
41
  _rust_available = True
40
42
  except ImportError:
@@ -56,6 +58,7 @@ except ImportError:
56
58
  _rust_compute_time_weights = None
57
59
  _rust_compute_noise_level = None
58
60
  _rust_sc_weight_fw = None
61
+ _rust_backend_info = None
59
62
 
60
63
  # Determine final backend based on environment variable and availability
61
64
  if _backend_env == 'python':
@@ -78,6 +81,7 @@ if _backend_env == 'python':
78
81
  _rust_compute_time_weights = None
79
82
  _rust_compute_noise_level = None
80
83
  _rust_sc_weight_fw = None
84
+ _rust_backend_info = None
81
85
  elif _backend_env == 'rust':
82
86
  # Force Rust mode - fail if not available
83
87
  if not _rust_available:
@@ -90,8 +94,25 @@ else:
90
94
  # Auto mode - use Rust if available
91
95
  HAS_RUST_BACKEND = _rust_available
92
96
 
97
+
98
+ def rust_backend_info():
99
+ """Return compile-time BLAS feature information for the Rust backend.
100
+
101
+ Returns a dict with keys:
102
+ - 'blas': True if any BLAS backend is linked
103
+ - 'accelerate': True if Apple Accelerate is linked (macOS)
104
+ - 'openblas': True if OpenBLAS is linked (Linux)
105
+
106
+ If the Rust backend is not available, all values are False.
107
+ """
108
+ if _rust_backend_info is not None:
109
+ return _rust_backend_info()
110
+ return {"blas": False, "accelerate": False, "openblas": False}
111
+
112
+
93
113
  __all__ = [
94
114
  'HAS_RUST_BACKEND',
115
+ 'rust_backend_info',
95
116
  '_rust_bootstrap_weights',
96
117
  '_rust_synthetic_weights',
97
118
  '_rust_project_simplex',
@@ -19,7 +19,7 @@ import pandas as pd
19
19
 
20
20
  from diff_diff.estimators import DifferenceInDifferences
21
21
  from diff_diff.results import _get_significance_stars
22
- from diff_diff.utils import compute_confidence_interval, compute_p_value
22
+ from diff_diff.utils import safe_inference
23
23
 
24
24
 
25
25
  @dataclass
@@ -661,7 +661,7 @@ def permutation_test(
661
661
  ci_lower = np.percentile(valid_effects, alpha / 2 * 100)
662
662
  ci_upper = np.percentile(valid_effects, (1 - alpha / 2) * 100)
663
663
 
664
- # T-stat from original estimate
664
+ # NOTE: Not using safe_inference — p_value is permutation-based, CI is percentile-based.
665
665
  t_stat = original_att / se if np.isfinite(se) and se > 0 else np.nan
666
666
 
667
667
  return PlaceboTestResults(
@@ -782,15 +782,9 @@ def leave_one_out_test(
782
782
 
783
783
  # Statistics of LOO distribution
784
784
  mean_effect = np.mean(valid_effects)
785
- se = np.std(valid_effects, ddof=1) if len(valid_effects) > 1 else 0.0
786
- t_stat = mean_effect / se if np.isfinite(se) and se > 0 else np.nan
787
-
788
- # Use t-distribution for p-value
785
+ se = np.std(valid_effects, ddof=1) if len(valid_effects) > 1 else np.nan
789
786
  df = len(valid_effects) - 1 if len(valid_effects) > 1 else 1
790
- p_value = compute_p_value(t_stat, df=df)
791
-
792
- # CI
793
- conf_int = compute_confidence_interval(mean_effect, se, alpha, df=df) if np.isfinite(se) and se > 0 else (np.nan, np.nan)
787
+ t_stat, p_value, conf_int = safe_inference(mean_effect, se, alpha=alpha, df=df)
794
788
 
795
789
  return PlaceboTestResults(
796
790
  test_type="leave_one_out",
@@ -27,9 +27,8 @@ from diff_diff.linalg import (
27
27
  from diff_diff.results import DiDResults, MultiPeriodDiDResults, PeriodEffect
28
28
  from diff_diff.utils import (
29
29
  WildBootstrapResults,
30
- compute_confidence_interval,
31
- compute_p_value,
32
30
  demean_by_group,
31
+ safe_inference,
33
32
  validate_binary,
34
33
  wild_bootstrap_se,
35
34
  )
@@ -1034,14 +1033,7 @@ class MultiPeriodDiD(DifferenceInDifferences):
1034
1033
  idx = interaction_indices[period]
1035
1034
  effect = coefficients[idx]
1036
1035
  se = np.sqrt(vcov[idx, idx])
1037
- if np.isfinite(se) and se > 0:
1038
- t_stat = effect / se
1039
- p_value = compute_p_value(t_stat, df=df)
1040
- conf_int = compute_confidence_interval(effect, se, self.alpha, df=df)
1041
- else:
1042
- t_stat = np.nan
1043
- p_value = np.nan
1044
- conf_int = (np.nan, np.nan)
1036
+ t_stat, p_value, conf_int = safe_inference(effect, se, alpha=self.alpha, df=df)
1045
1037
 
1046
1038
  period_effects[period] = PeriodEffect(
1047
1039
  period=period,
@@ -1085,15 +1077,9 @@ class MultiPeriodDiD(DifferenceInDifferences):
1085
1077
  avg_conf_int = (np.nan, np.nan)
1086
1078
  else:
1087
1079
  avg_se = float(np.sqrt(avg_var))
1088
- if np.isfinite(avg_se) and avg_se > 0:
1089
- avg_t_stat = avg_att / avg_se
1090
- avg_p_value = compute_p_value(avg_t_stat, df=df)
1091
- avg_conf_int = compute_confidence_interval(avg_att, avg_se, self.alpha, df=df)
1092
- else:
1093
- # Zero SE (degenerate case)
1094
- avg_t_stat = np.nan
1095
- avg_p_value = np.nan
1096
- avg_conf_int = (np.nan, np.nan)
1080
+ avg_t_stat, avg_p_value, avg_conf_int = safe_inference(
1081
+ avg_att, avg_se, alpha=self.alpha, df=df
1082
+ )
1097
1083
 
1098
1084
  # Count observations
1099
1085
  n_treated = int(np.sum(d))