Classification Metrics and Decision Thresholds
Learn how confusion matrices, precision, recall, ROC AUC, and average precision guide threshold selection under class imbalance and decision costs in practice.
Machine Learning from ScratchAll chapters Classification Metrics and Decision ThresholdsFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Classification Metrics and Decision Thresholds
A risk model produces a score; an alerting rule decides which scores lead to action; you choose where that cut sits. Suppose a transaction has a true conditional fraud probability of 0.04, a false alarm costs $5 and a missed fraud costs $150. With zero cost for correct decisions, alerting has expected cost 0.96 times $5, or $4.80, while letting it pass has expected cost 0.04 times $150, or $6. Under these assumptions, alerting costs less. An arbitrary score of 0.04 does not establish that probability: both the score scale and the consequences of the decision matter.
Class imbalance makes accuracy alone a poor guide to a decision. Suppose one transaction in fifty is fraudulent. A model that flags nothing is right 98 percent of the time but misses every fraud. A model that flags everything catches every fraud and sends every transaction for review. Accuracy happily reports 98 percent for the first and 2 percent for the second, so we need measurements that separate the two kinds of mistakes instead of merging them into one number.
We fix notation once and use it throughout. We start with the confusion matrix, the four counts every binary decision produces, and read precision, recall, and the false positive rate directly off it. We then follow those quantities as the threshold slides, which produces the threshold curves, the ROC curve, and the precision-recall curve, along with their summary numbers, ROC AUC and average precision. Finally we attach costs to the two mistakes and show that the threshold is not a matter of taste but an optimization problem: minimize observed cost on validation data, then evaluate the fixed rule once on test data.
Two distinctions run through the whole chapter and are worth stating early for you. The first is between ranking and calibration. ROC AUC and average precision care only about the order in which a model sorts examples; they are unchanged if we replace every score by its cube. The analytic probability cutoff needs a meaningful probability scale; empirical cost selection can instead evaluate arbitrary scores directly. The second is between measurement and decision. Metrics summarize how a fixed classifier behaves; a threshold turns a score into a classifier. Choosing the threshold is model selection in miniature, and the same leakage rules from Validation Strategies and Data Leakage apply: the data used to pick the threshold cannot also provide an independent evaluation of that choice.
Advantages
Metrics built on the confusion matrix tell us things accuracy hides. Precision answers the question the review team actually asks, which is what fraction of the alerts you are about to work through are real. Recall answers the question you ask as a risk owner, which is what fraction of the bad events we are catching. Keeping these separate makes the tradeoff visible instead of averaging it away, and it makes the tradeoff negotiable: we can move along the curve deliberately rather than discovering the operating point by accident.
The threshold is an inexpensive and effective control you can turn. Once a model is trained, moving the threshold changes the operating point without retraining, and the whole family of achievable confusion matrices can be read off in a single sweep of order operations. For a fixed score and the inclusive threshold convention, the sweep enumerates every achievable deterministic threshold state, including the endpoints and whole tied groups.
Curve summaries are useful for comparing models before you fix any threshold. ROC AUC has a probabilistic meaning: the probability that an independent positive outranks an independent negative, with half credit for ties. It is invariant to strictly increasing transformations of the score, so a model is not penalized for being badly calibrated when the question is only about ordering. Average precision focuses the same idea on the positive class and is more informative when positives are rare, because it is sensitive to the false positives that actually compete with true positives for review capacity.
Cost-based threshold selection converts an abstract tradeoff into a decision. Naming and forces the organization to state what each kind of mistake costs, and the population objective identifies a threshold that minimizes expected cost, while its empirical estimate selects a threshold from validation observations. When scores are calibrated, the optimal threshold has a closed form, , which is easy to audit and easy to challenge in a meeting.
Disadvantages
Every one of these metrics is a sample estimate, and at low prevalence the estimates are noisy precisely where it counts. Precision has predicted positives as its denominator, so few alerts can make its estimate sensitive to sampling. AUC uncertainty depends on both class counts and the score distributions. A point estimate describes the observed result but does not describe that uncertainty.
ROC AUC's indifference to prevalence is a feature for ranking comparisons and a trap for decision making. Because the false positive rate divides by the number of negatives, a model can achieve a flattering AUC while producing an unusable number of false alerts when negatives vastly outnumber positives. A high AUC also says nothing about the operating point we intend to use: two models with identical AUC can behave very differently in the low false positive rate region where a real review team lives.
Average precision fixes the prevalence problem in one direction and creates it in another. It depends on the class balance of the evaluation set, so an average precision of 0.6 measured on a sample with 5 percent positives cannot be assumed to hold at 1 percent prevalence. Re-evaluate or adjust under justified transport assumptions; a change is not inevitable, as perfect separation illustrates.
Calibration is a separate property that neither AUC nor average precision measures. Both are ranking statistics, blind to any strictly increasing rescaling of the scores. Empirical cost minimization can use a rank-only score, whereas the closed-form rule requires the stated probability assumption. A value between 0 and 1, including a vote fraction, is not by itself evidence of calibration for the deployment population.
Finally, threshold selection is itself a fitting procedure, so it consumes data. For a fixed candidate family whose individual validation cost estimates are unbiased, the expected validation minimum is no greater than the smallest population cost in that family. This expectation does not dictate the ordering in one realized validation/test comparison. The size of selection optimism depends on the joint variability of the estimates and the selection procedure; a flat cost curve alone does not bound it. Independent held-out observations evaluate the selected fixed rule.
Formula
A fitted classifier has not yet made a decision. It has assigned each example a score, and those scores induce an ordering. To act on that ordering we choose a cut point in the ranked list and alert on everything above it. That cut point is the threshold, and the formulas below are the vocabulary for describing what happens as the cut moves. We fix notation once and use it throughout. There are examples, each with a feature vector , a true label , and a score produced by some upstream model. The positive class is the event we want to catch: fraud, machine failure, a disease. We adopt the convention that larger scores mean more evidence for the positive class. A library score must be oriented to the selected positive label; for a two-column probability output, use that label's column. A threshold turns scores into labels through the indicator rule
where is the indicator function, equal to 1 when the statement inside the braces is true and 0 otherwise. Read the rule aloud as "predict positive when the score is at least ". The threshold is a single number; sweeping it produces a family of classifiers, and everything in this chapter is a way of describing that family.
Confusion matrices and class prevalence
At any fixed threshold, every example falls into exactly one of four cells. Those cells are not an arbitrary summary: they are the complete cross-tabulation of the true label and the decision. For a fixed sample and threshold, the four counts completely specify the unweighted binary confusion table. They do not retain score ranking, calibration or subgroup-specific behavior. Writing them as indicator sums,
Each term in each sum is 1 exactly when the example belongs to the cell in question and 0 otherwise, so the sum simply counts the examples in that cell. counts true positives, false positives, false negatives, and true negatives, the same four categories that appear under different names in Type I and Type II Errors: a false positive is a Type I error, a false negative a Type II error.
We arrange the four counts into a matrix, with actual class labelling the rows and predicted class labelling the columns:
The convention matters, so we state it: row 1 means the example is truly negative, column 2 means we predicted positive. If we stack the labels into a column vector and the predictions into with , the same four counts are dot products, with denoting the length- vector of ones:
To see why the translation works, we expand the top-right entry: . The product is 1 only when and , which is precisely the definition of a false positive, and it is 0 in the other three cases. A sum over an indicator product has become a single matrix multiplication, which is what lets us compute all four cells at once in vectorized code.
Two row sums are fixed by the data and do not depend on . The number of actual negatives is and the number of actual positives is , with . The prevalence, also called the base rate, is
The column sums move with : is the number of alerts raised, and is the number of examples we let pass.
Prevalence helps explain why low false-positive rates can coexist with low precision. With both classes present, we take recall and false positive rate . The precision, the fraction of alerts that are real, is
where:
- : the precision, the fraction of raised alerts that are real
- : the number of true positives at the chosen threshold
- : the number of false positives at the chosen threshold
Rewriting each count as a rate times its group size, and , and substituting and , gives
where:
- : the recall, the share of actual positives that are flagged
- : the prevalence, the share of examples that are actually positive
- : the false positive rate, the share of actual negatives that are wrongly flagged
- : the total number of examples
Now we put in the numbers. With , a screening problem with one positive in a thousand,
Only 1.6 percent of alerts are real. The same classifier on a population with would deliver . This comparison holds recall and false positive rate fixed while changing prevalence, as under a pure prevalence shift with unchanged class-conditional score distributions. More general population changes can alter those rates too. The precision formula requires a positive alert probability.
Precision, recall, and threshold curves
For positive denominators, the four counts generate the following metrics, each with an operational reading. Precision is the share of alerts that are correct, recall is the share of positives we catch, and the false positive rate is the share of negatives we wrongly flag:
Recall is identical to the true positive rate, , and the true negative rate is the specificity. The harmonic mean of precision and recall is the score,
where:
- : the harmonic mean of precision and recall at threshold
- : the precision at threshold
- : the recall at threshold
Substituting the definitions of and and clearing denominators, when those expressions are defined, gives the equivalent count form
where , , and are evaluated at threshold . The count form also has a denominator condition; reporting a value for a zero denominator requires an explicit convention. The harmonic mean punishes imbalance between the two: if and , the arithmetic mean is 0.55 but . This is why is described as balancing the two, though it does not encode the stated error costs. Because precision depends on prevalence, F1 can depend on prevalence too; maximizing it need not minimize operational cost.
As decreases, the prediction set grows, so and are nondecreasing while and are nonincreasing. Recall and the false positive rate are nonincreasing in ; precision is not monotone, because its numerator and denominator both grow as decreases and either can win.
The threshold curves plot these quantities against itself. They are the most direct picture of the tradeoff, and they are what you would show when you want to know what happens at a particular cut. The ROC and precision-recall curves are different views of the same sweep: instead of plotting against , they plot one metric against another, which erases from the picture and therefore erases the information about which threshold produced each point.
Two bookkeeping conventions matter in practice and are the source of most off-by-one arguments with software. First, at above the largest score no example is predicted positive, so and precision is . For the precision-recall curve, scikit-learn appends a precision-1, recall-0 plotting endpoint. We use that convention here; it is separate from the configurable zero-division behavior of a standalone precision metric. Second, the achievable confusion matrices are indexed by the distinct score values. Two thresholds lying between the same pair of adjacent sorted scores produce identical predictions, so the curve has one point per distinct score, plus the trivial corner. Ties in the scores are handled by moving all tied examples at once.
ROC AUC, average precision, and their limits
Plotting the true positive rate against the false positive rate as sweeps from down to gives the receiver operating characteristic curve, which runs from at the "alert nobody" corner to at the "alert everybody" corner. The area under it is
where:
- : the false positive rate coordinate, running from 0 to 1
- : the true positive rate on the ROC curve at that false positive rate
For a finite sample with both classes present, the area under the linearly connected empirical ROC points is exactly the trapezoid sum
where:
- : the number of non-origin threshold points, one per distinct score group
- : indexes those points from 1 to in order of nondecreasing FPR, with at the origin
- : the false positive rate at threshold point
- : the true positive rate at threshold point
Each term is a trapezoid with width and average height . Vertical segments have zero width.
The empirical trapezoid area also has an exact pair-count interpretation. Compare every positive score with every negative score. A correctly ordered pair contributes 1, an incorrectly ordered pair 0, and a tied pair one half:
Here indexes positives, indexes negatives, and is the number of positive-negative pairs. With distinct scores this is the fraction of correctly ordered pairs. In a tied score group with positives and negatives, the diagonal ROC segment contributes half of the within-group pairs, which explains the half-tie term. The population interpretation is the probability that an independent positive draw outranks an independent negative draw, with half credit for ties. Scores drawn independently of labels from the same distribution in both classes have expected AUC .
For the precision-recall curve we plot precision against recall and summarize with average precision,
where:
- : average precision, the noninterpolated summary used here
- : the number of distinct score groups
- : indexes the groups in order of decreasing score
- : the recall after including the -th group of tied scores, with
- : the precision at that point
- : the recall gained at step , the width of the staircase step
The formula is the area under a staircase: recall increases by at step , and over that sliver of recall we use the precision achieved at the step, . This is the definition used by scikit-learn's average_precision_score.
The trapezoidal area under the precision-recall curve is a different quantity, and the difference is not cosmetic. The trapezoid rule connects consecutive PR points with straight lines. For this finite deterministic threshold sweep, the interior points of a segment between distinct consecutive PR points are not attainable by a threshold; its endpoints are attainable. Whether the line sits above or below the AP staircase depends on the local slope, so the two summaries can differ in either direction. Here we report the noninterpolated average precision used by scikit-learn and name it explicitly. Other PR-area conventions exist, so the interpolation or summation rule should accompany a reported PR-AUC.
Both summaries have limits that decide when they are appropriate.
- ROC AUC is invariant to any strictly increasing transformation of the scores. Replacing by , a strictly increasing sigmoid rescaling, or when every score is positive leaves every threshold sweep unchanged in shape, so the area is identical. The invariance is a statement that AUC measures ranking only.
- Average precision is not invariant to class prevalence. Changing while holding the class-conditional score distributions fixed leaves the ROC curve exactly unchanged, because both axes normalize within their own class, but it can change precision and average precision, because precision mixes the two classes. A change is not guaranteed at every point; perfect separation is a boundary case. The demonstration is in the next section.
- Neither number identifies an operating point. A model with AUC 0.9 can be deployed at a threshold that produces a 40 percent false positive rate or one that produces a 1 percent false positive rate; both are consistent with the same area.
- Neither summary uniquely rewards truthful probability values: a strictly increasing score transformation preserves both ranking summaries. AUC remains 1 after such a distortion of a perfectly separating score. Check calibration separately when the probability scale is needed for a decision.
- The full ROC area can hide behavior in a small operational region. Adding easily ranked negatives changes the comparison population and may increase AUC; it does not establish how a chosen threshold will behave. Report threshold-specific counts alongside the area.
Decision costs and validation-based threshold selection
Suppose a false positive costs and a false negative costs , both in the same units, typically money or hours of your analyst time. We use a model with strictly positive error costs and zero cost for true positives and true negatives. Adding the same action-independent cost per example would shift every rule equally; costs that differ by action or outcome require a corresponding decision model. The empirical expected cost per example at threshold is
where:
- : the observed expected cost per example at threshold (the empirical, sample-based version of the population cost below)
- : the cost of a false positive
- : the cost of a false negative
- : the number of false positives at threshold
- : the number of false negatives at threshold
- : the number of examples in the sample
The factor converts the total cost into cost per example, matching the population expression's per-example scale. For the population version, now let and let FPR and TPR denote population conditional rates, with both classes present. Then
where:
- : the population expected cost per example at threshold
- : the cost of a false positive
- : the cost of a false negative
- : the population prevalence of the positive class
- : the population false positive rate at threshold
- : the population true positive rate at threshold
This expression is linear in the ROC coordinates, so level sets of constant cost are straight lines in the plane. Writing for the coefficient of and for the coefficient of , the cost is
Setting for a fixed budget and solving for gives the line
so the lines have slope
where:
- : the per-example weight on the false positive rate
- : the per-example weight on the false negative rate
- : the cost of a false positive
- : the cost of a false negative
- : the population prevalence of the positive class
The slope assumes . Setting gives . Lower cost favors smaller FPR and larger TPR; among the available threshold points, you choose the one with the smallest stated cost. Increasing false-positive cost favors fewer alerts, toward lower FPR and TPR; increasing false-negative cost favors more alerts, toward higher FPR and TPR. Ties and a discrete curve can leave the selected point unchanged.
When the score is the true conditional event probability, , the optimal threshold has a closed form. Consider a single example with score , the conditional probability of a positive label. If we predict positive, we pay when the label is 0 (probability ) and nothing when it is 1 (probability ), so the expected cost of a positive prediction is . If we predict negative, we pay when the label is 1 and nothing when it is 0, so the expected cost is . We predict positive exactly when the first is no larger than the second:
where is the score, i.e. the conditional probability that the example is positive. Expected total cost is the sum of conditional per-example costs, so minimizing each term yields a minimizing rule. This argument does not require the observed labels to be independent. At equality either action has the same conditional expected cost; our convention predicts positive. Writing for the boundary probability, its posterior odds equal the cost ratio,
With and the threshold is , far below the default 0.5, and the required odds are 1 to 30. Equal error costs give the cutoff 0.5 in this model. Unequal costs give a different algebraic cutoff, although a population with no scores between the two cutoffs can yield identical decisions. Class imbalance alone does not determine the error costs.
The closed form has a precondition that is easy to miss. It requires the stated calibrated-probability assumption for the relevant population; merely lying between 0 and 1 is insufficient. When that assumption is unavailable, we drop the closed form and minimize the empirical cost over the threshold grid directly. This evaluates the available deterministic threshold states without assuming a probability scale. Its validation minimizer is an empirical choice, not a proof that it equals the population Bayes rule. What we lose is the ability to check the answer against a formula, and what we gain is freedom from the calibration assumption.
The selection protocol follows from the fact that is estimated. We need three disjoint roles. The training split fits the model that produces the scores for you to threshold. The validation split selects the threshold, by minimizing over its grid. The test split is touched once, to report the cost of the chosen rule. Selection can make the reported validation minimum optimistic in repeated samples; using test data to choose the threshold contaminates the only honest measurement we have. This is the same discipline as cross-validation, applied to a parameter that happens to be a scalar, and it is the reason hyperparameter search and nested resampling are natural companions to what we build here.
Mathematical Properties
- Monotonicity. For , every prediction made at is also made at , so and . Recall and the false positive rate are therefore nonincreasing in , and the ROC curve is traced monotonically from to .
- Rank invariance. If is strictly increasing, then , so the entire threshold sweep, and hence the ROC curve, AUC, and the PR curve, are unchanged by replacing with .
- AUC as a probability. With continuous scores, for independent draws from the positive and negative classes. The empirical estimate is the Mann-Whitney statistic divided by , which is why AUC and the Wilcoxon rank-sum test are the same computation in different clothing.
- Prevalence effects. Under a pure prevalence shift with the class-conditional score distributions fixed, population ROC coordinates and AUC are unchanged. Precision and average precision can change; this need not happen under perfect separation. More general population changes can alter ROC behavior too.
- bounds. For defined nonnegative precision and recall with , write and . Then the harmonic mean satisfies . The lower bound follows from , so never falls below the smaller of the two inputs, and it never exceeds their arithmetic mean.
- Cost plateaus. Under the inclusive rule, consecutive observed scores give identical predictions for every . Empirical cost is constant on such a plateau. Distinct plateaus may tie for minimum cost, so the set of minimizing thresholds can be a union of intervals. Reporting a representative threshold does not imply its rounded value remains in the same plateau.
- Undefined corners. Precision is when no example is predicted positive, and recall is when the sample contains no positives at all. Both cases need an explicit convention, and the two curves in software differ in whether the corner point is returned.
Visualizing Classification Metrics and Thresholds
We now build a small alerting problem where every quantity is known by construction. Each account has two features, the linear predictor is , the label is drawn as with , and the model's score is exactly , the probability you would threshold. Because the label was generated from the same probability the score reports, the score is calibrated by construction. That lets us compare a finite-sample empirical cost minimum with the population cutoff from the previous section. They need not agree exactly on a realized sample; an arbitrary score would not justify the analytic probability cutoff at all.
The scores arrive from a model that would, in a real pipeline, have been fitted on a separate training split. Since we start from scores, the roles you need here are selection (validation) and reporting (test). We look at the two samples side by side to confirm they are drawn from the same population.
PythonIn [3]Expand
split_summary = pd.DataFrame(
{
"split": ["validation", "test"],
"n": [len(y_val), len(y_test)],
"positives": [int(y_val.sum()), int(y_test.sum())],
"prevalence": [y_val.mean(), y_test.mean()],
"mean score": [score_val.mean(), score_test.mean()],
}
)ConsoleOut [4]Expand
split n positives prevalence mean score
validation 4000 344 0.086 0.0856
test 4000 352 0.088 0.0877The two splits are drawn from the same generating process and land close on size, prevalence, and mean score. Small differences reflect sampling variation, so the validation and test sets can reasonably be treated as exchangeable draws from one population for the purposes of this example.
Next we write the sweep that does the work. These tutorial functions assume paired one-dimensional nonempty arrays with binary labels, both classes present and finite scores in [0, 1], as used in this chapter. They do not implement a general input-validation API. Everything below is a direct transcription of the formulas: counts at a single threshold, counts for every distinct threshold at once using cumulative sums over the score-sorted data, the two curves, and the cost minimization.
PythonIn [5]Expand
def confusion_counts(y_true, y_score, threshold):
"""2x2 count matrix [[TN, FP], [FN, TP]] at a single threshold."""
y = np.asarray(y_true)
pred = (np.asarray(y_score) >= threshold).astype(int)
return np.array(
[
[np.sum((pred == 0) & (y == 0)), np.sum((pred == 1) & (y == 0))],
[np.sum((pred == 0) & (y == 1)), np.sum((pred == 1) & (y == 1))],
]
)
def binary_metrics(y_true, y_score, threshold):
"""Precision, recall, false positive rate and F1 at one threshold."""
tn, fp, fn, tp = confusion_counts(y_true, y_score, threshold).ravel()
precision = tp / (tp + fp) if tp + fp > 0 else 1.0 # 0/0 convention
recall = tp / (tp + fn) if tp + fn > 0 else 0.0
fpr = fp / (fp + tn) if fp + tn > 0 else 0.0
f1 = (
2 * precision * recall / (precision + recall)
if precision + recall > 0
else 0.0
)
return {
"TN": float(tn),
"FP": float(fp),
"FN": float(fn),
"TP": float(tp),
"precision": float(precision),
"recall": float(recall),
"fpr": float(fpr),
"f1": float(f1),
}
def evaluate(y_true, y_score, threshold, cost_fp=COST_FP, cost_fn=COST_FN):
"""Everything we report for one threshold, including expected cost."""
m = binary_metrics(y_true, y_score, threshold)
m["cost"] = (cost_fp * m["FP"] + cost_fn * m["FN"]) / len(y_true)
return m
def threshold_sweep(y_true, y_score):
"""Counts for every distinct cut point, from 'alert nobody' to 'alert everybody'."""
y = np.asarray(y_true)
s = np.asarray(y_score, dtype=float)
order = np.argsort(-s, kind="mergesort")
y_sorted, s_sorted = y[order], s[order]
tp = np.cumsum(y_sorted == 1).astype(float)
fp = np.cumsum(y_sorted == 0).astype(float)
keep = np.r_[
s_sorted[1:] != s_sorted[:-1], True
] # last index of each tie group
tp, fp, s_kept = tp[keep], fp[keep], s_sorted[keep]
tp, fp = np.r_[0.0, tp], np.r_[0.0, fp]
n1, n0 = tp[-1], fp[-1]
return {
"threshold": np.r_[np.inf, s_kept],
"tp": tp,
"fp": fp,
"fn": n1 - tp,
"tn": n0 - fp,
"n1": n1,
"n0": n0,
}
def curve_points(y_true, y_score):
"""ROC and PR coordinates on the shared threshold grid."""
sw = threshold_sweep(y_true, y_score)
denom = sw["tp"] + sw["fp"]
precision = np.where(denom > 0, sw["tp"] / np.maximum(denom, 1e-12), 1.0)
recall = sw["tp"] / sw["n1"]
return {
"threshold": sw["threshold"],
"precision": precision,
"recall": recall,
"tpr": recall,
"fpr": sw["fp"] / sw["n0"],
}
def trapezoid_area(x, y):
"""Composite trapezoid rule on paired coordinate arrays."""
return float(np.sum(np.diff(x) * (y[1:] + y[:-1]) / 2.0))
def roc_auc(y_true, y_score):
pts = curve_points(y_true, y_score)
return trapezoid_area(pts["fpr"], pts["tpr"])
def average_precision(y_true, y_score):
pts = curve_points(y_true, y_score)
return float(np.sum(np.diff(pts["recall"]) * pts["precision"][1:]))
def pr_trapezoid(y_true, y_score):
pts = curve_points(y_true, y_score)
return trapezoid_area(pts["recall"], pts["precision"])
def cost_curve(y_true, y_score, cost_fp=COST_FP, cost_fn=COST_FN):
"""Expected cost per example for every distinct threshold."""
sw = threshold_sweep(y_true, y_score)
n = sw["n1"] + sw["n0"]
return sw["threshold"], (cost_fp * sw["fp"] + cost_fn * sw["fn"]) / n
def select_threshold(y_true, y_score, cost_fp=COST_FP, cost_fn=COST_FN):
"""Empirical cost minimiser with midpoint and endpoint representatives."""
thresholds, costs = cost_curve(y_true, y_score, cost_fp, cost_fn)
j = int(np.argmin(costs))
upper = thresholds[j]
lower = thresholds[j + 1] if j + 1 < thresholds.size else 0.0
chosen = upper if np.isinf(upper) else 0.5 * (upper + lower)
if chosen <= lower < upper:
chosen = upper
return chosen, float(costs[j])We compute the curve arrays and operating metrics before the figures and reuse them in the plotting cells.
The first picture is the raw material: how the scores are distributed in each class. The vertical axis is logarithmic so that density values across a wide range remain visible. Each class histogram is normalized separately, so the height comparison describes class-conditional density rather than the ratio of class headcounts.
VisualizationOut [7]Expand

