Skip to main content

Guide · why and how to select a model

How to choose a model

No model wins everywhere. Use this method and the runnable comparison to decide for your own data.

The full story · about 16 minutes

How to choose a model

1 · Start here

Start here

There is no best model, only a best model for your table, your goal and your constraints. The reliable method is not to guess: it is a short sequence of questions, a cheap baseline, and a fair comparison. This page gives the sequence, then runs the comparison for you.

  1. Name the target. A number means regression, a category means classification, and a ranking or probability means a classifier that outputs probabilities.
  2. Write down what a mistake costs and whether anyone needs the reason for each prediction. These two facts remove half the candidates.
  3. Always fit a baseline first: the average (regression) or the base rate (classification).
  4. Fit the simple, explainable model (linear or logistic regression) and keep its score as the bar to beat.
  5. Try one flexible model (a forest or gradient boosting) and one local model (kNN or an RBF SVM) on the same folds.
  6. Pick the simplest model whose score is within the fold-to-fold spread of the best. Test it once on held-out rows.

2 · The comparison table

The comparison table

Every model in this course, side by side
ModelBest atWeak atNeeds scaling?Explainable?Playbook
Linear regressionNumbers with straight effects; a first baselineCurves, interactionsHelpsYes, coefficientsLinear Regression
Polynomial regressionOne feature with a smooth bendMany features; extrapolationYesFairlyPolynomial
Ridge / Lasso / Elastic netMany or correlated featuresCurvesYesYesRidge, Lasso & Elastic Net
Logistic regressionYes/no with probabilities and odds ratiosCurved boundariesYesYesLogistic
k-nearest neighboursSmall data where similarity is clearMany irrelevant columns; big dataYesNeighbours onlykNN
Naive BayesText and tiny data; a quick baselineHonest probabilities; correlated featuresNoYesNaive Bayes
Support vector machineSmall data with a curved boundaryBig data; probabilitiesYesLittleSVM
Decision treeReadable rules, thresholdsStability, smooth effectsNoYes, if shallowDecision Trees
Random forestStrong default on tablesExtrapolation; readabilityNoImportances onlyRandom Forests
Gradient boostingHighest accuracy on tablesNeeds tuning; readabilityNoImportances onlyGradient Boosting
Neural network (MLP)Smooth patterns; images, text and audio at scaleSmall tabular data; needs scalingYesLittleMultilayer Perceptrons

Use the right-hand column to open the matching playbook: each has the from-scratch build, the scikit-learn pipeline, the tuning advice and the failure modes.

3 · A decision flow

A decision flow

If this is true, start with that
Your situationTry firstThen try
Number target, need to explain itLinear regressionRidge or Lasso if many features
Number target, one feature with a bendPolynomial (degree 2 or 3)A forest or boosting
Yes/no target, need odds and probabilitiesLogistic regressionBoosting
Yes/no target, text countsNaive BayesLogistic on word counts
Small table, curved boundaryRBF SVMBoosting
Large table with mixed typesGradient boostingA random forest as a cross-check
Must show rules to a non-technical readerA shallow decision treeLogistic regression
Images, audio, languageA neural networkPre-trained models

4 · Run the comparison

Run the comparison

Here are nine models on one shared dataset: the truth mixes a straight effect, an interaction (two features multiplied) and a threshold. All use the same five folds, so differences are fair. Change the seed or the data and watch the order move.

Nine models, the same five folds
import warnings
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold

warnings.filterwarnings("ignore")
rng = np.random.default_rng(21)
n = 900
X = rng.normal(size=(n, 8))
# the truth mixes a straight effect, an interaction and a threshold, plus noise
score = 1.0 * X[:, 0] + 1.2 * X[:, 1] * X[:, 2] + 1.5 * (X[:, 3] > 0.5) + rng.normal(0, 0.8, n)
y = (score > np.median(score)).astype(int)

models = {
    "majority baseline": DummyClassifier(),
    "logistic regression": make_pipeline(StandardScaler(), LogisticRegression()),
    "k-nearest neighbours": make_pipeline(StandardScaler(), KNeighborsClassifier(15)),
    "naive Bayes": GaussianNB(),
    "RBF support vector machine": make_pipeline(StandardScaler(), SVC()),
    "decision tree (depth 4)": DecisionTreeClassifier(max_depth=4, random_state=0),
    "random forest": RandomForestClassifier(60, min_samples_leaf=3, random_state=0),
    "gradient boosting": GradientBoostingClassifier(n_estimators=80, max_depth=2, random_state=0),
    "neural network (16 units)": make_pipeline(StandardScaler(), MLPClassifier((16,), solver="lbfgs", max_iter=400, random_state=0)),
}
cv = StratifiedKFold(5, shuffle=True, random_state=0)
print(f"{'model':28s} | accuracy | spread across folds")
for name, m in models.items():
    s = cross_val_score(m, X, y, cv=cv)
    print(f"{name:28s} | {s.mean():8.3f} | {s.min():.3f} to {s.max():.3f}")
