From f542694e214044d6db392451bde01fa84698a08d Mon Sep 17 00:00:00 2001 From: Jayesh Suryavanshi Date: Thu, 30 Jul 2026 02:11:43 -0700 Subject: [PATCH] FIX DUMMY: avoid spurious out-of-range threshold warning DUMMY.eval adds eps to the percentile threshold so the top-scoring point stays an inlier at contam=0 (cut uses decision >= limit), but passed the eps-bumped limit to _check_threshold. When the percentile is the maximum (== 1 on min-max normalized scores), limit == 1 + eps > 1 and tripped the "threshold outside the range" warning on every default call (the test suite triggered it 10 times, all from tests/test_dummy.py). Validate the in-range percentile instead; the stored thresh_ and the returned labels are unchanged. Adds a regression test. --- CHANGES.txt | 1 + pythresh/thresholds/dummy.py | 12 ++++++++++-- tests/test_dummy.py | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3876996..505ec3d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -114,3 +114,4 @@ v<1.1.0>, <05/08/2026> -- Updated docs infrastructure based on pyproject v<1.1.0>, <05/08/2026> -- Modernized repo infrastructure to use pyproject v<1.1.0>, <05/08/2026> -- Moved tests and updated them to use pytest v<1.1.0>, <05/08/2026> -- Modernized CI and publish pipelines +v<1.1.2>, <07/30/2026> -- Fixed spurious "threshold outside the range" warning emitted by the DUMMY thresholder on every default call diff --git a/pythresh/thresholds/dummy.py b/pythresh/thresholds/dummy.py index 2327eea..991d345 100644 --- a/pythresh/thresholds/dummy.py +++ b/pythresh/thresholds/dummy.py @@ -68,9 +68,17 @@ def eval(self, decision): eps = np.finfo(decision.dtype).eps perc = (1 - self.contam) * 100 - limit = np.percentile(decision, perc) + eps - self._check_threshold(limit) + # ``decision`` is min-max normalized to [0, 1], so the percentile is + # also in [0, 1]. Adding eps keeps the top-scoring point an inlier at + # contam=0 (``cut`` uses ``decision >= limit``). Validate the in-range + # percentile rather than the eps-bumped limit; otherwise the maximum + # score (percentile == 1) makes limit == 1 + eps and trips a spurious + # "threshold outside the range" warning on every default call. + perc_val = np.percentile(decision, perc) + limit = perc_val + eps + + self._check_threshold(perc_val) self.thresh_ = limit diff --git a/tests/test_dummy.py b/tests/test_dummy.py index a4d0d14..fdf47d6 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -1,3 +1,4 @@ +import warnings from itertools import product import joblib @@ -141,3 +142,24 @@ def test_save_and_load(tmp_path, scores, score_case): loaded = joblib.load(file) assert_equal(thres.predict(s), loaded.predict(s)) + + +# ----------------------- +# Regression: no spurious out-of-range warning +# ----------------------- + + +@pytest.mark.parametrize("contam,score_case", param_grid) +def test_no_spurious_threshold_warning(scores, contam, score_case): + # DUMMY's threshold is a percentile of min-max normalized scores plus eps. + # At the maximum (contam=0) the eps used to push the limit to 1 + eps and + # trip the "threshold outside the range" check on every call. It must not. + _, idx = score_case + s = scores[idx] + + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + DUMMY(contam=contam).eval(s) + + offending = [w for w in record if "outside the range" in str(w.message)] + assert not offending, f"DUMMY emitted a spurious out-of-range warning: {[str(w.message) for w in offending]}"