Answer

Bagging, boosting and stacking: how do these ensemble methods differ?

Inspired by a question on Cross Validated ·

model selectionbootstrapregression

The short answer

All three combine many models into one. Bagging averages flexible models fitted to bootstrap resamples, mainly cutting variance. Boosting fits simple models in sequence, each correcting the errors left so far, mainly cutting bias. Stacking trains a second model to blend out-of-fold predictions from different model types. None is best in general; in a simulated example boosting gave the lowest test error.

The short answer

An ensemble turns many models into one. The three classic recipes differ in how the members are trained and how their outputs are combined:

What they share: each builds a better predictor out of imperfect ones, each gives up some interpretability, and each still needs honest validation on held-out data. None of them is best in every problem.

How each one works, and why

Bagging. A fully grown tree has low bias but is unstable: a slightly different sample gives a very different tree. Averaging many such trees keeps the low bias and smooths out the instability. If each of B predictions has variance σ² and any two are correlated by ρ, their average has variance ρσ² + (1 − ρ)σ²/B. More trees shrink the second term towards zero, but the first stays, which is why bagged trees built from the same data level off. Random forests lower ρ by letting each split consider only a random subset of the predictors, so the trees disagree more and their average is steadier. Bagging is hard to overfit by adding trees, and the trees can be fitted in parallel.

Boosting. Start with a crude prediction, such as the mean. Fit a small tree to the residuals (more generally, to the gradient of the loss), add a shrunken copy of it to the prediction, recompute the residuals and repeat. Each step nudges the model towards the patterns it is still missing, so a sum of weak learners can capture structure no single small tree could. Because every step chases the training errors, boosting can overfit if it runs too long; the number of rounds, the learning rate (shrinkage) and the tree depth are tuned, often with early stopping on a validation set.

Stacking. Fit several base models of different types, say a linear regression, a random forest and a boosted model. Split the training data into folds and, for each fold, collect each model's predictions from a fit that excluded that fold. These out-of-fold predictions become the inputs to the blender, often a linear regression, perhaps with weights constrained to be non-negative. Using in-sample predictions instead would teach the blender to trust whichever model overfits most. Stacking pays off when the base models make different kinds of mistakes.

See it in R and Python

The example simulates 500 training and 5,000 test cases with four predictors. The outcome has a strong straight-line effect, a curve and a two-way interaction, plus noise with variance 0.25, which is the lowest test MSE any model could reach. It compares a linear regression, one fully grown tree, 100 bagged trees, 300 boosted depth-2 trees (learning rate 0.1) and a stack that blends the linear model and the bagged trees using 5-fold out-of-fold predictions. Trees come from the rpart package, which ships with R; the Python version uses scikit-learn.

R

library(rpart)   # tree package that ships with R
set.seed(18891)

sim <- function(n) {   # 4 predictors: one linear effect, a curve and an interaction
  X <- as.data.frame(matrix(runif(n * 4, -1, 1), n, 4))
  X$y <- 3 * X$V1 + sin(3 * X$V2) + 2 * X$V3 * X$V4 + rnorm(n, 0, 0.5)
  X
}
train <- sim(500); test <- sim(5000)
deep  <- rpart.control(cp = 0, minsplit = 2, minbucket = 1, xval = 0)   # fully grown
small <- rpart.control(cp = 0, maxdepth = 2, minsplit = 10, minbucket = 5, xval = 0)
mse <- function(p) mean((test$y - p)^2)

# Bagging: deep trees on bootstrap samples, averaged
bag <- function(d, new, B = 100) rowMeans(sapply(1:B, function(b) {
  t <- rpart(y ~ ., d[sample(nrow(d), replace = TRUE), ], control = deep)
  predict(t, new)}))

# Boosting: small trees fitted one after another to the current residuals
boost <- function(d, new, M = 300, rate = 0.1) {
  f <- rep(mean(d$y), nrow(d)); p <- rep(mean(d$y), nrow(new))
  for (m in 1:M) {
    r <- d; r$y <- d$y - f
    t <- rpart(y ~ ., r, control = small)
    f <- f + rate * predict(t, d); p <- p + rate * predict(t, new)
  }
  p
}

# Stacking: out-of-fold predictions of two base models feed a linear blender
lin <- function(d, new) predict(lm(y ~ ., d), new)
fold <- sample(rep(1:5, length.out = nrow(train)))
oof <- matrix(NA, nrow(train), 2, dimnames = list(NULL, c("lin", "bag")))
for (k in 1:5) {
  tr <- train[fold != k, ]; ho <- train[fold == k, ]
  oof[fold == k, ] <- cbind(lin(tr, ho), bag(tr, ho))
}
w <- coef(lm(train$y ~ oof))
base <- cbind(lin = lin(train, test), bag = bag(train, test))
stack <- w[1] + base %*% w[-1]

