HuntExams Academy logo
Data Science · Chapter 26 of 43

Model Evaluation

Pick a metric that matches the BUSINESS COST of errors. Report multiple metrics — a single number always hides something.

Always compare to a baseline (simple rule, majority class, previous model).

Example 1 (python)
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))
Output
precision recall f1-score support...

Precision, recall, F1 per class.

Example 2 (python)
from sklearn.metrics import mean_absolute_error, r2_score
print(mean_absolute_error(y_true, y_pred), r2_score(y_true, y_pred))
Output
3.4 0.82

Regression metrics.

Key points

  • Pick metrics that match business cost.
  • Always report a baseline.
  • Confusion matrix for classification.
  • MAE/RMSE/R² for regression.
💡 Note: A '95% accurate' model on 95%-negative data may be predicting 'negative' every time. Always check per-class metrics.

Understanding Model Evaluation

Model evaluation asks how useful predictions are on examples the model did not learn from. A training score measures performance on familiar data; it does not establish generalization. Before calculating anything, define the prediction target, the population you expect to serve, and the action a prediction triggers. These choices determine which errors matter and whether your evaluation data represents the intended use.

Consider a system that flags shipments likely to arrive late. Let 1 mean late and 0 mean on time. A false positive flags an on-time shipment, potentially wasting a support call. A false negative misses a late shipment, potentially leaving a customer unprepared. Accuracy counts both errors equally. That can be inappropriate when their consequences differ or most shipments arrive on time.

A confusion matrix exposes these counts. With labels ordered [0, 1], scikit-learn places actual classes in rows and predicted classes in columns: [[TN, FP], [FN, TP]]. Precision, TP/(TP+FP), answers how many flagged shipments were actually late. Recall, TP/(TP+FN), answers how many late shipments were caught. F1 is the harmonic mean of precision and recall; it rewards balance but ignores true negatives and does not encode business costs. Support is the number of actual examples belonging to each class.

Always evaluate a baseline on the same held-out examples. A majority-class classifier provides a useful minimum comparison, but its majority class must come from training data. A candidate can beat that baseline on recall while losing on accuracy. Neither result alone settles the decision. If credible error costs are available, report their weighted total alongside familiar metrics, and state the assumptions behind those costs.

For numerical predictions, mean absolute error (MAE) describes average absolute error in the target's units. Root mean squared error (RMSE) gives larger errors more influence while retaining those units. R² compares squared error with prediction by the evaluation targets' mean; for nonconstant targets, it can be negative and is not a percentage of correct predictions. The scikit-learn guide treats metrics, cross-validation, threshold tuning, and leakage prevention as related evaluation concerns. Together, they remind us that a useful score requires both an appropriate formula and a trustworthy evaluation procedure.

Practical walkthrough

Start by writing an evaluation agreement: late shipments are positive, missed late shipments cost five illustrative units, and unnecessary flags cost one. These are teaching assumptions, not measured financial losses. Assume costs add independently and correct decisions have zero cost. If support capacity is limited or losses vary across shipments, this simple cost calculation needs refinement.

Next, separate training, validation, and final test data before learning anything from features. Train model parameters on training data; choose settings and decision thresholds using validation data or cross-validation within the development data. Fit imputers, scalers, and feature selection on training portions only, preferably inside a pipeline. Keep the final test set untouched until choices are fixed. For future shipments, use a time-respecting split; repeated observations of the same entity may require grouped splitting.

Run the example with Python 3.10 or newer and a compatible scikit-learn installation, version 1.3 or newer. Familiarity with Python lists and binary labels is sufficient. No files or downloads are needed. The candidate predictions are deliberately supplied rather than trained, isolating evaluation arithmetic. Treat them as frozen predictions from an earlier development process, not evidence of a real model's performance.

