Bagging - Random Forest Regressor/Classifier
What you’ll learn
Section titled “What you’ll learn”- the bootstrap, and the derivation of the 63.2% / 36.8% split
- out-of-bag scoring — a validation estimate that costs nothing
- bagging against pasting, and why the resampling that seems wasteful wins
- what a random forest adds beyond bagging, and why it lowers correlation
- Extra-Trees, which makes each tree worse and the ensemble better
- why
feature_importances_favours high-cardinality columns, measured
Intuition
Section titled “Intuition”The Power of Ensembles established that averaging cancels variance only when the members disagree. Bagging manufactures that disagreement mechanically: train each model on a different random sample of the same data.
A deep decision tree is the ideal member. It has low bias, catastrophic variance, and — because the greedy split search amplifies small differences — two trees grown on slightly different samples can look nothing alike. High variance and low correlation are exactly what averaging needs.
flowchart TD D["Training set, n rows"] --> B1["bootstrap 1
n rows, with replacement"] D --> B2["bootstrap 2"] D --> B3["bootstrap ..."] B1 --> T1["deep tree 1"] B2 --> T2["deep tree 2"] B3 --> T3["deep tree m"] T1 --> A["Average (regression)
or vote (classification)"] T2 --> A T3 --> A
The math: what a bootstrap sample contains
Section titled “The math: what a bootstrap sample contains”Draw rows with replacement from a set of . For any particular row, the chance of not being picked on one draw is , and the draws are independent:
As grows this converges to a familiar constant:
| fraction of distinct rows | |
|---|---|
| 5 | 0.6723 |
| 20 | 0.6415 |
| 100 | 0.6340 |
| 1,000 | 0.6323 |
| 0.6321 |
Each tree therefore misses about a third of the data — and those rows are a ready-made validation set for that tree, at no extra cost.
See it move
Section titled “See it move”Twenty rows, drawn with replacement twenty times. Watch which rows get picked more than once and which are left out entirely — the leftovers are that tree’s out-of-bag set.
Out-of-bag scoring
Section titled “Out-of-bag scoring”For each training row, average the predictions of only those trees that never saw it. That is a genuine held-out estimate, computed during training, using data you already paid for.
from sklearn.datasets import make_moons
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
X, y = make_moons(n_samples=800, noise=0.32, random_state=4)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0)
for n in (10, 100, 400):
forest = RandomForestClassifier(n_estimators=n, oob_score=True,
random_state=0, n_jobs=-1).fit(X_tr, y_tr)
print(f"n={n:<4} oob {forest.oob_score_:.4f} held-out test {forest.score(X_te, y_te):.4f}")
# n=10 oob 0.8482 held-out test 0.9250
# n=100 oob 0.8857 held-out test 0.9333
# n=400 oob 0.8946 held-out test 0.9292Two honest observations:
- OOB is systematically pessimistic, by 3 to 4 points here. Each row is scored by only the ~37% of trees that excluded it, so the effective ensemble is a third the size.
- The pessimism shrinks as trees are added, from 7.7 points at to 3.5 at . At 10 trees only about 4 vote on each row, which is not an ensemble.
Use OOB for quick iteration on large data where cross-validation is expensive. Use cross-validation for the number you report.
Bagging against pasting
Section titled “Bagging against pasting”Bagging samples with replacement; pasting samples without. Pasting sounds cleaner — no duplicated rows — and it loses:
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bagging = BaggingClassifier(DecisionTreeClassifier(), n_estimators=100,
bootstrap=True, oob_score=True,
random_state=0, n_jobs=-1).fit(X_tr, y_tr)
pasting = BaggingClassifier(DecisionTreeClassifier(), n_estimators=100,
bootstrap=False,
random_state=0, n_jobs=-1).fit(X_tr, y_tr)
print(f"bagging test {bagging.score(X_te, y_te):.4f} oob {bagging.oob_score_:.4f}")
print(f"pasting test {pasting.score(X_te, y_te):.4f}")
# bagging test 0.9417 oob 0.8786
# pasting test 0.8833Bagging wins by 5.8 points. The reason is the correlation term from the previous page: sampling with replacement makes the training sets more different from each other, so falls. Each individual tree is slightly worse — it has seen only 63% of the distinct rows — and the ensemble is much better. Duplicated rows are a feature.
Pasting without replacement on 100 estimators reuses nearly the same rows every time, so the trees are near-identical and averaging them accomplishes little.
What a random forest adds
Section titled “What a random forest adds”Bagging alone leaves a problem. If one feature is strongly predictive, every tree will split on it first, whatever rows it received. The trees stay correlated through the feature, not the data.
A random forest fixes this by sampling features at each split, not just rows:
With features considered per split, the dominant feature is unavailable for most splits, forcing trees to find alternative structure. Individual trees get worse; drops; the ensemble improves.
Reading the plot
Section titled “Reading the plot”n_estimatorsis not a regularisation parameter. Adding trees cannot overfit; each is an independent draw and the average converges. Anything else in this curriculum with “more” on the dial eventually turns against you.- The plateau arrives quickly. By 60 trees the curve is flat. Going 100 → 400 changed held-out accuracy from 0.9333 to 0.9292 — noise, not improvement.
- OOB tracks the test curve in shape, sitting a few points below. Useful for choosing
n_estimatorseven though its level is pessimistic.
Set n_estimators as high as your latency budget allows, then tune the parameters that
actually trade off — max_depth, min_samples_leaf, max_features.
Extra-Trees
Section titled “Extra-Trees”Extremely Randomised Trees go one step further: instead of searching for the best threshold on each sampled feature, they pick thresholds at random and keep the best of those.
| Random Forest | Extra-Trees | |
|---|---|---|
| Rows | Bootstrap sample | Whole set by default |
| Features per split | Random subset | Random subset |
| Threshold | Best found by search | Random, best of the random ones |
| Individual tree quality | Better | Worse |
| Correlation between trees | Higher | Lower |
| Training speed | Slower | Much faster — no threshold search |
Measured on the same split: random forest 0.9333, Extra-Trees 0.9375. Deliberately worse members, a better ensemble, and faster to train. It is worth trying whenever a forest is your baseline.
Feature importance lies
Section titled “Feature importance lies”Measured on a constructed dataset where two columns carry signal and two are pure noise:
| Feature | Impurity importance | Permutation importance |
|---|---|---|
| signal, continuous | 0.5902 | +0.2785 |
| signal, binary | 0.1727 | +0.0206 |
| noise, continuous | 0.2245 | −0.0089 |
| noise, binary | 0.0126 | +0.0006 |
Impurity importance ranks a pure-noise column second, above a genuinely informative one. The mechanism is mechanical: a continuous feature offers hundreds of candidate split points, a binary feature offers exactly one. More chances to win a split by luck means more accumulated impurity reduction.
Permutation importance shuffles one column in the held-out data and measures how much the score drops. Noise scores zero because destroying noise costs nothing.
from sklearn.inspection import permutation_importance
result = permutation_importance(forest, X_test, y_test,
n_repeats=15, random_state=0, n_jobs=-1)
for name, mean, std in sorted(zip(names, result.importances_mean,
result.importances_std),
key=lambda row: -row[1]):
print(f"{name:<14} {mean:+.4f} +/- {std:.4f}") pch.algoApi sklearn.ensemble.RandomForestClassifier / RandomForestRegressor
pch.algoAssumes
- The base learner is high-variance and low-bias — a deep tree
- Bootstrap samples and feature subsets produce genuinely different trees
- Axis-aligned rectangular regions can approximate the target
pch.algoCost
- pch.algoTrain
O(m · n · log m · trees)- pch.algoPredict
O(depth · trees)- pch.algoMemory
O(nodes · trees)
m = samples, n = features; training parallelises perfectly across trees
pch.algoHyperparams
-
n_estimatorsdefault 100 More is never worse for accuracy, only slower. Set it by your latency budget; the curve plateaus around 60 to 100 here. -
max_featuresdefault sqrt (clf) / 1.0 (reg) The correlation dial. Lower means more diverse, weaker trees. Worth tuning — regression often benefits from about 0.3. -
max_depthdefault None Unlimited is usually right for a forest, because averaging handles the variance a single tree could not. -
min_samples_leafdefault 1 Raise to 5 to 20 on noisy data — the most effective regularisation knob here. -
oob_scoredefault False Enables the free validation estimate. Pessimistic by a few points, and it needs enough trees to be stable. -
class_weightdefault None 'balanced_subsample' reweights per bootstrap sample, which is the right variant for imbalanced data.
pch.algoReachFor
- You want a strong tabular baseline with almost no tuning
- Features are on mixed scales and you do not want to preprocess
- You need something robust to outliers and irrelevant columns
- Training can be parallelised across cores
pch.algoLookElsewhere
- You need an interpretable model — 300 trees is not a set of rules
- Prediction latency is tight; every tree runs on every request
- The data is very high-dimensional and sparse, such as text — linear models win there
- You need to extrapolate beyond the training range; trees predict a constant outside it
Pitfalls
Section titled “Pitfalls”Compare
Section titled “Compare”| Model | Trains in parallel | Overfits with more members | Needs scaling | Interpretable |
|---|---|---|---|---|
| Single tree | — | Yes, with depth | No | Yes, when shallow |
| Random Forest | Yes | No | No | No |
| Extra-Trees | Yes | No | No | No |
| AdaBoost | No — sequential | Yes | No | No |
| Gradient Boosting | No — sequential | Yes | No | No |
The “trains in parallel” column is the practical difference between this page and the next two. Bagging is embarrassingly parallel; boosting is inherently sequential.
-
Why does a bootstrap sample contain only about 63.2% of the distinct rows?
It falls straight out of sampling with replacement. The missing 36.8% is what makes out-of-bag scoring free.
pch.quizShowAnswer
B — Because P(a row is never drawn in n draws with replacement) = (1 - 1/n)^n, which converges to 1/e = 0.368 — It falls straight out of sampling with replacement. The missing 36.8% is what makes out-of-bag scoring free.
-
Bagging scored 0.9417 and pasting 0.8833 on the same data. Why does sampling with replacement win?
Each bagged tree is slightly worse for seeing only 63% of distinct rows, and the ensemble is much better because rho fell.
pch.quizShowAnswer
B — Replacement makes the training sets more different from each other, lowering correlation between trees — and correlation is what caps the averaging gain — Each bagged tree is slightly worse for seeing only 63% of distinct rows, and the ensemble is much better because rho fell.
-
What does a random forest add on top of bagging?
Row sampling alone leaves trees correlated through a dominant feature. Feature sampling at each split forces them to find alternative structure.
pch.quizShowAnswer
B — It samples a random subset of features at every split, so trees cannot all key on the same dominant feature — Row sampling alone leaves trees correlated through a dominant feature. Feature sampling at each split forces them to find alternative structure.
-
Impurity importance gave a pure-noise continuous column 0.2245 — second highest. Why?
The bias is mechanical and favours high-cardinality features. Permutation importance on held-out data correctly scored the same column at -0.0089.
pch.quizShowAnswer
B — A continuous column offers hundreds of candidate thresholds, so it wins splits by chance far more often than a binary column with one — The bias is mechanical and favours high-cardinality features. Permutation importance on held-out data correctly scored the same column at -0.0089.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Derive the 63.2%
Section titled “Exercise 1 – Derive the 63.2%”Exercise 2 – Free validation with out-of-bag scoring
Section titled “Exercise 2 – Free validation with out-of-bag scoring”Exercise 3 – Bagging against pasting
Section titled “Exercise 3 – Bagging against pasting”Exercise 4 – Extra-Trees against a forest
Section titled “Exercise 4 – Extra-Trees against a forest”Exercise 5 – Catch impurity importance lying
Section titled “Exercise 5 – Catch impurity importance lying”- A bootstrap sample of size contains of the distinct rows; the remaining 36.8% is the out-of-bag set.
- OOB scoring is free and systematically pessimistic — 0.8857 against a true 0.9333 at 100 trees.
- Bagging beat pasting 0.9417 to 0.8833, because replacement lowers the correlation between trees.
- A random forest samples features at every split, so no single dominant feature can keep the trees correlated.
- Extra-Trees randomises thresholds too: worse trees, lower correlation, better ensemble (0.9375), faster training.
n_estimatorscannot overfit — set it by your latency budget, and tunemax_featuresandmin_samples_leafinstead.- Impurity importance ranked pure noise second. Use permutation importance on held-out data.
Exercise 6 – How much of the data does one tree see?
Section titled “Exercise 6 – How much of the data does one tree see?”Continue to Boosting - Introduction to AdaBoost — the opposite strategy: instead of averaging strong models trained in parallel, chain weak ones sequentially so each fixes the last one’s mistakes.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading