Four machine-learning professionals reviewing prediction results, with “No feasible threshold” displayed on a monitor.

Why the Last Precision–Recall Curve Point Has No Threshold

Sat, Oct 24, 2026

A precision-recall curve can report a precision value of 1 even when no deployable threshold in the supplied score fixture reaches a precision floor of 0.8. That is not a contradiction in scikit-learn, and it is not evidence that a threshold has been lost. It is a boundary error in the consumer of the arrays.

In scikit-learn 1.8.0, precision_recall_curve returns one more precision/recall pair than it returns thresholds. The final pair is documented as precision 1 and recall 0, with no corresponding threshold; its purpose is to complete the plotted curve. For every real returned threshold, by contrast, the documented prediction semantics are score >= threshold. The API therefore provides a valid curve while deliberately not providing a deployable decision for its final point.

This distinction matters at the score-to-action boundary. A plotting convention, an application feasibility rule, an executable comparator, and a release approval are four different evidence objects. Treating them as interchangeable is how a selector can approve a nonexistent threshold, how np.inf can be padded into a threshold array, and how an empty prediction set can be misreported as “precision 1.”

The maintenance lab below stays deliberately narrow. It uses six synthetic records, binary labels 0/1, positive label 1, finite float64 ranking scores, drop_intermediate=False, and the decision rule score >= threshold. The scores happen to lie in [0,1], but they are ranking scores, not claimed calibrated probabilities. The accepted candidate set consists only of real thresholds returned by scikit-learn, and this application requires at least one flagged record. The supplied preparation record reports PASS_12_CASES on CPython 3.12.14, NumPy 2.3.5, and scikit-learn 1.8.0, with source SHA-256 cf8f22c44a8d44e67a0a375cf788a565126960e05a09ac63a27daeffd07ac923. Those six rows prove fixture arithmetic and selector behavior; they do not establish future model performance.

Define the operating contract and freeze the runtime

A curve does not approve an operating policy. The reviewed output for this task must be one of two things: a real threshold together with its exact semantics and evidence, or an explicit statement that no real candidate satisfies the declared constraints. A threshold selector that always returns a number is not automatically more useful than one that can return None; when the constraints are infeasible, manufacturing a number destroys the distinction between “no candidate exists” and “the software failed to choose.”

The application contract is intentionally strict. Inputs are one finite, nonempty, one-dimensional label vector and one aligned score vector of the same length. Both binary labels 0 and 1 must appear. Label 1 is positive. Higher scores mean “more positive.” A record is flagged when score >= threshold. Every distinct observed score returned as a threshold by precision_recall_curve(..., drop_intermediate=False) is eligible for consideration. Empty-output decisions are excluded by min_flagged >= 1. Precision and recall floors, minimum volume, and the deterministic ordering among feasible candidates are application choices, not universal scikit-learn requirements.

A useful review technique is to write that contract in the same vocabulary the application will use. “Choose the best precision-recall point” is too vague: it does not say whether a point has a real threshold, what comparison operator converts scores to predictions, whether zero selected records are allowed, what happens when no candidate satisfies the floor, or how ties among feasible candidates are resolved. “Choose among real returned thresholds using >=, require precision at least P, recall at least R, and at least N flagged records; otherwise return NO_FEASIBLE_THRESHOLD” is testable. The difference is not stylistic. The second statement can be replayed against record IDs and can fail in a way a reviewer can classify.

It also helps to separate three approvals that teams often compress into one boolean. Arithmetic approval asks whether counts and metrics match the declared fixture. Policy approval asks whether the application’s floors, comparator, and minimum volume reflect an owned decision. Release approval asks whether representative validation and final-test evidence justify using that policy with a particular score-producing system. This article can establish the first, demonstrate the mechanics needed for the second, and specify evidence required for the third. It cannot infer the third from six synthetic rows.

That narrow boundary complements the broader model-evaluation workflow, rather than replacing it. Refonte’s general evaluation example already handles the endpoint alignment correctly by comparing recall[:-1] and precision[:-1] with the threshold array; the bug investigated here is therefore not a defect in that example. This article isolates the maintenance problem that begins after scores and labels already exist: can a consumer turn the curve arrays into a defensible, executable action?

The runtime is part of the evidence. The supplied preparation was executed on CPython 3.12.14, NumPy 2.3.5, and scikit-learn 1.8.0. The relevant API contract should therefore be read against the versioned scikit-learn 1.8.0 documentation, not an unspecified “current” page. The 1.8.0 precision_recall_curve documentation states that precision and recall have shape n_thresholds + 1, thresholds have shape n_thresholds, thresholds are increasing, the last precision/recall pair is 1/0 without a corresponding threshold, and metric element i before that endpoint corresponds to predictions with score >= thresholds[i]. It also documents drop_intermediate=False as retaining thresholds rather than dropping suboptimal plotting points.

