Bagging is covered in section 8.2 of An Introduction to Statistical Learning. The method
comes from Leo Breiman’s 1996 paper
“Bagging Predictors”. The name is short for bootstrap aggregating.
The bias and variance post ended with a problem: a deep
tree has almost no bias but huge variance. Its answer depends heavily on which data it happened to
see. Bagging fixes that by averaging many trees.
The idea: ask a committee
If you ask one risk analyst how volatile next month will be, you get their answer, quirks and
all. Ask twenty analysts who each learned from a slightly different few years of history, and take
the average: their individual quirks partly cancel out, and what’s left is closer to the real
pattern.
Bagging does this with models:
Make many bootstrap samples of the training data.
Train one model (usually a deep decision tree) on each.
Average their predictions. For classification, average their probabilities, or take a vote.
Step 1: the bootstrap sample
A bootstrap sample is drawn from the training data with replacement, and it has the same
size as the original. Some rows get picked more than once, some not at all.
Eight training rows, numbered 1 to 8, and three bootstrap samples of them (Table 1):
Table 1. Three bootstrap samples of eight rows
Sample
Rows drawn
Left out
1
1, 3, 7, 7, 7, 7, 8, 8
2, 4, 5, 6
2
1, 1, 2, 5, 5, 7, 7, 8
3, 4, 6
3
2, 3, 4, 7, 7, 7, 8, 8
1, 5, 6
Each tree sees a different version of the history, so each tree comes out different.
How many rows does a bootstrap sample leave out? The chance that a given row is not picked in
one draw is 1−1/n. Over n draws:
(1−n1)n⟶e1≈0.368as n grows
So each tree never sees about 37% of the data, and uses about two-thirds of it (ISLP section
8.2.1, Murphy section 18.3). With 10,000 rows, I measured 36.7%. Those
left-out rows are called out-of-bag rows, and they’re useful: each tree can be tested on the
rows it never saw, which gives a free estimate of performance without a separate test set.
Step 3: why averaging reduces variance
An Introduction to Statistical Learning (section 8.2.1) starts from a simple fact: averaging
nindependent numbers, each with variance σ2, gives an average with variance
σ2/n. Bagged trees aren’t independent, though: they learn from overlapping data. If you
average B predictions that each have variance σ2, and every pair has correlation ρ,
the average has variance (symbols in Table 2):
ρ, how alike the trees' errors areσ2, one tree's variance
Var(average)=ρσ2+B1−ρσ2
Table 2. Symbols in the variance of an average
Symbol
Meaning
B
number of models averaged
σ2
variance of one model’s prediction
ρ
correlation between two models’ predictions
Plain meaning: the second term shrinks as you add models. The first term doesn’t. If the models
were completely independent (ρ=0), averaging 100 of them would cut variance 100-fold. But
trees trained on overlapping data make similar mistakes (ρ>0), so variance falls towards a
floor of ρσ2 and stops. ISLP (section 8.2.2) says the same in words: averaging
highly correlated quantities does not reduce variance as much as averaging uncorrelated ones.
(It’s the same maths as the diversification floor in a
portfolio of correlated assets.)
Where the formula comes from
The variance of a sum is the sum of every variance plus every covariance between pairs. B models
each have variance σ2, and each of the B(B−1) ordered pairs has covariance
ρσ2. Dividing the sum by B divides its variance by B2:
Same made-up problem as the bias and variance post: predict next month’s volatility from this
month’s, on a curve I invented so the truth is known. Same method: 200 fresh training samples of
80 made-up months, measuring how much the predictions move between samples (Table 3 and Figure 1).
Table 3. Bias2, variance and expected error by trees averaged, simulated
Trees averaged
Bias2
Variance
Expected error on new months
1 (one deep tree)
0.04
8.88
17.93
2
0.03
6.69
15.72
5
0.02
5.23
14.26
10
0.03
4.71
13.74
25
0.03
4.44
13.47
100
0.02
4.20
13.22
Figure 1. One deep tree against the average of 100, and variance by trees averaged
Bias stays near zero. Averaging deep trees doesn’t make them too simple.
Variance halves, from 8.9 to 4.2, most of it by the first 10 trees.
Then it levels off. That’s the ρσ2 floor. From these numbers, the trees’ predictions
are correlated at roughly ρ≈0.47 (my estimate: 4.2/8.9, ignoring the small
leftover term).
On this small, simple problem, the best single tree from the bias and variance post
(depth 3, error 11.8) still beats 100 bagged deep trees (13.2). Bagging removes variance. It
doesn’t remove the need to choose a sensible model.
On the stress-month data
Now the classification problem from the decision trees post:
will next month be a stress month? Scores in Table 4 are AUC on the 2,205 test rows of 2024 and 2025, one
per ticker per trading day (0.5 is a coin flip, 1.0 is perfect):
Table 4. Test AUC by base tree depth, one tree against a bag of 200
Base tree
One tree
Bag of 200
Change
Fully grown
0.742
0.881
big gain
Depth 8
0.834
0.888
good gain
Depth 5
0.873
0.890
small gain
Depth 3
0.876
0.894
small gain
Table 4 says most of what there is to say about bagging:
It helps a lot when the base model is unstable (high variance), like a fully grown tree.
It helps less when the model is already stable. A depth-3 tree changes little between
bootstrap samples, so there’s less noise to average away; it still gains about 0.02 of AUC here,
because real market data is noisier than my toy curve.
And a warning about the out-of-bag estimate. From the training data alone, the bag of fully grown
trees reported an AUC of 0.917. Its real test score was 0.881. The estimate is too
rosy, and the reason is the data, not the method: two rows a day apart are computed from almost
the same month of prices, so a row left out of a bootstrap sample has near-twins inside it, and
“never seen” isn’t really true. On a time series, the honest test is forward in time. The out-of-bag score is free and useful when rows are independent. Here it would have fooled
me.
Three more experiments
On the regression version of the problem, predicting next month’s volatility as a number, the
same ideas look like this.
How many trees. Fully grown trees, averaged, scored by RMSE (the typical size of a miss;
lower is better) on the 2024 to 2025 rows (Table 5):
Table 5. Test RMSE by number of fully grown trees averaged
Trees averaged
1
5
10
20
50
100
Test RMSE
17.19
13.84
13.70
13.39
13.22
13.15
The first twenty trees take over a fifth off the error; the next eighty take almost nothing. That levelling
off is the ρσ2 floor from the formula above, measured.
Random splits. A tree that picks each cut point at random instead of searching for the
best one is weaker alone and far cheaper to build: 7.3 milliseconds against 23.7 for a fully
grown tree here. On this noisy target it is also, oddly, better at every leaf size tried
(a test RMSE of 13.10 against 14.41 at ten rows per leaf), because the search was finding cuts
that fit the training noise. Bagged, the gap closes to almost nothing: twenty random trees scored 13.20 against
13.39 for twenty searching trees, because averaging has already removed most of the noise the random cuts
were guarding against. The lesson is the price, not the score: random splits lost nothing here
and cost a third of the time. Randomness acts as regularisation (a
brake that stops a model fitting noise), and it is the trade that random forests and “extra trees” (extremely randomised trees,
which pick their cut points at random as here) are built on.
Bagging a stable model. Bag twenty linear regressions and you get the single regression’s
answer back, to two decimal places. A straight line barely moves between resamples of the same
data, so there is nothing to average away. Bagging helps unstable learners, deep trees above
all, and does nothing for stable ones.
The code
All three ideas on made-up data, so it runs as pasted (pip install numpy scikit-learn):
one bootstrap sample, deep trees bagged on the made-up volatility curve from the
bias and variance post, and a straight line bagged for comparison.
X_train holds this month’s volatility as a one-column table, y_train next month’s.
import numpy as npfrom sklearn.ensemble import BaggingRegressorfrom sklearn.linear_model import LinearRegressionfrom sklearn.metrics import root_mean_squared_errorfrom sklearn.tree import DecisionTreeRegressorrng = np.random.default_rng(0)# 1. One bootstrap sample: draw 8 rows, with replacement, from rows 1 to 8rows = np.arange(1, 9)sample = rng.choice(rows, size=8, replace=True)print("drawn:", np.sort(sample), " left out:", np.setdiff1d(rows, sample))n = 10_000 # the same on 10,000 rowsleft_out = 1 - len(np.unique(rng.choice(n, size=n))) / nprint("share of rows left out:", round(left_out, 3)) # about 0.37# 2. Bagging deep trees on a made-up curve (next month's volatility from this month's)def true_curve(x): return 6 + 0.45 * x + 8 / (1 + np.exp(-(x - 25) / 3))x_train = rng.uniform(5, 45, 300)y_train = true_curve(x_train) + rng.normal(0, 3, 300)x_test = rng.uniform(5, 45, 1000)y_test = true_curve(x_test) + rng.normal(0, 3, 1000)X_train, X_test = x_train.reshape(-1, 1), x_test.reshape(-1, 1) # tables of one columnfor B in (1, 10, 100): # how many trees are averaged bag = BaggingRegressor(DecisionTreeRegressor(), n_estimators=B, random_state=0) bag.fit(X_train, y_train) # each tree gets its own bootstrap sample print(f"{B:3} deep trees: test RMSE {root_mean_squared_error(y_test, bag.predict(X_test)):.2f}")# 3. Bagging a stable model: twenty straight lines average back to one lineline = LinearRegression().fit(X_train, y_train)bagged = BaggingRegressor(LinearRegression(), n_estimators=20, random_state=0).fit(X_train, y_train)print("one line:", round(root_mean_squared_error(y_test, line.predict(X_test)), 2), " twenty bagged lines:", round(root_mean_squared_error(y_test, bagged.predict(X_test)), 2))
It prints one bootstrap sample (some rows several times, a few left out), then a left-out share
of about 0.37. Then the averaging: one deep tree misses by about 4.7, ten trees by about 3.85, a
hundred by about 3.8, the fast fall and then the floor from Table 3. The noise added to
y has a spread of 3, so 3 is the best any model can do. Last, the single line and twenty bagged
lines score the same 3.37: nothing to average away.
Hastie, Tibshirani and Friedman, The Elements of Statistical Learning (2nd ed., Springer, 2009), equation 15.1: the variance of an average of correlated trees, derived in the fold above