Gradient boosting
Article.
Verified 2026-08Verified 2026-08.
It is 2026, and on most structured business data a gradient-boosted tree still beats a neural network. That is a strange fact given everything else that has happened, and being able to say precisely why is the interview position — because the reasons are specific, they have been isolated experimentally, and they tell you when the answer flips.
The idea, and what “gradient” means
Fit a weak model. Look at what it got wrong. Fit another model to those errors. Add it in. Repeat.
F = initial_guess(y) # e.g. the mean, or log-odds
for m in range(M):
residuals = -gradient(loss, y, F(X)) # "pseudo-residuals"
h = fit_shallow_tree(X, residuals) # a weak learner on the errors
F = F + learning_rate * h # step in function spaceThe name is precise rather than decorative. This is gradient descent where the thing being updated is the function itself, not a parameter vector:
parameter space function space
θ ← θ - η ∇θ L F ← F - η ∇F L
↑
a tree approximates
this gradientEach tree is one step of steepest descent, and the learning rate is the step
size. For squared error the negative gradient is literally y - F(x) — the
residual — and for other losses it generalises, which is how the same machinery
handles log-loss, ranking objectives and quantile regression.
Two consequences fall straight out:
- Training is inherently sequential. Tree
m+1fits the errors left by treem, so you cannot parallelise across trees the way a Random Forest can. The parallelism lives inside each tree’s split-finding. - More trees eventually overfits. Each one is descending on the training loss, and past a point the remaining residuals are noise. That is the opposite of bagging, where extra trees only ever help.
Why the trees are shallow
Boosting reduces bias by sequentially correcting errors, so it needs weak, high-bias learners — depth 3 to 8, sometimes stumps. Give it deep trees and the first few fit the training data almost perfectly, leaving nothing to correct and overfitting immediately.
Exactly inverted from a Random Forest, which wants deep, low-bias, high-variance trees so that averaging can cancel the variance.
| Boosting | Bagging | |
|---|---|---|
| Trees are | sequential | independent |
| Reduces | bias | variance |
| Wants trees that are | shallow, weak | deep, strong |
| More trees | eventually overfits | never hurts |
That contrast is one of the most reliably asked questions in classical ML, and the reason it is asked is that the two answers derive from each other.
The mechanism: inductive bias, isolated
“Trees just work better on tables” is folklore. The 2022 result that made it concrete tested three specific hypotheses, and each was confirmed by an intervention that changes one property and watches the gap move.
1. Neural networks prefer smooth functions
An MLP is biased toward smooth decision boundaries. Real tabular targets are often irregular — a threshold at exactly £10,000, a rule that changes at age 65, a step at a policy boundary.
The test: smooth the target function artificially. MLP performance improves; tree performance does not change much. So the smoothness prior was costing the network real accuracy on the original problem.
2. Uninformative features hurt MLPs more
Real tables carry columns that mean nothing for the task. A tree simply never splits on them.
The test: remove uninformative features and the gap narrows; add more and it widens. The network has to learn to ignore them, and with limited data it does so imperfectly.
3. Rotational invariance is the wrong prior
The deepest of the three, and the most quotable.
An MLP’s first layer is effectively rotationally invariant — rotate the input space and the network can absorb the rotation into its weights, learning the same function. That is a sensible prior for pixels, where any individual coordinate means little on its own.
It is precisely wrong for a table. Column 3 is “age”. It has a meaning that a linear combination of age, income and postcode does not have.
image: rotate the input, same problem
(no single pixel carries meaning alone)
table: rotate the input, and "age" becomes
0.3*age + 0.7*income - 0.2*tenure
-> the meaning is destroyedThe test: randomly rotate the dataset. Tree performance collapses; MLP performance is essentially unchanged — which proves the network was never using the axis structure. Trees are axis-aligned by construction, and that constraint is the correct prior for data whose columns were chosen by a human because each one means something.
So when does the answer flip
The three reasons also predict the exceptions:
- High-cardinality relational structure worth embedding — user IDs, item IDs, graph neighbourhoods. Trees cannot learn a dense representation.
- Fusing tabular data with text or images in one model.
- Very large datasets, where the network has enough data to learn to ignore noise features and approximate irregular functions.
Absent those, the tabular prior is a real advantage and no amount of architecture search overcomes it.
The parameters that matter
from xgboost import XGBClassifier
model = XGBClassifier(
n_estimators=5000, # set absurdly high; early stopping picks the real one
learning_rate=0.05, # lower generalises better, needs more trees
max_depth=6, # 3-8; the main capacity knob
subsample=0.8, # row sampling per tree
colsample_bytree=0.8, # feature sampling per tree
min_child_weight=1, # minimum leaf weight; raises the bar for a split
reg_lambda=1.0, # L2 on leaf weights
early_stopping_rounds=50,
eval_metric="aucpr",
)
model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)Learning rate and n_estimators trade off directly. Halve the rate, roughly
double the trees. The standard recipe is a low rate (0.01-0.1), a huge
n_estimators, and early stopping to find the number — which turns two
hyperparameters into one.
Early stopping is not optional here, unlike with a Random Forest. Because each tree descends on the training loss, more trees will eventually fit noise.
The three libraries
| Pick | When |
|---|---|
| CatBoost | many categoricals, or no tuning time |
| LightGBM | large data, training speed matters |
| XGBoost | the most documented and stable option |
They differ in how the tree is grown and how categoricals are handled, and the choice rarely decides a project.
LightGBM grows leaf-wise — it splits whichever leaf reduces loss most rather
than completing each level. Lower loss per split, hence the speed, at the cost
of deep unbalanced trees that overfit small data. The real capacity knob is
num_leaves, not max_depth, and min_data_in_leaf is what stops it carving
out leaves of three rows.
CatBoost’s ordered target statistics is the genuinely clever bit. Naive target encoding leaks, because a category’s mean includes the current row’s own label. CatBoost encodes each row using only rows preceding it in a random permutation, which removes the leak by construction — see Data leakage.
XGBoost grows level-wise, completing each depth before going deeper, which produces balanced trees and predictable behaviour — the reason it remains the safe default.
Categoricals
model.fit(X, y, categorical_feature=["country", "device"]) # LightGBM
CatBoostClassifier(cat_features=["country", "device"]) # CatBoost
X["country"] = X["country"].astype("category") # XGBoost
XGBClassifier(enable_categorical=True, tree_method="hist")Do not one-hot encode a high-cardinality categorical for a tree model. It creates thousands of sparse binary columns each carrying almost no signal, and a depth-6 tree can only make six splits — it will never assemble them into anything useful.
Where it breaks in production
It cannot extrapolate
The failure that catches people who came from linear models, and the one worth naming unprompted.
A tree predicts a constant outside the range of values it saw in training. There is no trend to continue — the last split is the last split.
trained on tenure 0-36 months
linear model: keeps rising past 36
tree ensemble: flat forever after 36
─────────
/
/So any feature that trends over time — days_since_signup, cumulative_spend,
a price in an inflationary series — will silently saturate as production data
moves beyond the training range. Prefer ratios, differences and bounded
features over unbounded cumulative ones, or retrain often enough that the
range keeps up.
Early stopping against the test set
model.fit(X_tr, y_tr, eval_set=[(X_test, y_test)]) # the test set is now tuned onEarly stopping is a hyperparameter search, so the set it uses is a validation set by definition. Using the test set means the reported number is optimistic and you have no clean estimate left.
Probabilities that are not probabilities
XGBClassifier(scale_pos_weight=(y == 0).sum() / (y == 1).sum())Re-weighting fixes the ranking and distorts the predicted probabilities. So does early stopping on a ranking metric like AUC, which never asks whether 0.7 means 70%.
If a downstream decision multiplies the score by a monetary value — expected loss, expected revenue, a risk threshold — the numbers must be calibrated. Isotonic or Platt scaling on a held-out set, and check a reliability curve rather than assuming.
SHAP read as causality
import shap
shap_values = shap.TreeExplainer(model).shap_values(X_val)TreeExplainer computes exact Shapley values quickly for tree models, and it is
the right answer to “why was this application rejected”.
The caveat matters and is the senior half of the answer: SHAP explains the model, not the world. If the model leans on a proxy for something it should not use, SHAP faithfully reports the proxy. Two correlated features split their attribution arbitrarily. It is an audit of behaviour, not an estimate of a causal effect.
Training-serving skew in the feature vector
Tree models take a positional array. Reorder the columns between training and
serving and there is no error — age is now being compared against the
thresholds learned for income, and predictions are quietly nonsense.
Pin the column order as part of the model artefact, and assert it at load. Category dtypes need the same treatment: an unseen category or a differently ordered category index changes the encoding.
Nobody retrained
Boosted models degrade quietly as the distribution shifts, and the degradation looks like ordinary noise for a long time. Monitor the prediction distribution as well as the accuracy, since labels usually arrive late and the input drift arrives first.
The trade not taken
Fit a logistic regression first. It takes ten minutes, gives you a baseline that a boosted model must beat by enough to justify itself, and occasionally wins outright on small or genuinely linear problems. Skipping it means never finding out that the 200-tree ensemble bought two points of AUC over something you could explain in a sentence.
And when the model has to be defensible rather than merely accurate, gradient boosting is not automatically the right answer:
- Monotonic constraints —
monotone_constraintsin XGBoost and LightGBM — force a feature’s effect to move in one direction only. Regulators and domain experts often require this (“more income can never increase the risk score”), it costs little accuracy, and it is badly under-used. - A GAM or a scorecard is worse on the metric and better in a hearing, because every term is directly readable and no explanation layer stands between the model and the answer.
The reason gradient boosting keeps winning is that its inductive bias matches tabular data. The reason to sometimes not use it is that accuracy is not always the property being optimised, and knowing which one the problem actually wants is the difference between a model that ships and a model that wins a bake-off.
Related
Interview angle 10
- “How does gradient boosting work?” — fit weak learners sequentially to the
negative gradient of the loss at the current prediction, adding each scaled by
a learning rate. It is gradient descent in function space: each tree is one
step of steepest descent and the learning rate is the step size.
- Follow-up: “Why can’t you parallelise across trees?” — because tree
m+1fits the errors left by treem, so they are inherently ordered. The parallelism is inside each tree’s split-finding instead.
- Follow-up: “Why can’t you parallelise across trees?” — because tree
- “Boosting versus bagging?” — sequential against independent, bias against variance, shallow weak learners against deep strong ones, and more trees eventually overfits against more trees never hurting. The two sets of answers derive from each other.
- “Why does gradient boosting still beat neural networks on tabular data?” —
inductive bias, and it has been isolated. Networks prefer smooth functions
while tabular targets are irregular; uninformative features hurt an MLP more
because it has to learn to ignore them; and an MLP’s first layer is
rotationally invariant, which is exactly the wrong prior when column 3 is
“age” and a rotation destroys that meaning.
- Follow-up: “How would you know that rather than assume it?” — rotate the dataset. Tree performance collapses and MLP performance barely moves, which shows the network was never using the axis structure. Likewise, smoothing the target improves the MLP and not the trees.
- Follow-up: “When would you use a network instead?” — high-cardinality relational structure worth embedding, fusing tables with text or images, or a dataset large enough that the network can learn what to ignore.
Under pressure
- “Learning rate and number of trees?” — inverse. Halve the rate and you need
roughly twice the trees, and lower rates generalise better. Set it low, set
n_estimatorsabsurdly high, and let early stopping choose — which collapses two hyperparameters into one. - “XGBoost or LightGBM?” — level-wise against leaf-wise growth. LightGBM
reaches lower loss per split and is faster, but its deep unbalanced trees
overfit small data; tune
num_leavesandmin_data_in_leaf, notmax_depth. - “Why is CatBoost good with categoricals?” — ordered target statistics: each row’s encoding uses only rows preceding it in a random permutation, so the row’s own label cannot leak into its own feature.
- “Your model works in backtest and degrades on new data with no drift in the inputs.” — check whether a feature has moved outside its training range. Trees predict a constant beyond the last split, so any cumulative or trending feature silently saturates. Prefer ratios and differences over unbounded counters.
- “Can you use the predicted probability as a probability?” — not after
scale_pos_weight, and not if you early-stopped on AUC. Ranking metrics never ask whether 0.7 means 70%. Calibrate on a held-out set if a decision multiplies the score by money. - “How do you explain one prediction?” — SHAP with
TreeExplainer, exact and fast for trees. And say the caveat: it explains the model’s behaviour, not causality, so a model leaning on a proxy will have that proxy faithfully reported as an explanation. - “When would you not use gradient boosting?” — when the model must be defensible rather than most accurate. Monotonic constraints get you part of the way and cost little; a GAM or a scorecard is worse on the metric and better in front of a regulator. And always fit a logistic regression first, so you know what the ensemble actually bought.