Skip to content
SDB
ML Fundamentals

Chapter 09 · beginner · 38 min

Model Evaluation Metrics & Techniques

Classification, regression, clustering, and ranking metrics — plus cross-validation techniques

Subhendu Datta BhowmikAI Tutorials

Why Metrics Matter

Accuracy is the most intuitive metric — and often the most misleading. On a dataset where 99% of samples are negative (e.g., fraud detection), a model that always predicts "no fraud" achieves 99% accuracy while being completely useless.

Choosing the right metric is a business decision, not a technical one:

  • Fraud detection: minimize false negatives (missed fraud) → maximize Recall
  • Medical screening: missing a disease is worse than a false alarm → maximize Recall
  • Spam filter: false positives (blocking real email) are costly → maximize Precision
  • Ranking/search: care about top results → NDCG, MAP

Classification Metrics

From the confusion matrix (TPFNFPTN)\begin{pmatrix} TP & FN \\ FP & TN \end{pmatrix}:

MetricFormulaMeaning
AccuracyTP+TNTP+TN+FP+FN\frac{TP+TN}{TP+TN+FP+FN}Overall correctness (misleading when imbalanced)
PrecisionTPTP+FP\frac{TP}{TP+FP}Of all predicted positives, how many are correct?
Recall (Sensitivity)TPTP+FN\frac{TP}{TP+FN}Of all actual positives, how many did we catch?
F1 Score2PRP+R\frac{2 \cdot P \cdot R}{P + R}Harmonic mean of Precision and Recall
SpecificityTNTN+FP\frac{TN}{TN+FP}True Negative Rate
F-beta(1+β2)PRβ2P+R\frac{(1+\beta^2) \cdot P \cdot R}{\beta^2 P + R}Weight recall β\beta times more than precision

Precision vs Recall Trade-off

Changing the decision threshold (default 0.5) shifts between precision and recall:

  • Lower threshold → catch more positives → higher Recall, lower Precision
  • Higher threshold → only confident positives → higher Precision, lower Recall

ROC Curve and AUC

The ROC curve plots True Positive Rate (Recall) vs False Positive Rate (FPFP+TN\frac{FP}{FP+TN}) at all threshold values.

AUC-ROC (Area Under the Curve):

  • AUC = 1.0: perfect classifier
  • AUC = 0.5: random guessing (diagonal line)
  • AUC = 0.0: perfect inverse classifier

Probabilistic interpretation: AUC = probability that the model ranks a random positive example higher than a random negative example.

When NOT to Use ROC-AUC

When classes are highly imbalanced, ROC-AUC is optimistic because it accounts for TN (the large, easy majority class).

Use Precision-Recall AUC instead for imbalanced problems (fraud, rare disease):

  • PR curve plots Precision vs Recall
  • Average Precision (AP): area under the PR curve
  • No reliance on TN → more sensitive to performance on the minority class

Multi-Class Classification Metrics

For problems with K>2K > 2 classes, precision/recall/F1 must be aggregated across classes:

AveragingHowWhen to Use
MacroCompute per class, then average (equal weight per class)Balanced classes or when each class matters equally
WeightedCompute per class, weight by class support (sample count)Imbalanced classes — production default
MicroAggregate TP/FP/FN globally, then compute metricWhen total instance count matters; equals accuracy for F1

F1macro=1Kk=1KF1kF1weighted=k=1KnkF1kk=1KnkF1_{macro} = \frac{1}{K}\sum_{k=1}^{K} F1_k \qquad F1_{weighted} = \frac{\sum_{k=1}^{K} n_k \cdot F1_k}{\sum_{k=1}^{K} n_k}

Matthews Correlation Coefficient (MCC)

The MCC is a single number that summarizes the confusion matrix even for highly imbalanced classes. It considers all four quadrants (TP, TN, FP, FN):

MCC=TPTNFPFN(TP+FP)(TP+FN)(TN+FP)(TN+FN)MCC = \frac{TP \cdot TN - FP \cdot FN}{\sqrt{(TP+FP)(TP+FN)(TN+FP)(TN+FN)}}

  • Range: 1-1 (perfect inverse) to +1+1 (perfect) — 00 = random
  • MCC is the most informative binary classification metric for imbalanced data (even better than F1 in many cases)

Cohen's Kappa

Measures agreement between predicted and actual classes, correcting for chance agreement:

κ=pope1pe\kappa = \frac{p_o - p_e}{1 - p_e}