No estimator fit is necessary for this diagnostic. The API accepts a binary truth vector and a score vector; the score input can be a non-thresholded decision measure, so this fixture can test array semantics without training a classifier. That is useful reliability practice: remove model fitting, data loading, network access, and randomness when the alleged failure lives entirely in deterministic score-to-threshold logic.

A separate verification run used a normalized transcription of the script on CPython 3.13.5, NumPy 2.3.5, and scikit-learn 1.8.0; it also ended with PASS_12_CASES. That rerun is not the supplied preparation execution, and its transcription has SHA-256 01a87899f5c8a36f391470d0edac2d0de26025d7a09f48e7ef28a083c776a473, not the supplied source hash. That difference is exactly why a release record should bind results to the actual source identity and environment rather than treating semantically similar snippets as byte-identical evidence.

Build the six-record oracle and align only real thresholds

Before calling scikit-learn, write down the records. Doing so creates an oracle independent of curve-array indexing. There are three positive labels and three negative labels. R2 and R3 are especially important: they share the score 0.8 but have opposing labels, so any literal scalar threshold using >= must select both or neither.

Record

Label

Score

R1

0

0.9

R2

1

0.8

R3

0

0.8

R4

1

0.5

R5

1

0.4

R6

0

0.1

The distinct observed scores are 0.1, 0.4, 0.5, 0.8, 0.9, so with drop_intermediate=False the fixture has five real threshold candidates. Their nonempty selected sets can be derived without any metric function. At 0.1, all six records are flagged. At 0.4, R1 through R5 are flagged. At 0.5, R1 through R4 are flagged. At 0.8, R1, R2, and R3 are flagged. At 0.9, only R1 is flagged. A conceptual threshold above the maximum score would select nothing, but that is not one of the five thresholds returned for this fixture and is excluded anyway by the application’s minimum-volume rule.

These synthetic rows establish implementation behavior only. They do not make a population claim. For a real release, threshold selection belongs on representative validation evidence, with a protected final test reserved for the estimate after development choices are fixed. The scikit-learn 1.8 decision-threshold guide explicitly separates score estimation from the decision made by thresholding and warns against tuning a threshold on the same data used to train a prefit model. Refonte’s overview of validation and final-test roles gives adjacent context; this fixture is not a substitute for those roles.

The supplied execution returns these exact arrays:

thresholds = [0.1, 0.4, 0.5, 0.8, 0.9]
precision  = [0.5, 0.6, 0.5, 1/3, 0.0, 1.0]
recall     = [1.0, 1.0, 2/3, 1/3, 0.0, 0.0]

The important shape invariant is therefore:

len(precision) == len(recall) == len(thresholds) + 1
6              == 6           == 5 + 1

The first five precision/recall positions are threshold-bearing rows. The sixth position is not. The 1.8.0 API documents precisely this unequal-length contract: precision and recall have n_thresholds + 1 elements, while thresholds has n_thresholds; the last metrics are precision 1 and recall 0 with no corresponding threshold.

That last pair must remain visible in the explanation because deleting it mentally is what encourages fragile code. But it must not be inserted into the candidate ledger. The correct five-row ledger is:

Threshold

TN

FP

FN

TP

Flagged

Precision

Recall

0.1

0

3

0

3

6

1/2

1

0.4

1

2

0

3

5

3/5

1

0.5

1

2

1

2

4

1/2

2/3

0.8

1

2

2

1

3

1/3

1/3

0.9

2

1

3

0

1

0

0

There is no sixth threshold-bearing row. In particular, the endpoint is not threshold 0, not threshold inf, not the previous threshold repeated, and not a hidden value omitted by scikit-learn. The endpoint is a curve convention. A legitimate perfect-precision value can still occur earlier at a real threshold; the repair is to align candidates by threshold index, not to blacklist precision == 1.

This is the key answer to the “precision recall curve last point” puzzle. A precision value can be present in the plotting arrays without denoting an operating threshold because the curve object and the decision-candidate ledger have different cardinalities by documented design. Once the consumer accepts that contract, the apparent contradiction disappears.

The safest implementation shape mirrors that cardinality instead of fighting it. Treat thresholds as the authoritative iterator for candidate decisions. For each threshold index i, consume exactly precision[i] and recall[i], then verify them by replay. After the loop, there will be one unused precision/recall position. That leftover is expected evidence of the API contract, not a missing candidate to be repaired. An assertion such as len(p) == len(r) == len(t) + 1 turns that expectation into executable documentation. If a future dependency changes the contract, the test stops at the boundary instead of silently shifting threshold correspondence.

