Answer

Precision-recall or ROC: which should you use for rare outcomes?

Inspired by a question on Cross Validated ·

classification

The short answer

Sensitivity and specificity ignore how common positives are; precision does not. Use ROC to judge ranking regardless of prevalence, and precision-recall (PR) when positives are rare and what matters is how many flagged cases are real. Both come from the same confusion matrices and agree about a model that is better at every cut-off, but with a rare outcome a model can look strong on one and weak on the other. Reporting both is often the honest choice.

Two plots from the same confusion matrices

Both curves come from sweeping a cut-off across a model's scores and building a confusion matrix at each step. They just plot different ratios from that matrix:

That one difference drives everything else. Precision can be written as recall × prevalence divided by (recall × prevalence + false positive rate × (1 − prevalence)). Hold the classifier fixed and make positives rarer, and the ROC curve stays where it is, while precision falls, because the same false positive rate now applies to a far larger pool of negatives.

When each one is the better choice

A useful theoretical result (Davis and Goadrich, 2006) is that one curve dominates another in ROC space exactly when it dominates in PR space. So the two views cannot disagree about a model that is better everywhere. They can disagree when curves cross, because the PR curve gives far more weight to the high-score region where the rare positives must be found, while the ROC area spreads its weight over all cut-offs.

See the difference in R and Python

This simulation keeps the classifier's quality fixed (positives score 1.5 standard deviations higher than negatives on average) and changes only the prevalence: 1,000 positives and 1,000 negatives, then 100 positives and 9,900 negatives. It computes the ROC area from ranks, the average precision (the usual summary of the PR curve), and recall, false positive rate and precision at a cut-off of 1.5. The Python tab does the same with NumPy and checks the results against scikit-learn's roc_auc_score and average_precision_score.

R

set.seed(2718)
# Same classifier quality in both settings: positives score higher on average
make_data <- function(n_pos, n_neg) {
  data.frame(y = c(rep(1, n_pos), rep(0, n_neg)),
             score = c(rnorm(n_pos, mean = 1.5), rnorm(n_neg, mean = 0)))
}
balanced <- make_data(1000, 1000)   # prevalence 50%
rare     <- make_data(100, 9900)    # prevalence 1%

summarise <- function(d, cut = 1.5) {
  pos <- d$score[d$y == 1]; neg <- d$score[d$y == 0]
  n1 <- length(pos); n0 <- length(neg)
  # ROC AUC: share of positive-negative pairs ranked correctly
  r <- rank(d$score)
  auc <- (sum(r[d$y == 1]) - n1 * (n1 + 1) / 2) / (n1 * n0)
  # Average precision: mean precision at the rank of each positive
  o <- order(d$score, decreasing = TRUE)
  yy <- d$y[o]
  prec_at_k <- cumsum(yy) / seq_along(yy)
  ap <- mean(prec_at_k[yy == 1])
  # One working cut-off
  flagged <- d$score >= cut
  c(prevalence = n1 / (n1 + n0), roc_auc = auc, avg_precision = ap,
    recall = mean(pos >= cut), fpr = mean(neg >= cut),
    precision = sum(flagged & d$y == 1) / sum(flagged),
    n_flagged = sum(flagged))
}
round(rbind(balanced = summarise(balanced), rare = summarise(rare)), 3)

Python

import numpy as np
import pandas as pd
from scipy.stats import rankdata
from sklearn.metrics import roc_auc_score, average_precision_score

rng = np.random.default_rng(2718)

# Same classifier quality in both settings: positives score higher on average
def make_data(n_pos, n_neg):
    y = np.r_[np.ones(n_pos), np.zeros(n_neg)]
    score = np.r_[rng.normal(1.5, 1, n_pos), rng.normal(0, 1, n_neg)]
    return y, score

def summarise(y, score, cut=1.5):
    pos, neg = score[y == 1], score[y == 0]
    n1, n0 = len(pos), len(neg)
    # ROC AUC: share of positive-negative pairs ranked correctly
    r = rankdata(score)
    auc = (r[y == 1].sum() - n1 * (n1 + 1) / 2) / (n1 * n0)
    # Average precision: mean precision at the rank of each positive
    yy = y[np.argsort(-score)]
    prec_at_k = np.cumsum(yy) / np.arange(1, len(yy) + 1)
    ap = prec_at_k[yy == 1].mean()
    # One working cut-off
    flagged = score >= cut
    return {"prevalence": n1 / (n1 + n0), "roc_auc": auc, "avg_precision": ap,
            "sklearn_auc": roc_auc_score(y, score),
            "sklearn_ap": average_precision_score(y, score),
            "recall": np.mean(pos >= cut), "fpr": np.mean(neg >= cut),
            "precision": (flagged & (y == 1)).sum() / flagged.sum(),
            "n_flagged": flagged.sum()}

balanced = summarise(*make_data(1000, 1000))   # prevalence 50%
rare = summarise(*make_data(100, 9900))        # prevalence 1%
print(pd.DataFrame([balanced, rare], index=["balanced", "rare"]).round(3).to_string())
         prevalence roc_auc avg_precision recall   fpr precision n_flagged
balanced       0.50   0.864          0.86  0.501 0.065     0.885       566
rare           0.01   0.849          0.10  0.450 0.070     0.061       741

The figures come from one seeded run of the R code. Python uses a different random number generator, so its figures differ slightly (for example, an ROC AUC of 0.839 in both settings and an average precision of 0.134 in the rare one), but the pattern is the same, and the hand-computed values match scikit-learn's exactly.

Practical points

How to report average precision in APA style (7th edition)

Say in the method section what the scores were, whether performance was measured on a held-out set or by cross-validation, and how average precision was computed. Both measures cannot exceed 1, so write them without a leading zero, and always give the prevalence. Using the rare-outcome example above:

"In the evaluation sample (100 positive and 9,900 negative cases; prevalence 1%), the classifier achieved an area under the ROC curve of .85 and an average precision of .10 (chance level .01). At a cut-off of 1.5, recall was .45 and precision was .06."

Add confidence intervals in square brackets when you have them, for example from a bootstrap: AUC = .xx, 95% CI [.xx, .xx]. Show the ROC and PR curves side by side as a figure when classification performance is a main result.

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.