← All examples

Machine Learning: Trees and Ensembles

A decision tree is the rare model whose failure is as instructive as its success. It fits any shape you like — axis-aligned rectangles, arbitrarily fine — and that flexibility is exactly the problem: grown to purity it memorises the training set, and a single deep tree here pins a client's default probability only to within an SD of 0.26 across resamples. Nearly everything in this section is a response to that one fact. Pruning trades flexibility back for stability; bagging averages the variance away; the random subspace de-correlates the things being averaged; boosting abandons averaging altogether and fits trees to what the previous ones got wrong; and BART puts a prior on the whole ensemble so the answer comes with a posterior attached.

The examples are deliberately paired with a plain regression benchmark, because the interesting comparison is rarely the one the leaderboard reports. On this data a tuned tree beats a logistic regression by about 0.03 in AUC — area under the receiver-operating-characteristic curve, a ranking score defined below — — and the ordering flips when the tree is pruned by R's default rule instead of by cross-validation, which says more about tuning than about trees. A recurring theme is measuring the mechanism rather than asserting it: the de-correlation that random forests are supposed to buy does not show up in the correlation statistic everyone reaches for, but shows up clearly in the variance of the ensemble.

How a tree decides where to split. A tree grows by asking, at each node, which single yes/no question about one feature best separates the outcome — and “best” needs a definition. That is impurity: a measure of how mixed the classes are in a node, zero when every case in it shares a label. Two are in common use and they rarely disagree. Gini impurity is the chance that two cases drawn at random from the node have different labels; entropy is the information-theoretic version of the same idea. The split chosen is whichever gives the largest drop in impurity, weighted by how many cases go each way, and the tree keeps going until a stopping rule bites. That greedy, one-question-at-a-time construction is the source of both the method’s interpretability and its instability: nothing forces the second split to be sensible given the first, and a small change in the data can change the first split and everything below it.

Why averaging trees works, and why the forest goes further. A deep tree has low bias and high variance — it fits the training data almost exactly and would look quite different on a different sample. Averaging many such trees cancels that variance, which is bagging: fit each tree on a bootstrap resample and take the mean. But averaging only helps to the extent the trees make different mistakes, and bootstrapped trees on the same features tend to pick the same strong splitter at the top and end up highly correlated. The random forest’s addition is to restrict each split to a random subset of features, which forces the trees apart — decorrelation, and the actual reason a forest beats bagging. It also gets a free validation set: each tree omits about a third of the rows, so those out-of-bag cases can be predicted by the trees that never saw them.

Boosting’s three knobs. Boosting attacks the other half of the error. Instead of averaging independent trees it fits them in sequence, each one on what the ensemble still gets wrong, so it reduces bias where the forest reduces variance. Three settings govern it and they interact. The learning rate (or shrinkage) multiplies each tree’s contribution before adding it: small values need more trees but generalise better, and the pairing of a small rate with early stopping — halting when held-out performance stops improving — is what production practice actually does. Depth controls how much interaction each tree can express; boosting favours shallow trees precisely because it will fit thousands of them. And subsampling rows or columns per tree adds the forest’s randomisation on top. Turn the rate up and the ensemble climbs fast and then declines, which is overfitting made visible.

Reading the numbers. Two metrics recur. AUC — the area under the receiver-operating-characteristic curve — is the probability that a randomly chosen defaulter is scored above a randomly chosen non-defaulter; it measures ranking only and ignores whether the probabilities are believable. ECE, the expected calibration error, measures exactly what AUC ignores: bin the predictions, compare each bin’s claim with the frequency actually observed in it, and average the gaps. The pair matters here because the section’s most counterintuitive result is a calibration one — gradient boosting, which textbooks warn is badly calibrated, comes out the best of three on this data at 0.012, while the logistic regression that estimates probabilities by maximum likelihood is the worst at 0.059. The boosting pathology is a symptom of overfitting, and a regularised fit simply does not have it.

Where the error goes: one tree, a sequence of trees, and the scoreboard

Every panel is measured on the two real datasets the whole section runs on, not on simulated examples: credit default — 30,000 clients, 23 features, a 22.1% default rate — scored by test AUC, and California housing — median house value, in units of $100k — scored by test RMSE. Both appear in all five projects, so a model’s number here can be read against any other on the same page.

All values are committed notebook output on the same held-out rows — A from the CART project, B from gradient boosting, C from the running scoreboard the XGBoost project maintains, which refits every model on one 70/30 split rather than quoting each project’s own figure.

A · One tree — deeper stops helping tree depth test AUC 0.690 4 leaves 0.726 8 leaves 0.746 31 leaves 0.736 152 leaves peak at depth 5, then variance wins 2 3 5 8 0.68 0.70 0.72 0.74 B · Boosting — the step that is easy to leave out trees in the ensemble test AUC 0.0144 AUC 300 plain trees = 50 corrected ones 50 150 300 0.755 0.762 0.769 no per-leaf step with it scikit-learn C · The running scoreboard — same held-out rows throughout classification AUC (higher better) regression RMSE (lower better) logistic / linear 0.715 0.737 single tree 0.737 0.666 random forest 0.775 0.523 sklearn GBM 0.773 0.522 XGBoost 0.774 0.494 LightGBM 0.773 0.494 CatBoost 0.776 0.521 0.72 0.75 0.78 0.5 0.6 0.7 AUC RMSE

A is the problem the rest of the section exists to solve. A single tree improves as it deepens — and then stops: AUC peaks at 0.746 at depth 5 and falls back to 0.736 at depth 8, while the leaf count goes from 31 to 152. The extra depth is buying pure variance. Everything that follows is a way of spending that variance better: average many such trees, or grow shallow ones in sequence.