That design is stronger than merely slicing p[:-1] and r[:-1] in one selection expression. Slicing is correct for this API, but the ledger makes the reason auditable: each accepted row has a threshold, a literal prediction vector, a count tuple, and independently reproduced metrics. The array lengths then become one check among several rather than the sole defense against a positional bug.

Reproduce the broken selector and reject infinity padding

The minimal broken selector applies a precision floor to the entire precision array and assumes every passing index can index thresholds:

eligible_bad = np.flatnonzero(p >= 0.8)
threshold = t[eligible_bad[0]]

For this fixture, the five real precision values are 0.5, 0.6, 0.5, 1/3, 0. None reaches 0.8. The only element satisfying p >= 0.8 is the unpaired endpoint at index 5, so eligible_bad.tolist() is [5]. But t has valid indices 0 through 4. Indexing t[5] raises IndexError. That is the supplied P2 failure, and it is a REPAIR disposition: array alignment and feasible-candidate handling are broken.

The failure is valuable because it preserves the truth. The fixture contains no real threshold satisfying the requested precision floor. Error handling becomes dangerous when it tries to “help” by manufacturing a threshold, swallowing the exception, relaxing the floor, or silently returning a familiar default.

A default of 0.5 is demonstrably not a repair here. With the declared >= comparator, threshold 0.5 flags four records, producing TP=2 and FP=2, hence precision 2/(2+2)=0.5. A selector asked for precision at least 0.8 cannot justify that threshold while claiming to have honored the request. The correct outcome is NO_FEASIBLE_THRESHOLD with threshold=None.

A more subtle workaround is to pad the threshold array:

padded = np.r_[t, np.inf]
threshold = padded[5]
pred = (S >= threshold).astype(int)

This makes the array lengths equal, but it does not make the sixth point measured threshold evidence. With all fixture scores finite, S >= np.inf is false for every record, so the fabricated policy flags zero records. The application contract already says min_flagged >= 1, which independently makes that candidate infeasible.

The misleading part appears when the empty prediction vector is scored. Precision is defined as TP / (TP + FP). For an all-negative prediction vector, TP + FP == 0, so the empirical precision denominator is absent. The scikit-learn 1.8.0 precision_score documentation says the zero-division behavior is configurable: zero_division=1 returns 1, zero_division=0 returns 0, and the default warning mode behaves like zero while raising an UndefinedMetricWarning.

The supplied P3 case proves the distinction directly. The infinity-padded policy flags 0 records; precision_score(..., zero_division=1) returns 1, while zero_division=0 returns 0. Those values are reporting conventions for an undefined division. The 1 does not mean the policy made some positive decisions and all were correct. It means there were no positive predictions and the caller chose the convention “return one in this zero-division case.”

That distinction is operationally critical. An empty-output policy may be entirely appropriate in another application. scikit-learn does not universally forbid it, and this article does not claim otherwise. Here, however, the deliberately chosen policy requires at least one flagged record. Padding infinity therefore fails twice: first because it fabricates correspondence to an endpoint that the API says has no threshold, and second because the resulting decision violates min_flagged >= 1.

The reliable rule is simple: never make the threshold array look structurally convenient by inventing a value. Start from the returned real thresholds, align only p[i] and r[i] for i < len(thresholds), then apply application feasibility constraints to those real rows. If no row survives, preserve infeasibility as data.

Preserving that state is an engineering feature. Downstream code can route NO_FEASIBLE_THRESHOLD for review, retain the previous approved policy, stop a release, or invoke some separately specified fallback. What it must not do is reinterpret absence as a threshold chosen by the metric function. A null threshold with a typed decision communicates more information than an arbitrary scalar because it records that the candidate search was valid and exhausted under the declared constraints.

The same discipline applies to observability. A dashboard should distinguish “no candidate met the floor” from “input invalid,” “selector crashed,” and “policy selected zero records.” Collapsing those states into a single zero, NaN, or default threshold makes post-release diagnosis harder and can create exactly the false-feasibility story this fixture exposes. The repair therefore belongs both in selector logic and in the schema of the evidence it emits.

Replay literal predictions, counts, comparators, and ties

The most persuasive repair does not trust curve arrays alone. It rebuilds every threshold candidate from the raw records and compares the independently derived metrics with the corresponding curve positions. This candidate ledger creates a second route from inputs to evidence: threshold -> literal predictions -> confusion counts -> precision/recall.

