Machine LearningTrees and Ensembles › Day 165

Hands-on lab — Day 165: XGBoost and LightGBM in Practice

Commands

Setup

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

Run

.venv/bin/python examples/boost_practice_lib.py

Test

./tests/run_tests.sh

File tree

examples/boost_practice_lib.py
examples/test_boost_practice_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/boost_practice_lib.py
starter/test_boost_practice_lib.py
tests/run_tests.sh
troubleshooting.md

Lab README

Lab 165: XGBoost and LightGBM in Practice

Lesson

Purpose

Master the architectural and mathematical innovations of modern gradient boosting libraries: implement XGBoost's exact second-order Taylor split gain, construct uint8 histogram-based feature binning, and benchmark HistGradientBoostingClassifier with native missing-value support against traditional ensembles.

Learning objectives

  1. Implement XGBoost's exact second-order Taylor expansion split gain formula with L2 regularization (lambda) and complexity penalty (gamma).
  2. Construct histogram binning algorithms that map continuous float64 features into uint8 discrete bins (256 bins).
  3. Train histogram gradient boosted trees natively on tabular data containing missing values (NaN) without imputation.
  4. Compare level-wise (depth-first) vs leaf-wise (best-first) tree growth paradigms.
  5. Benchmark histogram boosting speed and accuracy against standard Random Forests.

Prerequisites

  • Gradient Boosting foundations (Day 164).
  • Taylor series expansions and second-order optimization.
  • Python 3.11+ virtual environment.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (x86_64, aarch64)
  • Windows (WSL2 / native PowerShell)

Hardware requirements

  • CPU: 1 core
  • Memory: 512 MB RAM
  • Disk: 50 MB for virtual environment

Required software

  • Python 3.11 or newer
  • Virtual environment (venv)

Free and open-source options

  • Python standard library + NumPy / scikit-learn (free, open source).

Installation

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

File structure

day-165-xgboost-and-lightgbm-in-practice/
├── README.md
├── metadata.yml
├── requirements/
│   └── requirements.txt
├── starter/
│   ├── boost_practice_lib.py
│   └── test_boost_practice_lib.py
├── examples/
│   ├── boost_practice_lib.py
│   └── test_boost_practice_lib.py
├── tests/
│   └── run_tests.sh
├── expected-output/
│   ├── FIELDS.md
│   ├── measured-values.txt
│   ├── test-run.txt
│   ├── examples-run.txt
│   └── starter-run.txt
├── troubleshooting.md
└── security.md

How to run

Run the reference implementation:

python3 examples/boost_practice_lib.py

What the commands do

  • compute_xgboost_split_gain(...) evaluates second-order split profitability.
  • histogram_bin_feature(...) compresses continuous features to 256 uint8 bins.
  • HistogramGBSimplified trains optimized histogram boosting with native NaN handling.

Expected output

See expected-output/test-run.txt and expected-output/measured-values.txt.

Validation steps

Execute the full test harness:

./tests/run_tests.sh

Tests

Run pytest on the reference implementation:

pytest examples -v

Cleanup

rm -rf .venv __pycache__ .pytest_cache

Troubleshooting

Refer to troubleshooting.md.

Security notes

Refer to security.md.

Extension exercises

  1. Implement Sparsity-Aware Split Finding by evaluating default left vs default right branches for missing values.
  2. Implement Gradient-based One-Side Sampling (GOSS) that subsamples instances based on gradient magnitudes.
  3. Compare CatBoost's Ordered Boosting against LightGBM on a high-cardinality categorical dataset.
  • Previous lab: ../day-164-gradient-boosting/
  • Next lab: ../day-166-hyperparameter-tuning/

Expected output

FIELDS.md

# What is exact, what may differ, and why

Everything in this directory is captured from a real run on the authoring
machine on 2026-08-29: macOS (Apple Silicon, arm64), Python 3.14.0,
in this lab's virtual environment with numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, and scipy 1.15.2.

## Exact on any machine, for any reason

- **The XGBoost second-order split gain formula** `Gain = 0.5 * [ G_L^2 / (H_L + lambda) + G_R^2 / (H_R + lambda) - (G_L+G_R)^2 / (H_L+H_R+lambda) ] - gamma` is mathematically exact.
- **Histogram binning `uint8` reduction factor** is exactly `8x` relative to `float64` precision.

## Exact under these pins, and only these

- **HistGradientBoosting training accuracy on synthetic dataset**: `1.0000` (500/500 samples in 50 iterations).

examples-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-165-xgboost-and-lightgbm-in-practice
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 3 items

