Skip to content

Stacking and Voting Classifiers

What you’ll learn

  • hard against soft voting, and a case where they disagree — computed by hand
  • why soft voting requires calibrated probabilities to be worth using
  • stacking: letting a second-level model learn the weights instead of fixing them
  • how the blender is trained on out-of-fold predictions, and why anything else leaks
  • a measured comparison where none of the three combinations beats the best member
  • when to reach for each, and when to ship the single model

Intuition

Bagging and boosting both build many copies of one kind of model. Voting and stacking do something different: they take models you already have — a logistic regression, a forest, a boosted tree, a KNN — and combine them.

The diversity is free. Those families make genuinely different assumptions and fail in genuinely different ways, which is exactly the condition Condorcet’s theorem requires.

The two approaches differ in one respect: who decides how much each member counts.

  • Voting — you decide, in advance. Equal weights, or weights you supply.
  • Stacking — a model learns them from data.
diagram Diagram mermaid

Hard against soft voting

Hard voting counts labels. Soft voting averages probabilities and then thresholds. They are not the same rule, and the difference matters most when confidence is unevenly distributed.

hard: y^=arg maxkj=1J1 ⁣[y^j=k]soft: y^=arg maxk1Jj=1Jp^jk\text{hard: } \hat{y} = \operatorname*{arg\,max}_{k}\sum_{j=1}^{J}\mathbb{1}\!\left[\hat{y}_j = k\right] \qquad \text{soft: } \hat{y} = \operatorname*{arg\,max}_{k}\frac{1}{J}\sum_{j=1}^{J}\hat{p}_{jk}

Worked example by hand

Three classifiers on one instance, predicting P(class 1)P(\text{class } 1):

ClassifierP(class 1)P(\text{class }1)Hard vote
logistic regression0.45class 0
decision tree0.48class 0
gradient boosting0.93class 1

Hard voting. Two votes for class 0, one for class 1 → predict class 0.

Soft voting. Average the probabilities:

pˉ=0.45+0.48+0.933=1.863=0.62    predict class 1\bar{p} = \frac{0.45 + 0.48 + 0.93}{3} = \frac{1.86}{3} = 0.62 \;\Rightarrow\; \textbf{predict class 1}

Opposite answers from identical inputs. Hard voting heard two mild “probably not”s outvote one emphatic “almost certainly”. Soft voting let the confident model carry the weight its confidence implies.

Which is right? Only if the probabilities are trustworthy. If the boosted model is systematically overconfident — and untuned boosted trees frequently are — then 0.93 is not really 0.93, and soft voting has been misled by a number that does not mean what it says.

Hard votingSoft voting
UsesPredicted labelsPredicted probabilities
Needs predict_probapredict_probaNoYes
Sensitive to calibrationNoVery
Confident membersCount the same as unsure onesCount more
Usually better whenMembers are poorly calibratedMembers are well calibrated

Stacking

Voting fixes the combination rule. Stacking learns it: fit the members, collect their predictions, and train a second model — the blender, or meta-learner — to map those predictions to the final answer.

The blender can learn things no fixed rule can express: that the forest is reliable except when the KNN strongly disagrees, or that logistic regression should simply be ignored.

Training the blender without cheating

Here is the trap. If you train the members on the training set and then ask them to predict that same training set, their predictions will be far too good — a fitted random forest predicts its own training rows almost perfectly. The blender learns “always trust the forest”, and then meets a forest that is much worse on new data.

The fix is out-of-fold prediction:

diagram Diagram mermaid

Every prediction the blender trains on comes from a model that did not see that row. Its view of each member’s reliability therefore matches what it will encounter in production. StackingClassifierStackingClassifier does this internally with its cvcv parameter; it is the entire reason the class exists rather than a five-line loop.

Measured

figureFour members, three combinations, identical foldsmatplotlib
Bar chart of 5-fold CV accuracy for four members and three combination methods, with a dashed red line marking the best individual member at 0.9020. Hard voting sits below the line; soft voting and stacking sit on it.Bar chart of 5-fold CV accuracy for four members and three combination methods, with a dashed red line marking the best individual member at 0.9020. Hard voting sits below the line; soft voting and stacking sit on it.
KNN alone reaches 0.9020. Hard voting drops to 0.8860; soft voting and stacking both reach 0.9000. Not one combination beat the best member.
Model5-fold CV accuracy
logistic regression0.8560
decision tree (depth 4)0.9000
KNN (k=15)0.9020
Gaussian naive Bayes0.8580
hard vote0.8860
soft vote0.9000
stacking (logistic blender)0.9000