For threshold 0.4, the predicate S >= 0.4 flags R1, R2, R3, R4, and R5. Their labels are 0,1,0,1,1, so there are three true positives and two false positives. R6 is the only unflagged record and is negative, so TN=1 and FN=0. Precision is therefore 3/(3+2)=3/5=0.6, and recall is 3/(3+0)=1. That reproduces the second threshold-bearing curve row exactly.

At threshold 0.8, the literal predicate flags R1, R2, and R3 because both tied 0.8 scores satisfy equality. Their labels are 0,1,0, giving TP=1 and FP=2. R4 and R5 are missed positives, while R6 is a true negative, so TN=1, FN=2. Precision is 1/(1+2)=1/3; recall is 1/(1+2)=1/3. Again, the row agrees with the aligned curve arrays.

The script obtains counts with confusion_matrix(y, pred, labels=[0, 1]).ravel(). The 1.8.0 confusion_matrix documentation defines rows by true class and columns by predicted class. For binary labels ordered [0,1], raveling the 2x2 matrix therefore yields TN, FP, FN, TP; passing labels=[0,1] fixes the order explicitly. This article uses that contract only to stabilize count extraction; it does not broaden into the separate label-subset issues covered elsewhere.

Literal replay also exposes comparator drift. The precision_recall_curve return contract defines threshold-bearing metrics using predictions with score >= thresholds[i]. The released policy must therefore preserve >= unless a separately reviewed policy intentionally changes it.

At 0.8, the difference is not cosmetic:

Comparator at 0.8

Flagged IDs

TN

FP

FN

TP

Precision

Recall

score >= 0.8

R1, R2, R3

1

2

2

1

1/3

1/3

score > 0.8

R1

2

1

3

0

0

0

The first row is the tested policy. The second row is a different policy. Changing one character changes which records are actioned and changes the confusion counts. A release record that stores only “threshold = 0.8” is therefore incomplete; it must store or otherwise bind the comparator semantics.

The tie at 0.8 provides another useful regression test. A scalar threshold cannot distinguish R2 from R3 because they have identical scores. Under >= 0.8, both are selected. Under > 0.8, both are excluded. Selecting R2 while excluding R3 would require another decision variable or a secondary tie-breaking rule. That may be legitimate in a different system, but it is no longer the score-threshold policy tested here and requires fresh evidence.

For the same reason, a count limit cannot silently split an equal-score group and still be described as the tested threshold rule. Imagine sorting records by score and taking only the first two records at or above 0.8. Depending on stable ordering, that could include R1 and R2 while dropping R3 even though R2 and R3 have equal scores. That is a top-K or capacity-constrained ranking policy, not score >= 0.8. It may be useful, but its behavior, tie handling, and evidence must be reviewed on their own terms.

The maintenance lesson is broader than this fixture but still precise: the threshold value, comparison operator, tie semantics, score direction, positive label, and any capacity rule together define the action. Curve arrays alone do not encode all of those implementation choices. Replaying predictions from record IDs turns hidden assumptions into inspectable evidence.

Implement explicit feasibility and qualify the repair with controls

The repaired selector works from an aligned candidate ledger, not from a padded array. For each real threshold it calculates literal predictions, fixed-order confusion counts, flagged volume, precision, and recall. It then applies three feasibility constraints: precision >= precision_floor, recall >= recall_floor, and flagged >= min_flagged. If no row survives, it returns {"decision": "NO_FEASIBLE_THRESHOLD", "threshold": None}.

If several rows survive, this fixture’s application policy chooses deterministically by maximum recall, then higher precision, then fewer flagged records, then larger threshold. That ordering is not a scikit-learn default and not a general recommendation. It is simply the declared policy needed to make this test reproducible. A different business policy could choose another ordering, but that would be a policy revision rather than a bug fix.

On the baseline labels, precision floor 0.8 produces P4: NO_FEASIBLE_THRESHOLD. That is the correct outcome because none of the five real threshold rows reaches 0.8. At precision floor 0.6, P5 has one best candidate under the declared ordering: threshold 0.4, five flagged records, precision 0.6, and recall 1. The correct disposition is ACCEPT_FIXTURE_POLICY for that tested arithmetic under the declared fixture contract. That wording matters: it does not promote a six-row synthetic pass into a production approval.

The controls show why a robust repair needs more than the one failing case.

First, P7 changes min_flagged to 6 while keeping precision floor 0.6. Threshold 0.1 flags six records but has precision 0.5; threshold 0.4 reaches precision 0.6 but flags only five. No row satisfies both constraints, so the correct result remains NO_FEASIBLE_THRESHOLD. A selector must preserve all constraints instead of relaxing volume merely because a precision-feasible row exists.

