Model Evaluation#

A model is only as trustworthy as the method used to evaluate it. This section covers the metrics, validation strategies, and diagnostic tools that tell you whether your model genuinely works — or just memorised the training data.


Classification Metrics#

Confusion Matrix#

The confusion matrix is the foundation of all classification metrics:

                  Predicted
                  Pos    Neg
Actual  Pos  [   TP  |  FN  ]
        Neg  [   FP  |  TN  ]
TermMeaning
TP (True Positive)Correctly predicted positive
TN (True Negative)Correctly predicted negative
FP (False Positive)Incorrectly predicted positive (Type I error)
FN (False Negative)Incorrectly predicted negative (Type II error)
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LogisticRegression(max_iter=5000, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

cm = confusion_matrix(y_test, y_pred)
ConfusionMatrixDisplay(cm, display_labels=["Malignant", "Benign"]).plot(cmap="Blues")

Core Metrics#

MetricFormulaWhen to Use
Accuracy(TP+TN) / (TP+TN+FP+FN)Balanced classes only
PrecisionTP / (TP+FP)Cost of false positives is high (spam filter)
Recall (Sensitivity)TP / (TP+FN)Cost of false negatives is high (disease detection)
F1 Score2 × (Precision × Recall) / (Precision + Recall)Need balance between precision and recall
SpecificityTN / (TN+FP)True negative rate matters
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

print(f"Accuracy:  {accuracy_score(y_test, y_pred):.4f}")
print(f"Precision: {precision_score(y_test, y_pred):.4f}")
print(f"Recall:    {recall_score(y_test, y_pred):.4f}")
print(f"F1:        {f1_score(y_test, y_pred):.4f}")

Why Accuracy Fails on Imbalanced Data#

If 95% of emails are not spam, a model that always predicts “not spam” achieves 95% accuracy while catching zero spam. Accuracy is misleading when classes are imbalanced.

Classification Report#

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred, target_names=["Malignant", "Benign"]))

Output includes per-class precision, recall, F1, and the macro/weighted averages.


ROC Curve and AUC#

The Receiver Operating Characteristic curve plots TPR (recall) against FPR (1 - specificity) at every decision threshold.

from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

y_proba = model.predict_proba(X_test)[:, 1]

fpr, tpr, thresholds = roc_curve(y_test, y_proba)
auc = roc_auc_score(y_test, y_proba)

plt.plot(fpr, tpr, label=f"AUC = {auc:.3f}")
plt.plot([0, 1], [0, 1], "k--", label="Random (AUC = 0.5)")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve")
plt.legend()
plt.show()
AUC ValueInterpretation
1.0Perfect classifier
0.9–1.0Excellent
0.8–0.9Good
0.7–0.8Fair
0.5Random guessing
< 0.5Worse than random (labels likely swapped)

Precision-Recall Curve#

For imbalanced datasets, the PR curve is more informative than ROC:

from sklearn.metrics import precision_recall_curve, average_precision_score

precision, recall, thresholds = precision_recall_curve(y_test, y_proba)
ap = average_precision_score(y_test, y_proba)

plt.plot(recall, precision, label=f"AP = {ap:.3f}")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.title("Precision-Recall Curve")
plt.legend()
plt.show()

Regression Metrics#

MetricFormulaInterpretation
MSEΣ(y - ŷ)² / nAverage squared error (penalises large errors)
RMSE√MSESame units as target
MAEΣ|y - ŷ| / nAverage absolute error (robust to outliers)
R²1 - SS_res / SS_totProportion of variance explained (1 = perfect)
MAPEΣ(|y - ŷ| / |y|) / n × 100Percentage error (undefined when y = 0)
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import numpy as np

# Example: regression evaluation
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"MSE:  {mse:.2f}")
print(f"RMSE: {rmse:.2f}")
print(f"MAE:  {mae:.2f}")
print(f"R²:   {r2:.4f}")

Choosing Between MSE and MAE#

  • MSE/RMSE — penalises large errors quadratically. Use when large errors are especially costly.
  • MAE — treats all errors equally. More robust to outliers. Use when outliers in the target are expected but not catastrophic.

