learn.aathan.in

Evaluating Models: Metrics for Regression & Classification

MAE, MSE, RMSE and R² for regression; the confusion matrix, precision, recall, F1 and ROC-AUC for classification — what each number means, when accuracy lies, and how to diagnose overfitting.

Training a model is easy. Knowing whether it’s any good is the part that separates working systems from confident nonsense — and it’s where the regression and classification paths finally diverge, because you cannot measure “how far off is this number?” the same way you measure “did it pick the right category?”

The golden rule

Always evaluate on data the model has never seen.

model.fit(X_train, y_train)
y_pred = model.predict(X_test)     # TEST set
score = metric(y_test, y_pred)

Scoring on training data measures memorisation. A deep decision tree can score 100% on data it has memorised and fail completely on anything new.


Regression metrics

Using the fit from the linear regression walkthrough (y = 0.6x + 2.2, actual y = [2, 4, 5, 4, 5], predicted = [2.8, 3.4, 4.0, 4.6, 5.2]):

MetricFormulaOur valueIn words
MAEmean of |y − ŷ|0.64Off by 0.64 on average
MSEmean of (y − ŷ)²0.48Squared units — for optimising
RMSE√MSE0.693Off by 0.69, in real units
1 − RSS/TSS0.60Explains 60% of the variance
import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)

MAE vs RMSE — the one distinction to remember. Both report error in the original units, but RMSE squares before averaging, so it punishes large misses far more:

Model A errors: [1, 1, 1, 1]   -> MAE 1.0,  RMSE 1.0
Model B errors: [0, 0, 0, 4]   -> MAE 1.0,  RMSE 2.0

Identical MAE, very different RMSE. If one catastrophic error is much worse than four small ones (delivery times, medical dosing), optimise RMSE. If all errors cost the same, MAE is the honest summary — and it’s far less swayed by outliers.

R² is the only one comparable across problems, because it’s unitless. 0.6 means 60% of the variance explained; 0 means no better than always guessing the mean; negative means worse than that (yes, that happens).


Classification metrics

The confusion matrix

Every classification metric is derived from this table, so learn it first.

PREDICTED Negative Positive ACTUAL Negative Positive TN FP FN TP True Negative False Positive "false alarm" False Negative "the miss" True Positive The diagonal is correct; the off-diagonal is the two ways to be wrong.
Everything below is computed from these four counts. The two error types are not interchangeable — which one hurts more depends entirely on your problem.
from sklearn.metrics import confusion_matrix, classification_report

print(confusion_matrix(y_test, y_pred))
# [[TN FP]
#  [FN TP]]

print(classification_report(y_test, y_pred))   # precision/recall/F1 in one call

Accuracy — and why it lies

Accuracy=TP+TNTP+TN+FP+FN\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}

The share of predictions that were right. Fine when classes are balanced, dangerously misleading when they aren’t.

The fraud example. 1,000 transactions, 10 fraudulent. A model that always predicts “legitimate” gets 99% accuracy and catches zero fraud. The headline number looks excellent; the model is worthless.

That single scenario is why the next two metrics exist.

Precision and recall

Precision=TPTP+FPRecall=TPTP+FN\text{Precision} = \frac{TP}{TP + FP} \qquad \text{Recall} = \frac{TP}{TP + FN}
  • Precision — “when it says yes, how often is it right?” Punishes false alarms. Denominator is everything predicted positive.
  • Recall — “of all the real positives, how many did it catch?” Punishes misses. Denominator is everything actually positive.

They trade off against each other. Lower your threshold and you catch more positives (recall up) but raise more false alarms (precision down).

Which to optimise depends on which error costs more:

SituationPrioritiseBecause
Cancer screeningRecallA missed case is fatal; a false alarm just means another test
Spam filterPrecisionJunk in the inbox is annoying; a lost job offer is a disaster
Fraud detectionRecallMissed fraud costs money directly
RecommendationsPrecisionBad suggestions erode trust fast

F1 score

The harmonic mean of precision and recall, when you need one number:

F1=2×Precision×RecallPrecision+RecallF_1 = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}}

Harmonic, not arithmetic, on purpose: it punishes imbalance. Precision 1.0 with recall 0.0 averages to 0.5 the normal way, but F1 = 0 — correctly calling that model useless.

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

accuracy_score(y_test, y_pred)
precision_score(y_test, y_pred)
recall_score(y_test, y_pred)
f1_score(y_test, y_pred)

ROC curve and AUC

Classifiers output probabilities; the 0.5 threshold is arbitrary. The ROC curve plots true-positive rate against false-positive rate across every possible threshold, and AUC is the area beneath it.

from sklearn.metrics import roc_auc_score, roc_curve

y_proba = model.predict_proba(X_test)[:, 1]     # probability of class 1
auc = roc_auc_score(y_test, y_proba)
fpr, tpr, thresholds = roc_curve(y_test, y_proba)
AUCMeaning
1.0Perfect separation
0.9+Excellent
0.7–0.8Acceptable
0.5Random guessing — no signal at all

AUC is threshold-independent, so it answers “does this model rank positives above negatives?” rather than “is it right at one particular cut-off”. For heavily imbalanced data, prefer the precision-recall curve — ROC-AUC can look flattering when negatives dominate.

Picking a classification metric

Balanced classes:            accuracy is fine
Imbalanced classes:          F1, or precision/recall separately
Misses are expensive:        recall
False alarms are expensive:  precision
Comparing models overall:    ROC-AUC
Heavily imbalanced:          PR-AUC

Diagnosing overfitting

Compare training and test scores. The gap tells you what’s wrong:

TrainTestDiagnosisFix
HighHighGood modelShip it
HighLowOverfitting — memorised the dataSimplify, regularise, more data
LowLowUnderfitting — too simpleMore complex model, better features
LowHighSomething’s wrong with your splitCheck for leakage or a tiny test set
print("Train:", model.score(X_train, y_train))
print("Test: ", model.score(X_test, y_test))

A tree scoring 1.00 on train and 0.62 on test is the textbook picture of overfitting.

Cross-validation

A single train/test split is one roll of the dice — an unlucky split can flatter or damn a good model. K-fold cross-validation splits the data k ways, trains k times, and averages:

from sklearn.model_selection import cross_val_score

scores = cross_val_score(model, X, y, cv=5, scoring='f1')
print(scores)                       # one score per fold
print(scores.mean(), scores.std())  # average, and how stable it is

The standard deviation matters as much as the mean — a model scoring 0.85 ± 0.02 is trustworthy; 0.85 ± 0.15 is a coin flip that happened to average well.

For imbalanced classification use StratifiedKFold so every fold keeps the class balance.

Data leakage — the silent killer

If information from the test set reaches training, your scores become fiction. The usual culprits:

# WRONG — the scaler learns from test data too
X_scaled = StandardScaler().fit_transform(X)
X_train, X_test = train_test_split(X_scaled, ...)

# RIGHT — fit on train only, apply to both
X_train, X_test = train_test_split(X, ...)
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)

Same rule for imputers and encoders — the discipline covered in data preprocessing. A Pipeline enforces it automatically:

from sklearn.pipeline import Pipeline

pipe = Pipeline([('scaler', StandardScaler()), ('model', LogisticRegression())])
cross_val_score(pipe, X, y, cv=5)    # scaling now re-fits inside each fold

Suspiciously perfect scores (0.99+) almost always mean leakage, not genius.

Takeaways

  • Score on the test set, never on training data.
  • Regression: RMSE to report, MAE when outliers shouldn’t dominate, to compare across problems.
  • Classification: start from the confusion matrix. Accuracy is meaningless on imbalanced data.
  • Precision = trust its yes; recall = catches everything. Choose based on which error costs more; F1 balances them; AUC is threshold-independent.
  • The train-vs-test gap diagnoses over/underfitting; cross-validation tells you whether a score is stable.
  • Guard against leakage — fit transformers on training data only, or use a Pipeline.