Second, P8 changes only the labels to Y_CONTROL = [1,0,0,1,1,0]. Now threshold 0.9 flags R1, and R1 is positive. The real threshold-bearing row therefore has TP=1, FP=0, precision 1, recall 1/3, and one flagged record. At precision floor 0.8, the selector legitimately chooses 0.9. This control proves that “drop every precision value equal to one” would be an incorrect repair. Perfect precision is not the problem; the unpaired position is.

Third, P9–P12 exercise the application guardrails. An all-zero label vector violates the requirement that both binary labels be present. A NaN score violates finite-score input. A shorter label vector violates alignment. A label vector containing 2 violates the {0,1} contract. These cases are not threshold infeasibility; the inputs themselves fail the declared application contract, so the disposition is HOLD_INVALID_INPUT.

This separation is important in operational review. NO_FEASIBLE_THRESHOLD says valid evidence was evaluated and no candidate met the constraints. HOLD_INVALID_INPUT says candidate evaluation should not proceed because the evidence object does not satisfy the input contract. REPAIR says the implementation or its threshold correspondence is wrong. ACCEPT_FIXTURE_POLICY says the tested code and arithmetic agree for the declared synthetic fixture. They answer different questions.

Those checks sit alongside, rather than replace, broader common model-evaluation mistakes such as unrepresentative data, leakage, or poor validation design. A perfectly repaired threshold selector can still be attached to weak labels or nonrepresentative validation scores. Correct code is necessary evidence for a release; it is not sufficient evidence of future model quality.

Changing a floor from 0.8 to 0.6 must therefore be treated differently from fixing the endpoint bug. The endpoint repair restores correspondence between real thresholds and their metric rows. Changing the floor alters which outcomes the application considers acceptable. Likewise, changing min_flagged, recall requirements, or deterministic ordering changes policy semantics. Those revisions deserve explicit ownership, versioning, and review rather than being hidden inside exception handling.

Preserve the executable evidence bundle and classify every case

The evidence bundle should retain source identity, environment, inputs, parameters, returned arrays, literal counts, selected record IDs where relevant, outcomes, and the final disposition. The supplied preparation record reports execution on 2026-10-24 with status PASS_12_CASES, CPython 3.12.14, NumPy 2.3.5, scikit-learn 1.8.0, and source SHA-256 cf8f22c44a8d44e67a0a375cf788a565126960e05a09ac63a27daeffd07ac923.

The complete executable fixture is reproduced below. The warnings import is harmless; the tests do not claim that a warning was captured. They test values and exceptions explicitly.

import json
import platform
import warnings

import numpy as np
import sklearn
from sklearn.metrics import confusion_matrix, precision_recall_curve, precision_score

IDS = np.array(["R1", "R2", "R3", "R4", "R5", "R6"])
Y = np.array([0, 1, 0, 1, 1, 0])
S = np.array([0.9, 0.8, 0.8, 0.5, 0.4, 0.1], dtype=np.float64)
CASES = []


def record(case_id, evidence):
    CASES.append({"case": case_id, "evidence": evidence})


def ledger(y, scores):
    y, scores = np.asarray(y), np.asarray(scores, dtype=np.float64)
    if y.ndim != 1 or scores.ndim != 1 or len(y) != len(scores) or len(y) == 0:
        raise ValueError("nonempty aligned one-dimensional arrays required")
    if set(y.tolist()) != {0, 1} or not np.isfinite(scores).all():
        raise ValueError("both binary labels and finite scores required")
    p, r, t = precision_recall_curve(y, scores, pos_label=1, drop_intermediate=False)
    assert len(p) == len(r) == len(t) + 1
    rows = []
    for i, threshold in enumerate(t):
        pred = (scores >= threshold).astype(int)
        tn, fp, fn, tp = confusion_matrix(y, pred, labels=[0, 1]).ravel().tolist()
        count = tp + fp
        row = {"threshold": float(threshold), "tn": tn, "fp": fp, "fn": fn,
               "tp": tp, "flagged": count, "precision": tp / count,
               "recall": tp / (tp + fn)}
        np.testing.assert_allclose([p[i], r[i]], [row["precision"], row["recall"]],
                                   rtol=0, atol=1e-15)
        rows.append(row)
    return p, r, t, rows


def choose(y, scores, precision_floor, recall_floor=0.0, min_flagged=1):
    if not (0 <= precision_floor <= 1 and 0 <= recall_floor <= 1):
        raise ValueError("floors must lie in [0, 1]")
    if not isinstance(min_flagged, int) or min_flagged < 1:
        raise ValueError("min_flagged must be a positive integer")
    , , , rows = ledger(y, scores)
    eligible = [row for row in rows if row["precision"] >= precisionfloor
                and row["recall"] >= recall_floor and row["flagged"] >= min_flagged]
    if not eligible:
        return {"decision": "NO_FEASIBLE_THRESHOLD", "threshold": None}
    best = max(eligible, key=lambda row: (row["recall"], row["precision"],
                                         -row["flagged"], row["threshold"]))
    return {"decision": "ACCEPT_FIXTURE_POLICY", **best}