Check that each prediction corresponds to the truth at the same list position. Read each confusion matrix before interpreting the summary metrics. Compare candidate and baseline costs on these identical ten cases. Finally, record sample size and class support: only two cases are positive. This tiny constructed sample demonstrates trade-offs, but cannot establish deployment reliability or provide a stable estimate of future performance.

Worked example

from sklearn.metrics import (
    accuracy_score,
    confusion_matrix,
    precision_recall_fscore_support,
)

# Separate training labels determine the baseline, not test labels.
y_train = [0, 0, 0, 0, 1, 1]
majority = max((0, 1), key=y_train.count)

# Constructed held-out outcomes and already-fixed candidate predictions.
y_test = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
candidate = [0, 0, 0, 0, 0, 0, 1, 1, 1, 0]
baseline = [majority] * len(y_test)

for name, predictions in [("Baseline", baseline), ("Candidate", candidate)]:
    matrix = confusion_matrix(y_test, predictions, labels=[0, 1])
    tn, fp, fn, tp = matrix.ravel()
    precision, recall, f1, _ = precision_recall_fscore_support(
        y_test, predictions, average="binary", pos_label=1,
        zero_division=0,
    )
    cost = int(fp + 5 * fn)
    print(name)
    print(f"matrix={matrix.tolist()}")
    print(f"accuracy={accuracy_score(y_test, predictions):.2f}")
    print(f"precision={precision:.2f} recall={recall:.2f} f1={f1:.2f}")
    print(f"cost={cost}")

Expected output

Baseline
matrix=[[8, 0], [2, 0]]
accuracy=0.80
precision=0.00 recall=0.00 f1=0.00
cost=10
Candidate
matrix=[[6, 2], [1, 1]]
accuracy=0.70
precision=0.33 recall=0.50 f1=0.40
cost=7

The baseline gets eight on-time shipments right but misses both late shipments. Its precision is mathematically undefined because it never predicts positive; zero_division=0 explicitly reports zero instead. The candidate catches one late shipment, misses one, and incorrectly flags two on-time shipments. Its precision is 1/3, recall is 1/2, and F1 is 0.40. Despite lower accuracy, its assumed cost is 2 + 5 = 7, versus 10 for the baseline. This preference depends on the cost assumptions: with equal error costs, the baseline wins. Fixed inputs and two-decimal formatting make the displayed output deterministic; no randomness or model fitting is involved.

Common mistakes and how to avoid them

  • Selecting the best threshold by repeatedly checking final test scores.

    Tune thresholds on validation data, then evaluate the frozen choice once on the test set. Repeated test-driven choices turn the test set into development data.

  • Reporting only a weighted average from classification_report.

    Include per-class precision, recall, and support. Weighted averages emphasize common classes; macro averages weight classes equally but can be unstable for very small classes.

  • Scaling or selecting features before splitting the data.

    Split first and fit preprocessing only within training data or each cross-validation training fold. Transform validation and test data using those already-fitted transformations.

  • Treating a lower observed error cost as guaranteed savings.

    Check whether cost assumptions reflect actual actions and whether the test population matches deployment. Report uncertainty and monitor later outcomes; predictive evaluation alone does not establish causal benefits.

Review questions

Why does the baseline's higher accuracy not make it the preferred option here?

It misses both positive cases. Under the stated five-to-one error costs, those misses cost more than the candidate's one miss and two false alarms.

What does candidate precision of 0.33 mean?

One of its three flagged shipments was actually late. It does not mean that one third of all late shipments were detected; that quantity is recall.

Why specify labels=[0, 1] in confusion_matrix?

It fixes class order and retains both classes even if one is absent, making the flattened entries reliably correspond to TN, FP, FN, and TP.

When might RMSE be more informative than MAE?

When large numerical errors deserve extra attention. RMSE emphasizes them through squaring, but is consequently more sensitive to extreme observations; reporting both makes that trade-off visible.

Further reading

📝 Quick Quiz

1. For imbalanced classification, accuracy is:

2. R² close to 1 means:

3. You should always compare to a: