diff-diff 2.8.1__tar.gz → 2.8.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 (66) hide show
  1. {diff_diff-2.8.1 → diff_diff-2.8.2}/PKG-INFO +1 -1
  2. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/__init__.py +1 -1
  3. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/linalg.py +55 -0
  4. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered.py +477 -166
  5. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_results.py +68 -3
  6. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_triple_diff.py +97 -4
  7. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_triple_diff_results.py +77 -12
  8. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/triple_diff.py +137 -11
  9. {diff_diff-2.8.1 → diff_diff-2.8.2}/pyproject.toml +1 -1
  10. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/Cargo.lock +1 -1
  11. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/Cargo.toml +1 -1
  12. {diff_diff-2.8.1 → diff_diff-2.8.2}/README.md +0 -0
  13. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/_backend.py +0 -0
  14. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/bacon.py +0 -0
  15. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/bootstrap_utils.py +0 -0
  16. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/continuous_did.py +0 -0
  17. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/continuous_did_bspline.py +0 -0
  18. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/continuous_did_results.py +0 -0
  19. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/datasets.py +0 -0
  20. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/diagnostics.py +0 -0
  21. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did.py +0 -0
  22. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_bootstrap.py +0 -0
  23. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_covariates.py +0 -0
  24. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_results.py +0 -0
  25. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_weights.py +0 -0
  26. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/estimators.py +0 -0
  27. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/honest_did.py +0 -0
  28. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/imputation.py +0 -0
  29. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/imputation_bootstrap.py +0 -0
  30. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/imputation_results.py +0 -0
  31. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/power.py +0 -0
  32. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/practitioner.py +0 -0
  33. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/prep.py +0 -0
  34. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/prep_dgp.py +0 -0
  35. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/pretrends.py +0 -0
  36. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/results.py +0 -0
  37. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/stacked_did.py +0 -0
  38. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/stacked_did_results.py +0 -0
  39. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_aggregation.py +0 -0
  40. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_bootstrap.py +0 -0
  41. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/sun_abraham.py +0 -0
  42. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/survey.py +0 -0
  43. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/synthetic_did.py +0 -0
  44. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop.py +0 -0
  45. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop_global.py +0 -0
  46. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop_local.py +0 -0
  47. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop_results.py +0 -0
  48. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/twfe.py +0 -0
  49. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/two_stage.py +0 -0
  50. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/two_stage_bootstrap.py +0 -0
  51. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/two_stage_results.py +0 -0
  52. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/utils.py +0 -0
  53. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/__init__.py +0 -0
  54. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_common.py +0 -0
  55. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_continuous.py +0 -0
  56. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_diagnostic.py +0 -0
  57. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_event_study.py +0 -0
  58. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_power.py +0 -0
  59. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_staggered.py +0 -0
  60. {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_synthetic.py +0 -0
  61. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/build.rs +0 -0
  62. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/bootstrap.rs +0 -0
  63. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/lib.rs +0 -0
  64. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/linalg.rs +0 -0
  65. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/trop.rs +0 -0
  66. {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/weights.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 2.8.1
3
+ Version: 2.8.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -210,7 +210,7 @@ Stacked = StackedDiD
210
210
  Bacon = BaconDecomposition
211
211
  EDiD = EfficientDiD
212
212
 
213
- __version__ = "2.8.1"
213
+ __version__ = "2.8.2"
214
214
  __all__ = [
215
215
  # Estimators
216
216
  "DifferenceInDifferences",
@@ -1122,6 +1122,7 @@ def _compute_robust_vcov_numpy(
1122
1122
  # in the logistic model (predicted probabilities collapse to 0/1).
1123
1123
  _LOGIT_SEPARATION_COEF_THRESHOLD = 10
1124
1124
  _LOGIT_SEPARATION_PROB_THRESHOLD = 1e-5
1125
+ _DEFAULT_EPV_THRESHOLD = 10
1125
1126
 
1126
1127
 
1127
1128
  def solve_logit(
@@ -1132,6 +1133,9 @@ def solve_logit(
1132
1133
  check_separation: bool = True,
1133
1134
  rank_deficient_action: str = "warn",
1134
1135
  weights: Optional[np.ndarray] = None,
1136
+ epv_threshold: float = _DEFAULT_EPV_THRESHOLD,
1137
+ context_label: str = "",
1138
+ diagnostics_out: Optional[dict] = None,
1135
1139
  ) -> Tuple[np.ndarray, np.ndarray]:
1136
1140
  """
1137
1141
  Fit logistic regression via IRLS (Fisher scoring).
@@ -1164,6 +1168,18 @@ def solve_logit(
1164
1168
  maximum likelihood estimator, matching R's ``svyglm(family=binomial)``.
1165
1169
  When None (default), behavior is identical to unweighted logistic
1166
1170
  regression.
1171
+ epv_threshold : float, default 10
1172
+ Events Per Variable threshold. When the ratio of minority-class
1173
+ observations to predictor variables (excluding intercept) falls
1174
+ below this value, a warning is
1175
+ emitted (or ValueError raised if ``rank_deficient_action="error"``).
1176
+ Based on Peduzzi et al. (1996).
1177
+ context_label : str, default ""
1178
+ Optional label for warning messages (e.g., "cohort g=4") to help
1179
+ users identify which logit estimation triggered the warning.
1180
+ diagnostics_out : dict, optional
1181
+ If provided, populated with EPV diagnostic info:
1182
+ ``{"epv": float, "n_events": int, "k": int, "is_low": bool}``.
1167
1183
 
1168
1184
  Returns
1169
1185
  -------
@@ -1273,6 +1289,45 @@ def solve_logit(
1273
1289
  kept_cols = np.arange(k)
1274
1290
  X_solve = X_with_intercept
1275
1291
 
1292
+ # Events Per Variable (EPV) check — Peduzzi et al. (1996)
1293
+ # Use effective (positive-weight) sample when weights have zeros,
1294
+ # since zero-weight rows don't contribute to the likelihood.
1295
+ k_solve = X_solve.shape[1]
1296
+ if weights is not None and np.any(weights == 0):
1297
+ y_eff = y[weights > 0]
1298
+ n_eff = len(y_eff)
1299
+ else:
1300
+ y_eff = y
1301
+ n_eff = n
1302
+ n_pos_y = int(np.sum(y_eff))
1303
+ n_neg_y = n_eff - n_pos_y
1304
+ n_events = min(n_pos_y, n_neg_y)
1305
+ # Peduzzi et al. (1996) define EPV using predictor variables, excluding
1306
+ # the intercept. k_solve includes the intercept column, so use k_solve - 1.
1307
+ n_predictors = k_solve - 1 # exclude intercept
1308
+ epv = n_events / n_predictors if n_predictors > 0 else float("inf")
1309
+
1310
+ if diagnostics_out is not None:
1311
+ diagnostics_out["epv"] = epv
1312
+ diagnostics_out["n_events"] = n_events
1313
+ diagnostics_out["k"] = n_predictors
1314
+ diagnostics_out["is_low"] = epv < epv_threshold
1315
+
1316
+ if epv < epv_threshold:
1317
+ ctx = f" for {context_label}" if context_label else ""
1318
+ msg = (
1319
+ f"Low Events Per Variable (EPV = {epv:.1f}) in propensity score "
1320
+ f"model{ctx}. {n_events} minority-class observations for "
1321
+ f"{n_predictors} predictor variable(s). "
1322
+ f"Peduzzi et al. (1996) recommend EPV >= {epv_threshold:.0f}. "
1323
+ f"Estimates may be unreliable (overfitting, biased coefficients, "
1324
+ f"inflated standard errors). "
1325
+ f"Consider estimation_method='reg' to avoid propensity scores."
1326
+ )
1327
+ if rank_deficient_action == "error":
1328
+ raise ValueError(msg)
1329
+ warnings.warn(msg, UserWarning, stacklevel=2)
1330
+
1276
1331
  # IRLS (Fisher scoring)
1277
1332
  beta_solve = np.zeros(X_solve.shape[1])
1278
1333
  converged = False