Skip to content

The ROC Curve and AUC

What you’ll learn

  • how an ROC curve is constructed, one threshold at a time
  • the full curve traced by hand from the ten-row example
  • the probabilistic meaning of AUC — it is not “accuracy over all thresholds”
  • why AUC = 0.5 is chance and AUC below 0.5 means something useful
  • exactly when ROC misleads, and why precision-recall is the right choice there
  • how to pick between ROC-AUC and average precision, with a rule

Intuition

Precision and recall describe one operating point. The ROC curve describes all of them at once.

Slide the threshold from 1 down to 0. At the top, nothing is predicted positive: no true positives, no false positives, and the curve starts at (0,0)(0, 0). At the bottom, everything is predicted positive and the curve ends at (1,1)(1, 1). In between, every threshold contributes a point.

The shape of the path between those corners is the model’s ranking ability. A model that scores every positive above every negative goes straight up and then straight across — through the top-left corner. A model that ranks randomly follows the diagonal.

diagram Diagram mermaid

The math

TPR (recall, sensitivity)=TPTP+FNFPR=FPFP+TN=1specificity\text{TPR (recall, sensitivity)} = \frac{TP}{TP + FN} \qquad \text{FPR} = \frac{FP}{FP + TN} = 1 - \text{specificity}

Both denominators are row totals of the confusion matrix — the number of actual positives and the number of actual negatives. That single fact explains everything ROC does well and everything it does badly: neither axis depends on how many positives there are relative to negatives, so the curve is invariant to class balance.

What AUC actually measures

The area under the ROC curve has an exact probabilistic interpretation:

AUC=P(score(x+)>score(x))\text{AUC} = P\big(\text{score}(x^{+}) > \text{score}(x^{-})\big)

AUC is the probability that a randomly chosen positive is ranked above a randomly chosen negative. It is a measure of ranking, not of classification. It never looks at your threshold, which is exactly why it is useful for comparing models before one is chosen — and why it cannot tell you whether a model is deployable.

AUCMeaning
1.0Every positive ranks above every negative
0.9Strong separation
0.7Modest, often still useful
0.5Chance — the diagonal
< 0.5Ranking is inverted; flip the sign and you have a good model

That last row is worth remembering. An AUC of 0.2 is not a bad model, it is a good model with its labels or its sign the wrong way round.

Worked example by hand

The ten-row example, sorted by score. Five positives, five negatives.

ranktruescorerunning TPrunning FPTPRFPR
(none flagged)000.00.0
1+0.95100.20.0
2+0.88200.40.0
3+0.76300.60.0
40.70310.60.2
50.62320.60.4
6+0.58420.80.4
7+0.31521.00.4
80.24531.00.6
90.13541.00.8
100.05551.01.0

Each row is one threshold. A positive steps the curve up by 1/51/5; a negative steps it right by 1/51/5.

Area by geometry. The curve is a staircase, so the area is a sum of rectangles — one per rightward step, whose height is the TPR at that moment:

AUC=0.2(0.6)+0.2(0.6)+0.2(1.0)+0.2(1.0)+0.2(1.0)=0.12+0.12+0.20+0.20+0.20=0.84\text{AUC} = 0.2(0.6) + 0.2(0.6) + 0.2(1.0) + 0.2(1.0) + 0.2(1.0) = 0.12 + 0.12 + 0.20 + 0.20 + 0.20 = 0.84

Area by counting pairs. The probabilistic definition gives the same answer directly. There are 5×5=255 \times 5 = 25 positive/negative pairs; count how many the model ranks correctly:

positive scorenegatives it beats (0.70, 0.62, 0.24, 0.13, 0.05)count
0.95all five5
0.88all five5
0.76all five5
0.580.24, 0.13, 0.053
0.310.24, 0.13, 0.053
21
AUC=2125=0.84\text{AUC} = \frac{21}{25} = 0.84

Two completely different routes, the same number — and roc_auc_scoreroc_auc_score returns exactly 0.84. The pair-counting view is the one worth carrying: AUC is the fraction of positive/negative pairs your model gets in the right order.

