Answer

What is AUC? The area under the ROC curve, explained

Inspired by a question on Cross Validated ·

classificationroc curvelogistic regression

The short answer

In prediction, AUC stands for area under the curve, and the curve is almost always the ROC (receiver operating characteristic) curve. It is a number from 0 to 1 that equals the probability that the model gives a randomly chosen case with the outcome a higher score than a randomly chosen case without it. A value of 0.5 is no better than a coin flip, and 1 is perfect separation.

Start with the ROC curve

Many models do not output a yes/no label directly. A logistic regression, a risk score or a lab biomarker gives each person a number, and you turn that number into a decision by picking a cut-off: above it you predict "has the condition", below it "does not". Every cut-off gives a different trade-off. A low cut-off catches most true cases (high sensitivity, also called the true positive rate) but raises many false alarms (a high false positive rate, which is 1 minus specificity). A high cut-off does the opposite.

The ROC curve plots the true positive rate against the false positive rate for every possible cut-off, from the strictest (bottom-left corner, nobody flagged) to the most lenient (top-right corner, everybody flagged). The name comes from signal detection work on radar during the Second World War, where operators had to separate real signals from noise. A model that separates the two groups well bends sharply toward the top-left corner; a useless model follows the diagonal line.

What the area means

The AUC is the area under that curve, so it runs from 0 to 1. Its most useful reading is as a probability: pick one person who has the outcome and one who does not, at random. The AUC is the chance that the model gives the first person the higher score (ties count as half). This is why AUC is also called the concordance or c-statistic in logistic regression output, and why it is exactly the Mann-Whitney U statistic divided by the number of case-control pairs.

Because AUC depends only on how the scores rank people, any transformation that keeps the order (probabilities, log-odds, or the raw score that fed the model) gives the same AUC. It also does not depend on any single cut-off, which is its main appeal, and it is not affected by how common the outcome is in the way plain accuracy is.

Compute it in R and Python

This simulation gives 300 people a risk score, generates a yes/no outcome from a logistic model, fits the model with base R's glm(), and then computes the AUC three ways: as the area from the ROC points, as the share of case-control pairs ranked correctly, and from the Mann-Whitney statistic. It adds a standard error from the Hanley-McNeil formula. The Python tab does the same with scikit-learn and SciPy. For real work in R, the pROC package draws ROC curves and gives DeLong confidence intervals and tests for comparing two AUCs.

R

set.seed(4417)
n <- 300
score <- rnorm(n)                                   # a risk score or biomarker
y <- rbinom(n, 1, plogis(-1 + 1.2 * score))         # 1 = has the condition
fit <- glm(y ~ score, family = binomial)
p <- fitted(fit)                                    # predicted probabilities
c(cases = sum(y), controls = sum(1 - y))

# ROC curve: sensitivity and 1 - specificity at every cut-off
cuts <- c(Inf, sort(unique(p), decreasing = TRUE))
tpr <- sapply(cuts, function(k) mean(p[y == 1] >= k))
fpr <- sapply(cuts, function(k) mean(p[y == 0] >= k))
auc_trap <- sum(diff(fpr) * (head(tpr, -1) + tail(tpr, -1)) / 2)

# Same number as a probability: a random case outranks a random control
pairs <- outer(p[y == 1], p[y == 0], "-")
auc_pairs <- mean(pairs > 0) + 0.5 * mean(pairs == 0)

# Same number again from the Mann-Whitney statistic
W <- wilcox.test(p[y == 1], p[y == 0], exact = FALSE)$statistic
auc_mw <- unname(W) / (sum(y) * sum(1 - y))
round(c(trapezoid = auc_trap, pairs = auc_pairs, mann_whitney = auc_mw), 4)

# Only the ranking matters: the raw score gives the same AUC
pr <- outer(score[y == 1], score[y == 0], "-")
round(mean(pr > 0) + 0.5 * mean(pr == 0), 4)

