Skip to content
  • Choosing & Combining Models · Machine Learning

Choosing & Combining Models

LearnViaNotes @learnvianotes· 4 min read

There is no single best algorithm (the “no free lunch” theorem) — but there are good defaults. Start simple, compare candidates with the same cross-validation, and combine models when it pays.

AlgorithmBoundary / shapeScaling neededInterpretabilityGood forWeak spots
Linear / logistic regressionLinearYes (when regularised)High (coefficients)Baselines, high-dimensional sparse data, probabilitiesNon-linear effects unless engineered
k-NNAny (local)YesMedium (show neighbours)Small, low-dimensional dataSlow predictions, high dimensions
Naive BayesLinear-ishNoMediumText, tiny data, speedCorrelated features, poor probabilities
SVM (RBF)Any (smooth)YesLowSmall/medium data, many featuresLarge n, needs tuning, no native probabilities
Decision treeAxis-aligned stepsNoHigh (rules)Explanations, rulesHigh variance
Random forestAxis-aligned, averagedNoMediumStrong default for tabular dataSize, extrapolation
Gradient boostingAxis-aligned, additiveNoMediumBest accuracy on most tabular problemsTuning, overfits without early stopping
Neural networksAnyYesLowImages, audio, text, very large dataData-hungry, tuning, compute

A practical order of attack

  1. Baseline

    DummyClassifier / DummyRegressor and any existing business rule.

  2. Simple model

    Regularised linear or logistic regression in a pipeline — fast, often surprisingly strong, and a sanity check for leakage.

  3. Tree ensemble

    Random forest, then gradient boosting (HistGradientBoosting / LightGBM / XGBoost) with early stopping.

  4. Unstructured data

    Pretrained deep models (fine-tuning or embeddings) — see Deep Learning and Generative AI.

  5. Pick with constraints

    Accuracy is one criterion; latency, memory, interpretability, calibration and maintenance matter too.

Compare candidates fairly

Python
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

X, y = load_breast_cancer(return_X_y=True)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)      # same folds for every model
candidates = {
    "logistic": make_pipeline(StandardScaler(), LogisticRegression()),
    "knn": make_pipeline(StandardScaler(), KNeighborsClassifier()),
    "svc": make_pipeline(StandardScaler(), SVC()),
    "random forest": RandomForestClassifier(random_state=0),
    "hist gboost": HistGradientBoostingClassifier(random_state=0),
}
for name, model in candidates.items():
    s = cross_val_score(model, X, y, cv=cv, scoring="roc_auc")
    print(f"{name:14} {s.mean():.3f} ± {s.std():.3f}")
Output
logistic       0.995 ± 0.006
knn            0.981 ± 0.009
svc            0.996 ± 0.005
random forest  0.992 ± 0.007
hist gboost    0.994 ± 0.005

On this small, clean dataset the linear model is as good as anything — a reminder to always try the simple option. Differences inside one standard deviation are usually noise (see Hypothesis Tests & Significance for proper comparisons).

Voting and stacking

MethodHowNotes
Hard votingMajority of predicted labelsSimple; ignores confidence
Soft votingAverage predicted probabilitiesUsually better; needs reasonably calibrated models
StackingA meta-model learns how to combine base models' out-of-fold predictionsMost flexible; base models should be diverse
Python
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier, StackingClassifier, VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier

X, y = make_classification(n_samples=600, n_features=20, n_informative=6, random_state=1)
base = [("lr", LogisticRegression()), ("rf", RandomForestClassifier(random_state=0)),
        ("knn", KNeighborsClassifier()), ("nb", GaussianNB())]

for name, est in base:
    print(name, round(cross_val_score(est, X, y, cv=5).mean(), 3))
vote = VotingClassifier(base, voting="soft")
stack = StackingClassifier(base, final_estimator=LogisticRegression(), cv=5)
print("soft vote", round(cross_val_score(vote, X, y, cv=5).mean(), 3))
print("stacking", round(cross_val_score(stack, X, y, cv=5).mean(), 3))
Output
lr 0.95
rf 0.937
knn 0.937
nb 0.915
soft vote 0.95
stacking 0.957

From ML to neural networks

Logistic regression is a one-neuron network: weighted sum → sigmoid → log-loss. A multi-layer perceptron stacks layers of such units with non-linear activations, so it can learn non-linear boundaries and its own features:

Python
from sklearn.datasets import make_moons
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.neural_network import MLPClassifier

X, y = make_moons(n_samples=400, noise=0.2, random_state=0)
print(round(cross_val_score(LogisticRegression(), X, y, cv=5).mean(), 3))    # 0.875 ← one linear boundary
mlp = MLPClassifier(hidden_layer_sizes=(16, 16), max_iter=3000, random_state=0)
print(round(cross_val_score(mlp, X, y, cv=5).mean(), 3))                     # 0.948 ← hidden layers bend the boundary

For anything beyond small experiments, neural networks are built in PyTorch or similar — backpropagation, optimisers, CNNs, RNNs and transformers are covered in Deep Learning.

How would you choose a model for a tabular classification problem?

What is stacking and how do you avoid leakage in it?

What does the no-free-lunch theorem say?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report