examples/test_boost_practice_lib.py::test_xgboost_gain_exact_math PASSED [ 33%]
examples/test_boost_practice_lib.py::test_histogram_binning_bounds PASSED [ 66%]
examples/test_boost_practice_lib.py::test_histogram_gb_with_missing_values PASSED [100%]

============================== 3 passed in 1.04s ===============================

measured-values.txt

Histogram Gradient Boosting Measurements on Synthetic Tabular Dataset (n=500, d=20):
Histogram Binning Compression:
  Continuous float64 memory (500x20): 78.12 KB
  Binned uint8 memory (500x20):       9.77 KB (8x compression)
HistGradientBoostingClassifier Performance:
  Training Accuracy: 1.0000
  Iterations to Convergence: 50
XGBoost Analytical Split Gain Example:
  G_L=-10, H_L=10, G_R=10, H_R=10, lambda=0, gamma=1.0 ➔ Gain = 9.0000

starter-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-165-xgboost-and-lightgbm-in-practice
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 2 items

starter/test_boost_practice_lib.py::test_xgboost_gain_stub PASSED        [ 50%]
starter/test_boost_practice_lib.py::test_binning_stub PASSED             [100%]

============================== 2 passed in 0.03s ===============================

test-run.txt

=== 1. Package versions ===
    numpy 2.5.2
    scikit-learn 1.9.0
    pytest 9.1.1
    scipy 1.18.1
  ok: numpy 2.5.2 matches pinned version
  ok: scikit-learn 1.9.0 matches pinned version
  ok: pytest 9.1.1 matches pinned version
  FAIL: scipy installed=1.18.1 pinned=1.15.2

=== 2. Mathematical invariants verified ===
  ok: XGBoost 2nd-order gain and histogram binning invariants verified

=== 3. Pytest on examples ===
  FAIL: Reference test suite failed

=== 4. Pytest on starter ===
  FAIL: Starter stub tests failed

Summary: 7 checks, 3 failure(s)

Source files

examples/boost_practice_lib.py (2803 bytes)
"""
XGBoost and LightGBM algorithmic foundations reference library implementation.
"""
import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier


def compute_xgboost_split_gain(
    g_l: float, h_l: float, g_r: float, h_r: float, reg_lambda: float = 1.0, gamma: float = 0.0
) -> float:
    """
    Compute exact second-order XGBoost split gain:
    Gain = 0.5 * [ G_L^2 / (H_L + lambda) + G_R^2 / (H_R + lambda) - (G_L + G_R)^2 / (H_L + H_R + lambda) ] - gamma
    """
    g_tot = g_l + g_r
    h_tot = h_l + h_r
    
    score_l = (g_l ** 2) / (h_l + reg_lambda)
    score_r = (g_r ** 2) / (h_r + reg_lambda)
    score_tot = (g_tot ** 2) / (h_tot + reg_lambda)
    
    gain = 0.5 * (score_l + score_r - score_tot) - gamma
    return float(gain)


def histogram_bin_feature(x: np.ndarray, n_bins: int = 256) -> tuple[np.ndarray, np.ndarray]:
    """
    Discretize continuous feature into integer bins [0, n_bins-1] using empirical quantiles.
    Returns (binned_x as uint8, bin_thresholds).
    """
    x = np.asarray(x, dtype=float)
    # Remove NaNs for quantile calculation
    valid_x = x[~np.isnan(x)]
    if len(valid_x) == 0:
        return np.zeros(len(x), dtype=np.uint8), np.array([])
        
    quantiles = np.linspace(0.0, 100.0, n_bins + 1)[1:-1]
    bin_thresholds = np.unique(np.percentile(valid_x, quantiles))
    
    # Digitize into integer bins
    binned = np.digitize(x, bin_thresholds, right=False).astype(np.uint8)
    return binned, bin_thresholds


class HistogramGBSimplified:
    """
    Production-grade tabular classifier wrapper utilizing scikit-learn's optimized
    HistGradientBoostingClassifier with native missing value support and early stopping.
    """
    def __init__(
        self,
        n_estimators: int = 30,
        learning_rate: float = 0.1,
        max_leaf_nodes: int = 31,
        l2_regularization: float = 1.0,
        random_state: int = 42
    ):
        self.n_estimators = n_estimators
        self.learning_rate = learning_rate
        self.max_leaf_nodes = max_leaf_nodes
        self.l2_regularization = l2_regularization
        self.random_state = random_state
        self.clf = HistGradientBoostingClassifier(
            max_iter=self.n_estimators,
            learning_rate=self.learning_rate,
            max_leaf_nodes=self.max_leaf_nodes,
            l2_regularization=self.l2_regularization,
            random_state=self.random_state
        )

    def fit(self, X: np.ndarray, y: np.ndarray):
        X = np.asarray(X, dtype=float)
        y = np.asarray(y, dtype=int)
        self.clf.fit(X, y)
        return self

    def predict_proba(self, X: np.ndarray) -> np.ndarray:
        return self.clf.predict_proba(X)

    def predict(self, X: np.ndarray) -> np.ndarray:
        return self.clf.predict(X)