Cross-Validation#

A single train/test split is unreliable — the score depends on which specific data ends up in each set. Cross-validation gives a more robust estimate.

K-Fold Cross-Validation#

  1. Split data into K equal folds
  2. For each fold: train on K-1 folds, evaluate on the held-out fold
  3. Report the mean and standard deviation of scores across all folds
from sklearn.model_selection import cross_val_score

scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
print(f"CV Accuracy: {scores.mean():.4f} ± {scores.std():.4f}")

Stratified K-Fold#

For classification, preserves the class distribution in each fold:

from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skf, scoring="f1")
print(f"CV F1: {scores.mean():.4f} ± {scores.std():.4f}")

Other CV Strategies#

StrategyUse When
KFoldGeneral-purpose
StratifiedKFoldImbalanced classification
GroupKFoldGroups (patients, users) must not span folds
TimeSeriesSplitTemporal data (no future leakage)
RepeatedKFoldWant more stable estimates (repeat K-fold N times)
LeaveOneOutVery small datasets (K = n)
from sklearn.model_selection import GroupKFold, TimeSeriesSplit

# Group K-Fold: ensure same patient is not in both train and test
gkf = GroupKFold(n_splits=5)
# Pass groups= argument to cross_val_score
# scores = cross_val_score(model, X, y, cv=gkf, groups=patient_ids, scoring="f1")

# Time series split: always train on past, test on future
tscv = TimeSeriesSplit(n_splits=5)
# scores = cross_val_score(model, X, y, cv=tscv, scoring="neg_mean_squared_error")

Learning Curves#

Learning curves show how model performance changes with the amount of training data. They diagnose underfitting and overfitting.

from sklearn.model_selection import learning_curve

train_sizes, train_scores, val_scores = learning_curve(
    model, X, y, cv=5,
    train_sizes=np.linspace(0.1, 1.0, 10),
    scoring="accuracy",
    n_jobs=-1
)

train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
val_mean = val_scores.mean(axis=1)
val_std = val_scores.std(axis=1)

plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.1, color="blue")
plt.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, alpha=0.1, color="orange")
plt.plot(train_sizes, train_mean, "o-", color="blue", label="Training score")
plt.plot(train_sizes, val_mean, "o-", color="orange", label="Validation score")
plt.xlabel("Training Set Size")
plt.ylabel("Accuracy")
plt.title("Learning Curve")
plt.legend()
plt.show()

Interpreting Learning Curves#

PatternDiagnosisAction
Both curves converge at a high scoreGood fitNone needed
Both curves converge at a low scoreUnderfitting (high bias)More complex model, more features
Large gap between curvesOverfitting (high variance)More data, regularisation, simpler model
Validation curve still risingMore data would helpCollect more training data

Choosing the Right Metric#

ScenarioRecommended MetricWhy
Balanced classificationAccuracy or F1Accuracy is interpretable, F1 balances precision/recall
Imbalanced classificationF1, PR-AUC, or recall at fixed precisionAccuracy is misleading
Ranking / scoringROC-AUCThreshold-independent
Cost-sensitive classificationCustom cost matrix or precision at fixed recallDirectly models business cost
Regression (no outliers)RMSEPenalises large errors
Regression (with outliers)MAERobust to extreme values
Business reportingMAPE, R²Interpretable to stakeholders

Key Takeaways#

  • Accuracy is only meaningful for balanced classes — use precision, recall, F1, or AUC for imbalanced problems.
  • The confusion matrix is the source of truth: every classification metric derives from TP, TN, FP, FN.
  • ROC-AUC evaluates across all thresholds and is useful for ranking; PR-AUC is more informative for imbalanced datasets.
  • Cross-validation (especially stratified K-fold) gives far more reliable estimates than a single train/test split — always report mean ± standard deviation.
  • Learning curves diagnose whether your model needs more data, more complexity, or more regularisation.
  • Choose your evaluation metric before training — it should reflect the real-world cost of errors, not just statistical convenience.