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.
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.
- Name the target. A number means regression, a category means classification, and a ranking or probability means a classifier that outputs probabilities.
- Write down what a mistake costs and whether anyone needs the reason for each prediction. These two facts remove half the candidates.
- Always fit a baseline first: the average (regression) or the base rate (classification).
- Fit the simple, explainable model (linear or logistic regression) and keep its score as the bar to beat.
- Try one flexible model (a forest or gradient boosting) and one local model (kNN or an RBF SVM) on the same folds.
- 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
| Model | Best at | Weak at | Needs scaling? | Explainable? | Playbook |
|---|---|---|---|---|---|
| Linear regression | Numbers with straight effects; a first baseline | Curves, interactions | Helps | Yes, coefficients | Linear Regression |
| Polynomial regression | One feature with a smooth bend | Many features; extrapolation | Yes | Fairly | Polynomial |
| Ridge / Lasso / Elastic net | Many or correlated features | Curves | Yes | Yes | Ridge, Lasso & Elastic Net |
| Logistic regression | Yes/no with probabilities and odds ratios | Curved boundaries | Yes | Yes | Logistic |
| k-nearest neighbours | Small data where similarity is clear | Many irrelevant columns; big data | Yes | Neighbours only | kNN |
| Naive Bayes | Text and tiny data; a quick baseline | Honest probabilities; correlated features | No | Yes | Naive Bayes |
| Support vector machine | Small data with a curved boundary | Big data; probabilities | Yes | Little | SVM |
| Decision tree | Readable rules, thresholds | Stability, smooth effects | No | Yes, if shallow | Decision Trees |
| Random forest | Strong default on tables | Extrapolation; readability | No | Importances only | Random Forests |
| Gradient boosting | Highest accuracy on tables | Needs tuning; readability | No | Importances only | Gradient Boosting |
| Neural network (MLP) | Smooth patterns; images, text and audio at scale | Small tabular data; needs scaling | Yes | Little | Multilayer 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
| Your situation | Try first | Then try |
|---|---|---|
| Number target, need to explain it | Linear regression | Ridge or Lasso if many features |
| Number target, one feature with a bend | Polynomial (degree 2 or 3) | A forest or boosting |
| Yes/no target, need odds and probabilities | Logistic regression | Boosting |
| Yes/no target, text counts | Naive Bayes | Logistic on word counts |
| Small table, curved boundary | RBF SVM | Boosting |
| Large table with mixed types | Gradient boosting | A random forest as a cross-check |
| Must show rules to a non-technical reader | A shallow decision tree | Logistic regression |
| Images, audio, language | A neural network | Pre-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.
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.789Edit 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.
See the numbers behind this picture
| Model (0 = baseline … 8 = neural net) | Cross-validated accuracy |
|---|---|
| Baseline | 0.5 |
| Logistic | 0.726 |
| kNN | 0.766 |
| Naive Bayes | 0.727 |
| SVM (RBF) | 0.798 |
| Tree d4 | 0.722 |
| Forest | 0.754 |
| Boosting | 0.737 |
| Neural net | 0.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.
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.786Edit 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
- Baseline first, linear model second. A flexible model must beat both by more than the fold spread.
- Use the same folds and the same metric for every candidate.
- Put scaling and any preprocessing inside a pipeline so each fold is fitted on its own training rows.
- Choose by cross-validation on training rows; touch the test set once, at the end.
- When two models tie, take the simpler and cheaper one.
- Re-check when the data changes: a ranking is only valid for the table it was measured on.