Reading the plot

  1. Hard voting was the worst combination, 1.6 points below its best member. Two weak members (0.856 and 0.858) get the same vote as KNN at 0.902 and can outvote it.
  2. Soft voting recovered to 0.9000 by weighting with confidence, which is what soft voting is for.
  3. Stacking matched soft voting but did not beat it. In principle the blender could learn to discard the weak members entirely; with 500 rows and 4 features of meta-data it does not have enough signal to.
  4. Nothing beat KNN alone. Report this honestly and ship the single model.

The condition for stacking to pay is that the members be both reasonably strong and genuinely complementary. Here they were complementary but two of them were markedly weaker, and that turned out to be the binding constraint.

See it move

Five classifiers report a probability for each incoming instance. The two tallies show what hard and soft voting each conclude — and how often they disagree.

sketch Hard and soft voting, side by side p5.js
Five members report probabilities for one instance. The bars show each member's confidence; the two verdicts below show what counting labels and averaging probabilities each decide.

In code

voting.py
from sklearn.datasets import make_moons
from sklearn.ensemble import 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
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
 
X, y = make_moons(n_samples=500, noise=0.3, random_state=1)
 
members = [
    ("logistic", make_pipeline(StandardScaler(), LogisticRegression())),
    ("tree", DecisionTreeClassifier(max_depth=4, random_state=0)),
    ("knn", make_pipeline(StandardScaler(), KNeighborsClassifier(15))),
    ("nb", GaussianNB()),
]
 
for name, model in members:
    print(f"{name:<10} {cross_val_score(model, X, y, cv=5).mean():.4f}")
 
hard = VotingClassifier(members, voting="hard")
soft = VotingClassifier(members, voting="soft")
print(f"{'hard':<10} {cross_val_score(hard, X, y, cv=5).mean():.4f}")   # 0.8860
print(f"{'soft':<10} {cross_val_score(soft, X, y, cv=5).mean():.4f}")   # 0.9000
 
# Unequal weights, when you know one member is better
weighted = VotingClassifier(members, voting="soft", weights=[1, 2, 3, 1])
voting.py
from sklearn.datasets import make_moons
from sklearn.ensemble import 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
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
 
X, y = make_moons(n_samples=500, noise=0.3, random_state=1)
 
members = [
    ("logistic", make_pipeline(StandardScaler(), LogisticRegression())),
    ("tree", DecisionTreeClassifier(max_depth=4, random_state=0)),
    ("knn", make_pipeline(StandardScaler(), KNeighborsClassifier(15))),
    ("nb", GaussianNB()),
]
 
for name, model in members:
    print(f"{name:<10} {cross_val_score(model, X, y, cv=5).mean():.4f}")
 
hard = VotingClassifier(members, voting="hard")
soft = VotingClassifier(members, voting="soft")
print(f"{'hard':<10} {cross_val_score(hard, X, y, cv=5).mean():.4f}")   # 0.8860
print(f"{'soft':<10} {cross_val_score(soft, X, y, cv=5).mean():.4f}")   # 0.9000
 
# Unequal weights, when you know one member is better
weighted = VotingClassifier(members, voting="soft", weights=[1, 2, 3, 1])
stacking.py
from sklearn.ensemble import StackingClassifier
 
stack = StackingClassifier(
    estimators=members,
    final_estimator=LogisticRegression(),   # keep the blender simple
    cv=5,                                   # out-of-fold predictions; do not skip
    passthrough=False,                      # True also gives the blender the raw features
)
 
print(f"stacking {cross_val_score(stack, X, y, cv=5).mean():.4f}")      # 0.9000
 
stack.fit(X, y)
for name, coef in zip([n for n, _ in members], stack.final_estimator_.coef_[0]):
    print(f"  blender weight for {name:<10} {coef:+.4f}")
stacking.py
from sklearn.ensemble import StackingClassifier
 
stack = StackingClassifier(
    estimators=members,
    final_estimator=LogisticRegression(),   # keep the blender simple
    cv=5,                                   # out-of-fold predictions; do not skip
    passthrough=False,                      # True also gives the blender the raw features
)
 
print(f"stacking {cross_val_score(stack, X, y, cv=5).mean():.4f}")      # 0.9000
 
stack.fit(X, y)
for name, coef in zip([n for n, _ in members], stack.final_estimator_.coef_[0]):
    print(f"  blender weight for {name:<10} {coef:+.4f}")

Reading the blender’s coefficients is the payoff over voting: it tells you, in numbers, how much the meta-model decided to trust each member.

algorithmVoting and StackingSupervised · Classification and Regression · Heterogeneous ensemble