The two distributions overlap almost everywhere, which is the whole difficulty. The negative density is concentrated below 0.05, but its tail is long, so a threshold placed low enough to catch most positives drags in a large absolute number of negatives. The default cut of 0.50 sits far out in the positive tail. It would raise very few alerts and miss most fraud. The cost-optimal threshold sits far below it, inside the overlap, which is the quantitative version of "we would rather review more accounts than miss the fraud".
Plotting the metrics against the threshold itself makes the tradeoff explicit. The figure below is a useful diagnostic for a non-technical audience, because the horizontal axis is the one thing they control.
VisualizationOut [8]Expand

Read the left edge first. When the threshold approaches zero every account is flagged, recall is 1, the false positive rate is 1, and precision collapses to the prevalence, which is the dotted line: if you alert on everything, the fraction of alerts that are real is exactly the base rate. Read the right edge next. When the threshold exceeds the largest score nothing is flagged and precision is 1 by convention. The interesting region is the middle, and the dashed line marks where the cost function prefers to sit. In this realization, the selected threshold lies below the recall–precision crossing. The crossing is determined by the scores and labels; the error costs choose where the selected marker sits relative to it.
The ROC and precision-recall curves are the same sweep with the threshold eliminated. The first figure adds the iso-cost line from the population cost formula; its slope encodes the ratio of false positive cost to false negative cost weighted by prevalence. The second figure shows the same classifier in precision-recall space, where the horizontal line is the prevalence. In the population, a score independent of the label has precision equal to prevalence at thresholds with positive alert probability. A finite empirical curve can fluctuate around that baseline, and its no-alert corner still uses the stated plotting convention.
::: {layout-ncol=2}
VisualizationOut [9]Expand