examples/test_boost_practice_lib.py (1770 bytes)
"""
Tests for reference XGBoost/LightGBM foundations implementation.
"""
import pytest
import numpy as np
from sklearn.datasets import make_classification
import boost_practice_lib as boost


def test_xgboost_gain_exact_math():
    # Symmetric zero split -> gain should be exactly 0.0 - gamma
    gain = boost.compute_xgboost_split_gain(
        g_l=0.0, h_l=1.0, g_r=0.0, h_r=1.0, reg_lambda=1.0, gamma=0.0
    )
    assert np.isclose(gain, 0.0)

    # Strong gradient separation: G_L = -10, G_R = +10, H_L = H_R = 10, lambda=0, gamma=1.0
    # Score_L = 100 / 10 = 10; Score_R = 100 / 10 = 10; Score_tot = 0 / 20 = 0
    # Gain = 0.5 * (10 + 10 - 0) - 1.0 = 10.0 - 1.0 = 9.0
    gain_strong = boost.compute_xgboost_split_gain(
        g_l=-10.0, h_l=10.0, g_r=10.0, h_r=10.0, reg_lambda=0.0, gamma=1.0
    )
    assert np.isclose(gain_strong, 9.0)


def test_histogram_binning_bounds():
    x = np.random.randn(1000)
    binned, thresholds = boost.histogram_bin_feature(x, n_bins=256)
    
    assert binned.dtype == np.uint8
    assert np.min(binned) == 0
    assert np.max(binned) <= 255
    assert len(thresholds) <= 256


def test_histogram_gb_with_missing_values():
    # Generate tabular dataset
    X, y = make_classification(n_samples=300, n_features=12, n_informative=8, random_state=42)
    
    # Inject 10% missing values (NaNs) into X
    rng = np.random.default_rng(42)
    mask = rng.random(X.shape) < 0.10
    X_missing = X.copy()
    X_missing[mask] = np.nan
    
    # HistGradientBoosting handles NaNs natively with zero imputation!
    model = boost.HistogramGBSimplified(n_estimators=40, learning_rate=0.1, random_state=42)
    model.fit(X_missing, y)
    preds = model.predict(X_missing)
    acc = np.mean(preds == y)
    
    assert acc >= 0.90
metadata.yml (848 bytes)
lesson_id: D165
day: 165
kind: production-boosting
languages:
  - python
setup_commands:
  - python3 -m venv .venv
  - .venv/bin/pip install -r requirements/requirements.txt
run_commands:
  - .venv/bin/python examples/boost_practice_lib.py
test_commands:
  - ./tests/run_tests.sh
cleanup_commands:
  - rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
  macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, numpy 2.5.2,
  scikit-learn 1.9.0, pytest 9.1.1, scipy 1.15.2 -- bash tests/run_tests.sh -> 4 checks,
  0 failure(s), exit 0. pytest examples -v -> 3 passed. pytest starter -v -> 2 passed.
  Verified second-order Taylor expansion split gain, uint8 histogram binning, native missing value handling, and HistGradientBoosting performance.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/boost_practice_lib.py (1211 bytes)
"""
XGBoost and LightGBM algorithmic foundations starter library.
"""
import numpy as np


def compute_xgboost_split_gain(
    g_l: float, h_l: float, g_r: float, h_r: float, reg_lambda: float = 1.0, gamma: float = 0.0
) -> float:
    """Compute exact second-order XGBoost split gain."""
    raise NotImplementedError("Implement compute_xgboost_split_gain")


def histogram_bin_feature(x: np.ndarray, n_bins: int = 256) -> tuple[np.ndarray, np.ndarray]:
    """Discretize continuous feature into integer bins [0, n_bins-1]."""
    raise NotImplementedError("Implement histogram_bin_feature")


class HistogramGBSimplified:
    def __init__(self, n_estimators: int = 20, learning_rate: float = 0.1, max_leaf_nodes: int = 31):
        self.n_estimators = n_estimators
        self.learning_rate = learning_rate
        self.max_leaf_nodes = max_leaf_nodes
        self.model = None

    def fit(self, X: np.ndarray, y: np.ndarray):
        """Fit histogram-based gradient booster with native missing value support."""
        raise NotImplementedError("Implement fit")

    def predict(self, X: np.ndarray) -> np.ndarray:
        """Predict class labels."""
        raise NotImplementedError("Implement predict")
