Choosing a model is a systems decision

ml basics

On this page
  1. Five models, one table
  2. Extrapolation: the question a test set never asks
  3. Stability
  4. The comparison table
  5. How to decide
  6. Traps
  7. The code
  8. Summary
  9. References

The series has now built a linear model, nearest neighbours, a tree, a forest and a boosted model on the same problem. This post puts them in one table and adds the columns that accuracy leaves out, because those columns decide which model gets deployed.

the chosen model or its predictionerror, or a cost

Five models, one table

Next month’s volatility from this month’s prices, fitted on 2019 to 2021, scored on 2022 to 2023, timed on one laptop core:

ModelTest RMSETrainPredict 2,405 rowsOne row, medianOne row, worst 1 in 100
Linear regression7.50\key{7.50}2 ms0.1 ms0.02 ms0.03 ms
Nearest neighbours, kk = 1007.501 ms45 ms0.16 ms0.44 ms
One tree, 200 rows per leaf8.415 ms0.1 ms0.03 ms0.03 ms
Random forest, 300 trees9.281,503 ms28 ms5.70\risk{5.70} ms5.94 ms
Gradient boosting, 200 trees9.30695 ms1.8 ms0.08 ms0.08 ms
Predict the training mean9.84

{"panels":[{"x0":53.72,"x1":565.2,"y0":29.16,"y1":219.13,"title":"1. Accuracy on the unseen months: the spread between models is one volatility point","xlabel":"Model","ylabel":"Test RMSE (volatility points)","xlog":false},{"x0":53.72,"x1":565.2,"y0":308.89,"y1":498.86,"title":"2. Cost: time to train, and time to answer one question (median and worst 1 in 100)","xlabel":"Model","ylabel":"Milliseconds (log scale)","xlog":false},{"x0":53.72,"x1":565.2,"y0":588.62,"y1":778.6,"title":"3. Extrapolation: what each model says about a month wilder than any it has seen","xlabel":"Model","ylabel":"Predicted next-month volatility (% a year)","xlog":false}],"series":[{"panel":0,"label":"baseline: predict the mean","color":"var(--chart-grey, #a3a19c)","x":[0.0,1.0],"y":[9.83918,9.83918],"sx":[105.07,207.27],"sy":[57.05,57.05]},{"panel":2,"label":"highest next-month volatility ever seen in training: 136","color":"var(--chart-grey, #a3a19c)","x":[0.0,1.0],"y":[136.434,136.434],"sx":[105.07,207.27],"sy":[615.4,615.4]}]} 2026-10-15T23:57:29.133920 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ linear k-NN one tree forest boosting Model 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 Test RMSE (volatility points) 7.50 7.50 8.41 9.28 9.30 1. Accuracy on the unseen months: the spread between models is one volatility point baseline: predict the mean linear k-NN one tree forest boosting Model 1 0 − 1 1 0 0 1 0 1 1 0 2 1 0 3 Milliseconds (log scale) 2. Cost: time to train, and time to answer one question (median and worst 1 in 100) training time (ms) one prediction, median (ms) one prediction, 99th percentile (ms) linear k-NN one tree forest boosting Model 0 20 40 60 80 100 120 140 Predicted next-month volatility (% a year) 151 61 62 63 72 3. Extrapolation: what each model says about a month wilder than any it has seen highest next-month volatility ever seen in training: 136

Read it column by column.

Accuracy. The spread is one volatility point, and the two simplest models win. On this problem the signal is weak and nearly linear, so the models with the least room to fit noise did best. That is not a general law; on the stress-month classification in the forests post the ordering ran the other way. It is a reminder that the accuracy column has to be measured, not assumed from the model’s reputation.

Training time. The forest took 1.5 seconds, 750 times the linear model. That only matters against a budget: a 40-minute fit inside a four-hour overnight window is free; the same fit every time a feature changes is not.

Prediction time, and its tail. The linear model answers a question in 20 microseconds; the forest in 5.7 milliseconds, because every prediction walks 300 trees. Nearest neighbours is cheap to train and the only model whose prediction cost grows with the data, because it scans every stored row. The last column is the one a trading deadline cares about: not the average latency but the worst one in a hundred. A mean of 2 ms inside a 10 ms deadline with a 99th percentile of 40 ms means one order in a hundred misses, and the misses cluster in the busy moments.

What it keeps. Nearest neighbours keeps every training row forever, which is a memory cost and sometimes a privacy or licensing one. The others keep parameters: seven numbers for the linear model, 15 leaves for the tree, 27,193 leaves for the forest.

Interpretability. The linear model’s seven coefficients can be read aloud: more 20-day volatility means more next-month volatility, at 573 points per unit. The tree with 15 leaves can be drawn on a page. The forest cannot, and a model that cannot be explained cannot be audited, which in some settings makes it unusable whatever its accuracy. Partial-dependence curves and importance scores recover some of it, at a cost.

Extrapolation: the question a test set never asks

The test months all lie within the range of the training months. Ask each model about a month twice as volatile as any it has seen, with the recent-volatility features doubled from the wildest training row:

ModelPredicted next-month volatility
Linear regression151\key{151}
Nearest neighbours61
One tree62
Random forest63
Gradient boosting72
Highest next-month volatility ever seen in training68

Every tree-based model and the neighbour model answer with roughly the most extreme value they have ever seen, because a tree’s leaf is a constant and a neighbour’s answer is an average of stored rows. The linear model follows its slope off the edge of the map. Neither is right; the point is that they fail differently, and that a model’s behaviour outside its training range is a design choice, not something a test score reveals. Finance meets the outside of the range on the worst day of the decade.

Stability

A linear model refitted on slightly different data gives nearly the same line. A single tree can flip its root split and everything below it; that instability is why the bagging post exists. A forest is stable again by averaging. Stability across random seeds is cheap to measure and worth reporting next to the score; it is not the same as robustness to a change of regime, which only forward testing shows.

The comparison table

ItemLinearNearest neighboursTreeForest or boosting
Shape it can capturestraight lines, or curves you add by handany local shapethresholds and interactionsthe same, smoother
Training costlownonelowhigh
Cost per predictionppN×pN \times p, grows with the datadepthtrees × depth
Keeps the datanoall of itnono
Needs scalingfor regularisationyesnono
Outside the rangeextrapolates the slopethe nearest stored rowsa constanta constant
Explainablecoefficients“like these past cases”a drawing, if shallowonly with extra tools
Stableyesfairlynoyes

Worked costs: a table of one million rows and twenty features. One nearest-neighbour prediction touches twenty million numbers; one linear prediction touches twenty; a depth-12 tree makes twelve comparisons; a 500-tree forest of them makes six thousand.

How to decide

  1. Write down the constraints before the accuracy: latency budget and its tail, how often the model refits, whether a human must be able to explain a prediction, how much data there is per feature.
  2. Measure the baseline, then the simplest model that fits the constraints. Linear first.
  3. Move up only when the gain is bigger than the noise between seeds and worth its extra cost.
  4. Check the behaviour outside the training range on purpose.
  5. Prefer the model whose failures you understand.

There is no best model for every dataset. There is a best model for this dataset under these constraints, and the constraints are most of the decision.

Traps

TrapTruth
Pick the lowest-error modelWeigh every column
Explainable means reliableIt means auditable
Mean latency within budget means safeThe tail breaks deadlines
More history always helpsOnly if it is still relevant
A more flexible model extrapolates betterIt usually extrapolates a constant
An ensemble fixes a bad inputNothing fixes a bad input

The code

import time
import numpy as np

for name, model in models.items():
    t0 = time.perf_counter(); model.fit(X_train, y_train); fit = time.perf_counter() - t0
    singles = []
    for row in X_test[:200]:
        t0 = time.perf_counter(); model.predict(row[None, :]); singles.append(time.perf_counter() - t0)
    p50, p99 = np.percentile(singles, [50, 99])                   # the median and the tail

extreme = X_train[np.argmax(X_train[:, 4])].copy(); extreme[3:5] *= 2   # off the edge of the map
model.predict(extreme[None, :])

Summary

ColumnWhy it is in the table
Accuracymeasured against the baseline, with its spread across seeds
Training timeagainst the refit budget
Prediction time, median and tailagainst the deadline, and its worst case
Keeps the datamemory, privacy, growth with NN
Explainableaudits and regulators
Outside the rangea design choice the test set cannot see
Stableacross seeds; and separately, across regimes

References