HardMetrics & Evaluation

Compute ROC Curve from Scores

Metrics & Evaluation

Hard

Problem

Build a receiver operating characteristic curve from binary labels and prediction scores. Sort samples by descending score and group tied scores into one threshold. At each threshold compute:

\operatorname{TPR} = \frac{TP}{P}

\operatorname{FPR} = \frac{FP}{N}

Here, P and N are the total positive and negative counts. Begin with fpr=0, tpr=0, and an infinite threshold, then append one point per unique score. Return fpr, tpr, and thresholds as NumPy arrays in a dictionary.

Theory

The ROC (Receiver Operating Characteristic) curve visualizes classifier performance across all possible thresholds. Instead of evaluating at one threshold, it shows the full trade-off between true positive rate and false positive rate.

X-axis: False Positive Rate (FPR) = FP / (FP + TN)

Y-axis: True Positive Rate (TPR) = TP / (TP + FN)

Each point on the curve corresponds to a specific classification threshold.


The Two Key Rates

True Positive Rate (TPR), also called Recall or Sensitivity:

\text{TPR} = \frac{TP}{TP + FN} = \frac{\text{correctly identified positives}}{\text{all actual positives}}

"Of all the real positives, what fraction did we catch?"

False Positive Rate (FPR), also called Fall-out:

\text{FPR} = \frac{FP}{FP + TN} = \frac{\text{incorrectly flagged negatives}}{\text{all actual negatives}}

"Of all the real negatives, what fraction did we incorrectly flag?"


How Thresholds Create the Curve

A classifier outputs scores (e.g., probabilities). We choose a threshold t: predict positive if score \geq t.

High threshold (e.g., t = 0.9):

Low threshold (e.g., t = 0.1):

Sweeping from high to low threshold traces the ROC curve from (0, 0) to (1, 1).


Computing ROC Curve Points

Step 1: Sort all samples by predicted score (descending)

Step 2: Start with threshold above max score: TPR = 0, FPR = 0

Step 3: Lower threshold to each unique score value:

Step 4: End with threshold below min score: TPR = 1, FPR = 1


Worked Example

Data (score, label):

(0.9, 1), (0.8, 1), (0.7, 0), (0.6, 1), (0.4, 0), (0.3, 0)

Totals: 3 positives, 3 negatives

Threshold = 1.0 (nothing predicted positive):

Threshold = 0.9:

Threshold = 0.8:

Threshold = 0.7:

Threshold = 0.6:

Threshold = 0.4:

Threshold = 0.3:


Interpreting the Curve Shape

Perfect classifier: Curve goes from (0, 0) straight up to (0, 1), then across to (1, 1). All positives are ranked above all negatives.

Random classifier: Diagonal line from (0, 0) to (1, 1). Positives and negatives are randomly mixed.

Good classifier: Curve bows toward the upper-left corner. High TPR achieved with low FPR.

Worse than random: Curve below the diagonal. The model is anti-predictive (flip predictions to improve).


The Upper-Left Corner

The ideal operating point is (0, 1): TPR = 1, FPR = 0. This means:

Real classifiers cannot reach this point. The goal is to get as close as possible.

The point on the curve closest to (0, 1) is often a good threshold choice if you want to balance TPR and FPR.


Choosing an Operating Point

Different applications need different trade-offs:

Medical screening (high TPR critical): Choose a point with high TPR even if FPR is moderate. Missing a disease is worse than extra tests.

Spam filtering (low FPR critical): Choose a point with low FPR even if TPR is lower. Losing important emails is worse than seeing some spam.

Balanced: Choose the point closest to (0, 1), or where TPR - FPR is maximized (Youden's J statistic).


ROC Curve vs. Precision-Recall Curve

ROC Curve:

Precision-Recall Curve:

For highly imbalanced data (e.g., 1% positives), ROC can look good even when precision is poor. Use PR curve instead.


Area Under ROC Curve (AUC)

The area under the ROC curve summarizes performance in a single number:

AUC = 1.0: Perfect separation

AUC = 0.5: Random classifier (diagonal line)

AUC < 0.5: Worse than random

AUC equals the probability that a random positive is ranked higher than a random negative.


Multi-Class ROC

For multi-class problems, compute ROC curves using:

One-vs-Rest (OvR): For each class, treat it as positive and all others as negative. Plot one curve per class.

Micro-average: Aggregate all classes together, compute global TPR and FPR.

Macro-average: Average the curves across classes.


Implementation Notes

Handling ties: When multiple samples have the same score, they should be grouped. Some implementations step through all of them at once.

Efficient computation: Sort once, then sweep through in O(n) time. No need to recompute confusion matrix at each threshold.

Returning values: Typically return arrays of FPR values, TPR values, and corresponding thresholds.


Plotting Best Practices

Include the diagonal: Draw the random classifier line for reference.

Mark operating points: Indicate specific thresholds of interest.

Show AUC: Include the AUC value in the plot title or legend.

Equal axes: Use equal scaling for X and Y axes so the diagonal is at 45 degrees.

Examples

Example 1

Input
y_true = [0, 1], y_score = [0.1, 0.9]
Output
{"fpr": [0.0, 0.0, 1.0], "tpr": [0.0, 1.0, 1.0], "thresholds": ["inf", 0.9, 0.1]}
Explanation
The positive sample crosses first, followed by the negative sample at the lower threshold.

Example 2

Input
y_true = [1, 0, 1, 0], y_score = [0.9, 0.7, 0.4, 0.2]
Output
{"fpr": [0.0, 0.0, 0.5, 0.5, 1.0], "tpr": [0.0, 0.5, 0.5, 1.0, 1.0], "thresholds": ["inf", 0.9, 0.7, 0.4, 0.2]}

Hints

  1. Use cumulative positive and negative counts after a stable descending sort.
  2. Select the final index of each tied-score group with np.diff(sorted_scores).

Requirements

Constraints

Starter Code

import numpy as np

def roc_curve(y_true: list, y_score: list) -> dict:
    """
    Returns a dictionary with fpr, tpr, and thresholds.
    """
    # Write code here
    pass

Test Cases

CaseMatches
Basic 2 samplespublic
Alternating labelspublic