ROC Curves vs Precision-Recall Curves
Both curves sweep the same thing: the decision threshold
A probabilistic classifier outputs a score between 0 and 1; a threshold (commonly 0.5, but not necessarily) converts that score into a hard class prediction. Both the ROC curve and the Precision-Recall (PR) curve are built by sweeping that threshold from 0 to 1 and recomputing a pair of metrics at every setting — they're two different lenses on the exact same underlying tradeoff, and the right choice between them depends heavily on class balance.
ROC: True Positive Rate vs. False Positive Rate
The ROC (Receiver Operating Characteristic) curve plots True Positive Rate (recall, $\frac{TP}{TP+FN}$) against False Positive Rate ($\frac{FP}{FP+TN}$) at every threshold. The diagonal line represents random guessing; a curve bowing toward the top-left corner represents a better classifier. AUC-ROC (area under this curve) summarizes performance across all thresholds into a single number, with a clean probabilistic interpretation: it's the probability that a randomly chosen positive example gets a higher score than a randomly chosen negative example.
PR curves: Precision vs. Recall
The PR curve plots Precision ($\frac{TP}{TP+FP}$) against Recall ($\frac{TP}{TP+FN}$) at every threshold. Unlike the ROC curve, it has no term involving True Negatives (TN) at all — which is exactly why it's the preferred tool for severely imbalanced problems like fraud detection or rare disease screening, where the negative class vastly outnumbers the positive class. Because TN count doesn't appear anywhere in precision or recall, the PR curve isn't distorted by a huge pool of easy true negatives the way the false-positive-rate denominator ($FP+TN$) in ROC can be — on heavily imbalanced data, FPR stays artificially small even when the model makes many false positives relative to the small number of true positives, making ROC curves look deceptively good.
import numpy as np
from sklearn.metrics import roc_auc_score, average_precision_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
# Severely imbalanced: 2% positive class
X, y = make_classification(
n_samples=20000, n_features=20, weights=[0.98, 0.02], random_state=0
)
model = LogisticRegression(max_iter=1000).fit(X, y)
scores = model.predict_proba(X)[:, 1]
roc_auc = roc_auc_score(y, scores)
pr_auc = average_precision_score(y, scores) # area under the PR curve
print(f"ROC-AUC: {roc_auc:.3f}") # often looks deceptively high
print(f"PR-AUC: {pr_auc:.3f}") # usually more honest on imbalanced data
# PR-AUC's baseline (random classifier) equals the positive class rate (~0.02 here),
# not 0.5 like ROC-AUC's baseline -- always compare PR-AUC against that baseline.
PR-AUC's baseline moves with class balance; ROC-AUC's doesn't
| Property | ROC curve | Precision-Recall curve |
|---|---|---|
| Axes | TPR (recall) vs. FPR | Precision vs. recall |
| Uses True Negatives | Yes (in FPR denominator) | No |
| Robust to severe class imbalance | No — can look misleadingly good | Yes — preferred for imbalanced data |
| Random-baseline AUC | Always 0.5 | Equals positive class base rate |
What's next?
Both curves describe behavior across every possible threshold, but a deployed model ultimately has to commit to one specific operating point. Picking that single threshold deliberately — based on the real costs of false positives versus false negatives for the actual business problem — is the practical step that follows directly from reading these curves.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.