where pop_o = observed accuracy and pep_e = expected accuracy by chance. κ>0.8\kappa > 0.8 = strong agreement.

Log Loss (Cross-Entropy)

Penalizes confident wrong predictions heavily:

Log Loss=1ni=1nk=1Kyiklog(p^ik)\text{Log Loss} = -\frac{1}{n}\sum_{i=1}^{n}\sum_{k=1}^{K} y_{ik}\log(\hat{p}_{ik})

Used as the training loss for probabilistic classifiers. Lower is better; 0 = perfect. Essential when predicted probabilities matter, not just the class label.

Regression Metrics

MetricFormulaProperties
MAE$\frac{1}{n}\sumy_i - \hat{y}_i
MSE1n(yiy^i)2\frac{1}{n}\sum(y_i - \hat{y}_i)^2Penalizes large errors more, used in training loss
RMSEMSE\sqrt{\text{MSE}}Same units as target, MSE amplified by squaring
1(yiy^i)2(yiyˉ)21 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2}Fraction of variance explained (1=perfect, 0=baseline)
MAPE$\frac{100}{n}\sum\left\frac{y_i - \hat{y}_i}{y_i}\right

Rule of thumb: use MAE when you want errors in interpretable units; use RMSE when large errors are especially costly; use R² to compare models to a naive baseline.

Clustering Metrics (Unsupervised Learning)

Evaluating clustering is harder than supervised learning because there is no ground truth label to compare against. Metrics split into two families:

Internal Metrics (no ground truth needed)

These measure cluster quality using only the data and cluster assignments.

Silhouette Score

For each sample ii, compute:

  • a(i)a(i) = mean distance to all other samples in the same cluster (cohesion)
  • b(i)b(i) = mean distance to samples in the nearest other cluster (separation)

s(i)=b(i)a(i)max(a(i),b(i))s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}

Silhouette Score=1ni=1ns(i)\text{Silhouette Score} = \frac{1}{n}\sum_{i=1}^{n} s(i)

  • Range: 1-1 to +1+1
  • +1+1 = sample is well inside its cluster, far from others
  • 00 = sample is near a cluster boundary
  • 1-1 = sample is likely in the wrong cluster
  • Best use: compare number of clusters (kk) — pick the kk that maximizes silhouette score

Davies-Bouldin Index (DBI)

Average ratio of within-cluster scatter to between-cluster separation:

DB=1Ki=1Kmaxjiσi+σjd(ci,cj)DB = \frac{1}{K}\sum_{i=1}^{K} \max_{j \neq i} \frac{\sigma_i + \sigma_j}{d(c_i, c_j)}

where σi\sigma_i = average distance from points in cluster ii to centroid cic_i, and d(ci,cj)d(c_i, c_j) = distance between centroids.

  • Lower is better (0 = perfect, no upper bound)
  • Penalizes clusters that are spread out or too close together

Calinski-Harabasz Index (Variance Ratio Criterion)

Ratio of between-cluster dispersion to within-cluster dispersion:

CH=tr(BK)/(K1)tr(WK)/(nK)CH = \frac{\text{tr}(B_K) / (K-1)}{\text{tr}(W_K) / (n-K)}

  • Higher is better — well-separated, compact clusters score high
  • Tends to favor convex, globular clusters (like K-Means)

Inertia (WCSS)

Sum of squared distances of each point to its cluster centroid:

Inertia=k=1KxCkxμk2\text{Inertia} = \sum_{k=1}^{K}\sum_{x \in C_k} \|x - \mu_k\|^2

  • Lower is better but always decreases as KK increases — use the Elbow Method: plot inertia vs KK and pick the KK at the "elbow" where improvement flattens.

External Metrics (ground truth labels available)

Use these when you have true labels (e.g., to evaluate on a benchmark) but trained without them.

MetricDescriptionRangePerfect
Adjusted Rand Index (ARI)Measures agreement between true and predicted clusters, corrected for chance1-1 to 1111
Normalized Mutual Information (NMI)Mutual information normalized by cluster entropies00 to 1111
Adjusted Mutual Information (AMI)NMI corrected for chance agreement00 to 1111
Fowlkes-Mallows ScoreGeometric mean of pairwise precision and recall00 to 1111
HomogeneityEach cluster contains only one class00 to 1111
CompletenessAll members of a class are in the same cluster00 to 1111
V-measureHarmonic mean of homogeneity and completeness (like F1)00 to 1111

