Model Evaluation & Selection
Level: 3 (Production — from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Question 1 — Confusion Matrix & Metrics from Scratch (12 marks)
A binary classifier is evaluated on 200 samples. The confusion matrix (positive = class 1) is:
| Predicted + | Predicted − | |
|---|---|---|
| Actual + | 45 | 15 |
| Actual − | 20 | 120 |
(a) Compute accuracy, precision, recall, and -score. Show every arithmetic step. (6)
(b) Derive the general algebraic identity relating to precision and recall , starting from the harmonic-mean definition. (3)
(c) The class distribution is imbalanced (60 positives vs 140 negatives). Explain out loud why accuracy is misleading here and which metric you would report to a stakeholder. (3)
Question 2 — Bias–Variance Decomposition (11 marks)
For squared-error loss, the expected test error of a model at point decomposes as:
(a) Derive this decomposition from first principles. State your assumptions about the noise. (7)
(b) A model has training error 4% and validation error 22%. Diagnose the regime (high bias / high variance) and give two concrete remedies. (4)
Question 3 — Cross-Validation Code from Memory (12 marks)
(a) Write a Python function kfold_indices(n, k, seed) that returns a list of (train_idx, val_idx) tuples for k-fold CV, without using scikit-learn. (6)
(b) Explain what stratified k-fold changes and why it matters for imbalanced data. (3)
(c) Explain out loud what nested CV is and the specific pitfall it avoids. (3)
Question 4 — ROC / AUC Derivation (10 marks)
A model outputs the following scores for 4 positives and 4 negatives:
- Positives: 0.9, 0.8, 0.6, 0.35
- Negatives: 0.7, 0.5, 0.4, 0.2
(a) Compute the AUC using the probabilistic (rank / pairwise) interpretation: the probability a random positive scores higher than a random negative. Show the count of concordant pairs. (6)
(b) State what AUC = 0.5 means and one situation where AUC is preferable to accuracy. (2)
(c) Explain out loud the difference between an ROC curve and a precision-recall curve, and when to prefer PR. (2)
Question 5 — Regression Metrics & Log-Loss (15 marks)
(a) Given predictions and truth , compute MAE, MSE, and RMSE. Show steps. (6)
(b) Explain why RMSE penalises large errors more than MAE, referencing the functional form. (3)
(c) For a binary probabilistic classifier, write the log-loss formula and compute it for predictions with labels . Use natural log. (4)
(d) Define calibration and describe how a reliability diagram is constructed. (2)
Answer keyMark scheme & solutions
Question 1 (12 marks)
(a) TP=45, FN=15, FP=20, TN=120. Total=200. (1 for reading values)
- Accuracy (1)
- Precision (2)
- Recall (1)
- (1)
(b) is the harmonic mean of and : (1) (2)
(c) A trivial "all negative" classifier scores accuracy while catching zero positives. (1) With imbalance, accuracy is dominated by the majority class and hides poor positive detection. (1) Report (or recall if false negatives are costly) since it balances precision and recall on the minority class. (1)
Question 2 (11 marks)
(a) Assume with , , noise independent of model. (1)
Let . Then: (1)
Expand around : add and subtract : (1)
Cross terms vanish: and , noise independent. (2) (2)
(b) Large gap (4% vs 22%) ⇒ high variance / overfitting. (2) Remedies (any two): more training data, stronger regularisation, reduce model complexity, feature selection, early stopping. (2)
Question 3 (12 marks)
(a) (6: shuffle 1, split logic 3, correct tuples 2)
import numpy as np
def kfold_indices(n, k, seed=0):
rng = np.random.default_rng(seed)
idx = rng.permutation(n)
folds = np.array_split(idx, k)
result = []
for i in range(k):
val = folds[i]
train = np.concatenate([folds[j] for j in range(k) if j != i])
result.append((train, val))
return result(b) Stratified k-fold preserves the class proportions in every fold. (2) Matters for imbalanced data because random splitting can produce folds with few/no minority samples, giving high-variance or invalid metric estimates. (1)
(c) Nested CV = outer loop estimates generalisation error; inner loop does hyperparameter selection on each outer training partition. (2) Avoids the pitfall of optimistic bias / data leakage from tuning hyperparameters on the same data used to report performance. (1)
Question 4 (10 marks)
(a) AUC = fraction of positive–negative pairs where positive scores higher. 4×4 = 16 pairs. (1)
Count discordant/tied pairs. Negatives: {0.7,0.5,0.4,0.2}.
- Pos 0.9 > all 4 → 4 (concordant)
- Pos 0.8 > all 4 → 4
- Pos 0.6 > {0.5,0.4,0.2} = 3; loses to 0.7 → 3
- Pos 0.35 > {0.2} = 1; loses to 0.7,0.5,0.4 → 1
Concordant = 4+4+3+1 = 12. (4) (1)
(b) AUC = 0.5 means the model ranks no better than random. (1) AUC preferable to accuracy when classes are imbalanced or a decision threshold isn't fixed (threshold-independent ranking quality). (1)
(c) ROC plots TPR vs FPR; PR plots precision vs recall. (1) Prefer PR when the positive class is rare, because ROC can look optimistic under heavy imbalance while PR focuses on positive-class performance. (1)
Question 5 (15 marks)
(a) Errors : . (1)
- MAE (2)
- MSE (2)
- RMSE (1)
(b) RMSE squares errors before averaging, so a large error contributes quadratically; a single big miss inflates RMSE far more than MAE (which is linear in error). (3)
(c) . (2)
Terms: ; ; . (1) (1)
(d) Calibration: predicted probabilities match observed frequencies (of predicted-0.7 events, ~70% are positive). (1) Reliability diagram: bin predictions by probability, plot mean predicted prob (x) vs empirical positive fraction (y); diagonal = perfectly calibrated. (1)
[
{"claim":"F1 = 0.72 from P=45/65, R=45/60","code":"P=Rational(45,65); R=Rational(45,60); F1=2*P*R/(P+R); result=(abs(float(F1)-0.72)<1e-9)"},
{"claim":"Accuracy = 0.825","code":"result=(Rational(165,200)==Rational(33,40))"},
{"claim":"AUC = 0.75 from 12 concordant of 16","code":"result=(Rational(12,16)==Rational(3,4))"},
{"claim":"MAE=0.75, MSE=0.875, RMSE=sqrt(0.875)","code":"errs=[0.5,0,-1.5,-1]; mae=sum(abs(e) for e in errs)/4; mse=sum(e**2 for e in errs)/4; import math; rmse=math.sqrt(mse); result=(abs(mae-0.75)<1e-9 and abs(mse-0.875)<1e-9 and abs(rmse-0.9354143)<1e-6)"},
{"claim":"LogLoss approx 0.2284","code":"import math; ll=(-math.log(0.9)-math.log(0.8)-math.log(0.7))/3; result=(abs(ll-0.22839)<1e-4)"}
]