- Compute accuracy, precision, recall, F1, specificity and cost from a confusion matrix
- Compare ROC-AUC and PR-AUC and choose between them under class imbalance
- Explain calibration and evaluate it with reliability diagrams, the Brier score and expected calibration error
- Choose among MAE, RMSE and MAPE for regression and explain each one's failure modes
- Select k-fold, stratified, grouped or time-series cross-validation to match the data's structure
- Choose a decision threshold that minimizes expected business cost and test whether one model is reliably better than another
Before this: The ML Workflow: Framing, Data, Splits and Leakage, Linear and Logistic Regression, Probability and Statistics for AI
A model is only as good as the evidence that it works. Evaluation decides whether a model ships, which of two models wins, what threshold it runs at, and whether a promised improvement is real or noise. Many failed ML projects did not have bad models; they had good-looking numbers produced by the wrong metric, a leaky split, or a test set used one time too many. This lesson covers the measurements professionals rely on and the judgment needed to pick among them.
Intuition: the metric is the contract
Think of a metric as the contract between the model and the business. A fraud model "scoring 95% accuracy" sounds excellent until you learn that 95% of transactions are legitimate, so a model that never flags anything also scores 95%. The contract was badly written. Good evaluation starts by asking what an error costs, who bears it, and how the predictions will be used (ranked? thresholded? summed into a forecast?), then chooses metrics that reward exactly that behaviour.
Classification metrics
The confusion matrix
For a binary classifier with a positive class (say, fraud), every prediction falls into one of four cells:
| Predicted positive | Predicted negative | |
|---|---|---|
| Actually positive | True positive (TP) | False negative (FN) |
| Actually negative | False positive (FP) | True negative (TN) |
From these counts:
Precision answers "when the model raises an alarm, how often is it right?" Recall (also called sensitivity or true positive rate, TPR) answers "of all real positives, how many did we catch?" is their harmonic mean, which is dragged down by whichever is smaller. The general weights recall times as much as precision.
Worked example
1,000 transactions, 50 of them fraudulent. The model flags 100; 40 of those are fraud.
- , , , .
- Accuracy .
- Precision . Recall .
- .
- Specificity , so FPR .
The trivial "never fraud" model has accuracy , higher than our model, with recall 0. Under imbalance, accuracy is nearly useless; report precision, recall and a cost.
For more than two classes, compute per-class metrics and average them: macro averaging weights classes equally (good when rare classes matter), weighted averaging weights by class frequency, and micro averaging pools all counts (for single-label multiclass problems, micro-F1 equals accuracy).
Thresholds, ROC and PR curves
Most classifiers output a score or probability ; the label comes from a threshold (predict positive if ). Every threshold gives a different confusion matrix, so a single precision or recall number describes one operating point. Curves describe all of them.
- The ROC curve plots TPR (recall) against FPR as sweeps from 1 to 0. ROC-AUC, the area under it, equals the probability that a randomly chosen positive is scored higher than a randomly chosen negative. Random scoring gives 0.5; perfect ranking gives 1.0.
- The precision-recall (PR) curve plots precision against recall. Its summary, average precision (AP, a step-wise estimate of PR-AUC), has a random baseline equal to the positive rate, not 0.5.
Why the difference matters: FPR divides by the number of negatives. When negatives vastly outnumber positives (say 1 fraud per 1,000), a model can produce many times more false alarms than true detections while FPR stays tiny, so ROC-AUC looks excellent. Precision exposes this because its denominator contains the false positives directly. Saito and Rehmsmeier (2015) showed empirically that PR plots are more informative than ROC plots on imbalanced data. Rules of thumb:
| Situation | Prefer |
|---|---|
| Balanced classes, care about ranking overall | ROC-AUC |
| Rare positives, care about the quality of alarms | PR-AUC / average precision |
| A fixed operating point is known | Precision and recall (or cost) at that threshold |
| Comparing across datasets with different prevalence | ROC-AUC (prevalence-invariant), with care |
Calibration
A model is calibrated if, among all cases where it predicts probability , the fraction that are positive is about . Ranking metrics like AUC ignore calibration entirely: multiply every score by 0.5 and AUC is unchanged. But calibration matters whenever probabilities are used as probabilities: expected-cost decisions, risk scores shown to clinicians, combining models, or the threshold formula below.
Tools:
- Reliability diagram: bin predictions (e.g. 10 bins), plot mean predicted probability against observed positive rate; a calibrated model lies on the diagonal.
- Brier score: , a proper scoring rule (lower is better). Log loss is another.
- Expected calibration error (ECE): over bins . It depends on the binning, so treat it as a diagnostic.
Fixes: Platt scaling (fit a logistic regression on the scores) and isotonic regression (a monotone step function, needs more data), both available through CalibratedClassifierCV, and temperature scaling for neural networks (Guo et al., 2017, found that modern deep networks are often overconfident). Always fit calibrators on data not used to train the model.
Regression metrics
With residuals :
Worked example. True values , predictions . Residuals: .
- MAE .
- MSE , so RMSE .
- MAPE .
How to choose:
- MAE is in the target's units, robust to outliers, and is minimized by predicting the conditional median.
- RMSE penalizes large errors quadratically, is minimized by the conditional mean, and is the right choice when big misses are disproportionately costly. RMSE is always at least MAE; a large gap signals a few big errors.
- MAPE is scale-free and easy to explain, but it is undefined at , explodes near zero, and is asymmetric: for positive targets and non-negative forecasts, an under-forecast can contribute at most 100%, while an over-forecast is unbounded, so optimizing MAPE biases forecasts low. Alternatives include weighted MAPE () and MASE (error scaled by a naive baseline's error), common in forecasting.
- is the fraction of variance explained relative to predicting the mean. It can be negative on test data.
Cross-validation
A single train/test split gives one noisy estimate. k-fold cross-validation splits the training data into folds, trains on and validates on the remaining one, rotating times, and reports the mean and spread of the scores. or is standard.
Pick the splitter to match how the model will meet data in production:
| Data structure | Splitter | Why |
|---|---|---|
| i.i.d. rows, balanced | KFold(shuffle=True) | basic case |
| Classification, imbalance | StratifiedKFold | each fold keeps the class ratio |
| Several rows per patient/user/device | GroupKFold | the same entity never appears in train and validation |
| Time-ordered data | TimeSeriesSplit | always train on the past, validate on the future |
Using shuffled k-fold on time series or on grouped data leaks information and inflates scores, sometimes dramatically. Hyperparameter tuning adds another trap: if you pick the best configuration by its cross-validation score, that score is optimistically biased. Report the final number on an untouched test set, or use nested cross-validation (an inner loop for tuning, an outer loop for estimation).
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_validate
X, y = make_classification(n_samples=5000, weights=[0.95, 0.05],
n_informative=5, random_state=0)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
res = cross_validate(LogisticRegression(max_iter=1000), X, y, cv=cv,
scoring=["accuracy", "roc_auc", "average_precision", "f1"])
for m in ["accuracy", "roc_auc", "average_precision", "f1"]:
s = res[f"test_{m}"]
print(f"{m:18s} {s.mean():.3f} +/- {s.std():.3f}")
On this 5%-positive synthetic dataset you will typically see high accuracy and ROC-AUC alongside markedly lower average precision and F1, the imbalance story in numbers.
Bias, variance and learning curves
For squared error, the expected test error of a learning algorithm at a point decomposes as
where is the true function, with noise variance , and expectations are over training sets (and noise). Bias is systematic error from a model too simple to capture ; variance is sensitivity to the particular training sample; noise is irreducible. Increasing model flexibility typically lowers bias and raises variance. (Very large neural networks can show "double descent," where test error falls again past the interpolation point, so treat the classic U-curve as a guide rather than a law.)
Learning curves plot training and validation scores against training-set size:
- High bias (underfitting): both curves converge to a poor score with a small gap. More data will not help; add capacity or better features.
- High variance (overfitting): training score is much better than validation, and the gap narrows as data grows. More data, regularization or a simpler model will help.
sklearn.model_selection.learning_curve and validation_curve (score against a hyperparameter) produce these directly.
Comparing models statistically
If model B beats model A by 0.4 points of accuracy, is that real? Some tools:
- Look at the spread. Compare fold-by-fold (paired) scores; if B wins on only 3 of 10 folds, the mean difference is not convincing.
- McNemar's test for two classifiers on the same test set. Let = cases A got right and B got wrong, = the reverse. Under the null of equal error rates, approximately follows a chi-squared distribution with 1 degree of freedom. Example: , gives , above the 5% critical value 3.84 ().
- Paired tests over CV folds need care, because folds share training data and the scores are correlated, which makes a naive paired t-test overconfident. Dietterich (1998) analysed this problem and proposed the 5×2 cv paired t-test; corrected resampled t-tests are another option.
- Bootstrap confidence intervals on the test-set metric difference are simple and work for any metric.
Also budget for the multiple-comparisons problem: if you try 50 variants and pick the best on the same validation set, some of its edge is luck.
Choosing metrics that match business costs
Assign a cost to each cell of the confusion matrix and choose the threshold that minimizes total cost. Continuing the fraud example, suppose a missed fraud (FN) costs 500 dollars and a false alarm (FP) costs 10 dollars of analyst time. Three candidate thresholds:
| Threshold | TP | FN | FP | Cost in dollars |
|---|---|---|---|---|
| High (strict) | 30 | 20 | 15 | 10,000 + 150 = 10,150 |
| Medium | 40 | 10 | 60 | 5,000 + 600 = 5,600 |
| Low (lenient) | 45 | 5 | 200 | 2,500 + 2,000 = 4,500 |
The lenient threshold has the worst precision () yet the lowest cost. F1 would have picked the opposite: it is about 0.63 at the strict threshold (precision , recall ), 0.53 at the medium one, and 0.31 at the lenient one, so maximizing F1 selects the most expensive option here. F1 encodes an implicit cost ratio that rarely matches the real one.
With calibrated probabilities there is a closed form. Predict positive when the expected cost of saying "negative" exceeds that of saying "positive": , i.e.
Real constraints also matter: if analysts can review only 120 alerts a day, the operating point is fixed by capacity, and the relevant metric becomes precision (or fraud value caught) in the top 120.
import numpy as np
from sklearn.metrics import confusion_matrix
def best_threshold(y_true, p, c_fp=10, c_fn=500):
best = None
for t in np.linspace(0.001, 0.999, 999):
tn, fp, fn, tp = confusion_matrix(y_true, p >= t, labels=[0, 1]).ravel()
cost = c_fp * fp + c_fn * fn
if best is None or cost < best[1]:
best = (t, cost)
return best # tune on validation data, report cost on test data
Common pitfalls
- Reporting accuracy on imbalanced data, or ROC-AUC alone when positives are rare.
- Tuning the threshold or hyperparameters on the test set, then reporting the test score as if it were unbiased.
- Leaky cross-validation: shuffling time series, splitting rows of the same customer across folds, or fitting scalers/encoders on all data before splitting.
- Ignoring variance: quoting a single split's score without a confidence interval or fold spread.
- Treating scores as probabilities without checking calibration, especially after resampling or class weighting, both of which distort predicted probabilities.
- Using MAPE on data with zeros or near-zero targets.
- Optimizing a proxy forever: offline metrics should be validated against the online outcome (A/B test, business KPI) at least once.
In practice
- Write down the cost of each error type and the operating constraint (review capacity, latency, regulatory requirements) before choosing metrics.
- Report a small, fixed panel: one ranking metric (ROC-AUC or AP), the metrics at the chosen threshold, a calibration check, and the business cost, each with fold spread or bootstrap intervals.
- Use stratified CV for classification, grouped CV whenever entities repeat, and time-based splits for anything temporal, including a final out-of-time test.
- Keep a locked test set touched once per major decision; use cross-validation or a validation set for everything else.
- Slice metrics by segment (region, device, customer type). Aggregate scores can hide a subgroup where the model fails, which matters for fairness as well as accuracy.
- For LLM and generative systems the same principles apply with different measurements; see the evaluation lessons in AI 300 and AI 330.
Key takeaways
- Accuracy misleads under class imbalance; report precision, recall and a business cost at the chosen threshold.
- ROC-AUC measures ranking and is insensitive to prevalence, while PR-AUC exposes false alarms when positives are rare.
- Calibration is separate from discrimination; check it with reliability diagrams or the Brier score before treating scores as probabilities.
- MAE targets the median and is robust, RMSE targets the mean and punishes large errors, and MAPE breaks near zero and biases forecasts low.
- The cross-validation splitter must mirror production: stratified for imbalance, grouped for repeated entities, forward-in-time for temporal data.
- With calibrated probabilities, the cost-minimizing threshold is C_FP / (C_FP + C_FN), which is usually far from 0.5.
Terms in this lesson
- Precision
- The fraction of predicted positives that are truly positive, TP / (TP + FP).
- Recall
- The fraction of actual positives the model detects, TP / (TP + FN), also called sensitivity or true positive rate.
- ROC-AUC
- The area under the curve of true positive rate versus false positive rate, equal to the probability a random positive outranks a random negative.
- Average precision
- A step-wise summary of the precision-recall curve whose random-classifier baseline equals the positive class rate.
- Calibration
- The agreement between predicted probabilities and observed frequencies of the positive outcome.
- Brier score
- The mean squared difference between predicted probabilities and binary outcomes, a proper scoring rule where lower is better.
- Stratified k-fold
- Cross-validation that preserves each class's proportion in every fold.
- Nested cross-validation
- An outer cross-validation loop for performance estimation wrapped around an inner loop used only for hyperparameter tuning.
- McNemar's test
- A paired significance test comparing two classifiers using only the test cases on which they disagree.
Check your understanding
- 1.
A model has TP = 30, FP = 20, FN = 10, TN = 940. What is its F1 score?
- 2.
Positives are 0.2% of your data and you care about how many of the model's alerts are real. Which summary metric is most informative?
- 3.
You are forecasting daily sales using three years of data. Which validation scheme is appropriate?
- 4.
A missed default costs 900 dollars and wrongly declining a good applicant costs 100 dollars. With calibrated default probabilities, what threshold minimizes expected cost?
- 5.
Training accuracy is 99% and validation accuracy 78%; the gap shrinks steadily as you add training data. What is the most likely diagnosis?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
A disease screening test is applied to 2,000 people, 100 of whom have the disease. It flags 180 people, of whom 90 truly have the disease. Build the confusion matrix and compute accuracy, precision, recall, specificity and F1. Then explain why precision is low despite high recall and specificity.
Show solution
, , , .
- Accuracy .
- Precision .
- Recall .
- Specificity .
- .
Precision is low because the disease is rare (5% prevalence). Even a 4.7% false positive rate applied to 1,900 healthy people produces about 90 false alarms, as many as the true detections. This is the base-rate effect: positive predictive value depends on prevalence, which is why screening programs follow positive results with a more specific confirmatory test.
- Exercise 2Core
For true values and predictions , compute MAE, RMSE and MAPE. Which metric is distorted, and why?
Show solution
Residuals : .
- MAE .
- MSE , RMSE .
- Percentage errors: , , , . MAPE .
MAPE is distorted by the near-zero target 0.5: an absolute error of 1 unit, the smallest in the set, contributes 200% and dominates the average. MAE and RMSE treat that miss as small. Weighted MAPE, , gives a far more sensible picture.
- Exercise 3Core
Write scikit-learn code that trains a gradient-boosting classifier on an imbalanced synthetic dataset, picks the threshold minimizing cost (FP = 5, FN = 100) on a validation split, and reports the cost on a separate test split, compared with the default 0.5 threshold.
Show solution
import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.ensemble import HistGradientBoostingClassifier X, y = make_classification(n_samples=20000, weights=[0.97, 0.03], n_informative=6, random_state=1) X_tr, X_tmp, y_tr, y_tmp = train_test_split(X, y, test_size=0.4, stratify=y, random_state=1) X_va, X_te, y_va, y_te = train_test_split(X_tmp, y_tmp, test_size=0.5, stratify=y_tmp, random_state=1) clf = HistGradientBoostingClassifier(random_state=1).fit(X_tr, y_tr) def cost(y_true, p, t, c_fp=5, c_fn=100): pred = p >= t fp = np.sum(pred & (y_true == 0)); fn = np.sum(~pred & (y_true == 1)) return c_fp * fp + c_fn * fn p_va = clf.predict_proba(X_va)[:, 1] grid = np.linspace(0.01, 0.99, 99) t_best = grid[np.argmin([cost(y_va, p_va, t) for t in grid])] p_te = clf.predict_proba(X_te)[:, 1] print("chosen threshold:", round(t_best, 2)) print("test cost @0.5 :", cost(y_te, p_te, 0.5)) print("test cost @best:", cost(y_te, p_te, t_best))The chosen threshold lands well below 0.5 (the theoretical optimum for calibrated probabilities is ), and the test cost is lower than at 0.5. The threshold is tuned on validation data and the cost reported on test data, so the reported number is not optimistically biased.
- Exercise 4Challenge
Two classifiers are evaluated on the same 500 test cases. Both are correct on 400 and both wrong on 40. A alone is correct on 38; B alone is correct on 22. (a) Compute each model's accuracy. (b) Run McNemar's test with continuity correction at the 5% level. (c) Explain why a naive paired t-test on 10-fold CV scores can overstate significance.
Show solution
(a) A correct , accuracy . B correct , accuracy . (Check: .)
(b) (A right, B wrong), . . The critical value for 1 degree of freedom at 5% is 3.84, so we cannot reject equal error rates (). A 3.2-point accuracy gap on 500 cases is suggestive but not conclusive; collect more test data.
(c) The 10 fold scores are not independent: any two training sets share about 89% of their data, and every case is in the test fold once. The paired t-test assumes independent differences, so the estimated variance of the mean difference is too small and the test rejects the null too often (an inflated Type I error). Dietterich (1998) documented this and recommended alternatives such as the 5x2cv paired t-test or McNemar's test on a single held-out set; corrected resampled t-tests adjust the variance for the overlap.
Further reading
- docsscikit-learn User Guide: Metrics and scoring: quantifying the quality of predictions
- paperSaito & Rehmsmeier, The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets (PLOS ONE, 2015)
- paperGuo, Pleiss, Sun & Weinberger, On Calibration of Modern Neural Networks (ICML 2017)
- paperDietterich, Approximate Statistical Tests for Comparing Supervised Classification Learning Algorithms (Neural Computation, 1998)
- bookJames, Witten, Hastie, Tibshirani, An Introduction to Statistical Learning, Chapters 2 and 5 (2nd ed. 2021; Python ed. 2023)
Go further
Sources
Fact-checked and last reviewed 2026-11-05