In this article
Before you start
This page belongs to the classical machine learning guide, which covers the models whose scores a ROC curve evaluates.
Why interviewers ask this
A candidate who can call roc_curve but cannot say what the points mean is exposed by the first follow-up. Common follow-ups are "what does the area under the curve tell you", "why use ROC instead of accuracy", or "what happens to ROC AUC when positives are rare". Each one checks that you separate how well scores rank cases from how well one cut-off performs.
A weak answer recites "higher is better". A strong answer says that the curve records one point per threshold, that the area depends only on the ordering of scores, and that a rare positive class can make the curve look better than the product feels.
The ROC curve in scikit-learn from first principles
In plain words
- ROC curve
- A receiver operating characteristic (ROC) curve is a plot of the true positive rate against the false positive rate that a classifier reaches as its decision threshold sweeps from high to low. Each point on it is one threshold.
- True positive rate
- The true positive rate is the share of real positives that the classifier flags, computed as true positives divided by all positives. It is the same quantity as recall.
- False positive rate
- The false positive rate is the share of real negatives that the classifier wrongly flags, computed as false positives divided by all negatives. A low value means few false alarms.
Think of a smoke alarm with a sensitivity dial. Turn it up and it catches every fire but also rings for toast. Turn it down and the toast is safe but a slow fire might be missed. Plot the share of fires caught against the share of false alarms for every dial setting and you have a ROC curve.
The analogy breaks here: a smoke alarm has one real dial, while a model's score can be re-cut at any value after the fact. The curve costs nothing extra.
In one precise sentence, a ROC curve lists the pair of rates for every possible threshold, and the area under it, the area under the curve (AUC), summarises how well the scores rank positives above negatives. The page on ROC AUC and the confusion matrix places this beside precision and recall.
Step 1: Start with true labels and scores, not decisions
The inputs are the true binary labels and a score for each example. The roc_curve reference says the scores can be probability estimates of the positive class or non-thresholded decision values such as decision_function output. If the labels are not 0 and 1 or minus 1 and 1, you must name the positive class with pos_label.
Our illustrative set has ten examples: five positives with scores 0.95, 0.85, 0.80, 0.55 and 0.40, and five negatives with scores 0.70, 0.45, 0.30, 0.20 and 0.101. A higher score means more confident that the example is positive. No threshold has been chosen yet, which is the whole point: the curve keeps every threshold.
Step 2: Sweep the threshold and count at each cut-off
At each threshold, the function counts how many positives and negatives score at or above it, then divides by the totals. The reference defines element i of the returned rates as the rate for predictions with a score at or above thresholds[i].
On our set, a threshold of 0.75 flags three positives and no negatives, so the true positive rate is 0.6 and the false positive rate is 0. At 0.5 it flags four positives and one negative: 0.8 and 0.2. At 0.35 it flags all five positives and two negatives: 1.0 and 0.41. Lower the threshold and neither rate ever falls, because a lower cut-off flags a superset of the cases flagged before. That is why the curve climbs from the bottom left to the top right.
Step 3: Pass scores, not hard labels, to roc_curve
You get the full curve only when y_score holds scores; if you pass 0 and 1 predictions, the function has only one real threshold to work with. In the call roc_curve(y_true, y_score, drop_intermediate=False) on our set, the function returns eleven points and the first threshold is infinity. scikit-learn 1.9.1 documents that infinite threshold as the point that predicts negative for everyone, with both rates at 0. Since version 1.3 the first threshold is infinity; before 1.3 the same point existed but its threshold was max(y_score) + 1.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, roc_auc_score, RocCurveDisplay
y_true = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
y_score = np.array([0.95, 0.85, 0.80, 0.55, 0.40, 0.70, 0.45, 0.30, 0.20, 0.10])
fpr, tpr, thresholds = roc_curve(y_true, y_score, drop_intermediate=False)
print(roc_auc_score(y_true, y_score)) # from scores: 0.88
print(roc_auc_score(y_true, (y_score >= 0.5))) # from hard labels: 0.8
RocCurveDisplay.from_predictions(y_true, y_score, plot_chance_level=True)
plt.show() # or plt.plot(fpr, tpr)Run as written, the first print gives 0.88 and the second 0.8, and the curve built from hard labels has only three points: (0, 0), (0.2, 0.8) and (1, 1)1. The area from labels is lower because the information about every other cut-off is gone. With predict output you have chosen one threshold for the model; with predict_proba or decision_function you keep all of them. The default drop_intermediate=True removes points that lie on a straight line between neighbours and, the reference says, does not change the area or the shape.
Step 4: Read the area and draw the curve
The area under the curve is a number from 0 to 1 that equals the fraction of positive-negative pairs the scores rank correctly, counting ties as half. On our set, 22 of the 25 pairs are ranked correctly, which is 0.88 and matches the function's value1. A random scorer sits near 0.5, and the roc_auc_score reference adds that the Gini coefficient is 2 times the area minus 1, so an area of 0.9 is a Gini of 0.8.
To draw it, the RocCurveDisplay page offers from_estimator, which takes a fitted classifier and data, and from_predictions, which takes labels and scores. Its score argument was named y_pred before version 1.7. The call returns a RocCurveDisplay that draws on matplotlib axes, with the curve and, with plot_chance_level=True, the diagonal of a random classifier. The ten-example curve is a staircase through (0, 0), (0, 0.6), (0.2, 0.6), (0.2, 0.8), (0.4, 0.8), (0.4, 1) and (1, 1), with area 0.88; the hard-label version is the straight-sided path (0, 0), (0.2, 0.8), (1, 1) with area 0.801.
Step-by-step description
- Start from true labels and a score per example.
- Count positives and negatives flagged at each cut-off.
- Scores keep every threshold; hard labels keep one.
- Read the area and draw the curve.
- The inputs are true labels and scores, not hard predictions.
- At a threshold of 0.5 the ten-example set has a true positive rate of 0.8 and a false positive rate of 0.2.
- Passing scores gives an area of 0.88; passing hard labels gives 0.8.
Plot from a fitted model
Pass the positive-class column predict_proba(X)[:, 1], or call RocCurveDisplay.from_estimator, which picks it for you. predict_proba returns one column per class, in the order of the classifier's classes_ attribute, so for labels 0 and 1 the second column is the score for the positive class. Passing that column gives the curve you want; passing the first column scores the negative class and reverses the ordering, and passing both columns raises an error.
With a fitted model, take the positive-class column of predict_proba, which has one column per class:
from sklearn.datasets import make_classification
from sklearn.metrics import RocCurveDisplay, roc_auc_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=1000, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
clf = LogisticRegression().fit(X_train, y_train)
scores = clf.predict_proba(X_test)[:, 1] # column 1 = class 1
print(roc_auc_score(y_test, scores)) # 0.985
RocCurveDisplay.from_estimator(clf, X_test, y_test, plot_chance_level=True)This run, with scikit-learn 1.8.0, gives an area of 0.985 on 250 test rows; column 0 gives 0.015, and from_estimator picks the right column itself1.
Knowing it is half the job. Saying it is the other half.
Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.
Start Free Trial →Worked example
Text description of the diagram
- At a threshold of 0.75 the true positive rate is 0.6 and the false positive rate is 0.
- At 0.5 the rates are 0.8 and 0.2.
- At 0.25 the rates are 1.0 and 0.6.
| Threshold | True positives | False positives | True positive rate | False positive rate |
|---|---|---|---|---|
| 0.9 | 1 | 0 | 0.2 | 0.0 |
| 0.75 | 3 | 0 | 0.6 | 0.0 |
| 0.6 | 3 | 1 | 0.6 | 0.2 |
| 0.5 | 4 | 1 | 0.8 | 0.2 |
| 0.35 | 5 | 2 | 1.0 | 0.4 |
| 0.25 | 5 | 3 | 1.0 | 0.6 |
The scores are illustrative. Each row is a count at or above the threshold divided by five positives or five negatives1.
| Check | Value |
|---|---|
| Area under the curve from scores, ten-example set | 0.88 |
| Area under the curve from hard labels at 0.5 | 0.80 |
| Area after squaring every score | 0.88 (the order is unchanged) |
| Area after negating every score | 0.12 (the order is reversed) |
| Documentation example, area | 0.75 |
| Areas in a seeded simulation, mean score gap of 0.5, 1.5 and 3.0 | 0.626, 0.841, 0.984 |
The area depends only on the order of scores, so squaring them leaves it at 0.88, while negating them gives 0.12, which is 1 minus 0.881. An area far below 0.5 therefore usually means the scores point the wrong way, not that the model is useless.
Text description of the diagram
- A mean score gap of 0.5 gives an area of 0.626.
- A gap of 1.5 gives 0.841.
- A gap of 3.0 gives 0.984.
The simulation draws 1,000 positive and 1,000 negative scores from normal distributions one standard deviation wide, with the positive mean above the negative mean by 0.5, 1.5 or 3.0, and the areas are 0.626, 0.841 and 0.9841.
Trade-offs and edge cases
ROC suits comparing how well models rank, and it can mislead when false alarms are the cost and the positive class is rare. Saito and Rehmsmeier (2015) argue that ROC plots can mislead on imbalanced data because they encourage an incorrect reading of specificity, and recommend precision-recall plots there. Reported practice differs: McDermott et al. (NeurIPS 2024, arXiv:2401.06091, v4 January 2025) argue that the area under the precision-recall curve "is not generally superior in cases of class imbalance", so neither summary replaces looking at the operating point.
The arithmetic is simple. Take a point with a true positive rate of 0.8 and a false positive rate of 0.05. With 500 positives and 500 negatives, precision is 0.941. With 10 positives and 990 negatives it is 8 true positives against an expected 49.5 false positives, a precision of 0.1391.
Text description of the diagram
- With 500 positives and 500 negatives the precision is 0.941.
- With 10 positives and 990 negatives it is 0.139: 8 true positives against 49.5 false positives.
Other edge cases follow from the function contracts.
Text description of the diagram
- roc_curve is for binary tasks.
- roc_auc_score needs multi_class set for more than two classes.
- RocCurveDisplay draws from an estimator or from predictions.
roc_curve is for binary tasks; its reference says multiclass support is not implemented. roc_auc_score handles multiclass, but its multi_class argument defaults to 'raise', so scikit-learn stops with an error until you pick one-vs-rest or one-vs-one, and the reference notes that one-vs-rest is sensitive to class imbalance1. Ties between scores are handled by the thresholds, and the area counts them as half a correct pair. Finally, the area ignores calibration: scores of 0.51 and 0.49 and of 0.99 and 0.01 give the same area if the order is the same, so check calibration with a loss such as cross-entropy when the probabilities themselves are used.
How to answer it in an interview
What does a ROC curve show, and how would you compute it in scikit-learn?
It plots the true positive rate against the false positive rate at every decision threshold. In scikit-learn I call roc_curve with the true labels and the model's scores, from predict_proba or decision_function, never hard predictions, and roc_auc_score for the area. The area is the chance that a random positive scores above a random negative, so it measures ranking, not calibration. When positives are rare I also look at precision, because the same ROC point can hide many false alarms.
The spoken structure is: define the two rates, say which function and which input, give the meaning of the area, then name the rare-class caveat.
Common mistakes
- "I pass model.predict to roc_curve" (wrong). Hard labels leave one real threshold and shrink the curve to a few points (right: pass probabilities or decision values).
- "An area of 0.9 means the model is right 90 percent of the time" (wrong). It is the probability that a random positive outranks a random negative, which is not accuracy (right).
- "ROC AUC tells me how many of my alarms will be false" (wrong). It measures ranking, not the share of flagged cases that are real; with rare positives the same ROC point can carry low precision (right: check precision at the threshold you will use).
- "I pass the whole predict_proba array, or its first column, to roc_curve" (wrong). The full two-column array raises a ValueError, and column 0 is the score for class 0, so the curve is reversed: 0.015 instead of 0.985 in our run (right: pass
predict_proba(X)[:, 1]). - "roc_auc_score works on multiclass out of the box" (wrong). It raises an error until multi_class is set (right: choose one-vs-rest or one-vs-one).
- "The area was 0.2, so the model is bad" (wrong). An area below 0.5 usually means the scores are reversed; flipping them gives 1 minus the area (right).
Follow-up questions
How do I plot a ROC curve in scikit-learn?
Call RocCurveDisplay.from_estimator with a fitted classifier and held-out data, or RocCurveDisplay.from_predictions with true labels and scores (the runnable snippets are in Steps 3 and 4). Both draw the curve using roc_curve underneath, as the display reference describes.
What does the first threshold of infinity mean in roc_curve?
thresholds[0] is the threshold of the point (0, 0), where every example is predicted negative. Since version 1.3 it is np.inf; before 1.3 it was max(y_score) + 1 (1.8 in the reference example), so older code that read thresholds[0] as a real cut-off, or plotted the thresholds, needs care.
How do I compute ROC AUC for a multiclass problem?
Pass predict_proba output to roc_auc_score with multi_class set to one-vs-rest or one-vs-one; the default raises an error. One-vs-one with macro averaging is the variant the reference lists as insensitive to class imbalance.
Is ROC AUC reliable when the positive class is rare?
It still measures ranking correctly, but it can read as strong while precision is low (and McDermott et al. argue the precision-recall area is no universal fix), as the same ROC point gave 0.941 and 0.139 above. Pair it with a precision-recall view, as in the confusion matrix page.
Should I pass predict or predict_proba to roc_curve?
Pass predict_proba for the positive class or decision_function. predict returns hard labels, which collapse the curve; on our set the area fell from 0.88 to 0.8. Raw logits from a neural network work as they are, because the curve depends only on the order of scores; only multiclass roc_auc_score needs probabilities that sum to 1, so apply softmax there.
- A ROC curve plots the true positive rate against the false positive rate for every decision threshold.
- Pass scores from predict_proba or decision_function to roc_curve; hard labels collapse it to a few points.
- The area equals the fraction of positive-negative pairs ranked correctly, so it depends only on the order of scores.
- With rare positives, the same ROC point can hide very low precision, so check precision at your operating threshold: 0.941 balanced against 0.139 at 10 positives in 1,000.
- roc_curve is binary only; roc_auc_score needs multi_class set for more than two classes.
Sources
- sklearn.metrics.roc_curve, scikit-learn 1.9.1 documentation — scikit-learn developers (page shows version only; read 2026-10-24), 24 Oct 2026
- sklearn.metrics.roc_auc_score, scikit-learn 1.9.1 documentation — scikit-learn developers (page shows version only; read 2026-10-24), 24 Oct 2026
- sklearn.metrics.RocCurveDisplay, scikit-learn 1.9.1 documentation — scikit-learn developers (page shows version only; read 2026-10-24), 24 Oct 2026
- The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets — PLOS ONE (Saito and Rehmsmeier), 4 Mar 2015
- sklearn.metrics.roc_curve, scikit-learn 1.2 documentation — scikit-learn developers (page shows version only; read 2026-10-24), 24 Oct 2026
- A Closer Look at AUROC and AUPRC under Class Imbalance — McDermott et al., arXiv (v4; NeurIPS 2024), 13 Jan 2025