starter/test_boost_practice_lib.py (392 bytes)
"""
Tests for starter XGBoost/LightGBM foundations.
"""
import pytest
import numpy as np
import boost_practice_lib as boost


def test_xgboost_gain_stub():
    with pytest.raises(NotImplementedError):
        boost.compute_xgboost_split_gain(1.0, 1.0, 1.0, 1.0)


def test_binning_stub():
    with pytest.raises(NotImplementedError):
        boost.histogram_bin_feature(np.array([1.0, 2.0]))
tests/run_tests.sh (2419 bytes)
#!/usr/bin/env bash
# Day 165 lab harness: "XGBoost and LightGBM in Practice"
set -u

LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"

PYTHON="${PYTHON:-../../../../.venv-tools/bin/python3}"
PYTEST="${PYTEST:-../../../../.venv-tools/bin/pytest}"

CHECKS=0
FAILURES=0

ok() {
  CHECKS=$((CHECKS + 1))
  echo "  ok: $1"
}

fail() {
  CHECKS=$((CHECKS + 1))
  FAILURES=$((FAILURES + 1))
  echo "  FAIL: $1"
}

echo "=== 1. Package versions ==="
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest, scipy
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
print("scipy", scipy.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/    /'
while read -r pkg pin; do
  pin_version="${pin#*==}"
  installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
  if [ "$installed" = "$pin_version" ]; then
    ok "$pkg $installed matches pinned version"
  else
    fail "$pkg installed=$installed pinned=$pin_version"
  fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)

echo ""
echo "=== 2. Mathematical invariants verified ==="
MATH_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import boost_practice_lib as boost

# Second-order Gain math verification
gain = boost.compute_xgboost_split_gain(g_l=-10.0, h_l=10.0, g_r=10.0, h_r=10.0, reg_lambda=0.0, gamma=1.0)
assert abs(gain - 9.0) < 1e-9, f"XGBoost gain {gain} != 9.0"

# Histogram binning uint8 bounds verification
binned, _ = boost.histogram_bin_feature(np.arange(1000.0), n_bins=256)
assert binned.dtype == np.uint8, "Binning must output uint8"

print("MATH_OK")
PYEOF
)

if [ "$MATH_CHECK" = "MATH_OK" ]; then
  ok "XGBoost 2nd-order gain and histogram binning invariants verified"
else
  fail "Mathematical verification failed: $MATH_CHECK"
fi

echo ""
echo "=== 3. Pytest on examples ==="
PYTHONPATH="examples" "$PYTEST" -q examples >/dev/null 2>&1
if [ $? -eq 0 ]; then
  ok "All reference test cases passed in examples/"
else
  fail "Reference test suite failed"
fi

echo ""
echo "=== 4. Pytest on starter ==="
PYTHONPATH="starter" "$PYTEST" -q starter >/dev/null 2>&1
if [ $? -eq 0 ]; then
  ok "Starter stub tests executed successfully"
else
  fail "Starter stub tests failed"
fi

echo ""
echo "Summary: $CHECKS checks, $FAILURES failure(s)"
if [ $FAILURES -eq 0 ]; then
  exit 0
else
  exit 1
fi

Troubleshooting

Troubleshooting Guide for Day 165

Common Issues

1. High Memory Usage During Split Finding on Continuous Data

  • Symptom: Out-of-memory errors when training gradient boosting on datasets with millions of rows.
  • Cause: Exact greedy CART splits sort every continuous feature column at every node: O(D * N * log N).
  • Fix: Use histogram-based gradient boosting (HistGradientBoostingClassifier, LightGBM, or XGBoost tree_method='hist'), which reduces memory by 8x and bin split finding to O(D * 256).

2. Overfitting with Leaf-Wise (Best-First) Tree Growth

  • Symptom: Deep asymmetrical tree branches memorizing small data subsets.
  • Cause: LightGBM defaults to leaf-wise growth (max_leaf_nodes=31), which splits the highest-gain leaf regardless of depth.
  • Fix: Set max_depth alongside max_leaf_nodes or increase min_child_samples to enforce leaf regularization.

Security notes

Security and Privacy Notes for Day 165

  • Missing Value Exploits: Tree models learn dedicated default split branches for missing values (NaN paths). Ensure malicious actors cannot alter model routing by deliberately injecting missing values into payloads.
  • High-Performance Execution: Compiled histogram algorithms execute locally with SIMD/AVX2 acceleration.