Skip to content

Bagging - Random Forest Regressor/Classifier

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_feature_importances_ favours high-cardinality columns, measured

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.

diagram Diagram mermaid

The math: what a bootstrap sample contains

Draw nn rows with replacement from a set of nn. For any particular row, the chance of not being picked on one draw is 11/n1 - 1/n, and the draws are independent:

P(row never drawn)=(11n)nP(\text{row never drawn}) = \left(1 - \frac{1}{n}\right)^{n}

As nn grows this converges to a familiar constant:

limn(11n)n=e10.3679\lim_{n\to\infty}\left(1 - \frac{1}{n}\right)^{n} = e^{-1} \approx 0.3679
  in-bag63.2%,out-of-bag36.8%  \boxed{\;\text{in-bag} \approx 63.2\%, \qquad \text{out-of-bag} \approx 36.8\%\;}
figureCoverage converges almost immediatelymatplotlib
Curve of the expected fraction of distinct rows in a bootstrap sample against n, dropping quickly from 0.67 and flattening at 0.632.Curve of the expected fraction of distinct rows in a bootstrap sample against n, dropping quickly from 0.67 and flattening at 0.632.
At n = 20 the fraction is already 0.6415; by n = 100 it is 0.6340. The limit 1 - 1/e = 0.6321 is reached for practical purposes almost at once.
nnfraction of distinct rows
50.6723
200.6415
1000.6340
1,0000.6323
\infty0.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

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.

sketch Drawing a bootstrap sample p5.js
Twenty rows are drawn with replacement. Rows picked once or more turn blue with their count; rows never picked stay amber and form the out-of-bag set.

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.

oob.py
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.9292
oob.py
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.9292

Two 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 n=10n = 10 to 3.5 at n=400n = 400. 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

Bagging samples with replacement; pasting samples without. Pasting sounds cleaner — no duplicated rows — and it loses:

bagging_vs_pasting.py
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.8833
bagging_vs_pasting.py
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.8833

Bagging 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 ρ\rho 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

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:

max_features=n  (classification),max_features=n  (regression, scikit-learn default)\text{max\_features} = \sqrt{n} \;\text{(classification)}, \qquad \text{max\_features} = n \;\text{(regression, scikit-learn default)}

With n\sqrt{n} features considered per split, the dominant feature is unavailable for most splits, forcing trees to find alternative structure. Individual trees get worse; ρ\rho drops; the ensemble improves.

figureOne tree, ten trees, three hundredmatplotlib
Three panels of the same two-moons data showing predicted probability surfaces for one tree, ten trees and 300 trees. The single tree has hard-edged rectangular regions; 300 trees produce a smooth gradient.Three panels of the same two-moons data showing predicted probability surfaces for one tree, ten trees and 300 trees. The single tree has hard-edged rectangular regions; 300 trees produce a smooth gradient.
The single tree is certain everywhere and wrong in places. The forest's surface is smooth, and the shading near the boundary is genuine uncertainty rather than a hard edge.
figureWhere adding trees stops payingmatplotlib
Held-out accuracy and out-of-bag estimate plotted against the number of trees on a log scale, both rising steeply then plateauing after about 60 trees.Held-out accuracy and out-of-bag estimate plotted against the number of trees on a log scale, both rising steeply then plateauing after about 60 trees.
Both curves flatten by roughly 60 trees. More trees never hurt accuracy — unlike almost every other capacity knob — they simply cost time and memory.

Reading the plot

  1. n_estimatorsn_estimators is 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.
  2. 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.
  3. OOB tracks the test curve in shape, sitting a few points below. Useful for choosing n_estimatorsn_estimators even though its level is pessimistic.

Set n_estimatorsn_estimators as high as your latency budget allows, then tune the parameters that actually trade off — max_depthmax_depth, min_samples_leafmin_samples_leaf, max_featuresmax_features.

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 ForestExtra-Trees
RowsBootstrap sampleWhole set by default
Features per splitRandom subsetRandom subset
ThresholdBest found by searchRandom, best of the random ones
Individual tree qualityBetterWorse
Correlation between treesHigherLower
Training speedSlowerMuch 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

figureFour features: two informative, two pure noisematplotlib
Two bar charts of the same forest's feature importances. Impurity importance gives the continuous noise feature 0.22, more than the informative binary feature. Permutation importance gives it approximately zero.Two bar charts of the same forest's feature importances. Impurity importance gives the continuous noise feature 0.22, more than the informative binary feature. Permutation importance gives it approximately zero.
Impurity importance ranks the continuous noise column above the informative binary one. Permutation importance, measured on held-out data, correctly gives it nothing.

Measured on a constructed dataset where two columns carry signal and two are pure noise:

FeatureImpurity importancePermutation importance
signal, continuous0.5902+0.2785
signal, binary0.1727+0.0206
noise, continuous0.2245−0.0089
noise, binary0.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.

permutation_importance.py
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}")
permutation_importance.py
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}")
algorithmRandom ForestSupervised · Classification and Regression · Bagging ensemble

APIsklearn.ensemble.RandomForestClassifier / RandomForestRegressor

Assumes

  • 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

Cost

train
O(m · n · log m · trees)
predict
O(depth · trees)
memory
O(nodes · trees)

m = samples, n = features; training parallelises perfectly across trees

Hyperparameters that matter

  • n_estimatorsdefault 100More 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 NoneUnlimited is usually right for a forest, because averaging handles the variance a single tree could not.
  • min_samples_leafdefault 1Raise to 5 to 20 on noisy data — the most effective regularisation knob here.
  • oob_scoredefault FalseEnables 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.

Reach for it when

  • 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

Look elsewhere when

  • 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

Compare

ModelTrains in parallelOverfits with more membersNeeds scalingInterpretable
Single treeYes, with depthNoYes, when shallow
Random ForestYesNoNoNo
Extra-TreesYesNoNoNo
AdaBoostNo — sequentialYesNoNo
Gradient BoostingNo — sequentialYesNoNo

The “trains in parallel” column is the practical difference between this page and the next two. Bagging is embarrassingly parallel; boosting is inherently sequential.

quizCheck yourself
  1. Why does a bootstrap sample contain only about 63.2% of the distinct rows?

    Show answer

    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.

  2. Bagging scored 0.9417 and pasting 0.8833 on the same data. Why does sampling with replacement win?

    Show answer

    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.

  3. What does a random forest add on top of bagging?

    Show answer

    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.

  4. Impurity importance gave a pure-noise continuous column 0.2245 — second highest. Why?

    Show answer

    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

Exercise 1 – Derive the 63.2%

Exercise 2 – Free validation with out-of-bag scoring

Exercise 3 – Bagging against pasting

Exercise 4 – Extra-Trees against a forest

Exercise 5 – Catch impurity importance lying

Recap

  • A bootstrap sample of size nn contains 1(11/n)n63.2%1 - (1-1/n)^n \to 63.2\% 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_estimatorsn_estimators cannot overfit — set it by your latency budget, and tune max_featuresmax_features and min_samples_leafmin_samples_leaf instead.
  • Impurity importance ranked pure noise second. Use permutation importance on held-out data.

Exercise 6 – How much of the data does one tree see?

Next

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.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did