Quant research is mostly prediction: tomorrow’s return, this order’s fill probability, a house’s price. This part follows The Elements of Statistical Learning, the book researchers reach for first. It opens with the two simplest predictors there are, because between them they show every trade-off that comes later.
The linear model
Given inputs , predict the output with a weighted sum plus an intercept:
Fit it by least squares: choose to minimise the residual sum of squares,
RSS is not an arbitrary choice. The best possible predictor of under squared error is the conditional expectation function , and least squares gives the best linear approximation to it. For classification, code the two classes as 0 and 1, fit, and predict class 1 when : the decision boundary is a straight line.
Nearest neighbours
Instead of fitting a formula, keep the training data and average the closest examples:
There is nothing to fit except the choice of . With the decision regions form a Voronoi tessellation: every training point owns the patch of space closest to it, so the boundary is jagged and full of islands. Larger smooths it. Although is a single number, the model behaves as if it had about parameters, one per neighbourhood, so small means a very flexible model.
Kernel methods refine this by weighting neighbours by distance with a smooth kernel instead of counting the nearest equally (we’ll use one to price opera seats in lesson 15).
Rigid versus flexible
| Linear model (least squares) | Nearest neighbours | |
|---|---|---|
| Bias and variance | high bias, low variance | low bias, high variance |
| Assumptions | the relationship is (close to) linear | almost none |
| Works best when | each class is roughly one Gaussian blob | each class is a mixture of many blobs |
| Dimensions | copes well with many inputs | needs low dimensions and lots of data |
The next lesson makes “bias” and “variance” precise and explains the last row: the curse of dimensionality.
In code
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.neighbors import KNeighborsClassifier
rng = np.random.default_rng(6)
def blobs(n, centre):
means = centre + rng.standard_normal((10, 2))
return means[rng.integers(0, 10, n)] + rng.standard_normal((n, 2)) * np.sqrt(0.2)
X = np.vstack([blobs(100, [1, 0]), blobs(100, [0, 1])]); y = np.r_[np.zeros(100), np.ones(100)]
Xt = np.vstack([blobs(500, [1, 0]), blobs(500, [0, 1])]); yt = np.r_[np.zeros(500), np.ones(500)]
lin = LinearRegression().fit(X, y)
print("linear test error", np.mean((lin.predict(Xt) > 0.5) != yt))
for k in (1, 15, 101):
knn = KNeighborsClassifier(k).fit(X, y)
print(k, "train", 1 - knn.score(X, y), "test", 1 - knn.score(Xt, yt))
Check yourself
Which model is guaranteed to have zero training error (assuming no two identical inputs with different labels)?