See it move

The sketch draws both routes at once. On the left, the ranked list is consumed one row at a time and the staircase is built step by step — up for a positive, right for a negative, with the rectangle under each rightward step shaded as it is added. On the right, the same event is scored as pairs: the 5×55 \times 5 grid fills in green for each correctly ordered pair. The two totals stay equal throughout.

sketch Two routes to 0.84, computed side by side p5.js
The ROC staircase built one ranked row at a time with its area shaded rectangle by rectangle, next to the five by five grid of positive-negative pairs filling in as each pair is resolved. The running area and the running pair fraction agree at every step and both finish at 0.84. Click to restart.

The four red cells in the grid are the model’s entire mistake: score 0.58 and 0.31 each lose to the negatives at 0.70 and 0.62. Twenty-one of twenty-five pairs are ordered correctly, and the staircase area is the same 0.84 because each rectangle is exactly the set of pairs resolved by that rightward step. The equivalence is not a coincidence to memorise; it is the same count, grouped two different ways.

On real data

figureROC on a balanced problem, and the imbalanced case where it misleadsmatplotlib
Left: ROC curves for logistic regression at AUC 0.992, Gaussian Naive Bayes at 0.974, and a random classifier along the diagonal at 0.496. Right: on 1% positive data, a ROC curve reaching AUC 0.855 alongside a precision-recall curve with average precision 0.133.Left: ROC curves for logistic regression at AUC 0.992, Gaussian Naive Bayes at 0.974, and a random classifier along the diagonal at 0.496. Right: on 1% positive data, a ROC curve reaching AUC 0.855 alongside a precision-recall curve with average precision 0.133.
Left: the diagonal is chance, and a good model bows toward the top-left corner. Right: the same predictions score 0.855 by ROC and 0.133 by average precision. Only one of those numbers is telling the truth.

Reading the plot

Left panel — a healthy comparison.

  • Logistic regression reaches AUC 0.992, Naive Bayes 0.974. Both are strong; the ranking between them is what AUC is for.
  • The random classifier lands on 0.496 — the diagonal, within sampling noise of exactly 0.5.
  • The steep initial rise matters most: it means the highest-scoring predictions are almost all genuine positives, which is what you exploit when you can only act on the top few.

Right panel — the failure mode.

  • Positive rate is 1%. ROC-AUC reports 0.855, which sounds like a good model.
  • Average precision reports 0.133.
  • Both are computed from identical predictions. The disagreement is not a bug; it is the two metrics answering different questions.

Why ROC is optimistic under imbalance

FPR has TNTN in its denominator, and under heavy imbalance TNTN is enormous. With 3,960 negatives and 40 positives, 200 false positives give:

FPR=200200+37600.05\text{FPR} = \frac{200}{200 + 3760} \approx 0.05

which barely moves the curve. But those same 200 false positives destroy precision:

precision=TPTP+FP=3030+2000.13\text{precision} = \frac{TP}{TP + FP} = \frac{30}{30 + 200} \approx 0.13

ROC absorbs false positives into a huge negative pool; precision does not, because it never counts true negatives at all. When the positive class is rare, precision is measuring the thing you actually care about — is a flag worth investigating? — and ROC is not.

Multiclass ROC

ROC is defined for binary problems. For KK classes, compute one curve per class one-versus-rest and average:

multiclass_roc.py
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
digits = load_digits()
X_tr, X_te, y_tr, y_te = train_test_split(
    digits.data, digits.target, test_size=0.3, random_state=0, stratify=digits.target
)
 
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
model.fit(X_tr, y_tr)
proba = model.predict_proba(X_te)
 
macro = roc_auc_score(y_te, proba, multi_class="ovr", average="macro")
weighted = roc_auc_score(y_te, proba, multi_class="ovr", average="weighted")
 