ARI is the most commonly used external metric — it handles variable numbers of clusters and corrects for random assignments.

Clustering Evaluation — Internal and External Metricspython
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans, DBSCAN
from sklearn.metrics import (
    silhouette_score, davies_bouldin_score, calinski_harabasz_score,
    adjusted_rand_score, normalized_mutual_info_score, v_measure_score,
)
import numpy as np
import matplotlib.pyplot as plt

# Synthetic data with 4 true clusters
X, y_true = make_blobs(n_samples=500, centers=4, cluster_std=0.8, random_state=42)

# ─── Elbow Method + Silhouette to find optimal K ─────────────
inertias, silhouettes, db_scores, ch_scores = [], [], [], []
K_range = range(2, 10)

for k in K_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = km.fit_predict(X)
    inertias.append(km.inertia_)
    silhouettes.append(silhouette_score(X, labels))
    db_scores.append(davies_bouldin_score(X, labels))
    ch_scores.append(calinski_harabasz_score(X, labels))

print(f"{'K':>4} {'Inertia':>10} {'Silhouette':>12} {'Davies-Bouldin':>16} {'Calinski-H':>12}")
print("-" * 58)
for k, inert, sil, db, ch in zip(K_range, inertias, silhouettes, db_scores, ch_scores):
    print(f"{k:>4} {inert:>10.1f} {sil:>12.4f} {db:>16.4f} {ch:>12.1f}")

# Best k: max silhouette / min Davies-Bouldin / max Calinski-H
best_k = K_range[np.argmax(silhouettes)]
print(f"\nBest K by silhouette: {best_k}")

# ─── External Metrics (when ground truth available) ──────────
km_best = KMeans(n_clusters=best_k, random_state=42, n_init=10)
y_pred = km_best.fit_predict(X)

print(f"\nExternal metrics (vs true labels):")
print(f"  Adjusted Rand Index (ARI): {adjusted_rand_score(y_true, y_pred):.4f}")
print(f"  Normalized Mutual Info:    {normalized_mutual_info_score(y_true, y_pred):.4f}")
print(f"  V-measure:                 {v_measure_score(y_true, y_pred):.4f}")

# ─── DBSCAN evaluation ───────────────────────────────────────
db = DBSCAN(eps=0.5, min_samples=5)
y_db = db.fit_predict(X)
n_clusters_db = len(set(y_db)) - (1 if -1 in y_db else 0)
n_noise = (y_db == -1).sum()
print(f"\nDBSCAN: {n_clusters_db} clusters, {n_noise} noise points")
if n_clusters_db > 1:
    # Silhouette ignores noise points (-1 labels)
    mask = y_db != -1
    print(f"  Silhouette (non-noise): {silhouette_score(X[mask], y_db[mask]):.4f}")
    print(f"  ARI:                    {adjusted_rand_score(y_true[mask], y_db[mask]):.4f}")

Ranking Metrics

Used when the model produces an ordered list of results (search, recommendations, information retrieval).

NDCG — Normalized Discounted Cumulative Gain

Measures quality of a ranked list, giving more credit to relevant items at the top:

DCG@k=i=1krelilog2(i+1)DCG@k = \sum_{i=1}^{k} \frac{rel_i}{\log_2(i+1)}

NDCG@k=DCG@kIDCG@kNDCG@k = \frac{DCG@k}{IDCG@k}

where IDCGIDCG = DCG of the ideal (perfect) ranking. Range: 00 to 11 — higher is better.

MAP — Mean Average Precision

Average of Average Precision (AP) scores across all queries:

AP=k=1nP(k)rel(k)number of relevant itemsAP = \frac{\sum_{k=1}^{n} P(k) \cdot \text{rel}(k)}{\text{number of relevant items}}

MAP=1Qq=1QAPqMAP = \frac{1}{Q}\sum_{q=1}^{Q} AP_q

MRR — Mean Reciprocal Rank

Average of the reciprocal rank of the first relevant item:

MRR=1Qq=1Q1rankqMRR = \frac{1}{Q}\sum_{q=1}^{Q} \frac{1}{\text{rank}_q}

MetricConsidersGraded RelevanceBest For
NDCG@kPosition + relevance scoreYesSearch engines, LLM evaluation
MAPPrecision at each relevant itemBinaryIR, document retrieval
MRROnly first relevant resultBinaryQuestion answering, single-answer search

