Trees, Forests and Boosting
A tree learns local partitions; forests stabilise them by averaging diverse fits, while boosting reduces remaining error by directing each new tree at the current loss.
TLDR
- A decision tree learns a piecewise-constant map by asking local threshold questions. Its power comes from matching irregular interactions; its weakness is that one early split can redirect every rule below it.
- A random forest perturbs data and candidate features, then averages many unstable trees. Variance falls only when the trees disagree usefully; correlated trees leave a hard variance floor.
- Gradient boosting builds dependent trees. Each stage follows the negative gradient of the chosen loss, so it corrects structured leftovers, but can also spend capacity fitting corrupted labels.
- Tree ensembles are strong on many tabular tasks, not universally. Smooth additive relations, extrapolation, oblique geometry, causal questions and weak labels can make a simpler or differently structured model preferable.
- Choose by diagnosing error: test partition adequacy, resampling instability, residual learnability and noise pursuit. Complexity earns release only when its claimed mechanism improves held-out decisions.
The five cases that moved an entire tree
A team builds a small decision tree to estimate how many minutes a payment-dispute case will need in manual review. The inputs are ordinary table columns: evidence gaps, days open, amount band, prior disputes and channel. The first tree asks whether the evidence-gap score exceeds 0.42. Below that branch it asks about age; above it, about prior disputes. The tree is short, inspectable and accurate enough on the training sample.
Then five borderline cases are corrected after a data-quality review. Their evidence-gap values move by less than two hundredths. No outcome changes. Yet the retrained tree now starts with days open, not evidence gap. Several lower branches reverse order. A case that previously followed three rules follows a different three. Training error barely changes, but the visible explanation has been rewritten.
This is not a software defect. It is a consequence of greedy partitioning. Two candidate splits can have nearly equal estimated gains. A small sample perturbation makes one look slightly better, and every later split is conditioned on that early choice. The resulting predictor can be useful while its exact branch story is unstable.
The central claim is that a tree, a forest and a boosted ensemble buy accuracy through three different error mechanisms. A tree creates local regions. A forest perturbs and averages those regions to reduce sampling variance. Boosting makes each new region respond to the loss left by the current ensemble. The algorithms share tree-shaped components, but they do not solve the same problem in the same way.
The consequence is practical. “Try random forest and gradient boosting” is not yet a model-selection method. A team should first ask what kind of error the single tree exhibits. If predictions jump when the sample changes, averaging diverse trees has a causal target. If stable residual patterns remain, sequential correction has a causal target. If the signal is smooth, additive or geometrically misaligned with axis splits, neither ensemble has earned its complexity.
Part I
A tree writes local rules into feature space
Begin with a sheet of paper containing two columns. The horizontal coordinate is evidence gap; the vertical coordinate is days open. Every case has a known review time. A tree does not fit one global slope through the sheet. It cuts the sheet, first into two rectangles, then cuts one or both rectangles again. Every final rectangle receives a constant prediction, usually the average outcome of training cases inside it.
A decision tree is therefore a piecewise-constant function built by recursive partitioning. The tree diagram is only one representation. The more revealing object is the partition it writes into the input space. Each root-to-leaf path names a region; each leaf supplies the local prediction for that region.
The CART formulation made this recursive partitioning explicit for classification and regression, while ID3 developed an information-based route for inducing classification trees.[1][2] Implementations differ in split criteria, categorical handling, missing-value strategy and pruning, but the local operation is recognisable: select a feature and threshold that most improves a chosen impurity or loss.
For classification, the same geometry can use Gini impurity, entropy or another criterion. Gini impurity for a node is 1 − Σ pₖ², where pₖ is the fraction of samples in class k. A pure node has one class probability equal to one and impurity zero. A split is attractive when the weighted impurity of its children is lower than the parent’s.
A minimal worked example
Consider eight synthetic cases. Four have evidence-gap scores below 0.4 and review times near 25 minutes. Four exceed 0.4. Among those, cases open longer than five days cluster near 55 minutes, while newer cases cluster near 39. A depth-two regression tree can express the pattern with two threshold questions: first evidence gap, then age within the high-gap branch.
| Case group | Evidence gap | Days open | Mean review time | Leaf rule |
|---|---|---|---|---|
| A | ≤ 0.40 | Any | 25 min | Low local burden |
| B | > 0.40 | ≤ 5 | 39 min | Evidence work, recent case |
| C | > 0.40 | > 5 | 55 min | Evidence work plus ageing |
No explicit interaction term was entered. The interaction appears because the days-open question is asked only inside the high-gap region. A linear model would need a feature cross or nonlinear basis to express the same conditional effect. Trees acquire this local compositionality naturally.
Greedy growth makes the map useful and brittle
Finding the globally best tree of a constrained size is combinatorial. Practical tree learners grow greedily: choose the best available split now, then repeat inside the children. Greed makes training tractable and often effective. It also means an early choice changes the candidate populations seen by every later choice.
Suppose evidence gap at 0.40 and days open at 4.8 produce almost equal training improvement. The algorithm selects evidence gap by a tiny margin. Move five observations and days open wins. Neither tree need be absurd. Each may approximate the same broad relation with a different partition. A branch is an estimated boundary, not a discovered law of nature.
Depth, minimum leaf size and cost-complexity pruning regulate the map. A deeper tree creates smaller regions, lowering training bias but increasing sensitivity to sample accidents. A larger minimum leaf smooths local estimates but can erase rare interactions. Pruning removes branches whose extra fit does not justify complexity under the pruning criterion.[13] These controls trade resolution against stability; they do not change the basic axis-aligned partition primitive.
Thought experiment 1: rotate the same problem
Draw a binary target separated by the diagonal line x₁ + x₂ = 0. A standard axis-aligned tree cannot cut diagonally. It approximates the line with a staircase of vertical and horizontal splits. Now rotate the coordinate system by 45 degrees while preserving every point and label. In the new coordinates the same target may be separated by one vertical threshold.
Only the feature orientation changed. Sample size, labels, Bayes error and information content stayed fixed. Yet tree depth and leaf count can change sharply. This reveals an inductive bias: ordinary trees favour irregular functions aligned with supplied feature axes. A benchmark study on typical tabular data connected tree performance partly to robustness to uninformative features, preservation of feature orientation and ease of learning irregular functions.[11]
The analogy has a boundary. Real tabular features are not always freely rotatable. “Income” and “account age” may have semantic contracts that arbitrary linear combinations would destroy. The thought experiment does not prescribe rotating business features. It isolates the geometric price of axis-aligned splits.
Part II
flowchart LR A["The five cases that moved an<br/>entire tree"] --> B["A tree writes local rules into<br/>feature space"] B --> C["A forest averages instability,<br/>not ignorance"] C --> D["Boosting follows the<br/>structured leftovers"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#087A98,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
A forest averages instability, not ignorance
The opening puzzle exposed a property Breiman called vital to bagging: instability. If modest changes to the learning set produce materially different predictors, fitting multiple perturbed versions and aggregating them can improve accuracy.[3] A full tree is a good candidate because it can react strongly to which observations happen to appear near a split boundary.
Bagging draws bootstrap samples, fits one predictor to each sample, then averages regression outputs or aggregates class predictions. A random forest adds another perturbation: at each node it considers only a random subset of features. This feature sampling can prevent a dominant predictor from steering every tree down nearly the same route. Breiman’s random-forest analysis tied generalisation to both individual-tree strength and correlation among trees.[4]
The forest gains when its members are individually useful and their errors are not too correlated. Randomness is therefore instrumental, not decorative. Too little diversity leaves the trees making the same mistake. Too much randomisation weakens each tree enough that averaging cannot recover the lost signal.
The variance floor in one equation
Assume an idealised set of M predictors. Each has prediction variance σ², and every pair has the same error correlation ρ. The variance of their average is:
This formula is a teaching model, not an exact description of every forest. Real trees have unequal variance and unequal pairwise correlations. Its causal message survives: adding estimators cannot average away a shared error.
ρ + (1 − ρ)/M, with equal member variance normalised to one. At ρ = 0, variance decays as 1/M; at ρ = 0.8, one hundred trees retain roughly 80% of one tree’s variance.Thought experiment 2: clone the forest
Train one deep tree and copy it one hundred times. The vote looks large, but every copy makes the same prediction. Set ρ = 1 in the equation and the variance is unchanged. Next, keep the data fixed but force each tree to inspect different feature subsets. Some trees weaken, yet their errors become less aligned. If the fall in correlation outweighs the loss of strength, the ensemble improves.
Vary one causal feature at a time. First increase M while holding correlation at one: no gain. Then lower correlation while holding member variance and mean error fixed: the average stabilises. This is the reason max_features is not merely a speed setting. Official scikit-learn guidance describes the same bias-variance tension: smaller feature subsets can reduce variance while increasing bias.[14]
A jury analogy is tempting: many independent judgements can beat one. Its structural correspondence is limited to error aggregation. Trees do not possess independent evidence or accountability. Their “independence” is engineered through sampling and feature restrictions, and all members still inherit the same labels, feature definitions and collection process. A forest cannot average away target leakage shared by every tree.
Measured stability in the synthetic laboratory
The accompanying experiment generated 32 independent training samples from the same axis-aligned workload, fitted a single tree and a 180-tree forest to each, then compared predictions on a fixed grid. Mean prediction variance across the grid fell from 36.41 square minutes for the single tree to 5.84 for the forest, an 84.0% reduction. Within one fitted forest, mean pairwise correlation among member prediction vectors was 0.462.
Worked scenario
Those values are measured from the supplied synthetic code, not estimates for a real service. They demonstrate the mechanism under a data-generating process designed to contain threshold interactions and sampling instability. A different sample size, noise process, feature correlation or forest configuration will produce different numbers.
What the average gives up
A forest smooths the jagged prediction map, but the result is no longer one inspectable rule list. Each observation may traverse hundreds of paths. Summary tools can show permutation effects, partial dependence, local attributions or representative trees, yet none turns the ensemble back into a single causal story.
Predictive stability and explanatory stability are separate properties. A forest can produce stable predictions while feature rankings remain ambiguous under correlated predictors. Impurity-based importance can also favour variables with more candidate split points or categories. Strobl and colleagues demonstrated misleading random-forest importance under differing measurement scales and category counts, tracing it to split-selection bias and bootstrap effects.[9]
Out-of-bag observations provide an efficient internal error estimate because each bootstrap sample leaves some training cases unused. They are useful, but they do not simulate future time, changed policy, new customers or a corrected label process. An internal estimate is not a substitute for a deployment-shaped test set.
Part III
Boosting follows the structured leftovers
A forest asks many versions of roughly the same question in parallel: what tree do I fit to this perturbed sample? Boosting asks a sequence of different questions. After the current ensemble predicts, where is the chosen loss still pointing downhill? The next tree is trained to approximate that direction.
Start with squared-error regression. Fit a shallow tree. Compute each training residual, yᵢ − F₁(xᵢ). Fit a second shallow tree to those residuals, then add a fraction of its prediction to the first. The second tree is not another vote on the original target. It models what the first tree systematically left behind.
Boosting converts a weak learner into a stagewise function optimiser. Friedman’s gradient-boosting formulation generalised the residual idea to any differentiable loss by fitting each new learner to negative gradients in function space.[6] AdaBoost arrived through a different route, using multiplicative example weights to construct a strong classifier from weak learners.[5] The methods are related, but “misclassified examples receive more weight” is not a complete account of modern gradient boosting.
Fₘ(x) = Fₘ₋₁(x) + η γₘ hₘ(x)
A two-stump correction
The minimal executable case contains a one-dimensional target with two jumps. One decision stump can represent only one jump. Its clean-test RMSE is 0.954. A second stump fitted to the first stump’s residuals captures the omitted jump; the sum reaches 0.431, a 54.8% reduction. Nothing mystical occurred. The first partition removed one structured component, making the second visible in the residual.
minimal_residual_correction() experiment. The first stump and second residual stump each have depth one; their sum represents two thresholds.Depth controls the order of interaction
A stump can express one threshold per stage. A depth-two tree can express a two-way conditional interaction inside one stage. Deeper base trees let boosting correct more complex residual structure at once, but increase the chance that a stage memorises narrow accidents. The number of leaves is often the more direct capacity measure because an unbalanced tree can reach depth without filling every level.
Learning rate and number of stages work together. A large update may remove error quickly but overshoot or lock later stages around an early mistake. A small update traces a finer path but consumes more computation and creates more opportunities to pursue noise. Subsampling rows or columns adds variance control; Friedman’s stochastic gradient boosting showed how random subsamples can improve accuracy and speed in studied settings.[7]
Modern systems add regularised leaf weights, histogram approximations, sparse-data handling, missing-value routes and distributed execution. XGBoost, for example, contributed a regularised objective, sparsity-aware split finding and systems techniques for scalable training.[8] These engineering advances matter, but they sit on the same stagewise correction mechanism.
The negative control: correction can become pursuit
The synthetic boosting path corrupts 12% of training targets by adding or subtracting 32 minutes, while keeping the test distribution clean. With squared loss, clean-test RMSE improves from 9.68 after the first tree to a minimum of 5.05 at stage 89. Training continues to fall. By stage 600, clean-test RMSE has risen to 6.75, 33.6% worse than the best stage.
A decreasing training loss proves optimisation, not generalisation. The later trees are doing exactly what the objective requests: correcting large remaining errors, including corrupted targets. Early stopping, shrinkage, robust losses and data-quality controls can change the path, but none removes the need for a validation design that resembles the intended operating distribution.
Long and Servedio proved a strong negative result for a broad class of convex-potential boosting algorithms under random classification noise.[10] Its scope matters. It does not establish that every gradient-boosted implementation fails on any noisy dataset. It establishes that familiar convex boosting machinery can have adversarial noise sensitivities even with common regularisation constraints. The practical lesson is to test label perturbation rather than repeat the slogan that boosting “focuses on hard examples”.
A serious counterexample: when the tree family is unnecessary
The second synthetic workload is smooth and additive: a weighted sum of five Gaussian features plus noise. Training sets contain only 180 observations. Across twelve repeats, Ridge regression reaches mean test RMSE 3.08. The single tree reaches 5.77, the forest 4.45 and histogram boosting 4.25. All three tree methods spend capacity approximating a plane with step functions.
This negative control matters because an ensemble comparison without a transparent baseline can reward complexity for solving the wrong geometry. Tree ensembles are empirically strong on many medium-sized tabular tasks. A 2022 benchmark across 45 datasets reported strong tree-based results and analysed their inductive biases.[11] A later analysis across 176 datasets found that the neural-network versus boosted-tree debate was often overemphasised, with light tuning sometimes mattering more than family choice.[12] Neither result licenses “boosting wins tabular data” as a universal law.
Part IV
stateDiagram-v2 [*] --> Claim state "A minimal worked example" as Claim state "Greedy growth makes the map<br/>useful and brittle" as Test state "Thought experiment 1: rotate<br/>the same problem" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
Diagnose the error before choosing the ensemble
Return to the review-effort model. The team can now replace algorithm shopping with a sequence of interventions. The first question is whether axis-aligned local partitions express the signal at all. The second is whether one fitted partition is unstable under plausible resampling. The third is whether held-out residuals contain learnable local structure. The fourth is whether sequential correction begins fitting weak or corrupted labels.
The proposed decision instrument is a partition, perturb, correct audit. It is practitioner synthesis, not a published standard. Its contribution is to bind each extra layer of complexity to an observable error mechanism and a falsifying test.
Gate 1: test partition adequacy
Fit a regularised linear or additive baseline and a shallow tree under the same split discipline. Compare held-out loss, not training fit. Inspect whether the tree’s gain comes from stable threshold interactions or from a handful of narrow leaves. Rotate or combine synthetic copies of continuous features where semantics permit, then measure sensitivity. If a small change of basis collapses performance, feature engineering or an oblique, additive, kernel or neural alternative may be more appropriate.
Partition adequacy is not the same as winning a metric by a small margin. Ask whether the local map is credible under measurement precision. A split at 0.421 and another at 0.424 may be statistically distinct but operationally indistinguishable when the source field is rounded to one decimal place.
Test 2: measure perturbation instability
Refit the single tree across bootstrap samples, folds, time windows and plausible label corrections. Record prediction variance, threshold dispersion, leaf membership agreement and decision flips near operational cut-offs. The forest has earned consideration when the single tree is useful but unstable and when diverse averaging reduces held-out error without erasing an important rare signal.
Do not inspect only mean accuracy. A routing system can preserve average RMSE while changing which cases cross a staffing threshold. Report both distributional error and consequence-shaped slices.
Test 3: measure residual learnability
Generate out-of-fold predictions from the current model. Fit a shallow tree to the held-out residuals, never to residuals produced by a model that saw the same outcomes. If the residual tree improves a separate validation set, the current model leaves structured, local error that boosting may exploit. If it only improves in-sample residuals, the evidence favours noise-fitting, leakage or insufficient data.
This test separates “boosting is powerful” from “there is something coherent left to correct”. It also identifies which slices contain residual structure. A model may need boosting for one channel and a deterministic adjustment for another.
Test 4: locate the noise-pursuit boundary
Trace staged validation loss, subgroup loss and threshold decisions. Inject realistic label errors, missingness and delayed outcomes. Compare squared, absolute and robust losses where the target supports them. Stop or reject when later stages improve training metrics while degrading future-like validation, minority slices or decision stability.
Probability use needs another check. Raw class probabilities from bagged or boosted trees are not guaranteed to be calibrated. Empirical work has shown that calibration can materially alter probability quality across supervised learners.[15] If the score drives cost, capacity or abstention, evaluate reliability diagrams and proper scoring rules rather than treating rank performance as probability evidence.
Training topology changes the operating bargain
A forest’s members can be fitted independently once their bootstrap samples and candidate-feature draws are fixed. Failed member jobs can be retried, and the proposed release can be compared with the incumbent ensemble while training continues. That parallelism does not create statistical independence by itself. Trees trained from the same leaking field, target defect or narrow period can reproduce the same mistake at scale.
Boosting has a different dependency chain. Stage m is trained against gradients produced by stages 1 to m − 1. Change an early tree, loss, sample order or learning-rate schedule and later pseudo-residuals change too. A reproducible boosting release therefore needs the ordered stage sequence, objective, sampling policy, random seed and selected stopping point. A prefix is a valid model only when that exact prefix has its own validation evidence.
Both families eventually score a case by traversing many small trees and combining their outputs, yet “number of trees” is not a comparable unit of complexity. A thousand shallow, parallel forest members and a thousand dependent corrective stages express different risks. Production comparison should measure consequence per unit of cost: future-like loss, decision flips near thresholds, tail latency, model size, retraining time, calibration, rollback granularity and slice stability. A small aggregate gain does not compensate for an ensemble whose operational behaviour cannot be reconstructed or safely reversed.
A production-shaped worked scenario
Suppose the model estimates review minutes to plan a manual queue. The target is observed only after a case closes. “Days open” at scoring time is legitimate; final age is leakage. Evidence-gap count must be reconstructed as it was known then, not read from a corrected end-state record. Cases from one customer or linked dispute should not straddle train and test if their shared history makes outcomes dependent.
The single tree is first evaluated as an operational baseline. Its branch map helps expose impossible thresholds and leaked features. Resampled fits then show whether queue positions change under small data corrections. A forest is accepted only if it reduces those flips on a future-time holdout. Boosting is accepted only if cross-fitted residuals are learnable and staged validation remains stable under plausible target corrections.
The predicted minutes enter a scheduling calculation with explicit capacity constraints. They do not approve a claim, determine customer fault or suppress review. A predictive model is evidence for an action rule, not authority to act. This boundary matters because a highly accurate ensemble can still encode an impermissible feature, inherit a stale target definition or optimise a metric misaligned with service outcomes.
The release comparison must use paired operational cases, not only aggregate error. For every future-time case, record the baseline, tree, forest and boosting predictions, the eventual outcome and whether the scheduling decision would change. A small average gain that flips many cases near a capacity threshold may be operationally important; a larger gain far from every threshold may change nothing operationally.
Stability is measured on the decision as well as the prediction. Refit under plausible row corrections, entity-respecting resamples and nearby hyperparameters, then count queue-position reversals. If a forest lowers prediction variance but the constrained scheduler still amplifies tiny differences, the downstream rule needs its own explicitly declared robustness margin. The ensemble has not solved an unstable operational decision interface.
| Observation | Mechanism inference | Permitted next move | Blocking evidence |
|---|---|---|---|
| Shallow tree does not beat a regularised linear or additive baseline | Local axis-aligned partitioning is not earning cost | Keep the baseline or redesign representation | Do not escalate to ensembles merely because deeper trees fit training data |
| Single-tree predictions vary sharply across resamples; forest reduces held-out dispersion | Sampling variance is material and decorrelation works | Consider a forest | Reject if rare-slice recall or temporal performance falls |
| Cross-fitted residual tree improves separate validation | Structured bias remains after the current model | Consider boosting | Reject if the gain disappears under label corrections or time split |
| Training loss falls while future-like validation and slice stability worsen | Sequential correction is pursuing noise or drift | Stop earlier, change loss, clean labels or reject | No release based on final-stage training metrics |
| Feature importance changes across correlated substitutes | Attribution is underidentified | Report grouped or conditional analyses | Do not present ranking as causal importance |
Explicit failure boundary
The partition, perturb, correct lens ceases to settle model choice when the target requires extrapolation beyond observed feature ranges, when useful geometry is strongly oblique or smooth, when time and entity dependence violate ordinary resampling, when labels encode unresolved human disagreement, or when the decision is causal or normative rather than predictive. Standard regression trees usually return combinations of observed leaf values; they do not infer a physical growth law outside the training support.
It also breaks when the data contract is wrong. No ensemble can repair a target that changes meaning across periods, a field available only after the decision, or a category whose encoding conflates distinct business states. More trees can make such defects look stable.
What would weaken the article’s thesis? A broad matched study could show that measured tree instability and residual learnability do not predict relative forest or boosting gains after controlling for data size, tuning budget and noise. That would reduce the value of the proposed audit. The mechanism equations would remain true under their assumptions, but the decision instrument would need revision.
Open hypothesis
Across tabular projects, a pre-registered partition, perturb, correct audit will reduce wasted hyperparameter search and improve out-of-time model selection compared with undirected family sweeps. The supplied laboratory is a discriminating prototype, not evidence that this operational benefit has already been established.
Run the model comparison
The executable artefact uses only synthetic data. It compares Ridge, one decision tree, a random forest and histogram gradient boosting across two regimes; estimates algorithm variance across resampled datasets; traces a corrupted-target boosting path; and verifies a two-stump residual correction. Assertions encode both positive and negative cases.
Install Python 3.11 or later, NumPy and scikit-learn, save the script as tree_ensemble_lab.py, then run python tree_ensemble_lab.py. The verified environment used NumPy 2.3.5 and scikit-learn 1.8.0. Later compatible versions may produce small timing or floating-point differences.
Partitioned workload (lower RMSE is better)
Ridge baseline 7.557 ± 0.071
Single tree 4.184 ± 0.185
Random forest 3.563 ± 0.085
Histogram boosting 3.427 ± 0.112
Smooth additive negative control
Ridge baseline 3.076 ± 0.045
Single tree 5.771 ± 0.199
Random forest 4.451 ± 0.122
Histogram boosting 4.245 ± 0.138
Stability
variance reduction: 84.0%
Boosting path
best stage: 89; test RMSE 5.052 -> 6.752 at stage 600
Residual correction
one stump: 0.954; two stages: 0.431Full executable Python laboratory
#!/usr/bin/env python3
"""Synthetic laboratory for decision trees, random forests, and gradient boosting.
Assumptions
-----------
* All data are synthetic. No client or personal data are used.
* The partitioned workload has axis-aligned threshold interactions.
* The smooth workload is additive and linear, serving as a negative control.
* Hyperparameters are fixed before evaluation. This is a mechanism test, not a
universal benchmark or a claim that one library is always best.
Expected high-level results
---------------------------
1. On the partitioned workload, the ensembles beat a single tree and Ridge.
2. Across resampled training sets, a random forest varies less than one tree.
3. Gradient boosting reduces validation error early, but can pursue corrupted
targets if continued too long with squared loss.
4. On the smooth additive negative control, Ridge is best on average.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import json
from pathlib import Path
from time import perf_counter
from typing import Callable
import numpy as np
import sklearn
from numpy.typing import NDArray
from sklearn.ensemble import (
GradientBoostingRegressor,
HistGradientBoostingRegressor,
RandomForestRegressor,
)
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
from sklearn.tree import DecisionTreeRegressor
FloatArray = NDArray[np.float64]
@dataclass(frozen=True)
class Score:
model: str
mean_rmse: float
sd_rmse: float
mean_fit_ms: float
def make_partitioned_workload(
n: int,
seed: int,
*,
corrupt_targets: bool = False,
) -> tuple[FloatArray, FloatArray]:
"""Synthetic review-effort target with local, axis-aligned interactions."""
rng = np.random.default_rng(seed)
x = rng.uniform(-1.0, 1.0, size=(n, 5))
y = (
24.0
+ 12.0 * (x[:, 0] > -0.15)
+ 9.0 * (x[:, 1] > 0.35)
+ 16.0 * ((x[:, 0] < 0.20) & (x[:, 2] > 0.10))
+ 11.0 * ((x[:, 3] > 0.55) & (x[:, 4] < 0.0))
+ 1.0 * x[:, 0]
+ rng.normal(0.0, 2.8, size=n)
)
if corrupt_targets:
mask = rng.random(n) < 0.12
y[mask] += rng.choice(np.array([-32.0, 32.0]), size=int(mask.sum()))
return x.astype(np.float64), y.astype(np.float64)
def make_smooth_workload(n: int, seed: int) -> tuple[FloatArray, FloatArray]:
"""Smooth additive workload used as the serious negative control."""
rng = np.random.default_rng(seed)
x = rng.normal(0.0, 1.0, size=(n, 5))
y = (
32.0
+ 7.0 * x[:, 0]
- 5.0 * x[:, 1]
+ 3.0 * x[:, 2]
+ 1.5 * x[:, 3]
+ rng.normal(0.0, 3.0, size=n)
)
return x.astype(np.float64), y.astype(np.float64)
def model_factories(seed: int) -> dict[str, Callable[[], object]]:
return {
"Ridge baseline": lambda: Ridge(alpha=1.0),
"Single tree": lambda: DecisionTreeRegressor(
min_samples_leaf=4,
random_state=seed,
),
"Random forest": lambda: RandomForestRegressor(
n_estimators=220,
max_features=0.70,
min_samples_leaf=4,
n_jobs=-1,
random_state=seed,
),
"Histogram boosting": lambda: HistGradientBoostingRegressor(
max_iter=240,
learning_rate=0.06,
max_leaf_nodes=15,
min_samples_leaf=12,
l2_regularization=1.0,
early_stopping=False,
random_state=seed,
),
}
def repeated_benchmark(
generator: Callable[[int, int], tuple[FloatArray, FloatArray]],
*,
train_n: int,
test_n: int,
repeats: int,
seed_offset: int,
) -> list[Score]:
rmse: dict[str, list[float]] = {name: [] for name in model_factories(0)}
fit_ms: dict[str, list[float]] = {name: [] for name in model_factories(0)}
for repeat in range(repeats):
seed = seed_offset + repeat
x_train, y_train = generator(train_n, seed)
x_test, y_test = generator(test_n, 100_000 + seed)
for name, factory in model_factories(seed).items():
model = factory()
start = perf_counter()
model.fit(x_train, y_train) # type: ignore[attr-defined]
fit_ms[name].append((perf_counter() - start) * 1000.0)
pred = model.predict(x_test) # type: ignore[attr-defined]
rmse[name].append(float(mean_squared_error(y_test, pred) ** 0.5))
return [
Score(
model=name,
mean_rmse=float(np.mean(values)),
sd_rmse=float(np.std(values, ddof=1)),
mean_fit_ms=float(np.mean(fit_ms[name])),
)
for name, values in rmse.items()
]
def stability_experiment(repeats: int = 32) -> dict[str, float]:
"""Estimate algorithm variance across independently resampled datasets."""
grid_axis = np.linspace(-1.0, 1.0, 24)
x0, x1 = np.meshgrid(grid_axis, grid_axis)
grid = np.zeros((x0.size, 5), dtype=float)
grid[:, 0] = x0.ravel()
grid[:, 1] = x1.ravel()
grid[:, 2] = 0.35
grid[:, 3] = 0.70
grid[:, 4] = -0.25
tree_predictions: list[FloatArray] = []
forest_predictions: list[FloatArray] = []
for seed in range(repeats):
x_train, y_train = make_partitioned_workload(260, 20_000 + seed)
tree = DecisionTreeRegressor(min_samples_leaf=3, random_state=seed)
forest = RandomForestRegressor(
n_estimators=180,
max_features=0.65,
min_samples_leaf=3,
n_jobs=-1,
random_state=seed,
)
tree.fit(x_train, y_train)
forest.fit(x_train, y_train)
tree_predictions.append(tree.predict(grid))
forest_predictions.append(forest.predict(grid))
tree_matrix = np.vstack(tree_predictions)
forest_matrix = np.vstack(forest_predictions)
tree_var = float(np.mean(np.var(tree_matrix, axis=0, ddof=1)))
forest_var = float(np.mean(np.var(forest_matrix, axis=0, ddof=1)))
# Estimate within-forest prediction correlation on the same grid.
x_train, y_train = make_partitioned_workload(500, 77)
forest = RandomForestRegressor(
n_estimators=180,
max_features=0.65,
min_samples_leaf=3,
n_jobs=-1,
random_state=77,
).fit(x_train, y_train)
member_predictions = np.vstack([tree.predict(grid) for tree in forest.estimators_])
corr = np.corrcoef(member_predictions)
upper = corr[np.triu_indices_from(corr, k=1)]
return {
"single_tree_mean_prediction_variance": tree_var,
"forest_mean_prediction_variance": forest_var,
"variance_reduction_percent": 100.0 * (1.0 - forest_var / tree_var),
"mean_member_prediction_correlation": float(np.nanmean(upper)),
}
def boosting_path() -> dict[str, object]:
"""Track sequential correction on clean test data with corrupted training targets."""
x_train, y_train = make_partitioned_workload(620, 900, corrupt_targets=True)
x_test, y_test = make_partitioned_workload(5000, 901, corrupt_targets=False)
model = GradientBoostingRegressor(
n_estimators=600,
learning_rate=0.04,
max_depth=3,
min_samples_leaf=5,
loss="squared_error",
random_state=900,
).fit(x_train, y_train)
train_rmse: list[float] = []
test_rmse: list[float] = []
for train_pred, test_pred in zip(
model.staged_predict(x_train),
model.staged_predict(x_test),
strict=True,
):
train_rmse.append(float(mean_squared_error(y_train, train_pred) ** 0.5))
test_rmse.append(float(mean_squared_error(y_test, test_pred) ** 0.5))
best_index = int(np.argmin(test_rmse))
stages_to_report = [1, 10, 25, 50, 100, 200, 400, 600]
path = [
{
"stage": stage,
"train_rmse": train_rmse[stage - 1],
"test_rmse": test_rmse[stage - 1],
}
for stage in stages_to_report
]
return {
"best_stage": best_index + 1,
"best_test_rmse": test_rmse[best_index],
"final_test_rmse": test_rmse[-1],
"final_train_rmse": train_rmse[-1],
"test_deterioration_after_best_percent": 100.0
* (test_rmse[-1] / test_rmse[best_index] - 1.0),
"path": path,
}
def minimal_residual_correction() -> dict[str, float]:
"""Fit one stump, then a second stump to its residuals."""
rng = np.random.default_rng(321)
x_train = rng.uniform(-1.0, 1.0, size=(500, 1))
x_test = np.linspace(-1.0, 1.0, 2000).reshape(-1, 1)
def truth(x: FloatArray) -> FloatArray:
z = x[:, 0]
return -2.0 + 3.0 * (z > -0.20) + 2.5 * (z > 0.55)
y_train = truth(x_train) + rng.normal(0.0, 0.35, size=len(x_train))
y_test = truth(x_test)
first = DecisionTreeRegressor(max_depth=1, random_state=1).fit(x_train, y_train)
first_train = first.predict(x_train)
residual = y_train - first_train
second = DecisionTreeRegressor(max_depth=1, random_state=2).fit(x_train, residual)
first_test = first.predict(x_test)
corrected_test = first_test + second.predict(x_test)
return {
"one_stump_test_rmse": float(mean_squared_error(y_test, first_test) ** 0.5),
"two_stage_test_rmse": float(mean_squared_error(y_test, corrected_test) ** 0.5),
"improvement_percent": 100.0
* (1.0 - mean_squared_error(y_test, corrected_test) ** 0.5
/ (mean_squared_error(y_test, first_test) ** 0.5)),
}
def main() -> None:
partitioned = repeated_benchmark(
make_partitioned_workload,
train_n=900,
test_n=5000,
repeats=12,
seed_offset=1000,
)
smooth = repeated_benchmark(
make_smooth_workload,
train_n=180,
test_n=5000,
repeats=12,
seed_offset=3000,
)
stability = stability_experiment()
path = boosting_path()
residual = minimal_residual_correction()
partitioned_by_name = {score.model: score for score in partitioned}
smooth_by_name = {score.model: score for score in smooth}
# Positive cases: the mechanisms should be visible under the declared DGP.
assert partitioned_by_name["Random forest"].mean_rmse < partitioned_by_name["Single tree"].mean_rmse
assert partitioned_by_name["Histogram boosting"].mean_rmse < partitioned_by_name["Single tree"].mean_rmse
assert stability["forest_mean_prediction_variance"] < stability["single_tree_mean_prediction_variance"]
assert residual["two_stage_test_rmse"] < residual["one_stump_test_rmse"]
# Negative cases: complexity is not automatically useful or harmless.
assert smooth_by_name["Ridge baseline"].mean_rmse == min(s.mean_rmse for s in smooth)
assert int(path["best_stage"]) < 600
assert float(path["final_test_rmse"]) > float(path["best_test_rmse"])
result = {
"partitioned_workload": [asdict(score) for score in partitioned],
"smooth_negative_control": [asdict(score) for score in smooth],
"stability": stability,
"boosting_path": path,
"minimal_residual_correction": residual,
"environment": {
"numpy": np.__version__,
"scikit_learn": sklearn.__version__,
},
}
output = Path(__file__).with_name("tree_ensemble_lab_results.json")
output.write_text(json.dumps(result, indent=2), encoding="utf-8")
print("Partitioned workload (lower RMSE is better)")
for score in partitioned:
print(f" {score.model:20s} {score.mean_rmse:6.3f} ± {score.sd_rmse:5.3f}")
print("\nSmooth additive negative control")
for score in smooth:
print(f" {score.model:20s} {score.mean_rmse:6.3f} ± {score.sd_rmse:5.3f}")
print("\nStability")
print(f" variance reduction: {stability['variance_reduction_percent']:.1f}%")
print("\nBoosting path")
print(
f" best stage: {path['best_stage']}; "
f"test RMSE {path['best_test_rmse']:.3f} -> {path['final_test_rmse']:.3f} at stage 600"
)
print("\nResidual correction")
print(
f" one stump: {residual['one_stump_test_rmse']:.3f}; "
f"two stages: {residual['two_stage_test_rmse']:.3f}"
)
print(f"\nWrote {output.name}")
if __name__ == "__main__":
main()
How to extend the laboratory without changing its claim
Add a temporal split, grouped entities, monotonic constraints and a calibrated classification target. Keep the data-generating process and tuning budget declared. Do not replace the negative control with a dataset chosen after seeing which ensemble wins.
For an implementation comparison, add XGBoost, LightGBM or CatBoost as separate systems tests. Their histogram, categorical, regularisation and distributed-training choices affect efficiency and quality. The article’s mechanism claim should be tested at matched capacity, loss and validation design rather than inferred from library names.
Compact glossary
- Partition
- A region of feature space defined by a sequence of split conditions.
- Impurity
- A node-level measure of outcome mixing used to rank candidate classification splits.
- Instability
- Material change in a fitted predictor after a plausible change to the training sample.
- Decorrelation
- Engineering member errors to align less, so aggregation can reduce variance.
- Pseudo-residual
- The negative gradient of the loss with respect to the current prediction at one stage.
- Shrinkage
- Multiplying each boosting update by a learning rate smaller than one.
Choose the error mechanism before the ensemble
A tree is valuable because it turns heterogeneous tabular relations into local questions. Its branch structure exposes thresholds, interactions and data defects that a global coefficient can hide. The same locality makes it discontinuous and sample-sensitive. A small movement near one candidate split can alter every rule below it.
A forest responds to that instability by creating diverse versions of the partition and averaging their predictions. Its improvement is bounded by member strength and error correlation. More trees help until the uncorrelated variance has been spent. Shared leakage, shared labels and shared representation defects remain.
Boosting responds to a different diagnosis. It treats the current ensemble as unfinished and trains the next tree against the loss gradient that remains. This can uncover structured leftovers with extraordinary efficiency. It can also pursue corrupted targets with equal discipline. The stopping stage, loss, validation design and label process are part of the model.
The changed decision is to stop treating tree, forest and boosting as three interchangeable entries in a benchmark menu. First establish that local partitions fit the problem. Then measure instability and residual learnability. Use a forest when diversity averaging attacks a demonstrated variance problem. Use boosting when sequential correction attacks demonstrated held-out structure. Keep or restore the simpler baseline when neither mechanism earns its cost.
The result is modest but durable: model selection becomes a causal experiment about error, not a contest among familiar names.
sequenceDiagram participant Q as Question participant M as Mechanism participant E as Evidence participant D as Decision Q->>M: The five cases that moved an entire tree M->>E: A minimal worked example E-->>M: compare rival explanations E->>D: Sources D-->>Q: revise scope or act
Sources
- Breiman, Friedman, Olshen and Stone. Classification and Regression Trees. Foundational monograph on tree-structured rules and CART.
- Quinlan. Induction of Decision Trees. Machine Learning, 1986.
- Breiman. Bagging Predictors. Machine Learning, 1996.
- Breiman. Random Forests. Machine Learning, 2001.
- Freund and Schapire. A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting. Journal of Computer and System Sciences, 1997.
- Friedman. Greedy Function Approximation: A Gradient Boosting Machine. Annals of Statistics, 2001.
- Friedman. Stochastic Gradient Boosting. Computational Statistics & Data Analysis, 2002.
- Chen and Guestrin. XGBoost: A Scalable Tree Boosting System. KDD, 2016.
- Strobl, Boulesteix, Zeileis and Hothorn. Bias in Random Forest Variable Importance Measures. BMC Bioinformatics, 2007.
- Long and Servedio. Random Classification Noise Defeats All Convex Potential Boosters. Machine Learning, 2010.
- Grinsztajn, Oyallon and Varoquaux. Why Do Tree-Based Models Still Outperform Deep Learning on Typical Tabular Data? NeurIPS, 2022.
- McElfresh et al. When Do Neural Nets Outperform Boosted Trees on Tabular Data? NeurIPS Datasets and Benchmarks, 2023.
- scikit-learn developers. Decision Trees user guide. Official implementation documentation.
- scikit-learn developers. Ensembles: gradient boosting, random forests and bagging. Official implementation documentation.
- Niculescu-Mizil and Caruana. Predicting Good Probabilities with Supervised Learning. ICML, 2005.