model                        | accuracy | spread across folds
majority baseline            |    0.500 | 0.500 to 0.500
logistic regression          |    0.726 | 0.700 to 0.761
k-nearest neighbours         |    0.766 | 0.739 to 0.778
naive Bayes                  |    0.727 | 0.706 to 0.744
RBF support vector machine   |    0.798 | 0.778 to 0.828
decision tree (depth 4)      |    0.722 | 0.678 to 0.744
random forest                |    0.754 | 0.739 to 0.778
gradient boosting            |    0.737 | 0.694 to 0.767
neural network (16 units)    |    0.758 | 0.733 to 0.789
Edit and run this code in your browser

Live Python

Nine models, the same five folds

Runs in your browser on your device. Nothing you type is sent anywhere, and nothing here changes your saved progress.

Press Ctrl+Enter (⌘+Enter on Mac) to run. Tab moves focus to the next control, as usual; use spaces to indent.

Nine models on the same 900 rows
0.40.50.60.70.8BaselineLogistickNNNaive BayesSVM (RBF)Tree d4ForestBoostingNeural netModel (0 = baseline … 8 = neural net)Accuracy
See the numbers behind this picture
Nine models on the same 900 rows
Model (0 = baseline … 8 = neural net)Cross-validated accuracy
Baseline0.5
Logistic0.726
kNN0.766
Naive Bayes0.727
SVM (RBF)0.798
Tree d40.722
Forest0.754
Boosting0.737
Neural net0.758

The baseline is 0.500, so every model learned something. The RBF SVM leads at 0.798, but look at the fold spread: most models span about 0.04 to 0.07 from their best to worst fold. Logistic regression, naive Bayes and the tree all sit near 0.72; the forest, network and kNN sit near 0.75 to 0.77. A gap smaller than the spread between folds is not evidence of a better model.

5 · The winner changes with the data

The winner changes with the data

Run the same three models on two kinds of signal and two data sizes. If a model won everywhere, this table would show it.

Signal type and sample size change the ranking
import warnings
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score, StratifiedKFold

warnings.filterwarnings("ignore")
cv = StratifiedKFold(5, shuffle=True, random_state=0)
def make(kind, n, seed):
    r = np.random.default_rng(seed)
    X = r.normal(size=(n, 6))
    if kind == "straight-line signal":
        s = X[:, 0] + 0.8 * X[:, 1] - 0.6 * X[:, 2] + r.normal(0, 1.0, n)
    else:
        s = X[:, 0] * X[:, 1] + 1.5 * (X[:, 2] > 0.3) + r.normal(0, 0.6, n)
    return X, (s > np.median(s)).astype(int)

models = {
    "logistic": make_pipeline(StandardScaler(), LogisticRegression()),
    "RBF SVM": make_pipeline(StandardScaler(), SVC()),
    "boosting": GradientBoostingClassifier(n_estimators=80, max_depth=2, random_state=0),
}
print("data | rows | " + " | ".join(models))
for kind in ["straight-line signal", "interactions and thresholds"]:
    for n in [120, 1200]:
        X, y = make(kind, n, 3)
        row = [cross_val_score(m, X, y, cv=cv).mean() for m in models.values()]
        print(f"{kind} | {n} | " + " | ".join(f"{v:.3f}" for v in row))
data | rows | logistic | RBF SVM | boosting
straight-line signal | 120 | 0.783 | 0.817 | 0.800
straight-line signal | 1200 | 0.798 | 0.786 | 0.793
interactions and thresholds | 120 | 0.733 | 0.808 | 0.733
interactions and thresholds | 1200 | 0.733 | 0.823 | 0.786
Edit and run this code in your browser

Live Python

Signal type and sample size change the ranking

Runs in your browser on your device. Nothing you type is sent anywhere, and nothing here changes your saved progress.

Press Ctrl+Enter (⌘+Enter on Mac) to run. Tab moves focus to the next control, as usual; use spaces to indent.

With a straight-line signal and 1,200 rows, logistic regression is actually the best of the three (0.798, against 0.786 for the SVM and 0.793 for boosting); with only 120 rows it is last, by two to three points. With interactions and thresholds, logistic regression stays at 0.733 whatever the size, while the RBF SVM reaches 0.808 and then 0.823, and boosting climbs from 0.733 to 0.786 as it gets more rows. Flexible models gain from more data and from structure; a linear model plateaus.

6 · Rules of thumb that hold up

Rules of thumb that hold up

  1. Baseline first, linear model second. A flexible model must beat both by more than the fold spread.
  2. Use the same folds and the same metric for every candidate.
  3. Put scaling and any preprocessing inside a pipeline so each fold is fitted on its own training rows.
  4. Choose by cross-validation on training rows; touch the test set once, at the end.
  5. When two models tie, take the simpler and cheaper one.
  6. Re-check when the data changes: a ranking is only valid for the table it was measured on.
Check yourself

Model A scores 0.762 and model B scores 0.756 on cross-validation, and the folds of each vary by about 0.04. Which should you choose?

Open a playbook