In 2014, during the Higgs Boson Machine Learning Challenge on Kaggle,
a new algorithm emerged and redefined the boundaries of efficiency.
It challenged Neural Networks. It outperformed Random Forests. It stunned the community.
They called it XGBoost.
A year later, 17 of the top 25 Kaggle solutions were powered by XGBoost.
Not because data scientists are lazy. Because it is unbeatable on structured data.
01 — Why sklearn's Gradient Boosting Wasn't Enough
Day 12 showed how Gradient Boosting works: fit a shallow tree to the residuals, add it to the ensemble scaled by η, repeat. The algorithm is elegant. sklearn's implementation of it is correct. But on a dataset of one million rows with 100 features, sklearn's GradientBoostingClassifier can take 45 minutes to train 500 trees. That's not a research problem — it's a deployment blocker.
Tianqi Chen's XGBoost (2016) and Microsoft's LightGBM (2017) attacked this problem from every angle simultaneously: a better objective, smarter tree building, systems engineering for modern hardware, and algorithmic tricks that reduce the data each tree needs to examine. Same algorithm. Completely different performance.
XGBoost: ~4 min (≈11× faster — cache-aware + approximate splits)
LightGBM: ~40 sec (≈67× faster — histograms + GOSS + EFB)
Same accuracy range. Dramatically different wall-clock time. The difference is not algorithmic — it's engineering.
LightGBM: histogram-based splits, leaf-wise tree growth, GOSS (skip low-gradient examples), EFB (bundle mutually exclusive features), native categorical support
Standard Gradient Boosting is a reliable family car: correct engineering, gets you there, does what it says. XGBoost is a Formula 1 car built on the same combustion engine — same physics, every component optimised to the limit. LightGBM is a newer F1 car with a lighter chassis and a better aerodynamic package.
The algorithm is the same engine. The engineering around it is completely different.
02 — XGBoost Innovation 1: The Regularised Objective
Standard Gradient Boosting minimises only the prediction loss. XGBoost minimises loss plus a regularisation term on the tree's leaf weights — baked into the training objective itself, not added as an afterthought.
At each boosting round t, XGBoost finds the tree hₜ that minimises:
Obj = Σᵢ L(yᵢ, ŷᵢ) + γT + ½λ Σⱼ wⱼ² + α Σⱼ |wⱼ|
Where:
L(yᵢ, ŷᵢ) — prediction loss (log-loss, MSE, etc.)
T — number of leaves in the tree × γ (L0 penalty: penalises tree complexity)
½λ Σwⱼ² — L2 (Ridge) on each leaf weight wⱼ (shrinks weights toward zero)
α Σ|wⱼ| — L1 (Lasso) on each leaf weight (can zero out leaves entirely)
The γ term means adding a new leaf to the tree only helps if it reduces loss by more than γ. This is automatic pruning — the regularisation decides how many leaves to use, not a fixed max_depth. Set γ=0 for no pruning, γ=5 for aggressive pruning.
The practical impact: XGBoost models trained with default regularisation (λ=1, α=0) generalise significantly better than sklearn's GB with no regularisation. You get better test performance without a separate cross-validation sweep for a regularisation parameter — it's already built into every tree.
03 — XGBoost Innovation 2: Newton Boosting (Second-Order Gradients)
Day 12's Gradient Boosting uses only the first derivative of the loss (the residual — which direction to move). XGBoost also uses the second derivative (the Hessian — how sharply the loss curves in that direction). This is Newton's method applied to boosting.
Imagine you're walking down a hill in fog. First-order (gradient descent): you feel the slope under your feet and step downhill. You take equal-sized steps regardless of how steep or shallow the terrain is.
Second-order (Newton's method): you also sense the curvature — how quickly the slope changes. On a steep, sharp bowl you take small steps (the minimum is close and you don't want to overshoot). On a gentle, flat slope you take large steps (the minimum is far away). The step size adapts to the terrain.
In XGBoost, the optimal leaf weight for each leaf j is computed analytically:
w*ⱼ = −(Σᵢ∈leaf gᵢ) / (Σᵢ∈leaf hᵢ + λ)
Where gᵢ = first derivative (gradient) and hᵢ = second derivative (Hessian) of the loss for example i. The denominator hᵢ + λ is the curvature — it automatically scales the step size. No learning rate needed for the leaf weights; XGBoost computes the exact optimal weight analytically.
04 — XGBoost Innovation 3: Column and Row Subsampling
XGBoost adds two variance-reduction controls directly borrowed from Random Forest's playbook — applied inside each boosting round.
05 — LightGBM Innovation 1: Histogram-Based Splits
The most expensive part of training a gradient boosted tree is finding the best split for each feature at each node. Naively: sort all N values, scan all N−1 split points. LightGBM replaces this with histograms.
06 — LightGBM Innovation 2: Leaf-wise vs Level-wise Tree Growth
Standard boosting (including XGBoost by default) grows trees level by level — all nodes at depth 1, then all at depth 2, etc. Every tree is symmetric and balanced. LightGBM instead grows leaf-wise: at each step, find the single leaf across the entire tree that would reduce loss the most, and split only that leaf.
# Visualising the difference in tree structure
# LEVEL-WISE (XGBoost default, max_depth=3)
# Depth 1: split root → 2 nodes
# Depth 2: split both → 4 nodes
# Depth 3: split all 4 → 8 leaves
# Total splits: 7. Tree is symmetric, balanced.
# LEAF-WISE (LightGBM)
# Round 1: split root (biggest gain) → 2 leaves
# Round 2: split left leaf (next biggest gain) → 3 leaves
# Round 3: split left-left leaf → 4 leaves
# Round 4: split right leaf (finally worth it) → 5 leaves
# Total splits: 4. Tree is asymmetric, deeper on one side.
# Same loss reduction with fewer nodes — that's the win.
import lightgbm as lgb
import xgboost as xgb
from sklearn.datasets import make_classification
import time, numpy as np
X, y = make_classification(n_samples=100_000, n_features=50,
n_informative=20, random_state=42)
for name, model in [
("XGBoost (level-wise)", xgb.XGBClassifier(n_estimators=200, max_depth=6,
verbosity=0, random_state=42)),
("LightGBM (leaf-wise) ", lgb.LGBMClassifier(n_estimators=200, max_depth=6,
verbosity=-1, random_state=42)),
]:
t0 = time.time()
model.fit(X, y)
elapsed = time.time() - t0
print(f"{name} trained in {elapsed:.2f}s")
Leaf-wise achieves greater loss reduction per split — it always picks the globally most valuable next split, not the next splits at a fixed depth. Fewer total splits → fewer nodes → less computation. This is why LightGBM uses fewer trees to reach the same training error.
The risk: leaf-wise can grow very deep on one branch (depth 30+) if that branch keeps having high-gain splits. This can overfit on small datasets. The fix: num_leaves (not max_depth) is the primary constraint in LightGBM — it limits the total number of leaves across the whole tree, preventing runaway depth. A good starting value is 31; for small datasets use 15–20.
07 — LightGBM Innovation 3: GOSS and EFB
These two innovations are why LightGBM is dramatically faster than XGBoost on massive datasets — they reduce both the number of examples and the number of features examined per split, without losing accuracy.
GOSS keeps all high-gradient examples in every round. From the low-gradient examples, it randomly samples only a fraction (default 10%). The gain calculation is corrected for this sampling so the result is unbiased. Effect: examine 10–30% of examples per round instead of 100%, while retaining almost all the informative signal. Accuracy loss: negligible. Speed gain: 3–5×.
EFB automatically detects mutually exclusive (or near-exclusive) feature groups and bundles them. A dataset with 1000 sparse features after OHE might compress to 50–100 effective features. Histogram construction cost drops proportionally. Effect: 2–10× reduction in effective feature count on sparse data.
Training dataset: 500,000 examples. Top 20% by gradient magnitude = 100,000 examples (always kept). Bottom 80% = 400,000 examples, but GOSS samples only 10% of these = 40,000. Total examples examined per round: 140,000 instead of 500,000 — a 3.6× reduction. The gain calculation multiplies low-gradient sample contributions by (1 − top_ratio)/sampling_ratio = 0.8/0.1 = 8 to compensate for the undersampling. Result: unbiased gain estimates from 28% of the data.
08 — Early Stopping with eval_set
Both XGBoost and LightGBM support validation-based early stopping natively. You pass a validation set to fit(), and training stops automatically when validation performance stops improving for N consecutive rounds. This is the right way to choose n_estimators — never guess, always let early stopping decide.
import xgboost as xgb
import lightgbm as lgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
# ── XGBoost: pass eval_set, set early_stopping_rounds ────────────────────
xgb_model = xgb.XGBClassifier(
n_estimators=1000, # upper bound — early stopping will find the real number
learning_rate=0.05,
max_depth=4,
subsample=0.8,
colsample_bytree=0.8,
reg_lambda=1.0, # L2 regularisation on leaf weights
reg_alpha=0.1, # L1 regularisation on leaf weights
early_stopping_rounds=20, # stop if no improvement for 20 rounds
eval_metric='logloss',
verbosity=0
)
xgb_model.fit(X_tr, y_tr, eval_set=[(X_val, y_val)])
print("── XGBoost ──")
print(f" Best iteration : {xgb_model.best_iteration}")
print(f" Best logloss : {xgb_model.best_score:.4f}")
print(f" Val accuracy : {xgb_model.score(X_val, y_val):.4f}")
# ── LightGBM: same idea, slightly different API ────────────────────────
lgb_model = lgb.LGBMClassifier(
n_estimators=1000,
learning_rate=0.05,
max_depth=4,
num_leaves=31, # primary depth control in LightGBM (not max_depth)
subsample=0.8,
colsample_bytree=0.8,
reg_lambda=1.0,
verbosity=-1
)
lgb_model.fit(
X_tr, y_tr,
eval_set=[(X_val, y_val)],
callbacks=[lgb.early_stopping(20, verbose=False),
lgb.log_evaluation(0)]
)
print("\n── LightGBM ──")
print(f" Best iteration : {lgb_model.best_iteration_}")
print(f" Val accuracy : {lgb_model.score(X_val, y_val):.4f}")
We set n_estimators=1000 as an upper bound. Early stopping found that XGBoost only needed 47 trees and LightGBM needed 52 trees to reach optimal validation performance. Training continued to round 67 and 72 respectively (47 + 20 patience rounds), then stopped. We saved training 930+ useless trees. This is the right workflow: always set n_estimators high and let early stopping find the real number.
Both reached the same val accuracy (97.37%). The best model is saved at round 47/52, not at the stopping round — the library automatically restores best weights.
09 — Hyperparameter Tuning Guide
These are not all equal. Tune in this order — the top ones matter most:
10 — Full Comparison: XGBoost vs LightGBM vs sklearn GB
import numpy as np
import time
import xgboost as xgb
import lightgbm as lgb
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
models = [
("sklearn GB ", GradientBoostingClassifier(
n_estimators=200, learning_rate=0.05,
max_depth=4, subsample=0.8, random_state=42)),
("XGBoost ", xgb.XGBClassifier(
n_estimators=200, learning_rate=0.05, max_depth=4,
subsample=0.8, colsample_bytree=0.8,
reg_lambda=1.0, verbosity=0, random_state=42)),
("LightGBM ", lgb.LGBMClassifier(
n_estimators=200, learning_rate=0.05, max_depth=4,
num_leaves=31, subsample=0.8, colsample_bytree=0.8,
reg_lambda=1.0, verbosity=-1, random_state=42)),
]
print(f"{'Model':<14} {'Test Acc':>9} {'CV Mean':>8} {'CV Std':>7} {'Train Time':>11}")
print("-" * 55)
for name, model in models:
t0 = time.time()
model.fit(X_tr, y_tr)
elapsed = time.time() - t0
cv = cross_val_score(model, X, y, cv=5)
print(f"{name} {model.score(X_te, y_te):>9.4f} {cv.mean():>8.4f} {cv.std():>7.4f} {elapsed:>9.2f}s")
Accuracy: XGBoost and LightGBM both improve on sklearn GB (0.9737 vs 0.9649) — the regularised objective and second-order gradients do real work even on this small dataset.
Speed: sklearn GB = 3.21s. XGBoost = 0.48s (6.7× faster). LightGBM = 0.09s (35× faster). This gap widens dramatically with more data — on 1M rows, it's the difference between overnight runs and minutes.
Stability: LightGBM's CV std (0.0174) is the lowest — leaf-wise growth with histogram splits produces slightly more stable models across folds on this dataset.
11 — XGBoost vs LightGBM: When to Use Which
You need battle-tested stability: XGBoost has been in production since 2016 at massive scale. Its behaviour is well-understood, its edge cases are documented, its defaults are conservative and safe.
You're starting from community resources: most Kaggle notebooks and tutorials use XGBoost. The ecosystem of tuning guides, parameter explanations, and SHAP integration is more mature.
You have high-cardinality categorical features: LightGBM handles categoricals natively without one-hot encoding. This is a significant advantage for features like city, product_id, or user_id with thousands of values.
Iteration speed matters: LightGBM's speed makes rapid hyperparameter search and experiment cycles much more practical. A search that takes 8 hours with XGBoost takes 1 hour with LightGBM.
12 — SHAP Values: Making the Black Box Speak
XGBoost and LightGBM are powerful but opaque — you can't read 200 trees the way you can read a single decision tree. SHAP (SHapley Additive exPlanations) fixes this. For any single prediction, SHAP computes exactly how much each feature pushed the prediction up or down from the baseline.
For a single prediction ŷ, SHAP decomposes it as:
ŷ = base_value + SHAP(feature_1) + SHAP(feature_2) + … + SHAP(feature_p)
The base_value is the mean prediction across all training examples. Each SHAP value is the contribution of that feature to this specific prediction — positive pushes toward the positive class, negative pushes toward the negative class. The sum of all SHAP values + base = exact prediction. No approximation; SHAP values for trees are computed exactly in O(TLD²) time, where T=trees, L=leaves, D=depth.
import shap
import numpy as np
import xgboost as xgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
data = load_breast_cancer()
X, y = data.data, data.target
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
model = xgb.XGBClassifier(n_estimators=100, learning_rate=0.1,
max_depth=4, verbosity=0, random_state=42)
model.fit(X_tr, y_tr)
# TreeExplainer: fast exact SHAP for tree models
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_te) # shape: (n_test, n_features)
# Explain one prediction in detail
i = 0 # first test example
pred_prob = model.predict_proba(X_te[i:i+1])[0, 1]
base_value = explainer.expected_value
print(f"Prediction: {pred_prob:.4f} (true: {data.target_names[y_te[i]]})")
print(f"Base value: {base_value:.4f} (mean prediction across training set)")
print(f"Sum check: base + SHAP sum = {base_value + shap_values[i].sum():.4f} ✓")
# Top 5 features driving THIS prediction
print(f"\nTop 5 SHAP contributions for example {i}:")
feature_shap = list(zip(data.feature_names, shap_values[i]))
feature_shap.sort(key=lambda x: abs(x[1]), reverse=True)
for feat, val in feature_shap[:5]:
direction = "↑ malignant" if val > 0 else "↓ benign"
bar = ("█" * int(abs(val) * 30))
print(f" {feat:<30} {val:+.4f} {direction} {bar}")
# Global feature importance via mean absolute SHAP
print(f"\nGlobal importance (mean |SHAP|) — top 5:")
mean_abs_shap = np.abs(shap_values).mean(axis=0)
top5 = np.argsort(mean_abs_shap)[::-1][:5]
for idx in top5:
bar = "█" * int(mean_abs_shap[idx] * 50)
print(f" {data.feature_names[idx]:<30} {mean_abs_shap[idx]:.4f} {bar}")
Individual prediction decomposed: The model predicted 0.0412 (strongly benign). The baseline is 0.627 (the average prediction — 63% of training examples are malignant). SHAP shows exactly how this example got from 0.627 down to 0.041: "worst concave points" alone pushed it down by 0.291, "worst perimeter" by 0.120, "mean concave points" by 0.089. Only "worst texture" pushed toward malignant (+0.021, overwhelmed by the others).
Sum check = exact: base_value + sum(SHAP) = 0.6271 + (−0.591) = 0.0412. Exact match. SHAP values for tree models are not approximations — they're mathematically guaranteed to decompose the prediction perfectly.
Global vs local importance: the global importance (mean |SHAP| across all test examples) confirms which features drive predictions across the dataset. Local SHAP (per-example) explains why the model made each specific decision. This is the explainability that regulators and domain experts actually care about.
In the age of GPT-4, XGBoost's dominance might seem surprising. But in industry, structured tabular data vastly outnumbers text and images. Financial transactions, medical records, sensor readings, e-commerce logs — these are tables. At JPMorgan, Amazon, Stripe, and Airbnb, XGBoost and LightGBM models make billions of daily decisions: fraud detection, credit scoring, pricing, inventory optimization. The "deep learning replaces everything" narrative is empirically false for tabular data — gradient boosted trees remain the kings of the spreadsheet, and SHAP has made them explainable enough for most regulatory requirements.
13 — A Thought to Sit With
SHAP provides post-hoc explanations — it tells you why the model made a decision after the model was trained. But the model itself was trained to predict accurately, not to be fair or causal. A SHAP explanation might show that "zip code" has a high SHAP value for a loan rejection model — factually true, but zip code is a proxy for race in many US cities.
Can a SHAP explanation satisfy a regulatory requirement for explainability if the feature it highlights is a proxy for a protected attribute? Is SHAP telling you what the model learned, or what the model should have learned?
14 — Coming Up Next
Day 14 — Feature Engineering & Selection. Every model from Days 4–13 is only as good as the features you feed it. Feature engineering is where domain expertise meets machine learning — and where the biggest performance gains often hide. We'll cover creating features, transforming them, selecting the ones that matter, and the traps that fool beginners.
15 — Go Deeper
▶ StatQuest: XGBoost Part 1 — Regression Trees for Boosting
YouTube · 25 min · Deep walkthrough of XGBoost's regularised objective and similarity scores. Watch after reading §02–03.
▶ StatQuest: SHAP Values — Main Effects + Interactions
YouTube · 20 min · How SHAP values are computed and interpreted for tree models. The companion video to §12.
📄 XGBoost: A Scalable Tree Boosting System — Chen & Guestrin (2016)
The original XGBoost paper. Sections 2–3 cover the regularised objective and weighted quantile sketch. Very readable for a systems paper — the engineering decisions are well-motivated.
- Why they exist: sklearn GB becomes too slow on large datasets. XGBoost (≈11× faster), LightGBM (≈67× faster) achieve same or better accuracy through algorithmic and engineering improvements.
- Regularised objective: XGBoost adds γT + ½λΣwⱼ² + αΣ|wⱼ| to the loss. γ controls tree complexity (auto-pruning), λ is L2 on leaf weights, α is L1 on leaf weights. Built-in regularisation baked into training.
- Newton boosting: uses both gradient gᵢ (direction) and Hessian hᵢ (curvature). Optimal leaf weight = w* = −G/(H+λ). Adapts step size per leaf based on local loss curvature. Fewer rounds needed.
- Subsampling: subsample (row fraction per tree) + colsample_bytree (feature fraction). Same decorrelation principle as Random Forest. Start both at 0.8; reduces overfitting.
- Histogram splits (LightGBM): bin features into 256 buckets. O(256) instead of O(N) per split. Adjacent-leaf subtraction halves work further. 10–20× speedup over exact splits.
- Leaf-wise growth (LightGBM): always expand the leaf with greatest loss reduction. Asymmetric trees reduce loss faster per split. Primary constraint: num_leaves (not max_depth).
- GOSS: keep all high-gradient examples, sample 10% of low-gradient ones. Examines 28% of data per round with negligible accuracy loss. 3–5× speedup on large datasets.
- EFB: bundle mutually exclusive sparse features. Reduces effective feature count by 5–20×. Critical for high-dimensional sparse data after one-hot encoding.
- Early stopping: always set n_estimators high, pass eval_set, set early_stopping_rounds=20. Library finds optimal number of trees automatically. Never guess n_estimators.
- SHAP values: exact decomposition of any prediction as base_value + Σ SHAP_i. Each SHAP value = how much that feature pushed this prediction, with direction. Local (per-prediction) explainability. Install: pip install shap.
- What are the key improvements XGBoost makes over vanilla Gradient Boosting?
Regularisation (L1+L2+γ on tree structure and leaf weights — baked into the objective), second-order gradients (Newton boosting — uses Hessian to compute optimal leaf weights analytically), approximate split finding via quantile sketches, native missing value handling by learning a default split direction per feature, and cache-aware column block access that maximises CPU cache efficiency during tree building. - How does LightGBM's leaf-wise growth differ from level-wise, and what's the tradeoff?
Level-wise grows all nodes at each depth before going deeper — balanced, symmetric trees. Leaf-wise always expands whichever single leaf has the greatest loss reduction, regardless of depth — asymmetric trees that reduce loss faster per split. The win: fewer total leaves needed to reach the same training loss. The risk: leaf-wise can grow very deep on one branch, overfitting on small data groups. Guard with num_leaves (primary constraint) and min_child_samples (minimum examples per leaf). - What are GOSS and EFB in LightGBM?
GOSS (Gradient-based One-Side Sampling): keep all high-gradient examples, sample only 10% of low-gradient ones per round. High-gradient examples carry most of the split information; low-gradient examples contribute little. Corrects for sampling bias in gain calculations. Examines ~28% of data per round with negligible accuracy loss — 3–5× speedup. EFB (Exclusive Feature Bundling): mutually exclusive sparse features (never both non-zero) are merged into one feature. Reduces effective feature count by 5–20× on sparse/OHE data. Histogram construction cost drops proportionally. - What is early stopping and why is it the right way to set n_estimators?
Early stopping monitors validation performance each round and stops when it hasn't improved for N consecutive rounds (patience). It prevents overfitting (too many boosting rounds memorise training noise) and removes the need to guess n_estimators. The correct workflow: set n_estimators=1000+ as an upper bound, pass a validation set as eval_set, set early_stopping_rounds=20. The library finds the real optimal number and automatically restores the best-round weights (not the final-round weights). - When would you use LightGBM over XGBoost?
Large datasets (>500K rows) where LightGBM's GOSS + EFB + histograms provide 5–20× speed advantage. High-cardinality categorical features — LightGBM handles them natively without one-hot encoding. Fast iteration cycles where training speed directly affects research velocity. XGBoost is preferable for smaller datasets where exact splits are more accurate than histograms, or where the mature ecosystem and battle-tested defaults are valued. In practice: try both and compare on a held-out validation set.
No attachments for this topic yet.