Tree-Based & Instance Methods
Level: 3 (Production — from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Show all working. Use exact fractions/logs where asked. Round decimals to 4 places unless stated.
Question 1 — Impurity from scratch (12 marks)
A node contains 10 samples: 6 of class A, 4 of class B. A candidate split on feature produces:
- Left child: 4 samples (4 A, 0 B)
- Right child: 6 samples (2 A, 4 B)
(a) Compute the entropy of the parent node (bits, base 2). (3) (b) Compute the weighted child entropy and the information gain of the split. (4) (c) Compute the Gini impurity of the parent and the weighted Gini after the split. (3) (d) In one sentence, explain why Gini and entropy usually agree on the best split but Gini is cheaper to compute. (2)
Question 2 — AdaBoost weight update by hand (12 marks)
You run AdaBoost (SAMME, binary, labels ) with uniform initial weights on samples. The first weak learner misclassifies 1 of the 5 samples.
(a) Compute the weighted error and the classifier weight . (4) (b) Give the unnormalized new weight for a correctly classified point and for the misclassified point (start weight each). (4) (c) Compute the normalization constant and the normalized weights. Verify they sum to 1. (3) (d) State what happens to if , and why. (1)
Question 3 — Code from memory: KNN classifier (12 marks)
Write a Python function knn_predict(X_train, y_train, x_query, k, metric='euclidean') from memory (numpy only) that:
- supports
'euclidean'and'manhattan'distances, - returns the majority-vote label among the nearest neighbours.
(8) for correct working code. (2) State the time complexity of one query for training points in dimensions. (2) Explain out loud (2–3 sentences) why feature scaling matters for Euclidean KNN but is irrelevant for a single decision tree split.
Question 4 — Out-of-bag error derivation (10 marks)
(a) For a bootstrap sample of size drawn with replacement from points, derive the probability that a specific point is never selected, and give its limit as . (5) (b) Hence state the expected fraction of points that are OOB for each tree. (2) (c) Explain how OOB error is aggregated across the forest and why it approximates cross-validation error without a separate validation set. (3)
Question 5 — Gradient Boosting intuition & derivation (8 marks)
For a regression GBM with squared-error loss :
(a) Derive the negative gradient (pseudo-residual) and interpret it. (4) (b) Explain why fitting the next tree to these residuals implements gradient descent in function space. (2) (c) Name two regularization knobs in XGBoost/GBM that combat overfitting and say what each controls. (2)
Question 6 — Explain out loud: Random Forest vs single tree (6 marks)
In 4–6 sentences, explain: (a) the two randomness sources in a random forest and why feature subsampling at each split decorrelates trees; (3) (b) why averaging decorrelated trees reduces variance but not bias, referencing the variance of an average of correlated variables. (3)
Answer keyMark scheme & solutions
Question 1 (12)
(a) Parent . (1 formula, 1 substitution, 1 answer)
(b) Left child pure → . Right child : Weighted: . Information gain bits. (1 , 1 , 1 weighted, 1 gain)
(c) Parent Gini . Left Gini . Right Gini . Weighted . (1 parent, 1 right, 1 weighted)
(d) Both measure node impurity and are monotone-similar convex functions of class proportions, so they rank splits almost identically; Gini avoids the logarithm, so it's computationally cheaper. (2)
Question 2 (12)
(a) Uniform weights ; 1 of 5 wrong → . (2 error, 2 alpha)
(b) Update : correct→, wrong→. Correct: . Wrong: . (2 correct, 2 wrong)
(c) There are 4 correct (0.1 each) and 1 wrong (0.4): . Normalized correct ; wrong . Sum . ✓ (1 Z, 1 normalized, 1 sum check)
(d) As , : a learner no better than random gets zero vote. (1)
Question 3 (12)
import numpy as np
from collections import Counter
def knn_predict(X_train, y_train, x_query, k, metric='euclidean'):
X_train = np.asarray(X_train); x_query = np.asarray(x_query)
if metric == 'euclidean':
dists = np.sqrt(np.sum((X_train - x_query)**2, axis=1))
elif metric == 'manhattan':
dists = np.sum(np.abs(X_train - x_query), axis=1)
else:
raise ValueError("unknown metric")
idx = np.argsort(dists)[:k]
labels = [y_train[i] for i in idx]
return Counter(labels).most_common(1)[0][0]Marks: distances (3), argsort/top-k (2), majority vote (2), metric branch (1) = 8.
Complexity: to compute distances + (or with partial selection) for the sort → per query. (2)
Scaling: Euclidean distance sums squared differences across features, so a large-scale feature dominates the distance and biases neighbours; scaling equalizes contributions. A decision tree splits one feature at a time by threshold and is invariant to any monotone rescaling, so scaling has no effect on tree splits. (2)
Question 4 (10)
(a) Each of draws independently picks the specific point with prob , misses with prob . Never selected over draws: (2 setup, 2 expression, 1 limit)
(b) Expected OOB fraction of points are OOB for a given tree. (2)
(c) Each training point is predicted by only those trees for which it was OOB; these OOB predictions are aggregated (vote/average), and OOB error is the mean loss over all points. Since each point's prediction uses only trees that never saw it, it acts as held-out data, so OOB error is a nearly unbiased estimate of generalization error without a separate CV split. (3)
Question 5 (8)
(a) , , so i.e. the ordinary residual — the error the current ensemble still makes on point . (2 derivative, 2 interpretation)
(b) GBM treats as the variable being optimized; the pseudo-residuals are the negative functional gradient of the loss, and fitting a tree to them then adding tree takes a descent step in the space of functions. (2)
(c) Any two, e.g.: learning rate/shrinkage (scales each tree's contribution), max_depth / min_child_weight (limits per-tree complexity), subsample / colsample (stochastic sampling), L2/L1 regularization on leaf weights. (1 each)
Question 6 (6)
(a) Randomness sources: (1) bootstrap resampling of rows for each tree, (2) random feature subset considered at each split. Feature subsampling prevents every tree from repeatedly choosing the same dominant feature at the top splits, so trees become structurally different and their prediction errors are less correlated. (3)
(b) For trees each with variance and pairwise correlation , the average has variance . Lowering (via decorrelation) drives variance down as grows, but averaging leaves the individual trees' expected value unchanged, so bias is unaffected. (3)
[
{"claim":"Q1 parent entropy = 0.9710 bits","code":"import sympy as sp\nH=-0.6*sp.log(0.6,2)-0.4*sp.log(0.4,2)\nresult = round(float(H),4)==0.9710"},
{"claim":"Q1 information gain = 0.4200 bits","code":"import sympy as sp\nHp=-0.6*sp.log(0.6,2)-0.4*sp.log(0.4,2)\nHr=-(sp.Rational(1,3))*sp.log(sp.Rational(1,3),2)-(sp.Rational(2,3))*sp.log(sp.Rational(2,3),2)\ngain=Hp-0.6*Hr\nresult = round(float(gain),4)==0.42"},
{"claim":"Q1 weighted Gini = 0.2667","code":"g=0.6*(1-((sp.Rational(1,3))**2+(sp.Rational(2,3))**2))\nresult = round(float(g),4)==0.2667"},
{"claim":"Q2 alpha1 = 0.6931 and Z1=0.8","code":"a=sp.Rational(1,2)*sp.log((1-sp.Rational(1,5))/sp.Rational(1,5))\nZ=4*0.2*sp.exp(-a)+0.2*sp.exp(a)\nresult = (round(float(a),4)==0.6931) and (round(float(Z),4)==0.8)"},
{"claim":"Q4 OOB limit prob = 1/e = 0.3679","code":"result = round(float(sp.exp(-1)),4)==0.3679"}
]