p, r, t, rows = ledger(Y, S)
np.testing.assert_array_equal(t, [0.1, 0.4, 0.5, 0.8, 0.9])
np.testing.assert_allclose(p, [0.5, 0.6, 0.5, 1 / 3, 0, 1], rtol=0, atol=1e-15)
np.testing.assert_allclose(r, [1, 1, 2 / 3, 1 / 3, 0, 0], rtol=0, atol=1e-15)
assert [[x[k] for k in ("tn", "fp", "fn", "tp")] for x in rows] == [
    [0, 3, 0, 3], [1, 2, 0, 3], [1, 2, 1, 2], [1, 2, 2, 1], [2, 1, 3, 0]]
record("P1", {"precision": p.tolist(), "recall": r.tolist(), "thresholds": t.tolist()})

eligible_bad = np.flatnonzero(p >= 0.8)
assert eligible_bad.tolist() == [5]
try:
    = t[eligiblebad[0]]
except IndexError:
    record("P2", "terminal point passed floor; indexing thresholds[5] raised IndexError")
else:
    raise AssertionError("expected unpaired endpoint")

padded = np.r_[t, np.inf]
bad_pred = (S >= padded[eligible_bad[0]]).astype(int)
assert not bad_pred.any()
assert precision_score(Y, bad_pred, zero_division=1) == 1
assert precision_score(Y, bad_pred, zero_division=0) == 0
record("P3", {"flagged": 0, "precision_zero_division_1": 1,
              "precision_zero_division_0": 0})

assert choose(Y, S, 0.8) == {"decision": "NO_FEASIBLE_THRESHOLD", "threshold": None}
record("P4", choose(Y, S, 0.8))

chosen = choose(Y, S, 0.6)
assert chosen["threshold"] == 0.4 and chosen["flagged"] == 5
assert chosen["precision"] == 0.6 and chosen["recall"] == 1
record("P5", chosen)

ge, gt = (S >= 0.8).astype(int), (S > 0.8).astype(int)
assert IDS[ge.astype(bool)].tolist() == ["R1", "R2", "R3"]
assert IDS[gt.astype(bool)].tolist() == ["R1"]
assert confusion_matrix(Y, ge, labels=[0, 1]).ravel().tolist() == [1, 2, 2, 1]
assert confusion_matrix(Y, gt, labels=[0, 1]).ravel().tolist() == [2, 1, 3, 0]
record("P6", {"ge_ids": IDS[ge.astype(bool)].tolist(), "gt_ids": IDS[gt.astype(bool)].tolist()})

assert choose(Y, S, 0.6, min_flagged=6)["decision"] == "NO_FEASIBLE_THRESHOLD"
record("P7", "volume floor 6 leaves no candidate at precision floor 0.6")

Y_CONTROL = np.array([1, 0, 0, 1, 1, 0])
control = choose(Y_CONTROL, S, 0.8)
assert control["threshold"] == 0.9 and control["precision"] == 1
assert control["recall"] == 1 / 3 and control["flagged"] == 1
record("P8", control)

for case_id, invalid_y, invalid_s in [
    ("P9", np.zeros_like(Y), S),
    ("P10", Y, np.array([np.nan, 0.8, 0.8, 0.5, 0.4, 0.1])),
    ("P11", Y[:-1], S),
    ("P12", np.array([0, 1, 2, 1, 1, 0]), S),
]:
    try:
        choose(invalid_y, invalid_s, 0.6)
    except ValueError as exc:
        record(case_id, {"decision": "HOLD_INVALID_INPUT", "reason": str(exc)})
    else:
        raise AssertionError(f"{case_id} should reject invalid fixture")

assert len(CASES) == 12
print(json.dumps({"environment": {"python": platform.python_version(),
                                  "numpy": np.__version__, "sklearn": sklearn.__version__},
                  "rows": rows, "cases": CASES, "status": "PASS_12_CASES"}, indent=2))

Save the source as a file, compute its SHA-256 before execution, then run it with the intended interpreter, for example:

sha256sum pr_threshold_fixture.py
python3 pr_threshold_fixture.py > pr_threshold_fixture.stdout.json
python3 -c 'import platform, numpy, sklearn; print(platform.python_version(), numpy.__version__, sklearn.__version__)'

Do not overwrite the supplied execution record with a rerun from another environment. Record the rerun separately, including its actual versions and source hash. If the source changes, the new result belongs to a new source identity even when the intended logic is similar.