print(f"macro OvR AUC    {macro:.4f}")      # 0.9992
print(f"weighted OvR AUC {weighted:.4f}")   # 0.9992
multiclass_roc.py
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
digits = load_digits()
X_tr, X_te, y_tr, y_te = train_test_split(
    digits.data, digits.target, test_size=0.3, random_state=0, stratify=digits.target
)
 
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
model.fit(X_tr, y_tr)
proba = model.predict_proba(X_te)
 
macro = roc_auc_score(y_te, proba, multi_class="ovr", average="macro")
weighted = roc_auc_score(y_te, proba, multi_class="ovr", average="weighted")
 
print(f"macro OvR AUC    {macro:.4f}")      # 0.9992
print(f"weighted OvR AUC {weighted:.4f}")   # 0.9992

macromacro treats every class equally and weightedweighted scales by support — the same choice as for multiclass F1, and the same advice: with imbalanced classes, macromacro is usually the honest one.

ROC-AUC versus average precision

ROC-AUCAverage precision
AxesTPR against FPRPrecision against recall
BaselineAlways 0.5The positive rate
Uses true negativesYes, in FPRNo
Invariant to class balanceYesNo — deliberately
Best forRanking quality overallRare-positive problems
InterpretationP(positive ranks above negative)Precision averaged over recall levels

The invariance is genuinely a feature and genuinely a bug, depending on the question. If you are comparing two models on the same data, invariance is helpful. If you are deciding whether a fraud model is worth deploying, invariance hides the fact that 87% of its alerts are wrong.

Pitfalls

quizCheck yourself
  1. What does an AUC of 0.84 mean, precisely?

    Show answer

    B — 84% of randomly chosen positive/negative pairs are ranked in the correct order — AUC is P(score of a random positive > score of a random negative). In the ten-row example, 21 of 25 pairs are correctly ordered, giving exactly 0.84.

  2. Your model has ROC-AUC 0.86 and average precision 0.13 on a dataset with 1% positives. Which number should drive the decision?

    Show answer

    B — Average precision, because with a rare positive class it reflects how often a flag is actually worth investigating — FPR divides by a huge pool of true negatives, so hundreds of false positives barely move the ROC curve. Precision has no true-negative term and therefore feels every one of them.

  3. A colleague reports AUC 0.22. What is the most likely explanation?

    Show answer

    B — The ranking is inverted — a label or sign flip would turn it into 0.78 — AUC below 0.5 means positives are systematically scored below negatives. Flipping the sign of the score gives 1 - 0.22 = 0.78. Check the label encoding before retraining.

  4. Why does the ROC curve start at (0,0) and end at (1,1) for every model?

    Show answer

    B — At a threshold above every score nothing is flagged, so both rates are 0; below every score everything is flagged, so both are 1 — The endpoints are forced by the extremes of the threshold sweep, for any model at all. Only the path between them carries information.

🧪 Try It Yourself

Exercise 1 – Compute one point on the curve

Exercise 2 – Trace the whole curve

Exercise 3 – AUC by counting pairs

Exercise 4 – Spot a near-random classifier

Exercise 5 – Watch ROC and AP disagree

Recap

  • The ROC curve sweeps every threshold, plotting TPR against FPR; both denominators are row totals, so the curve is invariant to class balance.
  • Each positive steps the curve up, each negative steps it right.
  • AUC is P(positive scores above negative)P(\text{positive scores above negative}) — a ranking statistic. The ten-row example gives 21 of 25 correctly ordered pairs, so AUC = 0.84 by geometry and by counting.
  • 0.5 is chance; below 0.5 means the ranking is inverted, not that the model is worthless.
  • Under heavy imbalance, ROC is optimistic because FPR hides false positives in a huge negative pool. The same predictions scored 0.855 by ROC and 0.133 by average precision.
  • Balanced or symmetric costs → ROC-AUC. Rare positives you act on → average precision.

Exercise 6 – The same 0.84, computed three ways

Next

Phase 4 ends here. Continue to Phase 5 - Ensemble Learning — where the instability of a single decision tree, noted at the end of the trees page, becomes the foundation of the strongest models in classical machine learning.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did