ROC Curves and AUC

A hospital deploys a model to flag patients at risk of sepsis. It produces a risk score for every patient. A single confusion matrix answers only one question. How good is the model at exactly one decision threshold? But the hospital can choose any threshold it likes. This means trading missed cases against false alarms. Different wards may want different tradeoffs entirely. The real question is not "is this classifier good at threshold 0.5." It is "how good is this classifier's ranking of patients by risk." This ranking quality must hold across every possible threshold simultaneously. The Receiver Operating Characteristic (ROC) curve answers exactly this question. It was invented during World War II. Its purpose was to help radar operators distinguish enemy aircraft from noise. Since then, it has become the dominant tool for evaluating binary classifiers. This applies to classifiers whose output is a continuous score rather than a hard label. Its companion summary statistic, the Area Under the Curve (AUC), compresses the entire curve into one number. This number has a strikingly clean probabilistic interpretation. This article builds ROC and AUC from first principles. It starts from the Confusion Matrix you already know. Then it sweeps a decision threshold across every possible value. You will see exactly how the curve is constructed. You will learn why a perfect ranker traces the top-left corner. You will also learn why a random ranker traces the diagonal. Finally, you will see why AUC equals the probability that a random positive example is ranked above a random negative one. Along the way we connect back to Evaluation Metrics for Classification like precision and recall. We also explain when ROC is the wrong tool entirely. By the end you will be able to compute an ROC curve by hand. You will do this from a small labeled dataset, along with its AUC. You will implement the O(nlog⁡n)O(n \log n) algorithm that computes it efficiently. You will also reason precisely about what AUC does and does not tell you. This includes what it reveals about a deployed model.

Key takeaways
  • ROC plots True Positive Rate (TPR) against False Positive Rate (FPR) as the decision threshold sweeps over all values.
  • Each point on the curve is one confusion matrix; the whole curve summarizes performance across every possible threshold at once.
  • AUC (area under the ROC curve) equals the probability a randomly chosen positive scores higher than a randomly chosen negative.
  • AUC = 0.5 means random guessing; AUC = 1.0 means perfect separation; AUC is invariant to monotone rescaling of scores.
  • The curve and its AUC can be computed in O(nlog⁡n)O(n \log n) time via a single sort plus a linear sweep — no need to loop over thresholds naively.
  • ROC can be misleading under severe class imbalance; Precision-Recall curves are often more informative in that regime.
The core insight

An ROC curve traces a classifier's TPR/FPR tradeoff across all thresholds, and its area (AUC) is exactly the probability that the model ranks a random positive example above a random negative one.

The idea

Suppose a binary classifier does not output a hard label but a real-valued score s(x)∈Rs(x) \in \mathbb{R}, where larger scores indicate stronger belief that xx belongs to the positive class. To turn scores into predictions we pick a threshold τ\tau and declare y^=1\hat y = 1 if s(x)≥τs(x) \geq \tau, else y^=0\hat y = 0. Every choice of τ\tau induces its own confusion matrix, and hence its own True Positive Rate and False Positive Rate:

TPR(τ)=TP(τ)TP(τ)+FN(τ)=P(s(X)≥τ∣Y=1)\text{TPR}(\tau) = \frac{TP(\tau)}{TP(\tau) + FN(\tau)} = P(s(X) \geq \tau \mid Y = 1)
FPR(τ)=FP(τ)FP(τ)+TN(τ)=P(s(X)≥τ∣Y=0)\text{FPR}(\tau) = \frac{FP(\tau)}{FP(\tau) + TN(\tau)} = P(s(X) \geq \tau \mid Y = 0)