# Hanley-McNeil standard error and a 95% CI
A <- auc_pairs; n1 <- sum(y); n0 <- sum(1 - y)
Q1 <- A / (2 - A); Q2 <- 2 * A^2 / (1 + A)
se <- sqrt((A * (1 - A) + (n1 - 1) * (Q1 - A^2) + (n0 - 1) * (Q2 - A^2)) / (n1 * n0))
round(c(AUC = A, SE = se, lower = A - 1.96 * se, upper = A + 1.96 * se), 3)

# Accuracy at a 0.5 cut-off, for contrast
round(mean((p >= 0.5) == y), 3)

Python

import numpy as np
from scipy import stats
from scipy.special import expit
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, roc_curve

rng = np.random.default_rng(4417)
n = 300
score = rng.normal(size=n)                            # a risk score or biomarker
y = rng.binomial(1, expit(-1 + 1.2 * score))          # 1 = has the condition
fit = LogisticRegression(penalty=None).fit(score.reshape(-1, 1), y)
p = fit.predict_proba(score.reshape(-1, 1))[:, 1]     # predicted probabilities
print("cases", y.sum(), "controls", n - y.sum())

# ROC curve: sensitivity and 1 - specificity at every cut-off
fpr, tpr, _ = roc_curve(y, p)
auc_trap = np.trapz(tpr, fpr)

# Same number as a probability: a random case outranks a random control
pairs = p[y == 1][:, None] - p[y == 0][None, :]
auc_pairs = np.mean(pairs > 0) + 0.5 * np.mean(pairs == 0)

# Same number again from the Mann-Whitney statistic
U = stats.mannwhitneyu(p[y == 1], p[y == 0]).statistic
auc_mw = U / (y.sum() * (n - y.sum()))
print("trapezoid", round(auc_trap, 4), "pairs", round(auc_pairs, 4),
      "mann_whitney", round(auc_mw, 4), "sklearn", round(roc_auc_score(y, p), 4))

# Only the ranking matters: the raw score gives the same AUC
print("raw score AUC", round(roc_auc_score(y, score), 4))

# Hanley-McNeil standard error and a 95% CI
A, n1, n0 = auc_pairs, y.sum(), n - y.sum()
Q1, Q2 = A / (2 - A), 2 * A**2 / (1 + A)
se = np.sqrt((A * (1 - A) + (n1 - 1) * (Q1 - A**2) + (n0 - 1) * (Q2 - A**2)) / (n1 * n0))
print("AUC", round(A, 3), "SE", round(se, 3), "CI", np.round([A - 1.96 * se, A + 1.96 * se], 3))

# Accuracy at a 0.5 cut-off, for contrast
print("accuracy", round(np.mean((p >= 0.5) == y), 3))

The figures below come from one seeded run of the R code. Python uses a different random number generator, so its figures differ slightly (for example, an AUC of 0.764 instead of 0.739), but the pattern is the same: all methods agree exactly.

What AUC does not tell you

One more source of confusion: in pharmacology, AUC usually means the area under a drug's concentration-time curve, a measure of total exposure. It is the same geometric idea applied to a different curve, so check which one a paper means.

How to report AUC in APA style (7th edition)

In the method section, say what the scores were (for example, predicted probabilities from a logistic regression), whether the AUC was computed on the training data, a test set or by cross-validation, and how the confidence interval was obtained (for example, DeLong or Hanley-McNeil). Because AUC cannot exceed 1, write it without a leading zero. Using the example above:

"The risk score discriminated between participants with and without the condition (93 cases, 207 controls), AUC = .74, 95% CI [.68, .80]."

If you chose a cut-off, add a sentence such as "At a cut-off of x.xx, sensitivity was .xx and specificity was .xx." Include the ROC curve as a figure when the AUC is a main result, and when comparing two models on the same people, report both AUCs with a test for correlated AUCs such as DeLong's.

Related tools and guides

More answered questions

Working with your own data?

General answers only go so far. Send us your situation and we'll reply by email within two business days.

Ask your question

Written by AskStats with AI assistance. This is general information, not advice for your specific data or study. When the results matter, check your approach with a qualified statistician.