Chapter 17 — Ensemble Classification Models
Random Forest • Extra Trees • Gradient Boosting • Histogram-Based Boosting
Combining many decision trees to improve stability, generalization, and predictive accuracy
| BRIDGE FROM CHAPTER 16 A single decision tree is easy to read but can have high variance. Ensemble methods deliberately combine many trees so that the weaknesses of one tree are reduced by the collective prediction. |
Chapter overview
Ensemble classification replaces the idea of relying on one fitted model with a stronger principle: build several related models and combine their predictions. Tree ensembles are especially important because a single decision tree is flexible but unstable. Random forests and Extra Trees create many diverse trees and average them, while gradient boosting constructs trees sequentially so that each stage improves the mistakes left by the current ensemble.
This chapter develops four practical ensemble families. Random forests use bootstrap resampling and random feature selection to decorrelate trees. Extra Trees introduce even more randomization by drawing candidate split thresholds. Gradient boosting adds shallow trees sequentially under a differentiable loss, with the learning rate and number of estimators controlling how aggressively the ensemble adapts. Histogram-based gradient boosting bins continuous features so large tabular datasets can be processed more efficiently and, in scikit-learn, provides native missing-value support.
| CENTRAL IDEA An ensemble succeeds when its component models are individually useful but make sufficiently different errors. Diversity reduces the chance that the same training-sample accident dominates every prediction. |
Learning objectives
- Explain why combining models can reduce variance and improve predictive stability.
- Distinguish averaging ensembles from sequential boosting ensembles.
- Explain bootstrap sampling, random feature selection, voting, and out-of-bag evaluation in a random forest.
- Compare random forests with Extra Trees and explain the effect of randomized split thresholds.
- Describe gradient boosting as sequential correction under a loss function and relate weak learners to ensemble strength.
- Explain the roles of learning_rate, n_estimators, tree depth, and regularization in the bias-variance trade-off.
- Explain why histogram binning accelerates gradient-boosted tree training on larger datasets.
- Use native missing-value handling in histogram-based gradient boosting when appropriate.
- Compare decision trees, random forests, and gradient boosting under one leakage-safe experimental protocol.
- Interpret impurity-based and permutation feature importance cautiously and avoid causal conclusions.
Table 17.1. Chapter structure
Section | Focus | Student outcome |
|---|---|---|
| 17.1 | Motivation for ensembles | Explain averaging, diversity, variance reduction, and stability |
| 17.2 | Random forest | Connect bootstrap samples, random features, voting, OOB scoring, and importance |
| 17.3 | Extra Trees | Explain additional split randomness and compare speed, bias, and variance |
| 17.4 | Gradient boosting | Understand sequential correction, weak learners, shrinkage, and complexity |
| 17.5 | Histogram gradient boosting | Use binned features, efficient training, and missing-value support |
| 17.6 | Model comparison | Select an ensemble according to accuracy, interpretability, compute, and data size |
| Lab | Tree vs forest vs boosting | Run a fair model comparison and protect the final test set |
17.1 Motivation for ensemble learning
A decision tree can adapt to nonlinear relationships and interactions, but it is sensitive to the exact observations used for training. If a slightly different sample changes the first few splits, the entire downstream tree can change. Ensemble learning addresses this instability by combining the predictions of multiple fitted models instead of trusting one model structure.
17.1.1 Combining multiple models
Figure 17.1. The ensemble principle
MODEL 1 | → | MODEL 2 | → | MODEL 3 | → | COMBINE |
different sample or randomness | different sample or randomness | different sample or randomness | vote or average probabilities |
The component models are often called base estimators or base learners. In tree ensembles, each base estimator is a decision tree produced under a different sample, feature subset, split randomization, or stage of boosting. The final classifier aggregates these component outputs into one prediction.
Table 17.2. Two major ensemble strategies
Strategy | How models are built | How predictions combine | Typical examples |
|---|---|---|---|
| Averaging / bagging-style | Models are trained largely independently with injected randomness | Average class probabilities or aggregate votes | Random forest, Extra Trees |
| Boosting | Models are added sequentially, each improving the current ensemble | Sum stage contributions, then transform to probabilities/classes | Gradient boosting, histogram gradient boosting |
17.1.2 Reducing variance
Variance describes sensitivity to the particular training sample. A high-variance model can fit one dataset extremely well but change noticeably when observations are added, removed, or resampled. Deep decision trees are a classic high-variance model family. Averaging several diverse trees can cancel part of this sample-specific noise because an unusual branch that appears in one tree is unlikely to appear in exactly the same form in every tree.
| VARIANCE INTUITION Averaging is most effective when the individual models are not perfectly correlated. If every tree makes the same mistake for the same reason, adding more identical trees does not remove that error. |
This is why random forests inject randomness at two levels: the training observations are resampled and the candidate features considered at each split are restricted. The goal is not to make every tree maximally accurate by itself. The goal is to create a collection whose averaged prediction generalizes well.
17.1.3 Improving predictive stability
Stability refers to how much the model output changes under reasonable perturbations of the training data or random seed. An ensemble can be more stable than a single tree because the final decision is distributed across many structures. One tree might classify an observation differently after a small resample, while a forest of hundreds of trees may preserve nearly the same average probability.
| PYTHON • Observe instability of single trees versus a forest |
| from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier X, y = load_breast_cancer(return_X_y=True) query_probabilities = {"tree": [], "forest": []} for seed in range(20): X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, stratify=y, random_state=seed ) tree = DecisionTreeClassifier(random_state=seed) forest = RandomForestClassifier( n_estimators=300, max_features="sqrt", random_state=seed, n_jobs=-1, ) tree.fit(X_train, y_train) forest.fit(X_train, y_train) query_probabilities["tree"].append(tree.predict_proba(X_test[[0]])[0, 1]) query_probabilities["forest"].append(forest.predict_proba(X_test[[0]])[0, 1]) print("Tree probabilities:", query_probabilities["tree"][:5]) print("Forest probabilities:", query_probabilities["forest"][:5]) |
| EXPERIMENTAL CAUTION The query row changes when the split seed changes in this short demonstration. For a formal stability study, hold a fixed external query set and repeatedly resample only the training data. |
17.1.4 Increasing model accuracy
Higher accuracy is a possible consequence of ensembling, not an automatic guarantee. A random forest may substantially outperform one unrestricted tree because variance is reduced. Gradient boosting may achieve even higher predictive accuracy on structured tabular data by correcting systematic residual errors. But ensemble gains depend on the dataset, metrics, hyperparameters, leakage control, and evaluation design.
| MODEL-SELECTION RULE Do not choose an ensemble because it is more sophisticated. Choose it because it produces a meaningful, reproducible improvement under the metric and deployment constraints defined for the problem. |
17.2 Random forest
A random forest is an averaging ensemble of decision trees. Each tree is trained with controlled randomness so the trees do not all reproduce the same structure. For classification, the forest combines the trees through class-probability averaging or an equivalent aggregate decision. The resulting model is usually less sensitive to sampling noise than a single deep tree.
Figure 17.2. Random forest training and prediction
BOOTSTRAP | → | RANDOM FEATURES | → | MANY TREES | → | AVERAGE |
sample rows with replacement | subset of features at each split | fit trees independently | combine class probabilities |
17.2.1 Bootstrap samples
A bootstrap sample is created by drawing training observations with replacement. If the training set contains n rows, a bootstrap sample also contains n draws by default, but some original rows can appear more than once and others may not appear at all. Different trees receive different bootstrap samples, which helps create diversity.
| OUT-OF-BAG INTUITION For a bootstrap sample of size n drawn from n training rows, an individual row is left out of a particular sample with probability close to 36.8% when n is large. These unused rows form that tree’s out-of-bag observations. |
| PYTHON • Inspect bootstrap behavior with resampling |
| import numpy as np rng = np.random.default_rng(42) n = 12 sample_indices = rng.choice(n, size=n, replace=True) selected = set(sample_indices.tolist()) out_of_bag = sorted(set(range(n)) - selected) print("Bootstrap draw:", sample_indices) print("Unique selected rows:", len(selected)) print("Out-of-bag rows:", out_of_bag) |
17.2.2 Multiple decision trees
Each bootstrap sample is used to fit a decision tree. Forest trees are often allowed to grow relatively deep because the ensemble will average their predictions. A single deep tree may overfit strongly, but hundreds of diverse deep trees can still produce a stable aggregate. Parameters such as max_depth, min_samples_leaf, and max_leaf_nodes remain available when stronger regularization is needed.
The number of trees is controlled by n_estimators. Increasing n_estimators usually makes the aggregate prediction more stable until the performance plateaus. It also increases training time, prediction time, and model size. Unlike tree depth, adding more trees normally does not create the same kind of overfitting curve; the main practical question is when additional trees stop producing a useful improvement.
17.2.3 Random feature selection
If every tree could consider every feature at every split, a very strong predictor might dominate the root of most trees, making the forest highly correlated. Random forests therefore consider only a random subset of features at each candidate split. The max_features parameter controls the size of that subset. For classification, sqrt is a common default because it creates substantial diversity while preserving informative choices.
Table 17.3. Sources of diversity in a random forest
Mechanism | What changes | Why it helps |
|---|---|---|
| Bootstrap rows | Each tree sees a different resampled training set | Perturbs the observations that influence the learned rules |
| Random feature subset | Each split sees only part of the feature set | Prevents the same dominant feature from controlling every tree |
| Random tie behavior | Equivalent candidate choices may resolve differently | Adds minor additional structural diversity |
17.2.4 Majority voting and probability averaging
A simplified classroom explanation says that the trees vote for a class and the majority wins. In scikit-learn’s RandomForestClassifier, the class probabilities predicted by the individual trees are averaged, and the final class is selected from those averaged probabilities. This distinction matters when leaf class distributions are not pure.
| FOREST PROBABILITY For class k, the forest probability can be viewed as the average of the class-k probabilities produced by the fitted trees. The final label is the class with the largest averaged probability. |
17.2.5 Training a random forest
| PYTHON • Fit a random forest with reproducible settings |
| from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, roc_auc_score data = load_breast_cancer(as_frame=True) X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, stratify=y, random_state=42, ) forest = RandomForestClassifier( n_estimators=400, max_features="sqrt", min_samples_leaf=2, bootstrap=True, oob_score=True, random_state=42, n_jobs=-1, ) forest.fit(X_train, y_train) pred = forest.predict(X_test) prob = forest.predict_proba(X_test)[:, 1] print("Test accuracy:", round(accuracy_score(y_test, pred), 3)) print("Test ROC AUC:", round(roc_auc_score(y_test, prob), 3)) print("OOB score:", round(forest.oob_score_, 3)) |
| REPRODUCIBILITY NOTE Set random_state when you want to reproduce the same bootstrap draws and randomized feature choices. n_jobs=-1 uses available CPU cores for independent tree operations and can reduce training time. |
17.2.6 Out-of-bag evaluation
Out-of-bag evaluation uses observations that were not included in a tree’s bootstrap sample. For each training observation, the forest can aggregate predictions only from trees for which that observation was out of bag. These predictions provide an internal estimate of generalization without creating a separate validation split for that specific purpose.
In scikit-learn, set oob_score=True together with bootstrap=True. The fitted classifier exposes oob_score_ and oob_decision_function_. The OOB estimate is useful for quick diagnostics and for following forest convergence, but it does not replace a carefully designed test set, group-aware split, temporal validation, or cross-validation when the data structure requires them.
| PYTHON • Inspect out-of-bag class probabilities |
| oob_probabilities = forest.oob_decision_function_ print("OOB probability matrix shape:", oob_probabilities.shape) print("First OOB probability vector:", oob_probabilities[0]) print("OOB score:", forest.oob_score_) |
| OOB LIMITATION A row may receive too few OOB predictions when the forest contains very few trees. Use enough estimators, and do not interpret OOB scoring as protection against time leakage, entity leakage, or an invalid target definition. |
17.2.7 Feature importance
A fitted random forest exposes impurity-based feature_importances_. The importance of a feature accumulates the weighted impurity reduction produced by splits using that feature across the ensemble. This can be useful for exploratory ranking, but it is model-specific and can favor continuous or high-cardinality variables with many possible split points. Correlated features can also divide or substitute for one another’s importance.
| PYTHON • Rank impurity-based forest importance |
| import pandas as pd importance = pd.Series( forest.feature_importances_, index=X_train.columns, ).sort_values(ascending=False) print(importance.head(10)) |
Permutation importance provides a more model-agnostic alternative. It measures how much a chosen evaluation score deteriorates when one feature is randomly permuted while the others remain unchanged. It should be computed on validation or test-like data rather than only on the training data.
| PYTHON • Compute permutation importance on held-out data |
| from sklearn.inspection import permutation_importance result = permutation_importance( forest, X_test, y_test, scoring="roc_auc", n_repeats=20, random_state=42, n_jobs=-1, ) permutation_rank = pd.Series( result.importances_mean, index=X_test.columns, ).sort_values(ascending=False) print(permutation_rank.head(10)) |
| INTERPRETATION CAUTION Feature importance is not causal importance. A feature can be predictive because it is correlated with an outcome, a proxy, a workflow artifact, or even a leakage variable. Always check availability at prediction time and domain plausibility. |
17.3 Extra Trees
Extremely Randomized Trees, implemented by ExtraTreesClassifier, form another ensemble of randomized decision trees. Like random forests, they use random subsets of candidate features. The main additional randomization is the split threshold: instead of exhaustively searching the best threshold for each candidate feature, random thresholds are drawn and the best among those randomized candidates is selected.
Figure 17.3. Random forest versus Extra Trees at a split
RANDOM FOREST | → | CANDIDATE FEATURES | → | EXTRA TREES |
search strong thresholds | random feature subset | draw random thresholds |
17.3.1 Increased split randomness
Extra randomness can decorrelate the fitted trees even when they see the same training observations. This can lower variance further, although the additional randomness can increase bias because the algorithm may not choose the locally optimal threshold available for the current node.
| BIAS-VARIANCE TRADE-OFF Extra Trees typically trades a little more bias for lower correlation and potentially lower variance. Whether this improves validation performance is empirical and dataset-dependent. |
17.3.2 Differences from random forests
Table 17.4. Random forest and Extra Trees
Aspect | Random forest | Extra Trees |
|---|---|---|
| Training rows | Bootstrap sampling is commonly used | Uses the full training sample by default; bootstrap is optional |
| Feature candidates | Random subset at each split | Random subset at each split |
| Threshold selection | Searches candidate thresholds to optimize the split criterion | Draws random thresholds and selects among them |
| Tree correlation | Reduced by row and feature randomness | Often reduced further by threshold randomness |
| Typical bias | Moderate | Can be slightly higher |
| Typical variance | Lower than a single tree | Can be lower still when randomness decorrelates trees |
| Parallelism | Trees can be fit in parallel | Trees can be fit in parallel |
17.3.3 Speed and variance considerations
Random threshold generation can reduce the amount of split-search work, so Extra Trees may train faster than a comparable random forest on some datasets. The exact runtime depends on sample size, feature count, tree depth, data representation, number of estimators, hardware, and parallelism. The stronger randomization also means that Extra Trees should not automatically be described as “better” or “worse”; it should be evaluated under the same cross-validation and resource constraints as the random forest.
| PYTHON • Compare random forest and Extra Trees under the same folds |
| from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier from sklearn.model_selection import StratifiedKFold, cross_validate cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) models = { "random_forest": RandomForestClassifier( n_estimators=400, max_features="sqrt", min_samples_leaf=2, random_state=42, n_jobs=-1, ), "extra_trees": ExtraTreesClassifier( n_estimators=400, max_features="sqrt", min_samples_leaf=2, random_state=42, n_jobs=-1, ), } for name, model in models.items(): scores = cross_validate( model, X_train, y_train, cv=cv, scoring=["accuracy", "roc_auc"], return_train_score=False, n_jobs=-1, ) print( name, "accuracy=", round(scores["test_accuracy"].mean(), 3), "roc_auc=", round(scores["test_roc_auc"].mean(), 3), ) |
| CURRENT IMPLEMENTATION NOTE In current scikit-learn, ExtraTreesClassifier uses bootstrap=False by default; out-of-bag scoring is therefore available only when bootstrap=True is explicitly enabled. |
17.4 Gradient boosting
Gradient boosting builds an additive model sequentially. The first stage creates an initial prediction. Each later tree is fitted to improve the current ensemble according to the selected loss function. Instead of averaging many independent trees, the algorithm constructs a coordinated sequence in which later stages focus on remaining errors.
Figure 17.4. Sequential correction in gradient boosting
INITIAL MODEL | → | TREE 1 | → | TREE 2 | → | TREE 3 | → | FINAL |
starting prediction | correct current errors | correct remaining errors | refine again | sum stage outputs |
17.4.1 Sequential correction of errors
For squared-error regression, boosting is often introduced as fitting residuals. For classification, the underlying formulation is more general: each stage fits a tree to the negative gradient of the classification loss with respect to the current model score. The practical intuition remains useful—new trees concentrate on aspects of the data that the current ensemble handles poorly.
| ADDITIVE MODEL A boosting ensemble can be written conceptually as F_M(x) = F_0(x) + learning_rate × Σ h_m(x), where each h_m is a fitted stage and M is the number of boosting iterations. |
17.4.2 Weak learners
Boosting commonly uses shallow decision trees as weak learners. A weak learner is not expected to solve the entire classification problem independently. Its role is to contribute a modest, structured improvement that becomes useful after many stages are combined. Tree depth therefore has a different interpretation than in a single decision tree: shallow trees often work well because the sequence can gradually build complex decision boundaries.
Table 17.5. Tree depth in boosting
Tree size | What each stage can represent | Typical effect |
|---|---|---|
| Decision stump / very shallow | Simple threshold or low-order interaction | High bias per tree, strong regularization |
| Depth 2–3 | Moderate interactions | Common practical compromise |
| Deep trees | Complex local structure per stage | Lower bias but higher variance and greater overfitting risk |
17.4.3 Learning rate
The learning rate scales the contribution of each new tree. A smaller learning_rate means the ensemble changes more cautiously at each stage and usually needs more estimators. This shrinkage can improve generalization because no single tree is allowed to dominate the fit. A large learning rate reaches a strong training fit quickly but can overshoot useful structure or overfit earlier.
| COUPLED HYPERPARAMETERS learning_rate and n_estimators should be considered together. Reducing the learning rate without increasing the number of stages can underfit; increasing the number of stages without sufficient shrinkage can overfit. |
17.4.4 Number of estimators
n_estimators controls how many boosting stages are added in GradientBoostingClassifier. Too few stages leave systematic errors uncorrected. Too many can fit noise, especially when individual trees are deep or the learning rate is high. Validation curves or cross-validation help identify a useful region.
| PYTHON • Train a gradient boosting classifier |
| from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import accuracy_score, roc_auc_score gb = GradientBoostingClassifier( n_estimators=250, learning_rate=0.05, max_depth=2, min_samples_leaf=5, random_state=42, ) gb.fit(X_train, y_train) pred = gb.predict(X_test) prob = gb.predict_proba(X_test)[:, 1] print("Accuracy:", round(accuracy_score(y_test, pred), 3)) print("ROC AUC:", round(roc_auc_score(y_test, prob), 3)) |
17.4.5 Inspecting the number of stages
GradientBoostingClassifier exposes staged prediction methods that make it possible to evaluate the partially built ensemble after each boosting iteration. This is useful for understanding when validation performance stops improving.
| PYTHON • Build a validation curve across boosting stages |
| import matplotlib.pyplot as plt from sklearn.metrics import roc_auc_score train_auc = [] valid_auc = [] for train_prob, valid_prob in zip( gb.staged_predict_proba(X_train), gb.staged_predict_proba(X_test), ): train_auc.append(roc_auc_score(y_train, train_prob[:, 1])) valid_auc.append(roc_auc_score(y_test, valid_prob[:, 1])) plt.figure(figsize=(8, 4.5)) plt.plot(train_auc, label="Training ROC AUC") plt.plot(valid_auc, label="Validation ROC AUC") plt.xlabel("Boosting stage") plt.ylabel("ROC AUC") plt.title("Performance across gradient boosting stages") plt.legend() plt.tight_layout() plt.show() |
| EVALUATION DISCIPLINE The code above uses X_test as a validation example only for illustration. In a real workflow, reserve a validation set or cross-validation for stage selection and keep the final test set untouched. |
17.4.6 Tree depth and interactions
The max_depth parameter controls the depth of each regression tree used internally by GradientBoostingClassifier. A depth-1 tree can represent one main split at a stage, while deeper trees can model conditional interactions. Because hundreds of stages can be added, moderate depths often provide enough flexibility for tabular classification without making every stage excessively complex.
17.4.7 Bias-variance trade-off
Table 17.6. Gradient boosting complexity controls
Parameter change | Likely training effect | Generalization risk |
|---|---|---|
| Increase n_estimators | More opportunities to reduce training loss | Can overfit if continued too far |
| Decrease learning_rate | Smaller correction at each stage | May need more stages and training time |
| Increase max_depth | More complex correction per stage | Higher variance and interaction complexity |
| Increase min_samples_leaf | Smoother, less local leaves | Can increase bias but reduce overfitting |
| Use subsample < 1 | Introduces stochastic boosting | Can reduce variance but adds randomness |
| PRACTICAL TUNING ORDER Start with shallow trees, choose a conservative learning rate, then adjust the number of stages with validation. Only add depth when the simpler boosted model clearly underfits. |
17.5 Histogram-based gradient boosting
Traditional gradient boosting repeatedly evaluates ordered feature values while constructing trees. Histogram-based gradient boosting first bins continuous values into a limited number of discrete intervals. Split search then operates on compact bin statistics rather than sorting every raw feature value at every node. This can make training substantially faster on larger datasets.
Figure 17.5. Histogram-based split search
RAW VALUES | → | BIN VALUES | → | HISTOGRAMS | → | SPLITS |
continuous measurements | map to integer bins | aggregate bin stats | search bin boundaries |
17.5.1 Efficient processing of larger datasets
The computational advantage becomes important when the dataset contains many thousands of observations or more. Scikit-learn’s user guide notes that HistGradientBoostingClassifier can be orders of magnitude faster than GradientBoostingClassifier when sample sizes are above tens of thousands. For small datasets, the traditional implementation can remain competitive because binning itself approximates the available split points.
The number of boosting iterations is controlled by max_iter rather than n_estimators. Other important controls include learning_rate, max_leaf_nodes, max_depth, min_samples_leaf, l2_regularization, and early_stopping.
17.5.2 Handling continuous variables
Continuous features are quantized into bins. Observations in the same bin are treated together during split search, which reduces the cost of evaluating candidate thresholds. The model still learns nonlinear tree-based relationships; the bins are an internal computational representation rather than a requirement that the analyst manually discretize every feature.
| DO NOT PRE-BIN AUTOMATICALLY Manual discretization before HistGradientBoostingClassifier is usually unnecessary unless the bins have domain meaning. Let the estimator construct its own internal histograms so binning remains part of the fitted model. |
17.5.3 Missing-value support
HistGradientBoostingClassifier has native support for NaN values. During training, the tree grower learns whether missing observations should be sent to the left or right child at each split according to the gain. At prediction time, missing values follow the learned direction. This can remove the need for a separate numerical imputation step when NaN has a meaningful and stable interpretation in the data pipeline.
| PYTHON • Fit histogram gradient boosting with missing values |
| import numpy as np from sklearn.ensemble import HistGradientBoostingClassifier X_missing = X_train.copy() # Educational example: insert missing values in one feature. rng = np.random.default_rng(42) rows = rng.choice(len(X_missing), size=25, replace=False) X_missing.iloc[rows, 0] = np.nan hist_gb = HistGradientBoostingClassifier( learning_rate=0.08, max_iter=250, max_leaf_nodes=15, min_samples_leaf=10, l2_regularization=0.1, random_state=42, ) hist_gb.fit(X_missing, y_train) print("Training score:", round(hist_gb.score(X_missing, y_train), 3)) |
| MISSING DOES NOT MEAN HARMLESS Native NaN support prevents a technical failure, but the meaning of missingness still requires domain analysis. A missing value caused by future workflow, selective measurement, or data collection bias can remain a leakage or fairness problem. |
17.5.4 Traditional versus histogram gradient boosting
Table 17.7. Two scikit-learn gradient boosting implementations
Aspect | GradientBoostingClassifier | HistGradientBoostingClassifier |
|---|---|---|
| Split representation | Uses raw ordered feature values | Uses binned feature values and histograms |
| Iteration parameter | n_estimators | max_iter |
| Large datasets | Can become slower as sample size grows | Designed for efficient larger-data training |
| Missing values | Typically require preprocessing | NaN values supported natively |
| Small datasets | Can be attractive because exact split candidates remain available | Binning can be an unnecessary approximation |
| Feature importance attribute | Provides impurity-based feature_importances_ | Prefer model-agnostic approaches such as permutation importance |
| PYTHON • Compare traditional and histogram boosting runtimes |
| from time import perf_counter from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier models = { "gradient_boosting": GradientBoostingClassifier( n_estimators=200, learning_rate=0.05, max_depth=2, random_state=42, ), "hist_gradient_boosting": HistGradientBoostingClassifier( max_iter=200, learning_rate=0.05, max_leaf_nodes=15, random_state=42, ), } for name, model in models.items(): start = perf_counter() model.fit(X_train, y_train) elapsed = perf_counter() - start print(name, "seconds=", round(elapsed, 4)) |
| RUNTIME EXPERIMENT Measure runtime on the dataset and hardware that matter to your project. A speed claim from a small classroom dataset does not automatically generalize to production scale. |
17.6 Comparing ensemble methods
Random forests, Extra Trees, gradient boosting, and histogram gradient boosting all use collections of trees, but they solve different modeling problems. Forest methods emphasize diversity and averaging. Boosting emphasizes sequential correction. The best choice depends on predictive accuracy, data size, missingness, interpretability expectations, latency, memory, training budget, and tuning capacity.
17.6.1 Random forest for robust general performance
A random forest is often a strong first nonlinear ensemble for tabular data. It requires relatively little feature scaling, tolerates complex interactions, offers straightforward parallel training across trees, provides OOB diagnostics, and usually reduces the instability of a single tree. It is therefore an excellent benchmark when a decision tree is too variable but a heavily tuned boosting system is not yet justified.
17.6.2 Gradient boosting for high predictive accuracy
Gradient boosting is often highly competitive on structured tabular datasets because later stages can focus on systematic errors left by earlier stages. The benefit comes with more interacting hyperparameters and a sequential training process. Learning rate, number of stages, tree complexity, subsampling, regularization, and early stopping should be validated carefully rather than selected from training performance.
17.6.3 Interpretability and computation trade-offs
A small single tree can be explained as explicit rules. An ensemble containing hundreds of trees cannot be interpreted in the same direct way even though each component is a tree. Global importance, permutation importance, partial dependence, accumulated local effects, SHAP-style explanations, and local surrogate tools can summarize behavior, but they are explanations of a complex system rather than a replacement for a simple rule list.
Table 17.8. Practical comparison of tree classifiers
Criterion | Decision tree | Random forest | Extra Trees | Gradient boosting | Hist gradient boosting |
|---|---|---|---|---|---|
| Main mechanism | One recursive tree | Bootstrap + random features + averaging | Random features + random thresholds + averaging | Sequential additive correction | Sequential correction using histogram bins |
| Variance | High when unrestricted | Usually much lower | Often very low through extra randomization | Controlled by shrinkage and tree size | Controlled by shrinkage, leaf limits, regularization |
| Training parallelism | Single tree | High across trees | High across trees | Stages are sequential | Stages are sequential but split computation is optimized |
| Scaling needed | Usually no | Usually no | Usually no | Usually no | Usually no |
| OOB evaluation | No | Yes with bootstrap | Only if bootstrap is enabled | Possible only in stochastic settings; CV often preferred | Use validation / CV / early stopping |
| Native NaN support in current sklearn | Estimator-dependent / check version | Current implementations have specific missing-value behavior; validate carefully | Current implementation supports NaN random splits | Typically preprocess NaN | Yes |
| Direct rule interpretability | High for small tree | Low as an ensemble | Low as an ensemble | Low as an ensemble | Low as an ensemble |
| Typical first use | Interpretable baseline | Robust nonlinear benchmark | Fast randomized benchmark | Accuracy-focused tabular model | Larger tabular data and/or NaNs |
| SELECTION PRINCIPLE Compare models under identical folds, features, preprocessing, metrics, and resource accounting. A 0.2-point metric gain may not justify a 10× increase in latency, memory, tuning effort, or explanation complexity. |
17.6.4 A compact decision guide
Table 17.9. Which model should I try?
Situation | Good starting candidate | Reason |
|---|---|---|
| Need a readable rule structure | Constrained decision tree | Direct paths and thresholds are visible |
| Single tree is unstable | Random forest | Averaging reduces sampling variance |
| Want stronger randomization and fast tree ensemble | Extra Trees | Random thresholds decorrelate trees |
| Need very strong tabular accuracy and can tune | Gradient boosting | Sequential correction can capture subtle structure |
| Large tabular dataset | HistGradientBoostingClassifier | Histogram split search is optimized for scale |
| Numerical features contain NaNs | HistGradientBoostingClassifier | Native missing-value routing |
| Need OOB diagnostic | RandomForestClassifier | OOB estimates are directly supported with bootstrap |
Practical lab — Compare a decision tree, random forest, and gradient boosting classifier
Students compare three tree-based classifiers under one controlled binary-classification experiment. The Breast Cancer Wisconsin diagnostic dataset is used because it is built into scikit-learn, contains only numerical features, and allows the lab to focus on model behavior rather than data acquisition. The exercise is educational and must not be interpreted as a clinical decision system.
Lab objectives
- Create stratified training, validation, and test subsets.
- Train one constrained decision tree, one random forest, and one gradient boosting classifier.
- Compare training and validation accuracy, balanced accuracy, ROC AUC, runtime, and model complexity.
- Inspect the random forest out-of-bag score without using it as the only model-selection criterion.
- Plot validation ROC curves for the three models.
- Select a model using a predeclared validation rule before touching the final test set.
- Refit the selected configuration on development data and perform one final test evaluation.
- Explain why the most accurate model is not automatically the best operational model.
Table 17.10. Practical lab design
Element | Choice | Reason |
|---|---|---|
| Dataset | Breast Cancer Wisconsin (diagnostic) | Built-in numerical binary classification dataset |
| Split | 60% train / 20% validation / 20% test | Separates model comparison from final evaluation |
| Stratification | Use target labels in both splits | Preserves class proportions approximately |
| Scaling | None | Tree splits are based on order and thresholds rather than feature scale |
| Primary selection metric | Validation ROC AUC | Uses ranking information across thresholds |
| Secondary metric | Balanced accuracy | Makes both classes contribute equally to the score |
| Tie rule | Prefer lower validation time/complexity when scores are practically tied | Avoid unnecessary complexity |
| Random state | 42 | Reproducible split and randomized model behavior |
| CLASS MEANING In this dataset, scikit-learn encodes malignant as 0 and benign as 1. Always attach the class names to class-specific precision, recall, and confusion-matrix results. |
Step 1 — Load the dataset and tools
| PYTHON • Load data and evaluation functions |
| from time import perf_counter import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_breast_cancer from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.metrics import ( accuracy_score, balanced_accuracy_score, classification_report, confusion_matrix, roc_auc_score, roc_curve, ) from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier data = load_breast_cancer(as_frame=True) X = data.data y = data.target print("Shape:", X.shape) print("Class names:", list(data.target_names)) print(y.value_counts().sort_index()) |
Step 2 — Create protected subsets
| PYTHON • Create 60/20/20 stratified partitions |
| X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=0.40, stratify=y, random_state=42, ) X_valid, X_test, y_valid, y_test = train_test_split( X_temp, y_temp, test_size=0.50, stratify=y_temp, random_state=42, ) print("Train:", X_train.shape) print("Validation:", X_valid.shape) print("Test:", X_test.shape) |
| PROTECTED TEST SET Do not use X_test or y_test to choose hyperparameters, select a model family, choose a threshold, or redesign features. The test set is opened only after the comparison rule is frozen. |
Step 3 — Define three comparable models
| PYTHON • Create the candidate classifiers |
| models = { "decision_tree": DecisionTreeClassifier( max_depth=4, min_samples_leaf=5, random_state=42, ), "random_forest": RandomForestClassifier( n_estimators=400, max_features="sqrt", min_samples_leaf=2, bootstrap=True, oob_score=True, random_state=42, n_jobs=-1, ), "gradient_boosting": GradientBoostingClassifier( n_estimators=250, learning_rate=0.05, max_depth=2, min_samples_leaf=5, random_state=42, ), } |
These settings are intentionally moderate rather than extensively tuned. The goal of the first comparison is to understand model-family behavior. A later extension can tune each family with cross-validation under a controlled search budget.
Step 4 — Train and collect validation metrics
| PYTHON • Fit each model under one evaluation function |
| def evaluate_model(name, model): start = perf_counter() model.fit(X_train, y_train) fit_seconds = perf_counter() - start train_pred = model.predict(X_train) valid_pred = model.predict(X_valid) valid_prob = model.predict_proba(X_valid)[:, 1] return { "model": name, "fit_seconds": fit_seconds, "train_accuracy": accuracy_score(y_train, train_pred), "valid_accuracy": accuracy_score(y_valid, valid_pred), "valid_balanced_accuracy": balanced_accuracy_score(y_valid, valid_pred), "valid_roc_auc": roc_auc_score(y_valid, valid_prob), } records = [ evaluate_model(name, model) for name, model in models.items() ] results = pd.DataFrame(records).sort_values( "valid_roc_auc", ascending=False, ) print(results.round(4).to_string(index=False)) |
Step 5 — Compare overfitting gaps
| PYTHON • Measure the training-validation accuracy gap |
| results["accuracy_gap"] = ( results["train_accuracy"] - results["valid_accuracy"] ) print( results[[ "model", "train_accuracy", "valid_accuracy", "accuracy_gap", ]].round(4).to_string(index=False) ) |
- Which model has the largest gap between training and validation accuracy?
- Does the random forest reduce the instability expected from a single tree?
- Does gradient boosting improve validation ranking performance without producing an excessive gap?
- Would a very high training score change your selection if validation performance were weaker?
Step 6 — Inspect the random forest OOB estimate
| PYTHON • Compare OOB and validation accuracy |
| forest = models["random_forest"] print("Forest OOB score:", round(forest.oob_score_, 4)) print( "Forest validation accuracy:", round(forest.score(X_valid, y_valid), 4), ) |
The two numbers do not need to be identical. They are based on different prediction sets and sampling mechanisms. The purpose is to see whether the OOB estimate gives a plausible internal diagnostic consistent with held-out validation behavior.
Step 7 — Plot validation ROC curves
| PYTHON • Compare ranking behavior across thresholds |
| plt.figure(figsize=(7.5, 5.5)) for name, model in models.items(): prob = model.predict_proba(X_valid)[:, 1] fpr, tpr, _ = roc_curve(y_valid, prob) auc = roc_auc_score(y_valid, prob) plt.plot(fpr, tpr, label=f"{name} (AUC={auc:.3f})") plt.plot([0, 1], [0, 1], linestyle="--", label="Chance") plt.xlabel("False positive rate") plt.ylabel("True positive rate") plt.title("Validation ROC curves") plt.legend() plt.tight_layout() plt.show() |
| THRESHOLD PERSPECTIVE ROC AUC compares ranking quality across thresholds. A deployed classifier still requires an operating threshold aligned with error costs, capacity, and class-specific consequences. |
Step 8 — Add structural diagnostics
| PYTHON • Summarize model structure and forest size |
| tree = models["decision_tree"] gb = models["gradient_boosting"] print("Decision tree depth:", tree.get_depth()) print("Decision tree leaves:", tree.get_n_leaves()) print("Random forest trees:", len(forest.estimators_)) print("Gradient boosting stages:", gb.n_estimators_) print("Trees per boosting stage:", gb.n_trees_per_iteration_) |
Model complexity cannot be reduced to one universal number. A single depth-4 tree, 400 independently fitted forest trees, and 250 sequential boosting stages represent different computational and explanatory structures. Record these differences rather than comparing only predictive scores.
Step 9 — Apply a predeclared selection rule
For this lab, select the model with the highest validation ROC AUC. If two models differ by less than 0.005 ROC AUC, prefer the one with the higher validation balanced accuracy. If both metrics are effectively tied, prefer the simpler or faster operational choice and document the reason.
| PYTHON • Select the model without reading the test set |
| selection = results.copy() best_auc = selection["valid_roc_auc"].max() near_best = selection[ selection["valid_roc_auc"] >= best_auc - 0.005 ].copy() selected_row = near_best.sort_values( ["valid_balanced_accuracy", "fit_seconds"], ascending=[False, True], ).iloc[0] selected_name = selected_row["model"] print("Selected model:", selected_name) print(selected_row.round(4)) |
| PREDECLARE BEFORE FINAL EVALUATION The exact tie tolerance and decision hierarchy should be defined before seeing test results. Otherwise, apparently objective rules can be modified until they favor the preferred test outcome. |
Step 10 — Refit the selected model on development data
| PYTHON • Combine train and validation after model selection |
| X_development = pd.concat([X_train, X_valid]) y_development = pd.concat([y_train, y_valid]) selected_model = models[selected_name] selected_model.fit(X_development, y_development) |
Step 11 — Evaluate the selected model once on test data
| PYTHON • Produce the final held-out evaluation |
| test_pred = selected_model.predict(X_test) test_prob = selected_model.predict_proba(X_test)[:, 1] print("Final test accuracy:", round(accuracy_score(y_test, test_pred), 3)) print( "Final test balanced accuracy:", round(balanced_accuracy_score(y_test, test_pred), 3), ) print("Final test ROC AUC:", round(roc_auc_score(y_test, test_prob), 3)) print() print("Confusion matrix:") print(confusion_matrix(y_test, test_pred)) print() print(classification_report( y_test, test_pred, target_names=data.target_names, digits=3, )) |
| CLINICAL-USE WARNING This lab demonstrates machine-learning methodology only. The built-in dataset, split, metrics, and model comparison are not sufficient for clinical deployment, diagnosis, or patient-level decision making. |
Step 12 — Inspect feature importance using a common method
Because the three model families expose different internal structures, permutation importance provides a common post-hoc comparison method. Compute it on validation-like or final evaluation data only after deciding what interpretive question you are asking.
| PYTHON • Calculate permutation importance for the selected model |
| from sklearn.inspection import permutation_importance perm = permutation_importance( selected_model, X_test, y_test, scoring="roc_auc", n_repeats=20, random_state=42, n_jobs=-1, ) importance = pd.Series( perm.importances_mean, index=X_test.columns, ).sort_values(ascending=False) print(importance.head(10)) |
Required student interpretation
1. Compare the validation ROC AUC and balanced accuracy of the three candidate models. Which model ranked first under the declared rule?
2. Compare each model’s training and validation accuracy. Which model shows the strongest evidence of overfitting?
3. How close is the forest OOB score to its validation accuracy? What does the difference suggest?
4. Does the ensemble advantage come mainly from variance reduction, sequential error correction, or both?
5. How much extra fitting time did the ensembles require relative to the decision tree?
6. If the random forest and gradient boosting model are practically tied, which would you deploy and why?
7. Why would direct inspection of one small tree remain easier than explanation of a forest or boosting ensemble?
8. Why is permutation importance more comparable across the three candidate families than their internal importance mechanisms?
9. Why should the final test set be evaluated only after the selection rule has been frozen?
10. Write a 250–350 word recommendation that considers predictive performance, stability, computation, interpretability, and the limitations of this educational dataset.
Expected observations
- The constrained decision tree is usually the easiest model to explain but may have a larger training-validation sensitivity than the forest.
- The random forest usually produces a more stable validation score than a single tree because many randomized trees are averaged.
- Gradient boosting can be highly competitive because later stages focus on systematic errors left by earlier stages.
- The OOB estimate should be treated as an internal diagnostic rather than a replacement for a protected final test set.
- Training time and artifact size increase when hundreds of trees are stored.
- A small difference in ROC AUC may be less important than simpler deployment, faster prediction, lower memory use, or clearer explanations.
- Feature importance rankings can change across model families because they summarize different fitted decision structures.
Lab deliverables
- A reproducible notebook containing the protected split and three candidate models.
- A comparison table containing training accuracy, validation accuracy, balanced accuracy, ROC AUC, fit time, and overfitting gap.
- The random forest out-of-bag score and a short explanation of OOB evaluation.
- One validation ROC curve containing all three models.
- The declared model-selection rule and selected model name.
- One final test confusion matrix and classification report for the selected model only.
- A top-10 permutation-importance ranking with an interpretation caution.
- A 250–350 word recommendation comparing accuracy, stability, computation, and interpretability.
Extension challenges
- Add ExtraTreesClassifier and HistGradientBoostingClassifier to the comparison while keeping the same validation protocol.
- Replace the single validation set with StratifiedKFold cross-validation and report mean plus standard deviation for each metric.
- Create a controlled search over n_estimators and max_features for the random forest.
- Create a learning_rate × n_estimators experiment for gradient boosting and visualize the validation surface.
- Compare impurity-based forest importance with permutation importance and explain disagreements.
- Inject NaN values into selected numerical features and compare explicit median imputation with HistGradientBoostingClassifier native missing-value handling.
- Measure prediction latency and serialized model size in addition to predictive metrics.
- Repeat the comparison across several random split seeds and report performance variability.
Chapter summary
Ensemble classification improves on the limitations of a single decision tree by combining multiple trees. Random forests create diversity with bootstrap samples and random feature subsets, then average predictions. This design usually reduces variance and provides a useful out-of-bag diagnostic. Extra Trees adds another source of diversity by selecting from randomized split thresholds, which can reduce tree correlation and sometimes improve computational efficiency at the cost of additional bias.
Gradient boosting follows a different strategy. Trees are added sequentially so that each stage improves the current ensemble under the selected loss. The learning rate controls how strongly each stage contributes, the number of estimators controls how long boosting continues, and tree depth controls the complexity of each correction. These choices jointly determine the bias-variance trade-off. Histogram-based gradient boosting accelerates split search by binning continuous features and is especially attractive for larger tabular datasets; in scikit-learn it also supports NaN values natively.
Model choice should be based on honest validation rather than algorithm reputation. A random forest is a strong robust nonlinear benchmark. Gradient boosting can provide excellent predictive accuracy but needs careful tuning and sequential training. Extra Trees is worth comparing when stronger randomization is useful. Histogram gradient boosting is a practical option when sample size or missing values make it advantageous. Interpretability, runtime, memory, stability, probability quality, and deployment cost belong in the decision alongside the primary metric.
Table 17.11. Key terms
Term | Meaning |
|---|---|
| Ensemble | A model that combines predictions from multiple fitted estimators. |
| Base learner | An individual model that contributes to an ensemble. |
| Bootstrap sample | A training sample drawn with replacement from the original training data. |
| Out-of-bag observation | A training row not selected in a particular bootstrap sample. |
| Random feature selection | Considering only a random subset of features at a tree split. |
| Random forest | An averaging ensemble of randomized decision trees. |
| Extra Trees | A randomized tree ensemble that also draws candidate split thresholds randomly. |
| Gradient boosting | A sequential additive ensemble that fits new stages to improve the current loss. |
| Weak learner | A deliberately simple component model whose small contribution becomes useful in an ensemble. |
| Learning rate | The shrinkage factor applied to each boosting stage. |
| Histogram binning | Grouping continuous values into discrete bins for more efficient split search. |
| Permutation importance | Importance measured by the decrease in evaluation performance after permuting one feature. |
Knowledge check
1. Why can averaging many decision trees reduce variance?
2. Why would training identical trees on identical data fail to create a useful ensemble?
3. What is a bootstrap sample, and how does it create out-of-bag observations?
4. Why does a random forest restrict the candidate features considered at each split?
5. How does scikit-learn combine class predictions in RandomForestClassifier?
6. What is an OOB score, and why does it not replace a carefully designed test set?
7. How does Extra Trees introduce more randomness than a random forest?
8. Why can Extra Trees reduce variance while potentially increasing bias?
9. What does it mean for gradient boosting to build trees sequentially?
10. Why are shallow trees often suitable weak learners for boosting?
11. How are learning_rate and n_estimators related?
12. What happens when boosting trees are made too deep?
13. Why can histogram-based gradient boosting train faster on large tabular datasets?
14. How does HistGradientBoostingClassifier handle NaN values?
15. When would you prefer a random forest over gradient boosting even if their validation scores were similar?
16. Why is a forest harder to interpret directly than a depth-4 decision tree?
17. Why should impurity-based feature importance not be interpreted as causality?
18. What experimental controls are required for a fair comparison of a tree, forest, and boosted model?
Instructor notes and suggested timing
Table 17.12. Suggested teaching plan
Session | Content | Suggested duration | Teaching method |
|---|---|---|---|
| 1 | Ensemble motivation; variance, diversity, averaging | 60–75 min | Lecture, instability demonstration |
| 2 | Random forest: bootstrap, random features, OOB | 75–90 min | Board explanation + Python demonstration |
| 3 | Feature importance and Extra Trees | 60–75 min | Comparison exercise + code |
| 4 | Gradient boosting mechanism and tuning | 75–90 min | Sequential visual explanation + validation curves |
| 5 | Histogram-based gradient boosting | 60–75 min | Runtime/missing-value demonstration |
| 6 | Model-comparison framework | 60 min | Trade-off table + discussion |
| 7 | Practical lab | 120–150 min | Notebook work, model selection, interpretation |
| 8 | Lab review and extension | 60–90 min | Peer comparison and instructor correction |
| NEXT STEP The next chapter can build on these ensemble foundations by introducing systematic hyperparameter tuning, cross-validation search strategies, and more formal model-selection procedures. |
Reference note
Implementation details in this chapter were aligned with the scikit-learn 1.9.0 stable documentation for ensemble methods, including RandomForestClassifier, ExtraTreesClassifier, GradientBoostingClassifier, and HistGradientBoostingClassifier. Students should check the documentation for the installed scikit-learn version when reproducing examples in another environment.