Choosing a model is a systems decision

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 2020 to 2023, scored on 2024 to 2025, timed on one laptop core. Test RMSE is the typical size of a miss in points of volatility; lower is better. The last two columns of Table 1 time a single prediction: the median, and the worst one in every hundred:

Table 1. Five models on next month’s volatility: test error and timings
ModelTest RMSETrainPredict 2,405 rowsOne row, medianOne row, worst 1 in 100
Linear regression12.78\key{12.78}3 ms0.1 ms0.03 ms0.03 ms
Nearest neighbours, kk = 10012.271 ms46.4 ms0.16 ms0.27 ms
One tree, 200 rows per leaf12.407 ms0.1 ms0.03 ms0.03 ms
Random forest, 300 trees12.332,039 ms28.2 ms5.89\risk{5.89} ms6.06 ms
Gradient boosting, 200 trees12.42941 ms1.5 ms0.09 ms0.10 ms
Predict the training mean17.69

{"panels":[{"x0":53.72,"x1":565.2,"y0":116.14,"y1":277.12,"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":366.88,"y1":527.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":617.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)","kind":"line","x":[0.0,1.0],"y":[17.6934,17.6934],"sx":[105.07,207.27],"sy":[-141.19,-141.19]},{"panel":2,"label":"highest next-month volatility ever seen in training: 138","color":"var(--chart-grey, #a3a19c)","kind":"line","x":[0.0,1.0],"y":[138.144,138.144],"sx":[105.07,207.27],"sy":[659.77,659.77]}]} 2026-10-31T01:55:05.718710 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ linear k-NN one tree forest boosting Model 6 7 8 9 10 Test RMSE (volatility points) 12.78 12.27 12.40 12.33 12.42 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 25 50 75 100 125 150 175 Predicted next-month volatility (% a year) 178 61 53 56 58 3. Extrapolation: what each model says about a month wilder than any it has seen highest next-month volatility ever seen in training: 138
Figure 1. Test error, timings and the out-of-range answer for five models

Read it column by column.

Accuracy. The spread is half a volatility point (the first panel of Figure 1), and the baseline is five points behind the worst model. Nearest neighbours wins the column by a hair over the forest, the tree and boosting sit within 0.1 of it, and the line is last. The signal is weak, and the ordering is not the one the models’ reputations predict; on the stress-month classification in the forests post it was different again. The accuracy column has to be measured, not assumed. The line is highlighted, not the winner, because the rest of the table is the decision.

Training time. The forest took 2 seconds, about 700 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 30 microseconds; the forest in 5.9 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 looks safe, but if the worst one in a hundred takes 40 ms, then at least one order in a hundred misses the deadline.

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, 19 leaves for the tree, 36,397 leaves for the forest.

Interpretability. The linear model’s seven coefficients can be read aloud: more 20-day volatility means more next-month volatility. Its coefficient is 678 per unit because the feature is measured as a daily standard deviation: a rise of 0.01 (about 16 points a year) adds about 6.8 points to next month’s forecast. The tree with 19 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 (how the prediction moves as one feature changes, the others held fixed) and importance scores recover some of it, at a cost.

Extrapolation: the question a test set never asks

The test rows all lie within the range of the training rows. 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 (Table 2):

Table 2. Each model’s answer for a month twice as volatile as any in training
ModelPredicted next-month volatility
Linear regression178\key{178}
Nearest neighbours61
One tree53
Random forest56
Gradient boosting58
Next-month volatility that followed the wildest training month52

Every tree-based model and the neighbour model answer with roughly what followed the wildest month they have ever seen, not the 138 that month itself reached, 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 holding up when the market itself changes, which only testing forward in time shows.

The comparison table

Table 3. Linear, neighbours, tree and ensembles compared on eight properties
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\text{trees} \times \text{depth}
Keeps the datanoall of itnono
Needs scalingonly with a penalty (ridge, lasso)yesnono
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 for the cost-per-prediction row of Table 3: 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

Table 4 lists the traps.

Table 4. Traps in choosing a model
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

Four of the models on made-up data, so it runs as pasted (pip install numpy scikit-learn). X_train holds this month’s volatility as a one-column table, y_train next month’s. models is a dictionary of name to model, so one loop fits, times and questions every model the same way.

import time
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LinearRegression
from sklearn.neighbors import KNeighborsRegressor
from sklearn.tree import DecisionTreeRegressor

# Made-up data: this month's volatility predicts next month's, nearly in a straight line
rng = np.random.default_rng(0)
x = rng.uniform(5, 45, 3000)
y = 6 + 0.6 * x + rng.normal(0, 4, 3000)
X_train, y_train = x[:2000].reshape(-1, 1), y[:2000]       # tables of one column
X_test = x[2000:].reshape(-1, 1)

models = {"linear": LinearRegression(),
          "nearest neighbours": KNeighborsRegressor(100),
          "one tree": DecisionTreeRegressor(min_samples_leaf=200),
          "random forest": RandomForestRegressor(300, min_samples_leaf=20, random_state=0)}

extreme = [[90]]                                   # twice the wildest month in training (45)
for name, model in models.items():
    t0 = time.perf_counter(); model.fit(X_train, y_train); fit_ms = (time.perf_counter() - t0) * 1000
    singles = []
    for row in X_test[:200]:                       # time 200 single-row predictions
        t0 = time.perf_counter(); model.predict(row.reshape(1, -1))
        singles.append((time.perf_counter() - t0) * 1000)
    p50, p99 = np.percentile(singles, [50, 99])    # the median, and the worst 1 in 100
    print(f"{name:18} fit {fit_ms:7.1f} ms | one row: median {p50:.3f} ms, worst 1 in 100 "
          f"{p99:.3f} ms | asked about 90: {model.predict(extreme)[0]:.0f}")

It prints one line per model: the fit time, the median and worst-in-a-hundred time for one prediction, and its answer for a month twice as wild as any in training. The forest is by far the slowest to ask (several milliseconds a row, against hundredths for the line). Asked about the extreme month, the line follows its slope to about 60, while the tree, the forest and nearest neighbours stay near 31 to 33, the most extreme values they have seen. Times depend on your machine; the order does not. time.perf_counter() is Python’s most precise clock.

The script behind the real numbers

The tables come from examples/choosing-a-model/choose.py, which runs the five models on the blog’s five tickers. To run it yourself, clone the examples repo: its README has a three-command Docker setup, or run uv run choosing-a-model/choose.py.

Summary

Table 5 is the post in seven lines.

Table 5. The columns of the decision and why each is there
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 changes in the market

References

Tagged machine-learning, finance, volatility, ensembles← Home