VisualizationOut [10]Expand

:::
The ROC curve shows a healthy ranking: the area is well above the diagonal. The operating point, however, sits at a false positive rate that will look alarming to you next to the default square marker. That is the correct reading. With a miss costing thirty times a false alarm, the optimum tolerates many false alarms, and the iso-cost line makes the arithmetic visible: in the (FPR, TPR) plane its positive slope is . The line passes through the minimum-cost empirical point and supports the available ROC points: a point above it would have smaller stated cost. A smooth tangency is not required for a discrete curve.
The precision-recall figure tells the same story in the units the review team cares about. Precision at the operating point is modest, but the alternative at the default threshold has recall of only 0.25 and therefore misses three quarters of the frauds, so its higher precision is largely irrelevant. Precision alone does not identify a useful rule, and it need not rise when the threshold rises. In the worked example below, increasing the threshold from 0.45 to 0.50 reduces precision from 4/7 to 1/2 as well as reducing recall from 1.000 to 0.750, because rank 7 (a true positive) drops out of the alert queue.
The final figure of this section plots the cost itself, and it is where the selection decision actually happens. The validation curve is what we minimize; the test curve evaluates fixed candidate rules on held-out observations and would stay unopened until after the validation choice was recorded. One realized test curve does not prove that the validation minimizer is the population optimum. The test curve's minimum and its value at the selected threshold are descriptive of this split, not an independent re-selection; the test set is used only to evaluate the fixed validation-selected rule.
VisualizationOut [12]Expand