B is the detail that separates a working implementation from a nearly-working one, and it is worth a panel because it is invisible in any summary. Boosting fits each tree to the gradient, but the tree’s own output is not the right step to take — Friedman’s correction replaces it with the constant that actually minimises the loss in each leaf, a Newton step. Leave it out and the ensemble still converges, just slowly: 300 plain-gradient trees reach only what 50 corrected ones reach. Put it back and the from-scratch loop matches scikit-learn to four decimals, which is the check that the implementation is right rather than merely close.

C is the honest summary of the whole section, and its shape is the point. Its two columns are the two datasets: the AUC column is credit default, the RMSE column California housing. Moving from a linear model to a single tree buys a lot on classification (0.715 to 0.737) and moving from one tree to an ensemble buys a lot more (0.737 to 0.775). Moving between ensembles buys almost nothing: the forest, scikit-learn’s boosting, XGBoost, LightGBM and CatBoost span 0.773 to 0.776 — a range narrower than the width of the bars. On regression the gradient-boosted libraries do pull clear on RMSE (0.494 against the forest’s 0.523 — roughly $49k against $52k of typical error on a house value), so the ordering is not identical across tasks. The practical reading is that the large gains come from the class of model and the small ones from its brand, which is the reverse of where most tuning effort goes. BART is left off this chart deliberately: it is fitted on a subsample, so it is not like-for-like, and its case rests on returning an interval rather than on winning the column.

How the five examples relate

One model, then two different ways of combining it, then an engineering of the second and a Bayesian version of it. Each part answers a failure of the one before.

Parts 3 and 5 are the pair worth reading together. Both are sums of many shallow trees; the difference is that boosting fits them greedily to minimise a loss while BART samples them from a posterior, which is why only one of the two can say how sure it is. The price is compute, and the honest verdict on this data is that the two BART implementations disagree with each other by more than either differs from the forest.

Decision Trees — CART from Scratch

The foundation: impurity, the cumulative sweep that makes an exhaustive split search tractable, and cost-complexity pruning, which takes a 1,804-leaf tree down to 7 leaves and lifts test AUC from 0.654 to 0.744. The from-scratch implementation is checked against scikit-learn precisely rather than loosely — bit-identical at depths 2–3, 99.31% identical at depth 8, with the residual gap traced to tie-breaking between equal-gain splits. It closes on the fact that motivates everything after it: one deep tree is so unstable that averaging 25 of them lifts AUC from 0.651 to 0.758. Benchmarked against a logistic regression, which loses by 0.03 in Python — and wins in R, where rpart prunes harder.

View example →

Random Forests — Averaging Away the Variance

Bagging plus the random subspace, and a measurement of why the second one matters. The textbook argument is that restricting features de-correlates the trees, so the ρσ² floor in the ensemble variance comes down — but the pairwise correlation between trees is nearly flat across the whole sweep, because it is dominated by the signal every tree captures. Measure the ensemble variance instead and the mechanism is unmistakable: a 44% reduction as features are restricted, tracking the AUC gain, while individual trees get noisier. Also here: out-of-bag error as a free validation set (0.761 against a test 0.764), and a planted noise feature that default impurity importance ranks 3rd of 25 while permutation importance correctly puts it at zero.

View example →

Gradient Boosting — Correcting Errors in Sequence

Boosting is bagging's opposite: many small dependent trees in sequence, each fitted to what the running ensemble still gets wrong, cutting bias where the forest cuts variance. Friedman's framing is gradient descent in function space — and one detail separates a working implementation from a slow one. The step is not the tree's output: under log-loss it needs a per-leaf Newton step, and omitting it costs 0.016 AUC at 50 trees while still converging, so nothing looks broken. Restored, the from-scratch booster matches scikit-learn to the fourth decimal at every tree count — and the regression case was already exact, because squared loss needs no such correction. AdaBoost, the ancestor, matches exactly too. The example closes on the knob that distinguishes boosting from forests: too many trees hurts, so the count must be tuned rather than simply raised.

View example →

XGBoost, LightGBM & CatBoost — Production Gradient Boosting

The libraries people actually reach for, and a measurement of what their engineering buys. Mostly not accuracy: on classification the three are near-identical at AUC 0.773–0.776, a spread of 0.003, and every ensemble in this section finishes within 0.003 AUC of the others. On regression they separate a little — RMSE 0.494 / 0.494 / 0.521, CatBoost 6% behind at defaults — but the real gap is fit time, 7.4× between fastest and slowest. What they add is production machinery: early stopping that finds round 98 by itself, and lands on the same answer in R (97 rounds); SHAP, which is signed and per-prediction where the forest's impurity importance was biased; and calibration curves showing a predicted 30% really defaults 30% of the time — with the flattening at the top of the housing plot being the data's $500k cap rather than model failure.

View example →

BART — Bayesian Additive Regression Trees

Every other ensemble here returns a number; BART returns a distribution. It keeps boosting's sum-of-shallow-trees but puts a prior on it and fits by MCMC, so uncertainty is a first-class output — low- and high-risk clients pinned down tightly, the mid-range carrying wide bands. It also supplies the section's sharpest methodological caution, twice over. First, credible is not predictive: the posterior on the mean function covers just 50.2% of held-out values, while the predictive interval including observation noise covers 91.7% against a nominal 90% — quoting the wrong one halves your stated uncertainty. Second, the two mature implementations disagree by ~27% RMSE (dbarts 0.526, PyMC-BART 0.665) after sampling, ensemble size and the prediction path were each tested and ruled out. A BART result is an implementation's result, and that only became visible by running the same analysis in both languages.

View example →