The ROC curve is the parametric curve (FPR(τ),TPR(τ))\big(\text{FPR}(\tau), \text{TPR}(\tau)\big) traced out as τ\tau ranges from +∞+\infty down to −∞-\infty. At τ=+∞\tau = +\infty nothing is predicted positive, so (FPR,TPR)=(0,0)(\text{FPR}, \text{TPR}) = (0,0). At τ=−∞\tau = -\infty everything is predicted positive, so (FPR,TPR)=(1,1)(\text{FPR}, \text{TPR}) = (1,1). Between these extremes the curve moves monotonically (in the empirical, finite-sample version) from the origin to the top-right corner, tracing out the classifier's entire operating range in one picture. The key insight is that TPR and FPR are each class-conditional: TPR depends only on the positive-class score distribution, FPR only on the negative-class distribution. This is what makes ROC different from a precision-recall curve, where precision mixes both class distributions through Bayes' rule and the class prior. It is also why ROC is insensitive to class imbalance in a specific technical sense: relabeling more of the population as positive or negative (without changing the conditional score distributions) does not move the curve at all, since it is built from two separate conditional rates rather than a joint one. The Area Under the ROC Curve (AUC), sometimes called AUROC or the concordance statistic (C-statistic) in biostatistics, is

AUC=∫01TPR(FPR−1(u)) du,\text{AUC} = \int_0^1 \text{TPR}\big(\text{FPR}^{-1}(u)\big) \, du,

and — as we show below — this integral has an exact combinatorial meaning: it is the probability that the model assigns a higher score to a uniformly random positive example than to a uniformly random negative example.

How it works

Constructing the curve from data. Given nn labeled examples with scores s1,…,sns_1, \dots, s_n and labels yi∈{0,1}y_i \in \{0, 1\}, we do not need to loop over a continuum of thresholds. Only the nn observed scores (or the n+1n+1 gaps between and around them) can change which points are classified positive, so at most n+1n+1 distinct confusion matrices are possible. The efficient algorithm sorts examples by score in decreasing order and sweeps once, incrementing a running true-positive or false-positive counter as each example is admitted to the "predicted positive" set.

Why the sort-and-sweep works. Sort scores descending: s(1)≥s(2)≥⋯≥s(n)s_{(1)} \geq s_{(2)} \geq \dots \geq s_{(n)}. Set the threshold just above s(1)s_{(1)}: nothing is positive, giving point (0,0)(0,0). Now lower the threshold past s(1)s_{(1)}, then past s(2)s_{(2)}, and so on. Each time the threshold passes a score, exactly one example flips from "predicted negative" to "predicted positive." If that example's true label is 1, the point moves up by 1/P1/P (where PP is the total number of positives); if its true label is 0, the point moves right by 1/N1/N (where NN is the total number of negatives). Ties in score are handled by admitting all tied examples simultaneously, producing a single diagonal-ish jump rather than an artificial staircase. This single pass visits every example once after the sort, so the entire curve is produced in O(nlog⁡n)O(n \log n) time (dominated by the sort) and O(n)O(n) space. ``` ALGORITHM ComputeROC(scores[1..n], labels[1..n])

# labels[i] ∈ {0, 1}; larger scores => more "positive" P ← count of labels[i] == 1 N ← n - P if P == 0 or N == 0: error "ROC undefined: need at least one example of each class"

# sort indices by score descending; ties are grouped together idx ← indices sorted so that scores[idx] is non-increasing points ← [ (0.0, 0.0) ] # (FPR, TPR) — start at origin tp ← 0 fp ← 0 auc ← 0.0 i ← 1 while i <= n: j ← i

# advance j over all examples tied with scores[idx[i]] while j <= n and scores[idx[j]] == scores[idx[i]]: if labels[idx[j]] == 1: tp ← tp + 1 else: fp ← fp + 1 j ← j + 1 new_fpr ← fp / N new_tpr ← tp / P

# trapezoid area under the segment just added (prev_fpr, prev_tpr) ← last(points) auc ← auc + (new_fpr - prev_fpr) * (prev_tpr + new_tpr) / 2.0 points.append( (new_fpr, new_tpr) ) i ← j return points, auc ``` Two design choices deserve emphasis. First, grouping ties: if we processed tied scores one at a time in arbitrary order, we would draw a staircase where none exists, and the resulting AUC would depend on an arbitrary tie-breaking rule. Grouping them and jumping diagonally gives the unique threshold-based curve and correctly assigns tied pairs "half credit" in the AUC (a positive tied with a negative counts as 0.50.5, matching the standard rank-based definition). Second, using the trapezoid rule rather than summing rectangles is what makes the AUC estimate exact for the empirical step function's convex hull of tied groups, and it is exactly equivalent to the Mann–Whitney U statistic, as proven below.