The empirical cost curve is a step function because predictions change only when a score is crossed. A plateau between adjacent distinct scores has width equal to their numerical gap. The cost jump at a score depends on the labels and number of observations tied there, as well as the stated error costs; neither fixes the shape of the minimum on its own. Inspect both the cost differences and threshold values; curve shape alone does not establish stability across new samples.
The two curves nearly coincide, which is the optimistic but expected outcome here: validation and test are drawn from the same distribution and both are large. The closed-form threshold for calibrated scores lands close to, but not exactly on, the empirical minimizer, because the empirical minimizer is a noisy estimate. Finite-sample differences are possible despite calibration. Convergence to one numeric threshold additionally requires conditions such as a unique population minimizer and suitable sampling; calibration alone does not establish those conditions.
The invariance claims of the previous section deserve a picture. The following reweights the validation sample so that the positive share becomes 20 percent and then 2 percent, holding the class-conditional score distributions fixed. The weights are constant within each class, and because the ROC coordinates normalize within each class, the ROC curve cannot change; precision, which mixes the classes, changes at mixed operating points with both true and false positives. Pure-positive prefixes and the plotting endpoint can stay unchanged.
::: {layout-ncol=2}
VisualizationOut [14]Expand

VisualizationOut [15]Expand

:::
The two ROC curves lie on top of each other and the two AUC values agree to three decimals, while the average precision values differ substantially. Both plots are honest; they answer different questions. The ROC answer is "how well does this score order the two classes", which does not depend on how the classes are mixed. The precision-recall answer is "how useful would this score be here", which very much does. If we tuned the alerting model on a sample with 20 percent positives and deployed it where positives are 2 percent, the ROC curve would have told us nothing was wrong.
Example
The simulated data shows the machinery at scale, but its numbers arrive from code. A tiny hand-worked case shows where each number comes from and makes the formulas concrete. Sixteen accounts have been scored, four of them are fraudulent, and the scores are sorted from highest to lowest. Every number below is small enough for you to check with a calculator.
| rank | score | rank | score | ||
|---|---|---|---|---|---|
| 1 | 0.94 | 1 | 9 | 0.31 | 0 |
| 2 | 0.88 | 1 | 10 | 0.27 | 0 |
| 3 | 0.81 | 0 | 11 | 0.21 | 0 |
| 4 | 0.70 | 1 | 12 | 0.16 | 0 |
| 5 | 0.62 | 0 | 13 | 0.11 | 0 |
| 6 | 0.58 | 0 | 14 | 0.07 | 0 |
| 7 | 0.45 | 1 | 15 | 0.04 | 0 |
| 8 | 0.38 | 0 | 16 | 0.01 | 0 |
There are positives and negatives, so the prevalence is . The costs are dollars for reviewing an account that turns out to be clean and dollars for missing a fraudulent one. In other words, each false positive costs 5 dollars and each false negative costs 150 dollars.
Start at the default threshold . The rule flags the first six accounts, since their scores are at least 0.50. Of those six, three are fraudulent (ranks 1, 2, 4) and three are not (ranks 3, 5, 6). The one remaining fraud sits at rank 7 with a score of 0.45, so it is missed. These four counts are exactly the four cells of the confusion matrix, and they are complete: every account appears once.
The metrics follow mechanically. Precision is , recall is , the false positive rate is , and , which the count form confirms: . The expected cost per account is
Now lower the threshold to 0.45, which adds rank 7 to the alert queue. Nothing else changes, because no other score lies between 0.45 and 0.50. The confusion matrix becomes
so precision rises to , recall rises to , the false positive rate is unchanged at , and .
where:
- : the number of flagged frauds after including rank 7
- : the number of flagged clean accounts (ranks 3, 5, 6)
- : no fraud remains unflagged
- : the number of unflagged clean accounts
The cost drops to
Recall rises and precision also rises here because the one added alert is a true positive, which is the opposite of the general tendency: we noted earlier that raising the threshold from 0.45 to 0.50 reduced precision from 4/7 to 1/2 while reducing recall. When moving toward lower thresholds, adding a true positive can lift both metrics, while adding a false positive leaves recall unchanged and lowers precision when the queue already contains a true positive. This example shows why precision need not move in the opposite direction from recall. The cost, however, falls by more than a factor of ten. Multiplying this observed difference by a hypothetical 10,000 accounts gives $93,750. Interpreting that as a future daily saving would additionally require the example error rates and stated costs to transfer to the future stream.
The threshold need not be exactly 0.45. Any value in the interval between the seventh and eighth scores, , produces the same predictions and therefore the same cost. This is the cost plateau of the previous section, and it means the reported threshold should be understood as a representative of an interval. The midpoint rule used in the code would report 0.415.
Next we check the curve summaries against the same sixteen points. Ordering the positives by rank, the true positive count after each is at ranks , so recall takes the values 0.25, 0.50, 0.75, and 1.00 with precisions 1/1, 2/2, 3/4, and 4/7. Average precision sums the recall increments weighted by the precision achieved:
For the AUC we count, for each positive, how many negatives it outranks. Rank 1 outranks all twelve, rank 2 also outranks all twelve, rank 4 outranks the eleven negatives below it, and rank 7 outranks the nine below it:
The trapezoidal area under the same precision-recall points is a different number. The horizontal segments contribute and , the segment from recall 0.50 to 0.75 connects precision 2/3 to 3/4, and the segment from 0.75 to 1.00 connects precision 1/2 to 4/7:
The two do not agree in this case; the trapezoid differs from average precision. Here average precision is about 0.8304 and the trapezoidal area about 0.8110, a difference of . The definitions can sometimes yield the same number, even when precision is not constant, because segment differences can cancel. Name the convention rather than treating the two definitions as interchangeable.
Finally, the closed form for calibrated scores gives . Applying it here would flag every account scoring at least 0.0323, which is ranks 1 through 15, giving and for a cost of . The toy scores are used here as an arbitrary ranking, so we have not established the conditional-probability assumption for that cutoff. Its observed cost is larger than the empirical optimum of 0.9375. That finite-sample comparison alone would not prove miscalibration: a sample-selected rule can also beat a population-optimal rule on the sample used to select it.
We close the example by checking the hand arithmetic against the code, which is the habit that keeps your worked examples honest.
ConsoleOut [17]Expand
threshold TP FP FN TN precision recall F1 cost
0.5000 3 3 1 9 0.5000 0.75 0.6000 10.3125
0.4500 4 3 0 9 0.5714 1.00 0.7273 0.9375
0.0323 4 11 0 1 0.2667 1.00 0.4211 3.4375ConsoleOut [18]Expand
AUC (rank/trapezoid) : 0.9167 average precision : 0.8304 PR trapezoid area : 0.8110 closed-form threshold: 0.0323
The preceding code checks the hand calculation against the same definitions. Agreement on this example is a useful implementation check, rather than proof that every later use is correct.
The four counts at the default threshold can be read as one grid. This grid summarizes the aggregate unweighted binary outcomes at that cut, before any ratio such as precision or recall is formed.
PythonIn [19]Expand
cm_default = confusion_counts(toy_label, toy_score, 0.50)
cm_names = [["TN", "FP"], ["FN", "TP"]]
cm_annotations = [
[f"{cm_names[i][j]}={int(cm_default[i, j])}" for j in range(2)]
for i in range(2)
]VisualizationOut [20]Expand

