Skip to content

Commit a2f9cd5

Browse files
igerberclaude
andcommitted
fix(rdd): exact-constancy variance rescue for constant-float outcome sides
np.var of a constant 0.7 vector is ~1.3e-32 (single-pass mean roundoff) while R's two-pass var() is exactly 0, so the var==0 rescue was skipped for constant non-representable-float outcomes and the resulting Inf J_MV crashed bin construction on a zero jump. Route the side variances through the port's _var0 exact-constancy helper (the R semantics already ported for the fuzzy identification stop), reproducing R's J=1 rescue, its Inf J_IMSE / NaN J_MV echoes, and the variability warning. Regression test with a constant-0.7 side added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwhFMnFQGBumYfwbeQaUjm
1 parent 4af6867 commit a2f9cd5

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

diff_diff/rdplot.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from ._rdrobust_port import (
4040
_covs_gamma,
4141
_normalize_kernel,
42+
_var0,
4243
covs_drop_fun,
4344
qrXXinv,
4445
rdrobust_kweight,
@@ -854,8 +855,15 @@ def _selector_grams() -> Tuple[int, np.ndarray, np.ndarray, np.ndarray, np.ndarr
854855
drk_i_l = _pow_outer(x_bar_i_l, k - 1) * j_arange
855856
drk_i_r = _pow_outer(x_bar_i_r, k - 1) * j_arange
856857

857-
var_y_l = float(np.var(y_l, ddof=1))
858-
var_y_r = float(np.var(y_r, ddof=1))
858+
# R's two-pass var() is EXACTLY zero on a constant vector; numpy's
859+
# single-pass mean leaves ~1e-32 roundoff for constants like 0.7,
860+
# which would skip the var==0 rescue below and crash the bin grid on
861+
# a zero jump (J_MV = Inf). Route through the port's exact-constancy
862+
# helper (_var0, the #686-ported R semantics) so constant sides give
863+
# var_y = 0.0 exactly, reproducing R's rescue and its 0/0 -> NaN
864+
# selector echoes.
865+
var_y_l = 0.0 if _var0(y_l) else float(np.var(y_l, ddof=1))
866+
var_y_r = 0.0 if _var0(y_r) else float(np.var(y_r, ddof=1))
859867

860868
mu0_i_l = rk_i_l @ gamma_k1_l
861869
mu0_i_r = rk_i_r @ gamma_k1_r

tests/test_rdplot.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,27 @@ def test_manual_h_with_zero_effective_obs_raises(self):
350350
with pytest.raises(ValueError, match="zero effective observations"):
351351
RDPlot(h=0.1).fit(df, "y", "x")
352352

353+
def test_constant_float_outcome_side_rescued_exactly(self):
354+
# np.var of a constant 0.7 vector is ~1.3e-32, NOT 0 (single-pass
355+
# mean roundoff), while R's two-pass var() is exactly 0 - without
356+
# the _var0 exact-constancy routing the var==0 rescue is skipped
357+
# and the zero jump crashes bin construction (ZeroDivisionError;
358+
# caught by CI codex on the rebased head). R fires the rescue:
359+
# J=1 with the variability warning, Inf J_IMSE, NaN J_MV.
360+
rng = np.random.default_rng(7)
361+
x = np.concatenate([-rng.uniform(0.1, 1, 15), rng.uniform(0.1, 1, 15)])
362+
y = np.where(x < 0, 0.7, rng.normal(size=30))
363+
df = pd.DataFrame({"y": y, "x": x})
364+
with warnings.catch_warnings(record=True) as caught:
365+
warnings.simplefilter("always")
366+
r = RDPlot().fit(df, "y", "x")
367+
messages = [str(w.message) for w in caught]
368+
assert any("variability" in m and "below" in m for m in messages)
369+
assert r.J[0] == 1.0
370+
assert np.isinf(r.J_IMSE[0])
371+
assert np.isnan(r.J_MV[0]) # 0/0 through the MV selector, as in R
372+
assert np.isfinite(r.J[1]) and r.J[1] >= 1
373+
353374
def test_finite_singular_selector_gram_does_not_downgrade(self):
354375
# Companion to the overflow-ladder golden: a finite rank-deficient
355376
# k=4 Gram (4 distinct x values per side) is absorbed inside

0 commit comments

Comments
 (0)