Cross-Validation

Never estimate performance on the same data you trained on. Cross-validation gives an unbiased estimate of generalization performance.

K-Fold Cross-Validation

  1. Split data into kk equal folds (typically k=5k=5 or k=10k=10)
  2. Train on k1k-1 folds, evaluate on the held-out fold
  3. Repeat kk times; average the scores

CV Score=1ki=1kscorei\text{CV Score} = \frac{1}{k} \sum_{i=1}^{k} \text{score}_i

Stratified K-Fold

Preserves the class distribution in each fold. Always use this for classification — especially important for imbalanced datasets.

Time-Series Cross-Validation (Walk-Forward)

For temporal data, never shuffle — use expanding or rolling windows:

Fold 1: Train [1..100]   → Test [101..110]
Fold 2: Train [1..110]   → Test [111..120]
Fold 3: Train [1..120]   → Test [121..130]

Leave-One-Out (LOO)

k=nk = n — each sample is its own validation fold. Unbiased but expensive. Only practical for very small datasets.

Complete Evaluation Pipelinepython
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    classification_report, confusion_matrix, roc_auc_score,
    average_precision_score, RocCurveDisplay, PrecisionRecallDisplay,
)
from sklearn.preprocessing import label_binarize
import matplotlib.pyplot as plt

X, y = load_breast_cancer(return_X_y=True)
model = RandomForestClassifier(n_estimators=100, random_state=42)

# ─── Cross-Validation with Multiple Metrics ─────────────────
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
results = cross_validate(
    model, X, y, cv=cv,
    scoring=["accuracy", "f1", "roc_auc", "average_precision"],
    return_train_score=True,
)

print("5-Fold CV Results:")
for metric in ["accuracy", "f1", "roc_auc", "average_precision"]:
    test_scores = results[f"test_{metric}"]
    print(f"  {metric:<22} {test_scores.mean():.4f} ± {test_scores.std():.4f}")

# ─── Detailed Evaluation on Hold-out Set ────────────────────
from sklearn.model_selection import train_test_split
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
model.fit(X_tr, y_tr)
y_pred = model.predict(X_te)
y_proba = model.predict_proba(X_te)[:, 1]

print("
Classification Report:")
print(classification_report(y_te, y_pred, target_names=["malignant", "benign"]))

cm = confusion_matrix(y_te, y_pred)
print(f"Confusion Matrix:
{cm}")
print(f"ROC-AUC:           {roc_auc_score(y_te, y_proba):.4f}")
print(f"Average Precision: {average_precision_score(y_te, y_proba):.4f}")

# Threshold analysis
thresholds = np.arange(0.1, 1.0, 0.1)
print("
Threshold Analysis:")
print(f"{'Threshold':>12} {'Precision':>10} {'Recall':>8} {'F1':>6}")
for t in thresholds:
    from sklearn.metrics import precision_score, recall_score, f1_score
    y_t = (y_proba >= t).astype(int)
    p = precision_score(y_te, y_t, zero_division=0)
    r = recall_score(y_te, y_t)
    f = f1_score(y_te, y_t, zero_division=0)
    print(f"{t:>12.1f} {p:>10.3f} {r:>8.3f} {f:>6.3f}")

Knowledge check

A fraud detection model achieves 99.5% accuracy on a dataset where 0.5% of transactions are fraudulent. What is the most appropriate evaluation metric to use instead?

Summary

ProblemPrimary MetricSecondary
Balanced classificationAccuracy, F1 (weighted)ROC-AUC
Imbalanced classificationPR-AUC, MCCRecall at fixed Precision
Multi-classF1 (weighted/macro)Cohen's Kappa
Probabilistic classifierLog LossBrier Score
RegressionRMSE or MAE
Clustering (no labels)Silhouette ScoreDavies-Bouldin Index
Clustering (labels known)Adjusted Rand IndexNMI, V-measure
Ranking / searchNDCG@kMAP, MRR
SurvivalC-index
  • Always use stratified k-fold for classification; walk-forward for time series
  • Check for data leakage — use Pipelines
  • Use MCC instead of F1 for highly imbalanced binary problems
  • For clustering: combine an internal metric (silhouette) with the elbow method to pick kk
  • Report confidence intervals (mean ± std across folds), not just point estimates

Next: Important Hyperparameters — a systematic guide to tuning what matters.

ML Fundamentals