Implementation in NumPy and scikit-learn
We now run the full protocol on the simulated alerting data, using the toolkit from the visualization section. The pipeline has three roles and we keep them strictly separate: a training split would fit the scorer, the validation split selects the threshold, and the test split reports. Since the scores are given, only the last two appear in the code.
The comparison we want is between rules, not models. We evaluate three decision rules: the default 0.50 cut, the closed-form threshold that would be correct if the scores were calibrated, and the threshold selected by minimizing empirical cost on validation. Each fixed rule is applied to both splits. Their realized costs can be compared, but one validation/test difference does not identify the expected selection bias.
PythonIn [21]Expand
rule_rows = []
for rule_name, t in [
("default 0.50", 0.50),
("closed form", t_analytic),
("selected on validation", t_star),
]:
for split_name, y_split, s_split in [
("validation", y_val, score_val),
("test", y_test, score_test),
]:
m = evaluate(y_split, s_split, t)
rule_rows.append(
{
"rule": rule_name,
"split": split_name,
"threshold": t,
"TP": int(m["TP"]),
"FP": int(m["FP"]),
"FN": int(m["FN"]),
"precision": m["precision"],
"recall": m["recall"],
"F1": m["f1"],
"cost per account": m["cost"],
}
)
rule_table = pd.DataFrame(rule_rows)ConsoleOut [22]Expand
rule split threshold TP FP FN precision recall F1 cost per account
default 0.50 validation 0.5000 86 38 258 0.6935 0.2500 0.3675 9.7225
default 0.50 test 0.5000 89 48 263 0.6496 0.2528 0.3640 9.9225
closed form validation 0.0323 321 1507 23 0.1756 0.9331 0.2956 2.7462
closed form test 0.0323 330 1498 22 0.1805 0.9375 0.3028 2.6975
selected on validation validation 0.0470 314 1229 30 0.2035 0.9128 0.3328 2.6612
selected on validation test 0.0470 319 1212 33 0.2084 0.9062 0.3388 2.7525The table reports each rule on both splits. The default 0.50 rule has the smallest recall and largest cost per account on both. The validation-selected rule has the smallest validation cost, while the closed-form rule has the smaller test cost of the two alternatives. Those test costs evaluate the three fixed rules on the same held-out observations; the test split is not used to choose among them.
Three things are worth reading off this table.
-
The default rule is clearly worse than the alternatives. Recall is about a quarter of the positives, and the cost per account is more than three times that of the alternatives.
-
The closed-form threshold, despite resting on an assumption the empirical rule does not need, does well, because in this simulation the assumption holds; it has slightly higher validation cost than the selected threshold and slightly lower test cost in this realization. Empirical selection minimizes the validation objective; it does not guarantee the ordering on test observations.
-
The selected threshold performs almost identically on test and validation. The realized validation and test costs differ in this run; the difference can reflect sampling as well as threshold selection and does not by itself estimate the expected selection bias.
A flat observed cost curve shows that nearby thresholds can have similar observed costs. It does not establish the size of selection optimism or sampling stability. Assessing those properties requires the sampling design and an evaluation of the full selection procedure.
The implementation so far is our own, so it needs a check against a reference. scikit-learn provides all the same quantities, and agreement to machine precision confirms that your reading of the definitions matches the community's.
PythonIn [23]Expand
from sklearn.metrics import (
average_precision_score,
confusion_matrix,
precision_recall_curve,
roc_auc_score,
roc_curve,
)
sk_auc = roc_auc_score(y_val, score_val)
sk_ap = average_precision_score(y_val, score_val)
sk_cm = confusion_matrix(y_val, score_val >= t_star, labels=[0, 1])
sk_precision, sk_recall, _ = precision_recall_curve(y_val, score_val)
sk_fpr, sk_tpr, _ = roc_curve(y_val, score_val)
agreement = pd.DataFrame(
{
"metric": ["ROC AUC", "average precision"],
"from scratch": [auc_val, ap_val],
"scikit-learn": [sk_auc, sk_ap],
}
)
agreement["abs difference"] = (
agreement["from scratch"] - agreement["scikit-learn"]
).abs()ConsoleOut [24]Expand
metric from scratch scikit-learn abs difference
ROC AUC 0.889709 0.889709 0.0
average precision 0.518929 0.518929 0.0The two summary numbers agree to machine precision, which confirms that our from-scratch ROC AUC and average precision match the reference implementations of the same definitions. This agreement checks the implementation of those definitions; it does not bear on whether either metric is appropriate for a given decision.
ConsoleOut [25]Expand
confusion matrices identical: True largest recall discrepancy : 0.00e+00 largest precision discrepancy: 0.00e+00 largest TPR discrepancy : 0.00e+00
The confusion matrix is identical and the curves agree to floating-point noise. The one structural difference is ordering: scikit-learn returns recall in decreasing order for precision_recall_curve, because it walks the thresholds from the lowest to the highest, while our implementation walks from the highest downward. Sorting before comparing removes the disagreement, which is a useful reminder that the direction of a curve array is a convention and not a property of the data. That comparison checks array contents, not the shape of the interpolated curve; the ROC discrepancy above uses drop_intermediate=False so that scikit-learn returns all interior points.
The last piece of the protocol concerns the threshold itself. A single number from a single validation split hides how much the choice would move under resampling, and that variability is exactly what you should tell a stakeholder about.
PythonIn [26]Expand
boot_rng = np.random.default_rng(11)
boot_thresholds = np.empty(300)
for b in range(boot_thresholds.size):
idx = boot_rng.integers(0, y_val.size, y_val.size)
boot_thresholds[b], _ = select_threshold(y_val[idx], score_val[idx])| quantity | threshold | validation cost at that threshold | |
|---|---|---|---|
| 0 | 5th percentile threshold | 0.0224 | 2.7850 |
| 1 | median threshold | 0.0408 | 2.6900 |
| 2 | 95th percentile threshold | 0.0646 | 2.8775 |
ConsoleOut [28]Expand
quantity threshold validation cost at that threshold
5th percentile threshold 0.0224 2.7850
median threshold 0.0408 2.6900
95th percentile threshold 0.0646 2.8775ConsoleOut [29]Expand
selected threshold on the full validation split: 0.0470 validation cost at the selected threshold : 2.6612 validation cost at the analytic threshold : 2.7462 share of bootstrap thresholds within 2x of selected: 0.94
The 5th and 95th percentile bootstrap thresholds are about 0.0224 and 0.0646, separated by a factor of about 2.89. Evaluating those two rules on the original validation sample gives costs of 2.7850 and 2.8775 per account, about 4.7 and 8.1 percent above its selected minimum of 2.66125. The selected threshold on the full validation split is about 0.0470. These are observations under this resampling scheme, not proof of stable future cost or a guarantee that every threshold between the quantiles has similar cost. These three costs measure sensitivity at threshold quantiles on the reused validation sample; they are not a bootstrap distribution of cost. Estimating cost uncertainty requires a separate evaluation design, which this illustration does not implement. Reporting the threshold variation and observed costs separately communicates more than the threshold alone.
The 300 bootstrap thresholds were generated on the validation split with seed 11 and the midpoint plateau rule. Their spread is easier to judge as a distribution than as three summary numbers.
PythonIn [30]Expand
boot_hist_counts, boot_hist_edges = np.histogram(boot_thresholds, bins=30)
boot_hist_centers = 0.5 * (boot_hist_edges[:-1] + boot_hist_edges[1:])
boot_hist_width = np.diff(boot_hist_edges)VisualizationOut [31]Expand