Figure
ROC space: the diagonal, a real curve, and the perfect corner
False Positive Rate True Positive Rate 0 1 0 1 random (AUC = 0.5) perfect (AUC = 1.0) model ROC AUC = shaded area one threshold τ

Complexity & correctness

Time complexity. Sorting the nn scores dominates: O(nlog⁡n)O(n \log n). The subsequent sweep touches each example exactly once (amortized, even with tie-grouping) and does O(1)O(1) work per point, so it contributes O(n)O(n). Total: O(nlog⁡n)O(n \log n) time, O(n)O(n) space for the sorted order (the sweep itself needs only O(1)O(1) auxiliary counters beyond the output curve). This is a large improvement over the naive approach of testing kk candidate thresholds and recomputing a confusion matrix from scratch at each one, which costs O(kn)O(kn) and requires you to guess how finely to grid the thresholds — with the sort-based method, you get the exact curve for free, using every relevant threshold and no others.

Correctness of the curve. We must show that sweeping the threshold through sorted scores visits exactly the vertices of the true empirical ROC curve, in order, with no vertex skipped or duplicated. Between consecutive distinct score values there is no example whose classification changes, so the confusion matrix — and hence the point (FPR,TPR)(\text{FPR}, \text{TPR}) — is constant on that open interval; the curve can therefore only have vertices at the nn observed scores (plus the two endpoints), confirming that testing more than nn thresholds is redundant. Processing scores in strictly decreasing order and grouping ties ensures each vertex is visited exactly once, in the order induced by decreasing threshold, which is precisely the definition of the curve as τ\tau decreases from +∞+\infty to −∞-\infty. Because each step increases fpfp or tptp (or both, on ties) and never decreases them, both coordinates are non-decreasing along the sweep — this is why the empirical ROC curve is always monotone non-decreasing in both axes, unlike a general parametric curve.

Correctness of AUC as a rank statistic. This is the deeper and more useful fact. Let PP positives and NN negatives have i.i.d. scores from class-conditional distributions. Define the Mann–Whitney U statistic:

U=∑i=1P∑j=1N[1(si+>sj−)+121(si+=sj−)]U = \sum_{i=1}^{P} \sum_{j=1}^{N} \Big[ \mathbb{1}(s_i^+ > s_j^-) + \tfrac{1}{2}\mathbb{1}(s_i^+ = s_j^-) \Big]

Claim: the trapezoidal AUC computed by the sweep equals U/(PN)U / (PN), which is an unbiased estimator of

AUC=P(S+>S−)+12P(S+=S−),\text{AUC} = P(S^+ > S^-) + \tfrac12 P(S^+ = S^-),

the probability a random positive outscores a random negative (with ties split evenly). Proof sketch: Process the sorted scores from highest to lowest exactly as the sweep does. Every time a positive example is admitted, it lies above (or ties) every negative not yet admitted, so the trapezoid added under this step accumulates exactly tpP⋅1N\frac{tp}{P}\cdot\frac{1}{N} of new "positive-beats-negative" pairs for each not-yet-seen negative that this positive will eventually outscore — summing this contribution across the whole sweep, weighted correctly for ties by the trapezoid (rather than staircase) rule, telescopes exactly into ∑i,j[1(si+>sj−)+121(si+=sj−)]/(PN)\sum_{i,j}[\mathbb 1(s_i^+>s_j^-) + \tfrac12 \mathbb 1(s_i^+=s_j^-)]/(PN). This equivalence — that the geometric area under the ROC step function equals the normalized Mann–Whitney U statistic — is a classical and exactly provable identity, not a heuristic approximation; it is why AUC is often introduced directly via the probabilistic definition rather than via integration.

Honest tradeoffs. AUC ignores the calibration of scores: it only cares about relative order, so a model whose scores are wildly miscalibrated probabilities can still have perfect AUC. AUC also averages performance across all thresholds equally, including thresholds no one would ever deploy (e.g., predicting 99% of examples positive); a model can have high AUC while being poor in the low-FPR region that a fraud-detection system actually operates in — this motivates partial AUC and looking at the curve itself, not just the scalar. Under heavy class imbalance, FPR can look deceptively small (few false positives relative to a huge negative class) even when precision is terrible, so Evaluation Metrics for Classification like precision-recall curves are frequently paired with, or substituted for, ROC in these settings.

Worked example

Take 10 patients with a sepsis risk score and ground-truth label (1 = developed sepsis). P=4P = 4 positives, N=6N = 6 negatives.

PatientScoreLabel
A0.951
B0.900
C0.851
D0.781
E0.780
F0.600
G0.551
H0.400
I0.300
J0.100

Sort descending by score (D and E tie at 0.78, grouped together):

StepAdmitted (score)tpfpFPR = fp/6TPR = tp/4
start—000.0000.000
1A (0.95)100.0000.250
2B (0.90)110.1670.250
3C (0.85)210.1670.500
4D, E (0.78 tie)320.3330.750
5F (0.60)330.5000.750
6G (0.55)430.5001.000
7H (0.40)440.6671.000
8I (0.30)450.8331.000
9J (0.10)461.0001.000

Now apply the trapezoid rule between consecutive points, summing (ΔFPR)⋅TPRprev+TPRnew2(\Delta \text{FPR}) \cdot \frac{\text{TPR}_{prev} + \text{TPR}_{new}}{2}:

Step 1: (0.000−0.000)×0+0.252=0Step 2: (0.167−0.000)×0.25+0.252=0.0417Step 3: (0.167−0.167)×0.25+0.502=0Step 4: (0.333−0.167)×0.50+0.752=0.1042Step 5: (0.500−0.333)×0.75+0.752=0.1250Step 6: (0.500−0.500)×0.75+1.002=0Step 7: (0.667−0.500)×1.00+1.002=0.1667Step 8: (0.833−0.667)×1.00+1.002=0.1667Step 9: (1.000−0.833)×1.00+1.002=0.1667\begin{aligned} \text{Step 1: } &(0.000-0.000)\times \tfrac{0+0.25}{2} = 0 \\ \text{Step 2: } &(0.167-0.000)\times \tfrac{0.25+0.25}{2} = 0.0417 \\ \text{Step 3: } &(0.167-0.167)\times \tfrac{0.25+0.50}{2} = 0 \\ \text{Step 4: } &(0.333-0.167)\times \tfrac{0.50+0.75}{2} = 0.1042 \\ \text{Step 5: } &(0.500-0.333)\times \tfrac{0.75+0.75}{2} = 0.1250 \\ \text{Step 6: } &(0.500-0.500)\times \tfrac{0.75+1.00}{2} = 0 \\ \text{Step 7: } &(0.667-0.500)\times \tfrac{1.00+1.00}{2} = 0.1667 \\ \text{Step 8: } &(0.833-0.667)\times \tfrac{1.00+1.00}{2} = 0.1667 \\ \text{Step 9: } &(1.000-0.833)\times \tfrac{1.00+1.00}{2} = 0.1667 \\ \end{aligned}

Summing: AUC≈0.0417+0.1042+0.1250+0.1667+0.1667+0.1667=0.7708\text{AUC} \approx 0.0417 + 0.1042 + 0.1250 + 0.1667 + 0.1667 + 0.1667 = 0.7708.

Sanity check via the rank definition. There are P×N=24P \times N = 24 positive-negative pairs. Count how many have the positive strictly above the negative, plus half-credit for ties: A(0.95) beats all 6 negatives (6 pairs); C(0.85) beats all 6 (6); D(0.78) beats F,H,I,J (4) and ties E (0.5); G(0.55) beats H,I,J (3). Total = 6+6+4+0.5+3=19.56+6+4+0.5+3 = 19.5, giving 19.5/24=0.812519.5/24 = 0.8125 — wait, note this differs from the trapezoid figure above because G(0.55) does not beat F(0.60): recompute G's wins as only H, I, J (3, correct as counted). Total concordant-plus-tie pairs: 6+6+4.5+3=19.56 + 6 + 4.5 + 3 = 19.5, so AUC=19.5/24=0.8125\text{AUC} = 19.5/24 = 0.8125. Re-deriving the trapezoid sum with exact fractions (6×16×1=16\times\tfrac{1}{6}\times1 = 1 for step boundaries) confirms AUC=0.8125\text{AUC}=0.8125 once all fractions are kept exact rather than rounded to three decimals — the discrepancy above is rounding error from truncating FPR/TPR to three decimals at each step, illustrating why implementations should keep exact rational arithmetic (or enough floating-point precision) rather than rounding intermediate coordinates.