The twelve cases should remain individually reviewable:

Case

Observed or asserted result

Review implication

P1

Five thresholds, six metric pairs; every real row matches independent counts

Baseline arithmetic established

P2

Floor 0.8 over unsliced precision selects index 5; thresholds[5] raises IndexError

REPAIR array alignment and infeasible-candidate handling

P3

Infinity padding flags zero; zero_division=1 returns 1 and zero_division=0 returns 0

REPAIR fabricated correspondence; enforce count rule

P4

Correct selector returns NO_FEASIBLE_THRESHOLD, threshold=None at floor 0.8

Preserve infeasibility

P5

Floor 0.6 selects 0.4; flagged 5; precision 0.6; recall 1

ACCEPT_FIXTURE_POLICY under declared fixture and ordering

P6

>=0.8 selects R1/R2/R3; >0.8 selects only R1

Comparator is release-critical; mismatch is REPAIR

P7

Floor 0.6 plus min_flagged=6 has no candidate

Preserve all constraints

P8

Changed-label control selects real threshold 0.9 with precision 1, recall 1/3, flagged 1

Keep legitimate perfect-precision candidates

P9–P12

Missing positive class, NaN, length mismatch, or nonbinary label is rejected

HOLD_INVALID_INPUT under this application contract

For review, the four dispositions mean the following:

Disposition

Use it when

Threshold field

ACCEPT_FIXTURE_POLICY

Code, comparator, aligned ledger, constraints, and replay agree on the declared synthetic fixture

Real selected threshold

REPAIR

Selector indexing is broken, a threshold correspondence is fabricated, or released comparator differs from tested semantics

Do not approve affected policy

NO_FEASIBLE_THRESHOLD

Valid evidence was evaluated and no real returned threshold satisfies all declared constraints

None

HOLD_INVALID_INPUT

Input contract fails before candidate evaluation

No candidate evaluation

This matrix prevents one common review collapse: treating every non-acceptance as the same kind of failure. Missing or invalid evidence is not the same as valid evidence showing infeasibility. Conversely, a code defect should not be disguised as an infeasible business constraint.

For a reviewer, the most useful release artifact is not a screenshot of a curve but a compact record that can be regenerated. It should answer: which labels and scores were used; which score meaning and positive class were approved; which real thresholds were returned; which records each threshold flagged; which counts produced each metric; which feasibility rules were applied; which deterministic ordering broke ties; which candidate, if any, was selected; and which source/environment generated the result. A curve image can help communicate trade-offs, but it cannot carry all of those semantics.

The candidate ledger is especially valuable when a change looks harmless. A dependency update may preserve the selected scalar threshold while changing intermediate behavior. A refactor may preserve aggregate precision while changing which tied records are flagged. A policy edit may preserve counts on one fixture while altering fallback behavior when no candidate exists. Comparing record-level replay, counts, arrays, and final disposition gives reviewers several invariants rather than one headline number.

A production release needs more. The reviewed positive class must match the meaning of the score; the scoring/model identity and preprocessing path must be compatible with the evidence; the comparator must match the replayed implementation; policy owners must approve floors and minimum volume; representative validation must be sufficient for the intended claim; and a protected final test should estimate performance after development choices are fixed. The scikit-learn decision-threshold guide supports the separation between scoring and action and cautions against reusing training data for threshold tuning.

The same ownership principle appears in a very different domain in Refonte’s discussion of threshold governance in entity resolution: an action threshold needs explicit decision semantics and an owner. That article concerns identity matching and AWS-specific governance, not this local scikit-learn selector, so none of its product behavior is imported here.

Restore a compatible policy, own it, and preserve regression meaning

Once the selector is repaired, release engineering has to preserve the meaning of the decision across code, evidence, and operations. Ownership should be explicit rather than implied by who happened to edit the notebook.

The data/evaluation owner should be accountable for label meaning, positive-class definition, score provenance, validation population, and whether the evidence is representative enough for the claim being made. The application owner should be accountable for the executable predicate, score direction, threshold loading, tie semantics, minimum-volume handling, and what happens when the selector returns NO_FEASIBLE_THRESHOLD or HOLD_INVALID_INPUT. The policy owner should approve precision/recall floors, minimum flagged volume, candidate ordering, and any business consequence attached to a flag. The release approver should verify that those pieces are bound together in one compatible evidence bundle rather than independently “green.” This division resembles the collaboration boundary described in Refonte’s overview of data-science and ML-engineering responsibilities, where evaluation and statistical interpretation overlap with engineering responsibility for executable systems.