single <- predict(rpart(y ~ ., train, control = deep), test)
round(c(linear = mse(base[, "lin"]), single_tree = mse(single),
        bagging = mse(base[, "bag"]), boosting = mse(boost(train, test)),
        stacking = mse(stack)), 3)
round(w, 3)   # blender intercept and weights

Python

import numpy as np
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression

rng = np.random.default_rng(18891)

def sim(n):   # 4 predictors: one linear effect, a curve and an interaction
    X = rng.uniform(-1, 1, (n, 4))
    y = 3 * X[:, 0] + np.sin(3 * X[:, 1]) + 2 * X[:, 2] * X[:, 3] + rng.normal(0, 0.5, n)
    return X, y

X_tr, y_tr = sim(500)
X_te, y_te = sim(5000)
deep = dict(random_state=0)   # fully grown
small = dict(max_depth=2, min_samples_split=10, min_samples_leaf=5, random_state=0)
mse = lambda p: np.mean((y_te - p)**2)

# Bagging: deep trees on bootstrap samples, averaged
def bag(X, y, new, B=100):
    preds = []
    for b in range(B):
        i = rng.integers(0, len(y), len(y))
        preds.append(DecisionTreeRegressor(**deep).fit(X[i], y[i]).predict(new))
    return np.mean(preds, axis=0)

# Boosting: small trees fitted one after another to the current residuals
def boost(X, y, new, M=300, rate=0.1):
    f = np.full(len(y), y.mean()); p = np.full(len(new), y.mean())
    for m in range(M):
        t = DecisionTreeRegressor(**small).fit(X, y - f)
        f += rate * t.predict(X); p += rate * t.predict(new)
    return p

# Stacking: out-of-fold predictions of two base models feed a linear blender
lin = lambda X, y, new: LinearRegression().fit(X, y).predict(new)
fold = rng.permutation(np.resize(np.arange(5), len(y_tr)))
oof = np.zeros((len(y_tr), 2))
for k in range(5):
    tr, ho = fold != k, fold == k
    oof[ho] = np.column_stack([lin(X_tr[tr], y_tr[tr], X_tr[ho]), bag(X_tr[tr], y_tr[tr], X_tr[ho])])
blender = LinearRegression().fit(oof, y_tr)
base = np.column_stack([lin(X_tr, y_tr, X_te), bag(X_tr, y_tr, X_te)])
stack = blender.predict(base)

single = DecisionTreeRegressor(**deep).fit(X_tr, y_tr).predict(X_te)
print({k: round(v, 3) for k, v in dict(linear=mse(base[:, 0]), single_tree=mse(single),
       bagging=mse(base[:, 1]), boosting=mse(boost(X_tr, y_tr, X_te)), stacking=mse(stack)).items()})
print("blender intercept", round(blender.intercept_, 3), "weights", np.round(blender.coef_, 3))
     linear single_tree     bagging    boosting    stacking 
      0.854       1.256       0.696       0.419       0.685 
(Intercept)      ooflin      oofbag 
      0.003       0.103       0.912

All figures come from one seeded run of the R code. Python's random numbers differ from R's, so its figures differ slightly, but they show the same pattern: the single tree is worst, bagging beats the linear model, stacking edges out bagging, and boosting comes out clearly on top.

What the numbers show

Which one should you use?

There is no general winner. On tabular data, well-tuned gradient boosting is often the most accurate single method, but it needs tuning and can overfit. Random forests are a strong default that works well with almost no tuning and gives a free error estimate from the cases left out of each bootstrap sample. Stacking is worth the extra work when you already have several good, different models and a small gain in accuracy matters, as in forecasting competitions.

Whatever you choose, compare the candidates on data they were not trained on, using cross-validation or a separate test set, as described in training, validation and test sets explained. The resampling idea behind bagging is the same one explained in why bootstrapping works. For help choosing a classical analysis instead, try the test chooser or browse the DASS blog.

How to report mean squared error in APA style (7th edition)

In the method section, name each ensemble and its main settings (number of trees, tree depth, learning rate, how stacking weights were estimated) and say how test performance was measured. In the results, give the error on held-out data, in the outcome's squared units. Using the example above:

"Five models were compared on a held-out test set of 5,000 cases. Gradient boosting (300 trees of depth 2, learning rate 0.1) had the lowest mean squared error (MSE = 0.419), followed by a stacked ensemble of a linear regression and bagged trees (MSE = 0.685), bagged trees alone (MSE = 0.696), linear regression (MSE = 0.854) and a single decision tree (MSE = 1.256)."

Spell out the abbreviation at first use. With more than a few models, put the error measures in a table with a note naming the evaluation sample, and report the root mean squared error as well if readers will want the error in the outcome's own units.

Related tools and guides

More answered questions

Working with your own data?

General answers only go so far. Send us your situation and we'll reply by email within two business days.

Ask your question

Written by AskStats with AI assistance. This is general information, not advice for your specific data or study. When the results matter, check your approach with a qualified statistician.