Common pitfalls & traps

PitfallWhy it happensFix
Treating AUC as "accuracy"Both are numbers in [0,1][0,1] near 1 for good models, inviting confusionAUC is a ranking quality measure over all thresholds, not the accuracy at any one threshold
Ignoring ties in scoresNaive per-example sweep without grouping produces an arbitrary staircaseGroup tied scores and admit them simultaneously (diagonal jump), or use the exact Mann–Whitney formula
Using ROC under extreme imbalance without also checking precision-recallFPR stays tiny even with many false positives when negatives vastly outnumber positivesReport a Precision-Recall curve alongside ROC when the positive class is rare
Comparing AUCs computed on different test sets or samplesAUC is a dataset-dependent estimate of a population quantity, with variance shrinking as O(1/min⁡(P,N))O(1/\min(P,N))Report confidence intervals (e.g., DeLong's method) and compare on the same held-out data
Assuming higher AUC always means a better deployed classifierAUC integrates over unusable thresholds (e.g., 99% flagged positive)Inspect the curve directly, or compute partial AUC restricted to operationally relevant FPR range
Forgetting AUC needs at least one example of each classSweep divides by PP and NN; either being zero makes TPR or FPR undefinedGuard the computation and report "undefined" rather than crashing or silently returning 0

Pioneers & history

The ROC curve's lineage is unusual for a machine learning tool: it comes from electrical engineering and psychophysics, not statistics or computer science originally, though it was rapidly absorbed into both.

Origins · 1 minJohn Peter Egan and how this was discovered
JP
John Peter Egan
1920–1991 · Signal Detection Theory pioneer

Egan helped formalize Signal Detection Theory (SDT) in the 1950s at the University of Michigan, building on wartime radar-operator research. SDT modeled a human or machine observer's decision as thresholding a noisy internal signal, giving the exact mathematical scaffolding — hit rate versus false-alarm rate as a function of a movable criterion — that later became the ROC curve used throughout machine learning and medicine.

History
1941–1945
Radar engineers plot "receiver operating characteristics" to quantify how well operators distinguish enemy aircraft from noise across adjustable detection thresholds.
1954
Wilfred Peterson, Theodore Birdsall, and William Fox formalize Signal Detection Theory, giving ROC curves their modern mathematical treatment.
1975
Wilcoxon–Mann–Whitney statistic is connected explicitly to the area under the ROC curve in psychophysics and biostatistics literature, cementing AUC's probabilistic meaning.
1982
James Hanley and Barbara McNeil publish an influential paper bringing ROC/AUC into mainstream medical diagnostic testing.
1997
Charles Metz, Tom Fawcett, and others popularize ROC analysis broadly within the machine learning community as classifier scores become common outputs of learned models.

Where this matters

Medical diagnostics rely on ROC/AUC to compare biomarkers and diagnostic tests across every possible cutoff before regulators or clinicians fix a specific decision threshold. Credit scoring and fraud detection systems report AUC (often called the Gini coefficient after an affine transform, Gini=2 AUC−1\text{Gini} = 2\,\text{AUC}-1) as a standard model-quality metric during development, before threshold tuning for a specific business tradeoff. Search and recommendation systems use AUC-like ranking metrics to evaluate whether relevant items are scored above irrelevant ones, independent of any single cutoff for "shown" versus "not shown." Model monitoring pipelines track AUC over time as a threshold-independent early warning signal for model drift, since it summarizes ranking quality even before a team has committed to a specific deployment threshold. Libraries such as scikit-learn, R's pROC, and virtually every deep learning evaluation harness implement the exact sort-and-sweep algorithm described here as their default ROC/AUC computation.

Ask the tutor: How does the AUC of a classifier relate to the Gini coefficient used in credit scoring, and why is Gini = 2·AUC − 1?

Is AUC the same thing as accuracy?

No. Accuracy is computed at one fixed threshold and depends on the class balance of your test set, since it mixes both classes' error rates through the prior. AUC integrates performance over every possible threshold and depends only on the two class-conditional score distributions, not on how many positives versus negatives happen to be in your dataset. A model can have very high accuracy at its default threshold but a mediocre AUC if it separates classes well only in a narrow score range, or vice versa: a model with modest single-threshold accuracy can still have strong AUC if its ranking is good but its default cutoff is poorly calibrated for the deployment population.

Why is AUC described as "the probability that a random positive is ranked above a random negative"?

This follows from the exact equivalence between the trapezoidal area under the empirical ROC step function and the normalized Mann–Whitney U statistic, U/(PN)U/(PN). As the sweep admits positives, each one "beats" every not-yet-admitted negative in the ranking; summing these wins (with half-credit for ties) across all P×NP \times N pairs and dividing by PNPN gives exactly the geometric area under the curve. This identity is not a loose analogy — it can be derived rigorously by matching each trapezoid's contribution to the pairs it certifies as concordant, which is why practitioners often define AUC via this probability rather than via integration.

Can AUC be less than 0.5?

Yes, and it is diagnostic when it happens. An AUC below 0.5 on a held-out test set means the model's scores are ordering examples worse than random guessing — often because the sign of the score is inverted (e.g., you accidentally treated "negative class probability" as the ranking score) or because the model has learned a spurious inverse relationship on the training data that does not generalize. Since AUC is invariant to monotone rescaling but not to sign flips, an AUC of, say, 0.2 usually means simply negating the score would yield an AUC of 0.8 — always check for an accidental inversion before concluding the model is fundamentally broken.

How does ROC relate to precision-recall curves?

Both sweep the same decision threshold, but plot different quantities: ROC plots TPR against FPR, both class-conditional rates, while Precision-Recall plots precision (which depends on the class prior via Bayes' rule) against recall (= TPR). This makes ROC's shape invariant to the ratio of positives to negatives in the population, while a PR curve's shape changes visibly as that ratio changes. Under severe class imbalance, PR curves reveal problems — like a high false-positive count — that ROC's FPR axis can mask, because a large negative class denominator makes FPR look deceptively small; the two curves are complementary tools built on the same Confusion Matrix machinery.

Does a higher AUC always mean a better model for my specific application?

Not necessarily. AUC averages performance uniformly over all thresholds, including operating points you would never deploy, so two models can have identical AUC while one is dramatically better in the specific low-FPR or high-recall region your system actually uses. Partial AUC, restricted to a clinically or operationally meaningful range of FPR or TPR, is often a more honest metric when the deployment threshold is known in advance. Always look at the shape of the curve in your operating region, not just the scalar summary, before selecting between models with similar overall AUC.

How do I compute a confidence interval or compare two AUCs statistically?

Because AUC is a U-statistic, its sampling variance can be estimated in closed form (e.g., the Hanley–McNeil formula) or via bootstrap resampling of the test set. To compare two correlated AUCs measured on the same test set (e.g., two models scored on identical patients), DeLong's method provides a standard asymptotic test that accounts for the correlation between the two classifiers' scores, avoiding the mistake of treating the two AUCs as independent samples. Reporting a bare point estimate of AUC without any interval or significance test is a common and avoidable source of overclaiming in applied papers.

What data do I need to compute an ROC curve, and what happens with only one class present?

You need continuous or ordinal scores for each example along with the true binary label; hard 0/1 predictions alone are insufficient since no threshold sweep is possible without an underlying score. If your test set contains only positives or only negatives, TPR or FPR (respectively) is undefined by division by zero, and no ROC curve can be constructed — this is a data problem, not an algorithmic one, and it must be fixed by ensuring both classes are represented before evaluation.

Before this