Skip to content

Commit 96c71de

Browse files
igerberclaude
andcommitted
Honor rank_deficient_action="error" in propensity score paths
Re-raise ValueError in IPW/DR except blocks when rank_deficient_action is "error" instead of silently falling back to unconditional estimation. Applies to CallawaySantAnna and TripleDifference PS paths. Add estimator-level regression tests asserting ValueError propagation with collinear covariates under error mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 46077d4 commit 96c71de

4 files changed

Lines changed: 67 additions & 0 deletions

File tree

diff_diff/staggered.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,8 @@ def _ipw_estimation(
14891489
if pscore_cache is not None and pscore_key is not None:
14901490
pscore_cache[pscore_key] = beta_logistic
14911491
except (np.linalg.LinAlgError, ValueError):
1492+
if self.rank_deficient_action == "error":
1493+
raise
14921494
# Fallback to unconditional if logistic regression fails
14931495
warnings.warn(
14941496
"Propensity score estimation failed. "
@@ -1662,6 +1664,8 @@ def _doubly_robust(
16621664
if pscore_cache is not None and pscore_key is not None:
16631665
pscore_cache[pscore_key] = beta_logistic
16641666
except (np.linalg.LinAlgError, ValueError):
1667+
if self.rank_deficient_action == "error":
1668+
raise
16651669
# Fallback to unconditional if logistic regression fails
16661670
warnings.warn(
16671671
"Propensity score estimation failed. "

diff_diff/triple_diff.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,8 @@ def _estimate_ddd_decomposition(
772772
rank_deficient_action=self.rank_deficient_action,
773773
)
774774
except Exception:
775+
if self.rank_deficient_action == "error":
776+
raise
775777
pscore_sub = np.full(n_sub, np.mean(PA4))
776778
ps_estimated = False
777779
warnings.warn(

tests/test_methodology_triple_diff.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,3 +1556,24 @@ def test_r_squared_respects_rank_deficient_action(self):
15561556
# Both should produce finite results regardless
15571557
assert np.isfinite(result_silent.att)
15581558
assert np.isfinite(result_warn.att)
1559+
1560+
@pytest.mark.parametrize("method", ["ipw", "dr"])
1561+
def test_rank_deficient_action_error_raises_in_ps_path(self, method):
1562+
"""rank_deficient_action='error' raises ValueError in PS-based paths with collinear covariates."""
1563+
data = generate_ddd_data(n_per_cell=50, seed=42, add_covariates=True)
1564+
data["age_dup"] = data["age"].copy()
1565+
1566+
ddd = TripleDifference(
1567+
estimation_method=method,
1568+
rank_deficient_action="error",
1569+
)
1570+
1571+
with pytest.raises((ValueError, RuntimeError)):
1572+
ddd.fit(
1573+
data,
1574+
outcome="outcome",
1575+
group="group",
1576+
partition="partition",
1577+
time="time",
1578+
covariates=["age", "age_dup"],
1579+
)

tests/test_staggered.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,46 @@ def test_reg_nyt_rank_deficient_action_warn(self):
12751275
assert results.overall_att is not None
12761276
assert results.overall_se > 0
12771277

1278+
def test_ipw_rank_deficient_action_error_raises(self):
1279+
"""IPW path raises ValueError with rank_deficient_action='error' and collinear covariates."""
1280+
data = generate_staggered_data_with_covariates(seed=42)
1281+
data["x1_dup"] = data["x1"].copy()
1282+
1283+
cs = CallawaySantAnna(
1284+
estimation_method="ipw",
1285+
rank_deficient_action="error",
1286+
)
1287+
1288+
with pytest.raises(ValueError, match="[Rr]ank"):
1289+
cs.fit(
1290+
data,
1291+
outcome="outcome",
1292+
unit="unit",
1293+
time="time",
1294+
first_treat="first_treat",
1295+
covariates=["x1", "x1_dup"],
1296+
)
1297+
1298+
def test_dr_rank_deficient_action_error_raises(self):
1299+
"""DR path raises ValueError with rank_deficient_action='error' and collinear covariates."""
1300+
data = generate_staggered_data_with_covariates(seed=42)
1301+
data["x1_dup"] = data["x1"].copy()
1302+
1303+
cs = CallawaySantAnna(
1304+
estimation_method="dr",
1305+
rank_deficient_action="error",
1306+
)
1307+
1308+
with pytest.raises(ValueError, match="[Rr]ank"):
1309+
cs.fit(
1310+
data,
1311+
outcome="outcome",
1312+
unit="unit",
1313+
time="time",
1314+
first_treat="first_treat",
1315+
covariates=["x1", "x1_dup"],
1316+
)
1317+
12781318
def test_bootstrap_single_unit_cohort_handles_gracefully(self, ci_params):
12791319
"""Test that bootstrap handles cohort with 1 treated unit without crashing."""
12801320
# Build small dataset where one cohort has exactly 1 unit

0 commit comments

Comments
 (0)