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.
- {diff_diff-2.8.1 → diff_diff-2.8.2}/PKG-INFO +1 -1
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/__init__.py +1 -1
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/linalg.py +55 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered.py +477 -166
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_results.py +68 -3
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_triple_diff.py +97 -4
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_triple_diff_results.py +77 -12
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/triple_diff.py +137 -11
- {diff_diff-2.8.1 → diff_diff-2.8.2}/pyproject.toml +1 -1
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/Cargo.lock +1 -1
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/Cargo.toml +1 -1
- {diff_diff-2.8.1 → diff_diff-2.8.2}/README.md +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/_backend.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/bacon.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/bootstrap_utils.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/continuous_did.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/datasets.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/diagnostics.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_bootstrap.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/estimators.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/honest_did.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/imputation.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/imputation_results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/power.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/practitioner.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/prep.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/pretrends.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/stacked_did.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/survey.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop_global.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop_local.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/trop_results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/twfe.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/two_stage.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/two_stage_results.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/utils.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_event_study.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/build.rs +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/bootstrap.rs +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/lib.rs +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/linalg.rs +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/trop.rs +0 -0
- {diff_diff-2.8.1 → diff_diff-2.8.2}/rust/src/weights.rs +0 -0
|
@@ -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
|