APIsklearn.ensemble.VotingClassifier / StackingClassifier

Assumes

  • Members make genuinely different errors — different families, not different seeds
  • For soft voting, member probabilities are calibrated
  • For stacking, enough training data for the blender to learn from J numbers per row

Cost

train
sum of member costs; stacking multiplies by k folds
predict
sum of member costs
memory
sum of member sizes

J = number of members, k = folds used for out-of-fold predictions

Hyperparameters that matter

  • votingdefault hard'soft' averages probabilities and usually wins when members are calibrated. Check calibration first.
  • weightsdefault NoneManual per-member weights for voting. A cheap alternative to stacking when you already know the ranking.
  • final_estimatordefault LogisticRegressionThe blender. Keep it simple; it only sees J features.
  • cvdefault 5Folds for out-of-fold predictions. Lower is faster and leakier; do not set it to 'prefit' unless you know exactly why.
  • passthroughdefault FalseGive the blender the original features as well. Occasionally helps, frequently overfits.

Reach for it when

  • You already have several tuned models of different families
  • The members' errors are genuinely uncorrelated
  • The last fraction of a point matters — this is standard practice in competitions
  • Inference cost is not a constraint

Look elsewhere when

  • Members are all the same family — bagging or boosting is the right tool
  • Prediction latency matters; every member runs on every request
  • One member clearly dominates the rest, as measured above
  • The training set is small; the blender needs data too

Pitfalls

Compare

BaggingBoostingVotingStacking
MembersOne family, resampledOne family, sequentialDifferent familiesDifferent families
CombinationEqual averageWeighted by α\alphaFixed ruleLearned
Diversity fromData resamplingReweightingAlgorithm choiceAlgorithm choice
TrainingParallelSequentialParallelParallel, then the blender
Tuning burdenLowHighLowestModerate
Typical useDefault tabular baselineStrongest single modelQuick easy winCompetition final step

Voting is the cheapest ensemble in this phase: if you already have three tuned models, it is one line and no new training.

quizCheck yourself
  1. Three classifiers predict P(class 1) of 0.45, 0.48 and 0.93. What do hard and soft voting decide?

    Show answer

    B — Hard predicts class 0 (two votes to one); soft predicts class 1 (mean 0.62) — Hard voting counts two mild negatives against one positive. Soft voting averages to 0.62 and lets the confident member carry the weight its confidence implies.

  2. Why must a stacking blender be trained on out-of-fold predictions?

    Show answer

    B — Because in-sample predictions from fitted members are unrealistically good, so the blender would learn to trust members far more than it should — A fitted forest is near-perfect on its own training rows. The blender must see each member's performance on data that member did not train on, which is exactly what the cv parameter arranges.

  3. Hard voting scored 0.8860 while its best member scored 0.9020. What went wrong?

    Show answer

    B — Two of the four members are markedly weaker, and equal votes let them outvote the strongest — Hard voting gives every member an identical say. Members at 0.856 and 0.858 can jointly outvote one at 0.902. Soft voting recovered 1.4 of the lost points.

  4. You are soft voting over logistic regression, a calibrated SVM and Gaussian naive Bayes. What should you check?

    Show answer

    B — Naive Bayes's calibration — it routinely emits 0.9999 without meaning it, which would dominate the average — Soft voting averages probabilities as if they were comparable. An overconfident member effectively gets extra votes. Wrap it in CalibratedClassifierCV or use hard voting.

🧪 Try It Yourself

Exercise 1 – Hard and soft disagree

Exercise 2 – Build a voting ensemble

Exercise 3 – Read the blender’s weights

Exercise 4 – Why out-of-fold matters

Exercise 5 – Did the ensemble actually earn its place?

Recap

  • Hard voting counts labels; soft voting averages probabilities. On 0.45, 0.48 and 0.93 they give opposite answers.
  • Soft voting is only as good as the members’ calibration — an overconfident member effectively gets extra votes.
  • Stacking learns the combination weights instead of fixing them, and its coefficients tell you how much each member was trusted.
  • The blender must be trained on out-of-fold predictions. A fitted forest scores 1.0 on its own training rows and 0.88 on unseen ones.
  • Keep the blender simple; it sees only one number per member.
  • Measured here: hard 0.8860, soft 0.9000, stacking 0.9000, best member 0.9020. No combination won. Always run that comparison.

Exercise 6 – Hard voting against soft voting on the same probabilities

Next

Phase 5 ends here. Continue to Phase 6 - Unsupervised Learning — the same rigour applied to problems with no labels at all, where even “is this good?” needs a new answer.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did