Key Parameters
- The threshold itself is the decision variable. It is selected on data that plays no part in fitting the scorer and is fixed before any test evaluation.
c_fpandc_fnare the two costs, and they must be in the same units. Their ratio, not their absolute size, determines the threshold that a calibrated score would use, so the closed form is and the corresponding odds are .- The class prevalence affects precision and average precision. Transporting them to a different prevalence requires assumptions about the class-conditional score distributions. The closed-form probability cutoff instead depends on the error-cost ratio; the probabilities to which it is applied must represent the deployment population.
- The calibration status of the score decides whether the closed form is admissible. A rank-only score supports empirical cost minimization but not the analytic rule.
- Uncertainty depends on the metric denominator and the sampling and selection design. Recall uses actual positives as its denominator; precision uses predicted positives, or alerts.
Key Methods
confusion_countsandbinary_metricsgive the four counts and the derived metrics at a single threshold. They are the primitives; everything else is a loop or a vectorization over them.threshold_sweepcomputes the whole family of confusion matrices in one pass by sorting the scores once and taking cumulative sums. Its cost is , dominated by the sort.curve_points,roc_auc,average_precisionandpr_trapezoidturn the sweep into curves and summary numbers. Keepingaverage_precisionandpr_trapezoidas separate functions is deliberate, because they are separate quantities.cost_curveandselect_thresholdimplement the decision rule.select_thresholduses a midpoint for an interior optimal plateau when it is representable, falling back to the inclusive upper score if rounding reaches the excluded lower score. It reports infinity for the no-alert state and a valid representative between zero and the minimum score for the all-alert state. These reproducible conventions do not guarantee greater sampling stability than an endpoint.- The scikit-learn counterparts are
roc_auc_score,average_precision_score,precision_recall_curve,roc_curveandconfusion_matrix. Recent versions also provideTunedThresholdClassifierCV, which wraps the same select-on-validation logic in a cross-validated estimator; the primitive version here is easier to audit and to adapt to asymmetric costs.
Practical Implications
Metrics serve two different jobs: comparing scores before a deployment decision and operating a score after one. For comparison, ROC AUC is a reasonable default when both classes are represented and the question is ordering; average precision is often more informative when positives are rare and false alerts compete for finite review capacity. For deployment, neither summary is the deliverable. The deliverable is a fixed threshold, the confusion matrix it produces on held-out data, and a statement of the cost or service level implied by that operating point. A model comparison can end with an AUC; an operational decision cannot.
Cost elicitation belongs to the modeling work rather than to a final sign-off. A false positive may consume analyst time, create customer friction, or trigger unnecessary account freezes; a false negative may produce chargebacks, missed maintenance, or regulatory exposure. Estimate these components where evidence permits and record the uncertainty in the ratio. When no credible ratio can be assembled, present recall, precision, false positive rate and alert volume over the thresholds. Cost curves can still be shown for explicitly assumed ratios, but cannot be specified without costs. Choosing an operating point expresses an operational tradeoff even when that tradeoff has not been fully priced. The same discussion should establish whether one threshold is defensible across segments. Different error-cost ratios can justify different probability cutoffs. Different base rates can change the appropriate cutoff on a shared raw score or require segment-aware probability estimation. If scores already represent the relevant segment-conditional probabilities and error costs are common, the same probability cutoff remains optimal across segments.
Best Practices
Keep the roles of training, validation, and test data distinct. The scorer is fitted on training data, the threshold is selected on validation data by minimizing observed cost or by applying a justified closed-form cutoff, and the test split is opened once to report the resulting rule. When the score is a calibrated probability, you should compare the analytic cutoff with the empirical minimizer; disagreement is information about calibration or finite-sample variability, not a reason to force the two numbers together. When the score is only a ranking, skip the closed form and minimize empirical cost directly.
Report the decision as an interval and a cost, not as a suspiciously precise threshold. Because predictions change only when a score is crossed, the optimal threshold is usually a plateau; the midpoint is a convenient representative, but the endpoints and the cost across the plateau are more informative than three decimal places. Bootstrap the selection procedure to show how far the threshold moves under resampling, and, when estimating cost uncertainty, report it separately because the two quantities have different units. The illustration above used 300 resamples; more resamples can reduce Monte Carlo noise in this resampling summary, but cannot repair limited validation data or establish interval coverage by themselves.
Alongside the chosen threshold, report recall, precision, false positive rate, alert volume, and the confusion matrix on held-out data. State the positive class and the score orientation explicitly, since reversing the score reverses its ranking and requires changing the decision convention. If average precision is quoted, quote the prevalence of the evaluation set with it; if ROC AUC is quoted, make clear that it is a ranking summary and not evidence that the deployed alert volume is acceptable.
Data Requirements and Preprocessing
The minimum inputs are paired labels and scores for validation and test data, with the positive class oriented so that larger scores mean stronger evidence for the event of interest. Both classes must be present for ROC AUC; the stated average-precision definition requires at least one positive, and at least one positive prediction must occur at a threshold for precision to be defined without an explicit convention. Costs should be supplied in the same units, and any segment identifiers needed for subgroup reporting should be retained. For the closed-form probability cutoff, the scores must be probabilities for the deployment population; for empirical cost selection, arbitrary finite scores are acceptable.
Little preprocessing is required for the rank-based summaries because ROC AUC and average precision are invariant to strictly increasing transformations of the score. Rescalings that are defined and strictly increasing on the score domain do not change its ordering; a logarithm, for example, requires positive scores, but they do change the numeric threshold, so the threshold must be expressed on the same scale and orientation as the scores that will be seen in deployment. Ties should be handled as tied groups rather than broken arbitrarily. Missing scores need an explicit policy, and a calibration layer, if used, is a separate fitted component with its own validation requirements.
Prevalence deserves its own check. Under a pure prevalence shift that holds both class-conditional score distributions fixed, the identity translates recall and false positive rate into deployment precision when the alert probability is positive. Label-only enrichment of a validation set can create such a shift. More general population changes can alter the class-conditional distributions and therefore the ROC curve itself, so changing in the formula alone is not a substitute for re-evaluating on a representative sample.
Common Pitfalls
A damaging mistake is selecting the threshold on the test set and then reporting performance on the same observations. That is leakage with extra steps: the reported cost is optimized on the data used to judge it, which can bias the reported cost downward and removes an independent evaluation of the choice. This selection effect is a repeated-sample statement, not a guaranteed ordering for every realized result. The fix is procedural rather than mathematical: record the validation-selected threshold before opening the test split, and do not revise it after seeing test results.
A second recurring mistake you should avoid is treating as if it were a decision rule. Maximizing encodes an implicit and usually unstated tradeoff between precision and recall, and it does not use the stated error costs. It is entirely possible for the -optimal threshold to sit far from the cost-optimal one, especially when prevalence or the cost ratio is extreme. If a stakeholder requests the -optimal threshold, show it beside the cost-optimal rule and name the difference in expected cost.
Other pitfalls follow from using the wrong summary for the wrong job. A high ROC AUC can coexist with an unusable number of false alerts at the operating point, and average precision estimated at one prevalence cannot be assumed to transfer unchanged to another. Applying the closed form to an uncalibrated margin or vote fraction produces a threshold that looks principled but has no probability interpretation. Reporting a threshold to many decimal places when the cost curve is flat over a wide interval overstates the precision of the selection, and ignoring segment-level error rates can hide a rule that is acceptable on average and harmful to a subgroup.
Computational Considerations
The threshold sweep requires one sort followed by linear scans; its runtime depends on the sample size and implementation. Sorting the scores once costs , cumulative sums produce all distinct confusion matrices in , and the ROC curve, precision-recall curve, and cost curve can all reuse the same sweep. Memory is linear in the sample size, dominated by the sorted scores and the cumulative count arrays. For a single validation or test set, this is practical for millions of observations on a modern machine, and the dominant cost is usually the sort rather than the metric arithmetic.
Repeated resampling multiplies that cost. A bootstrap with resamples performs sorts and sweeps, so the total work is ; the 300-resample illustration is modest, while thousands of resamples on very large data can become the bottleneck. Reuse curve arrays across plotting and reporting cells, avoid recomputing the sweep for each figure, and parallelize resamples when the implementation allows. Calibration adds a separate fitting cost and should be validated as its own step rather than folded silently into threshold selection.
Performance and Deployment Considerations
Track alert volume and, when outcomes are available, precision and confusion counts at the fixed threshold. Precision and the confirmation rate among alerts describe the same fraction; estimate it from complete or representative alert outcomes. False-negative counts and recall also need outcomes for unflagged cases, so reviewed alerts alone cannot supply a full confusion matrix. ROC AUC can complement these operational quantities, but needs both labeled classes and can stay unchanged under a pure prevalence shift. Segment-level summaries can expose differences hidden by aggregate results.
The threshold should be revisited when error costs, prevalence, or the class-conditional score distribution change enough to move the operating point. A change in prevalence alone can leave recall and false positive rate unchanged under a pure shift while moving precision, so alert capacity and review workload should be monitored separately from ranking quality. If the score is used as a probability, check its calibration for the target population and recalibrate if needed before applying the closed-form cutoff; if it is used only as a ranking, rerun empirical cost selection on a representative validation sample. The honest deployment report includes the chosen threshold or plateau, the held-out cost, the alert volume, and the uncertainty from the selection procedure.
Summary
A score is not a decision. Turning a score into a decision requires a threshold, and the threshold is chosen by trading false positives against false negatives. The confusion matrix records that trade at one point; precision, recall, and the false positive rate read the trade off the matrix; the threshold curves, ROC curve, and precision-recall curve show the whole family of trades available from a fixed ranking.
The summary numbers answer narrow questions. ROC AUC compares independent positive and negative draws, awarding half credit for ties. It is invariant to strictly increasing score transformations and is unchanged by a prevalence-only shift that preserves both class-conditional score distributions. Average precision concentrates on the positive class, summarizes the precision-recall curve using the stated noninterpolated convention, and can depend on prevalence, so it cannot be assumed to transfer unchanged between populations. Neither number identifies an operating point, and neither establishes calibration. The trapezoidal area under a precision-recall curve uses a different summation convention from average precision; state which one is reported.
The threshold itself is an optimization problem once costs are named. Minimizing expected cost gives the population objective , whose level sets are straight lines in ROC space with slope . When the score is a calibrated probability, the optimum collapses to the closed form , or equivalently to posterior odds equal to the cost ratio. When that probability assumption is unavailable, sweeping validation cost still selects an empirical threshold for the given ranking; it does not certify the same population optimum.
The discipline that makes the resulting number trustworthy is the split discipline. Threshold selection is a fitting step, so it consumes validation data; the test split is touched once, to report. The selected threshold is typically not unique, since the cost is a step function with plateaus, and thresholds can vary across resamples while their observed costs remain similar. Threshold and cost dispersion have different units and should be reported on their own scales; these observations alone do not guarantee future-risk stability. Reporting a range and a cost, rather than three decimal places, is the honest summary, and it is what a team needs in order to know whether the decision is stable enough to automate.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about classification metrics, threshold selection, and the cost-based decision framework.
Classification Metrics and Decision Thresholds
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.


Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!