Bagging Vs Boosting The Difference Ensemble Diagra
You’ve seen the diagram a hundred times: bagging trains models in parallel, boosting trains them in sequence. Neat boxes and arrows. But that picture never tells you why the shape matters. It’s like saying a car has four wheels and an engine – technically true, but it doesn’t explain why it moves.
By the end of this article, you’ll understand the real difference: bagging reduces variance, boosting reduces bias. And you’ll see that difference in code, not just in theory.
1. The Problem: One Model Isn’t Enough
Let’s start with a single decision tree. A deep tree memorizes every detail of the training data – including the noise. That’s high variance: change the training set slightly, and the tree’s predictions change a lot. A shallow tree, on the other hand, misses important patterns. That’s high bias: it’s too simple to capture the true relationship.
What if we could combine many trees to get the best of both worlds? That’s the core idea behind ensemble methods. But here’s the catch: the standard parallel-vs-sequential diagram doesn’t explain why the combination works. It just shows the shape.
Let’s see the problem with a single tree on a simple synthetic dataset.
# Single decision tree on synthetic moons data – shows overfitting
from sklearn.datasets import make_moons
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# Generate data
X, y = make_moons(n_samples=200, noise=0.3, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Deep tree (no pruning)
deep_tree = DecisionTreeClassifier(max_depth=None, random_state=42)
deep_tree.fit(X_train, y_train)
train_acc = accuracy_score(y_train, deep_tree.predict(X_train))
test_acc = accuracy_score(y_test, deep_tree.predict(X_test))
print(f"Deep tree - Train accuracy: {train_acc:.3f}, Test accuracy: {test_acc:.3f}")
What the numbers tell us: The deep tree achieves near-perfect training accuracy (probably 1.0) but much lower test accuracy – a classic sign of overfitting (high variance). A shallow tree would have the opposite problem: low training accuracy (high bias).
So how do ensembles fix this? Let’s start with bagging.
2. Bagging: Parallel Independence Reduces Variance
Think of bagging like polling: if you ask one person, you might get a random opinion. But if you ask 100 people and average their answers, the average is much more stable. Each person’s opinion has high variance, but the average has low variance.
Bagging (Bootstrap Aggregating) does the same with models:
- Create many bootstrap samples (random samples with replacement) from the training data.
- Train a separate model (usually a deep tree) on each sample – all in parallel.
- Average the predictions (for regression) or take a majority vote (for classification).
Because each tree sees a slightly different dataset, their errors are somewhat independent. Averaging cancels out the random errors, reducing variance without increasing bias.
This is the hardest part of bagging: understanding why averaging reduces variance without reducing bias. Each tree is still a strong learner (low bias), but its predictions are noisy. The average of many noisy predictions is much less noisy – the bias stays the same, but variance drops.
Let’s see it in code.
# Compare a single deep tree vs a bagging ensemble
from sklearn.datasets import make_moons
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score
import numpy as np
# Generate data (same as before)
X, y = make_moons(n_samples=200, noise=0.3, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Single deep tree
single_tree = DecisionTreeClassifier(max_depth=None, random_state=42)
single_tree.fit(X_train, y_train)
single_test_acc = accuracy_score(y_test, single_tree.predict(X_test))
# Bagging with 100 deep trees
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(max_depth=None),
n_estimators=100,
random_state=42
)
bagging.fit(X_train, y_train)
bagging_test_acc = accuracy_score(y_test, bagging.predict(X_test))
# Also measure variance across 10 different train-test splits
single_scores = cross_val_score(DecisionTreeClassifier(max_depth=None), X, y, cv=10)
bagging_scores = cross_val_score(
BaggingClassifier(estimator=DecisionTreeClassifier(max_depth=None), n_estimators=100, random_state=42),
X, y, cv=10
)
print(f"Single tree test accuracy: {single_test_acc:.3f}")
print(f"Bagging test accuracy: {bagging_test_acc:.3f}")
print(f"\nSingle tree CV accuracy: mean={single_scores.mean():.3f}, std={single_scores.std():.3f}")
print(f"Bagging CV accuracy: mean={bagging_scores.mean():.3f}, std={bagging_scores.std():.3f}")
What the numbers tell us: The bagged model’s test accuracy is higher than the single tree’s. More importantly, the standard deviation across cross-validation folds is much lower for bagging – that’s the variance reduction in action. The mean accuracy is also higher because the single tree sometimes fits a bad split; bagging smooths that out.
Bagging works best with unstable base models – models that change a lot when the training data changes slightly. Deep decision trees are perfect for this. Random Forests, which we’ll see later, add extra randomness by selecting a random subset of features at each split, making the trees even more independent.
3. Boosting: Sequential Correction Reduces Bias
Now let’s contrast bagging with boosting. Instead of training models independently, boosting trains them one after another, each one focusing on the mistakes of the previous. Think of a student who takes a test, reviews the questions they got wrong, and studies those topics more. The next test, they’ll do better on those questions – but they might over-focus on the tricky ones and miss the big picture.
AdaBoost (Adaptive Boosting) is the classic algorithm:
- Start with a weak learner (e.g., a shallow tree – high bias).
- Train it on the data, giving equal weight to all samples.
- Increase the weights of misclassified samples.
- Train a new weak learner on the reweighted data.
- Combine all learners with a weighted vote (more accurate learners get higher weight).
This is the hardest part of boosting: understanding why focusing on mistakes reduces bias. Each weak learner has high bias (it’s too simple). But by sequentially correcting errors, the ensemble can fit complex patterns that no single weak learner could. The bias drops. However, if you boost too much, the ensemble starts memorizing noise – variance increases.
Let’s see it in code.
# Compare a single shallow tree, AdaBoost, and bagging
from sklearn.datasets import make_moons
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier, BaggingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# Generate data
X, y = make_moons(n_samples=200, noise=0.3, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Single shallow tree (max_depth=1 – a stump, high bias)
shallow_tree = DecisionTreeClassifier(max_depth=1, random_state=42)
shallow_tree.fit(X_train, y_train)
shallow_train_acc = accuracy_score(y_train, shallow_tree.predict(X_train))
shallow_test_acc = accuracy_score(y_test, shallow_tree.predict(X_test))
# AdaBoost with 50 stumps
ada = AdaBoostClassifier(
estimator=DecisionTreeClassifier(max_depth=1),
n_estimators=50,
random_state=42
)
ada.fit(X_train, y_train)
ada_train_acc = accuracy_score(y_train, ada.predict(X_train))
ada_test_acc = accuracy_score(y_test, ada.predict(X_test))
# Bagging with 100 deep trees (for comparison)
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(max_depth=None),
n_estimators=100,
random_state=42
)
bagging.fit(X_train, y_train)
bagging_test_acc = accuracy_score(y_test, bagging.predict(X_test))
print("Single shallow tree (stump):")
print(f" Train accuracy: {shallow_train_acc:.3f}, Test accuracy: {shallow_test_acc:.3f}")
print("\nAdaBoost (50 stumps):")
print(f" Train accuracy: {ada_train_acc:.3f}, Test accuracy: {ada_test_acc:.3f}")
print("\nBagging (100 deep trees):")
print(f" Test accuracy: {bagging_test_acc:.3f}")
What the numbers tell us: The single stump has low training accuracy (high bias). AdaBoost dramatically improves training accuracy – it can fit the training data almost perfectly. But look at the test accuracy: it might be lower than bagging’s. That’s the variance cost: boosting overfits to noise if you use too many estimators or if the data is noisy.
Let’s also visualize the decision boundaries to see how boosting creates more complex boundaries.
# Plot decision boundaries for shallow tree, AdaBoost, and bagging
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import make_moons
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier, BaggingClassifier
# Generate data
X, y = make_moons(n_samples=200, noise=0.3, random_state=42)
# Train models (redefine for self-contained block)
shallow = DecisionTreeClassifier(max_depth=1, random_state=42).fit(X, y)
ada = AdaBoostClassifier(estimator=DecisionTreeClassifier(max_depth=1), n_estimators=50, random_state=42).fit(X, y)
bag = BaggingClassifier(estimator=DecisionTreeClassifier(max_depth=None), n_estimators=100, random_state=42).fit(X, y)
# Create mesh grid
xx, yy = np.meshgrid(np.linspace(X[:,0].min()-0.5, X[:,0].max()+0.5, 200),
np.linspace(X[:,1].min()-0.5, X[:,1].max()+0.5, 200))
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
titles = ['Shallow Tree (depth=1)', 'AdaBoost (50 stumps)', 'Bagging (100 deep trees)']
models = [shallow, ada, bag]
for ax, model, title in zip(axes, models, titles):
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
ax.scatter(X[:,0], X[:,1], c=y, edgecolor='k', cmap='coolwarm')
ax.set_title(title)
ax.set_xlabel('Feature 1')
ax.set_ylabel('Feature 2')
plt.tight_layout()
plt.show()
What the plot shows: The shallow tree draws a simple horizontal or vertical split (very high bias). AdaBoost creates a wiggly, complex boundary that tries to separate every point – it’s fitting the noise. Bagging’s boundary is smoother; it captures the overall shape without overfitting to individual points.
4. The Real Difference: Bias-Variance Tradeoff (Not Just Parallel vs Sequential)
Most diagrams show bagging as parallel boxes and boosting as sequential boxes, but they never explain why that shape matters. Here’s the real story:
- Bagging (parallel): Each model is trained independently on a bootstrap sample. Their errors are uncorrelated. Averaging cancels out the random errors, reducing variance. Bias stays roughly the same because each individual model is already strong (low bias).
- Boosting (sequential): Each new model focuses on the mistakes of the previous ones. This systematically reduces bias – the ensemble can fit patterns that no single weak learner could. But because it keeps correcting, it can also start fitting noise, increasing variance.
Think of it this way:
- Bagging is like asking many independent experts and averaging their answers. The average is more reliable than any single expert (lower variance).
- Boosting is like one expert who keeps studying their mistakes. They get better at the hard questions (lower bias), but they might start memorizing the exact exam rather than learning the subject (higher variance).
Here’s a simple table:
| Method | Effect on Bias | Effect on Variance | Typical Base Learner |
|---|---|---|---|
| Bagging | Little change | Reduces | Strong (deep tree) |
| Boosting | Reduces | May increase | Weak (shallow tree) |
The catch: In practice, bagging can also slightly reduce bias if the base learners are unstable (they average to a better fit), and boosting can reduce variance if you stop early or use regularization. But the primary mechanism is as stated.
Let’s estimate bias and variance empirically using multiple train-test splits.
# Estimate bias and variance from multiple train-test splits
from sklearn.datasets import make_moons
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier, AdaBoostClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
np.random.seed(42)
X, y = make_moons(n_samples=200, noise=0.3, random_state=42)
n_splits = 50
single_scores = []
bagging_scores = []
ada_scores = []
for i in range(n_splits):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=i)
# Single deep tree
single = DecisionTreeClassifier(max_depth=None, random_state=42).fit(X_train, y_train)
single_scores.append(accuracy_score(y_test, single.predict(X_test)))
# Bagging
bag = BaggingClassifier(estimator=DecisionTreeClassifier(max_depth=None), n_estimators=100, random_state=42)
bag.fit(X_train, y_train)
bagging_scores.append(accuracy_score(y_test, bag.predict(X_test)))
# AdaBoost
ada = AdaBoostClassifier(estimator=DecisionTreeClassifier(max_depth=1), n_estimators=50, random_state=42)
ada.fit(X_train, y_train)
ada_scores.append(accuracy_score(y_test, ada.predict(X_test)))
print("Mean test accuracy (lower bias = higher mean):")
print(f" Single tree: {np.mean(single_scores):.3f}")
print(f" Bagging: {np.mean(bagging_scores):.3f}")
print(f" AdaBoost: {np.mean(ada_scores):.3f}")
print("\nStandard deviation (higher std = higher variance):")
print(f" Single tree: {np.std(single_scores):.3f}")
print(f" Bagging: {np.std(bagging_scores):.3f}")
print(f" AdaBoost: {np.std(ada_scores):.3f}")
What the numbers tell us: Bagging has lower variance (smaller std) than a single tree, and slightly higher mean accuracy. AdaBoost has the highest mean accuracy (lowest bias) but also the highest variance (largest std) – it’s more sensitive to which data points end up in the training set.
5. Code Comparison: Bagging vs Boosting on a Noisy Dataset
Let’s put theory into practice with a dataset that has added label noise – a common real-world challenge. We’ll use make_classification with 20% label noise. For bagging, we’ll use RandomForestClassifier (bagging + feature randomness). For boosting, we’ll use GradientBoostingClassifier (modern boosting that fits residuals).
We’ll compare test accuracy, overfitting behavior, and learning curves.
# Bagging vs Boosting on noisy data
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# Generate noisy classification data (20% label noise)
X, y = make_classification(n_samples=1000, n_features=20, n_informative=15,
n_redundant=5, flip_y=0.2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Random Forest (bagging variant)
rf = RandomForestClassifier(n_estimators=100, max_depth=None, random_state=42)
rf.fit(X_train, y_train)
rf_train_acc = accuracy_score(y_train, rf.predict(X_train))
rf_test_acc = accuracy_score(y_test, rf.predict(X_test))
# Gradient Boosting (boosting variant)
gb = GradientBoostingClassifier(n_estimators=100, max_depth=3, learning_rate=0.1, random_state=42)
gb.fit(X_train, y_train)
gb_train_acc = accuracy_score(y_train, gb.predict(X_train))
gb_test_acc = accuracy_score(y_test, gb.predict(X_test))
print("Random Forest (bagging):")
print(f" Train accuracy: {rf_train_acc:.3f}, Test accuracy: {rf_test_acc:.3f}")
print("\nGradient Boosting:")
print(f" Train accuracy: {gb_train_acc:.3f}, Test accuracy: {gb_test_acc:.3f}")
What the numbers tell us: On noisy data, Random Forest’s test accuracy is likely higher than Gradient Boosting’s. Gradient Boosting may have near-perfect training accuracy (memorizing noise) but a larger gap to test accuracy – that’s the variance cost. Random Forest is more robust.
Let’s also plot learning curves (accuracy vs number of estimators) to see how each method behaves as we add more trees.
# Learning curves: accuracy vs n_estimators for Random Forest and Gradient Boosting
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# Generate data
X, y = make_classification(n_samples=1000, n_features=20, n_informative=15,
n_redundant=5, flip_y=0.2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
n_estimators_range = [1, 5, 10, 20, 50, 100, 200]
rf_train_scores = []
rf_test_scores = []
gb_train_scores = []
gb_test_scores = []
for n in n_estimators_range:
rf = RandomForestClassifier(n_estimators=n, max_depth=None, random_state=42)
rf.fit(X_train, y_train)
rf_train_scores.append(accuracy_score(y_train, rf.predict(X_train)))
rf_test_scores.append(accuracy_score(y_test, rf.predict(X_test)))
gb = GradientBoostingClassifier(n_estimators=n, max_depth=3, learning_rate=0.1, random_state=42)
gb.fit(X_train, y_train)
gb_train_scores.append(accuracy_score(y_train, gb.predict(X_train)))
gb_test_scores.append(accuracy_score(y_test, gb.predict(X_test)))
plt.figure(figsize=(10, 5))
plt.plot(n_estimators_range, rf_train_scores, label='RF Train', marker='o')
plt.plot(n_estimators_range, rf_test_scores, label='RF Test', marker='s')
plt.plot(n_estimators_range, gb_train_scores, label='GB Train', marker='^')
plt.plot(n_estimators_range, gb_test_scores, label='GB Test', marker='v')
plt.xlabel('Number of Estimators')
plt.ylabel('Accuracy')
plt.title('Learning Curves: Random Forest vs Gradient Boosting (20% label noise)')
plt.legend()
plt.grid(True)
plt.show()
What the plot shows: Gradient Boosting’s training accuracy rises quickly and stays near 1.0, but test accuracy plateaus and may even drop after a certain point – overfitting. Random Forest’s test accuracy steadily improves and stabilizes, with a smaller gap between train and test – more robust to noise.
6. Modern Variants: Random Forests and Gradient Boosting (XGBoost/LightGBM)
The algorithms we’ve used are classic, but modern practice relies on optimized versions:
- Random Forests (Breiman, 2001) = bagging + random feature subsets at each split. This decorrelates the trees even more, further reducing variance. It’s the go-to bagging method today.
- Gradient Boosting (Friedman, 2001) generalizes AdaBoost to any differentiable loss function. Instead of reweighting data points, each new tree fits the residuals (errors) of the current ensemble. This is more flexible and powerful.
- XGBoost (Chen & Guestrin, 2016) adds regularization, parallelized tree construction, and sparsity handling. It’s the dominant boosting library in competitions.
- LightGBM and CatBoost are newer variants that optimize for speed and categorical features.
The same bias-variance principles apply: Random Forests reduce variance; Gradient Boosting reduces bias. But modern implementations also include regularization (e.g., learning rate, subsample) that can reduce the variance cost of boosting.
# Quick example: XGBoost (requires xgboost installed)
# Uncomment to run if xgboost is available
# import xgboost as xgb
# model = xgb.XGBClassifier(n_estimators=100, max_depth=3, learning_rate=0.1, random_state=42)
# model.fit(X_train, y_train)
# print(f"XGBoost test accuracy: {accuracy_score(y_test, model.predict(X_test)):.3f}")
7. When to Use Which: Practical Guidelines
So which should you use? Here’s a simple decision rule:
- Start with Random Forests if:
- Your data is noisy (many mislabeled examples).
- You have limited time for hyperparameter tuning.
- You want a robust baseline that works well out-of-the-box.
- Try Gradient Boosting (XGBoost/LightGBM) if:
- You need the best possible accuracy on clean, structured data.
- You’re willing to invest time in tuning (learning rate, number of trees, regularization).
- You have a large dataset and can use early stopping to prevent overfitting.
Rule of thumb: Bagging works best with high-variance base models (deep trees). Boosting works best with high-bias base models (shallow trees). In practice, Random Forests use deep trees; Gradient Boosting uses shallow trees (max_depth=3-8).
8. Conclusion: What You Learned and What’s Next
Let’s recap the key insights:
- Bagging reduces variance by averaging independent models trained on bootstrap samples. It’s like polling many experts.
- Boosting reduces bias by sequentially correcting errors. It’s like a student who focuses on their mistakes.
- The parallel vs. sequential shape is just the surface – the real difference is in the bias-variance tradeoff.
- On noisy data, bagging (Random Forests) is more robust. On clean data, boosting (Gradient Boosting) can achieve higher accuracy with careful tuning.
In the next part of this series, we’ll dive into hyperparameter tuning for both families – how to find the sweet spot between underfitting and overfitting. You’ll learn how to use learning curves, validation sets, and early stopping to get the best performance from your ensembles.
Check Your Understanding
Test your knowledge with these questions (based on Bloom’s taxonomy):
Remember:
- Define bagging and boosting in one sentence each.
Understand:
- Explain why bagging reduces variance without significantly changing bias.
- Explain why boosting reduces bias but can increase variance.
Apply:
- You have a dataset with 30% label noise. Which ensemble method would you try first? Why?
Analyze:
- Look at the learning curves from Section 5. Why does Gradient Boosting’s test accuracy plateau while Random Forest’s keeps improving?
Evaluate:
- Critique the statement: “Boosting always outperforms bagging because it achieves higher training accuracy.”
Create:
- Design a simple experiment to compare the bias and variance of Random Forest and XGBoost on a synthetic dataset with varying noise levels. What metrics would you track?
Apply What You Learned is for Supporter and Insider subscribers.
Subscribe to unlock the exercises on this post.
See plansRelated articles
- Explainability Under review
Machine Learning In Robotics Where Classical Contr
Let's start with the simplest classical controller: PID. The name stands for Proportional-Integral-Derivative, but you don't need to remember that. Here's what it actually does:
- Explainability Under review
Machine Learning In Banking And Finance Credit Ris
Picture this: you're a data scientist at a mid-sized bank. Every morning, three different problems land on your desk.
- Explainability Under review
SHAP Values: Why Your Model Predicted That
Learn to use SHAP values to explain individual model predictions, see which features drove each decision, and uncover hidden bias using Python and force plots.
- Explainability Under review
LIME vs. SHAP: Choosing the Right 'Translator' for Your Black-Box Models
Learn how LIME's fast local perturbations and SHAP's game-theoretic Shapley values explain black-box model predictions, and when to use each for your projects.
Looking for something else?
Search every article by title, summary or topic.