A release record should retain at least the model or scoring identity, the exact positive-label meaning, the score-source contract, the comparator, the selected threshold or explicit no-threshold outcome, application constraints, policy revision, dependency versions, evidence dataset identity, source hash, and replay results. Recording only the scalar threshold is not enough. 0.8 with >= is a different policy from 0.8 with >, and a threshold evaluated on one score-producing pipeline is not automatically validated for another.

Rollback should restore the last approved bundle, not merely the last numeric threshold. After restoration, replay the known fixture or release ledger and verify that the record-level predictions and counts match the approved evidence. If a dependency, score source, comparator, or policy changes materially, rerun the relevant regression evidence and record the new environment rather than asserting equivalence across an untested runtime.

Rollback also has a hard boundary: restoring a selector does not reverse decisions that have already propagated downstream. If flags caused reviews, messages, holds, merges, or other actions, those systems need their own reversal or remediation mechanisms. The threshold release record can tell you which policy produced the decision, but it cannot by itself undo the consequence.

Regression tests should preserve semantics, not merely line coverage. Keep the unequal-length assertion so future maintainers remember that the terminal curve point is unpaired. Keep the P2 failure reproduction so a full-array precision mask cannot silently return. Keep P3 so infinity padding and zero-output “precision 1” remain rejected. Keep P6 so equality at tied scores remains visible. Keep P4 and P7 so infeasibility remains a valid outcome. Keep P8 so legitimate threshold-bearing perfect precision is never removed by an overbroad fix. Keep P9–P12 so invalid inputs do not drift into metric code.

A relevant dependency change should trigger those tests again, but a passing rerun should be described narrowly. It shows that the tested semantics still hold in that recorded environment; it does not establish equivalence for every path in the application or for untested runtime combinations. Likewise, changing policy parameters should produce a new reviewed policy record even if no Python code changes. Source versioning alone cannot represent a decision contract when floors and minimum volumes are configuration.

This is also why the source hash belongs beside, not instead of, the environment and policy parameters. The same bytes can execute against a different library build; the same environment can execute different source; the same code and libraries can receive different policy inputs. Reproducible release evidence binds all of those identities together.

The reviewed operational decision for this fixture is therefore exact. At precision floor 0.8, with recall floor 0, min_flagged=1, real returned thresholds only, positive label 1, higher-is-positive scores, and comparator >=, the correct disposition is NO_FEASIBLE_THRESHOLD with threshold=None. The unsliced endpoint selector and infinity-padding workaround require REPAIR. At precision floor 0.6 under the declared deterministic ordering, threshold 0.4 earns ACCEPT_FIXTURE_POLICY for this fixture: it flags five records, with precision 0.6 and recall 1. Invalid contract inputs receive HOLD_INVALID_INPUT. None of those labels constitutes production approval.

Build the statistical and engineering judgment behind the gate

The central reliability habit is to keep evidence objects separate. precision_recall_curve can validly return a terminal precision of 1 with no threshold because that point completes the curve by documented convention. A deployable candidate, by contrast, must come from a real returned threshold, satisfy the application’s explicit feasibility constraints, and reproduce the expected record-level predictions under the released comparator. A release decision adds still more evidence: representative validation, trustworthy labels, sufficient sample support, an untouched final test, policy ownership, and a compatible implementation record.

That separation prevents two opposite errors. The first is to overread a plotting point as measured operational precision. The second is to overread a deterministic six-row unit test as evidence that a future model will meet the same precision or recall. The fixture proves arithmetic and control-flow behavior under fixed inputs. It says nothing by itself about calibration, prevalence shift, population representativeness, statistical stability, or whether a 0.6 or 0.8 precision floor is appropriate for a real decision.

For practitioners developing the foundations needed to review this kind of boundary, Refonte Learning’s Data Science & AI program currently lists a three-month schedule at 12–14 hours per week and competencies including Python Data Science, Statistical Modelling, Machine Learning and Predictive Modelling, model optimization/problem solving, visualization, and industry projects. Its FAQ lists Python, Jupyter Notebook, pandas, NumPy, Matplotlib, scikit-learn, and TensorFlow, and the page describes a virtual internship and certificates upon successful completion. The page also contains inconsistent prerequisite language: the admissions section says applicants must be working toward a bachelor’s or higher-level degree, while the FAQ says there are no strict prerequisites beyond beneficial basics. For this specialist threshold-endpoint topic, readers should request the current syllabus and admissions details rather than assume this exact laboratory, scikit-learn version, mentor schedule, or placement outcome is included.

The final gate remains technical, not promotional: real thresholds plus literal replay can support a code-level decision; the unpaired plotting endpoint cannot supply an operating policy. A production release requires a separate body of representative evidence and explicit ownership beyond this synthetic arithmetic pass.