LEARN · GRADIENT BOOSTING WITH XGBOOST
The result in one paragraph
Under the same 20-second CPU tuning budget, CatBoost produced the best test AUC with its first configuration, despite taking roughly 11 times longer to fit than the winning XGBoost trial. XGBoost delivered the best threshold accuracy, the most completed trials, and a strong speed–quality balance. LightGBM used the least memory but needed more careful regularization and still finished third on this mixed, high-cardinality retail dataset.
That is the central lesson: the fastest training library is not necessarily the fastest route to a good model.
What was benchmarked
The dataset is a seeded synthetic retail-conversion problem:
-
80,000 transactions
-
10 numeric features
-
7 categorical features
-
Cardinalities ranging from 5 to 350
-
A binary purchase-conversion target
-
Numeric nonlinearities, categorical effects, and cross-feature interactions
-
A fixed 60%/20%/20% train, validation, and test split
Each library received:
-
Eight CPU threads
-
Native categorical features
-
Histogram-based training where applicable
-
Early stopping
-
The same validation and test rows
-
A 20-second tuning budget
-
A predetermined configuration sequence
-
No GPU and no external hyperparameter optimizer
A new trial was skipped when the previous trial’s duration suggested that it would exceed the remaining budget. Prediction latency was measured after a warm-up, using repeated predictions over the complete 16,000-row test set.
Versions: current releases versus measured builds
As of July 31, 2026, the current stable releases are XGBoost 3.3.0, LightGBM 4.7.0, and CatBoost 1.2.10.
The measured benchmark image contained XGBoost 3.1.3, LightGBM 4.6.0, and CatBoost 1.2.8. The script below pins the current releases and uses their current APIs. Absolute timings can move between hardware and package builds, so rerun it in your deployment environment before making a production decision.
python -m pip install \
xgboost==3.3.0 \
lightgbm==4.7.0 \
catboost==1.2.10 \
scikit-learn \
pandas \
numpy \
psutil
The benchmark results
| Library | Measured version | Test AUC | Accuracy | Winning fit time | Prediction latency | Peak RSS | Tuning effort | Time to validation AUC ≥ 0.730 |
|---|---|---|---|---|---|---|---|---|
| CatBoost | 1.2.8 | 0.7402 | 0.6840 | 14.71 s | 2.11 ms/1,000 rows | 475 MB | 1 trial | 14.89 s |
| XGBoost | 3.1.3 | 0.7378 | 0.6843 | 1.29 s | 3.39 ms/1,000 rows | 299 MB | 14 trials | Not reached |
| LightGBM | 4.6.0 | 0.7338 | 0.6826 | 3.97 s | 5.18 ms/1,000 rows | 255 MB | 4 trials | Not reached |
Do not overinterpret the third decimal place. This is one seeded split, not a confidence interval. CatBoost’s AUC advantage over XGBoost was only about 0.0025, so a serious model-selection exercise should repeat the benchmark across several seeds or time-based folds.
The operational differences were less ambiguous:
-
XGBoost iterated through configurations extremely quickly.
-
LightGBM had the smallest memory footprint.
-
CatBoost required almost no tuning but had an expensive first fit.
-
CatBoost’s symmetric trees also gave it the fastest batch prediction in this run.
Why the libraries behave differently
XGBoost: controlled, level-wise growth
XGBoost’s histogram tree method defaults to a depth-wise growth policy. In practical terms, it expands a tree level by level, producing a comparatively balanced structure. Its histogram implementation can also use a loss-guided policy, but that must be selected explicitly.
This structure makes XGBoost relatively predictable:
-
max_depthhas an intuitive effect. -
Regularization parameters behave consistently.
-
A bad configuration usually fails gracefully rather than growing one pathological branch.
-
Short trials make broad parameter exploration affordable.
That is exactly what happened here. XGBoost completed 14 trials in less than 20 seconds and gradually improved as min_child_weight and L2 regularization increased.
Where it bites:
-
High-cardinality categoricals still require deliberate validation.
-
Its categorical support remains marked experimental in the 3.3 documentation.
-
Fast trials can tempt you into an unnecessarily large search.
-
The best configuration is often task-specific rather than an obvious default.
XGBoost is the strongest default when you need a mature, controllable baseline and expect to tune systematically.
LightGBM: aggressive leaf-wise growth
LightGBM grows the leaf that offers the greatest immediate loss reduction, rather than expanding every node at the same depth. With the number of leaves held constant, this best-first strategy can reduce training loss faster than level-wise growth. It can also create highly asymmetric trees.
That aggressiveness is both its superpower and its trap.
On large datasets, it can find productive interactions quickly. On smaller or noisy datasets, one branch can become too specialized. LightGBM’s own documentation warns that leaf-wise growth can overfit when the dataset is small and recommends controlling depth.
The relevant controls are not just num_leaves:
-
Increase
min_child_samples. -
Add
reg_lambdaorreg_alpha. -
Limit
max_depth. -
Reduce
num_leaves. -
Smooth high-cardinality categoricals with
cat_smooth.
In this benchmark, increasing minimum leaf size and categorical smoothing helped considerably. The naive configuration was not competitive.
GOSS and EFB
LightGBM is also associated with two efficiency techniques:
-
Gradient-based One-Side Sampling, or GOSS, retains examples with large gradients and samples from examples with small gradients.
-
Exclusive Feature Bundling, or EFB, combines sparse features that rarely contain nonzero values simultaneously.
These techniques reduce the rows or effective features considered during split search. The original LightGBM paper introduced both approaches.
One current-API detail matters: GOSS is a selectable sampling strategy, not something you should assume is active in ordinary gradient boosting. Modern LightGBM exposes it through data_sample_strategy="goss". EFB is normally enabled and can be disabled with enable_bundle=false.
Our dense, 17-column dataset gave EFB little room to shine. It becomes more compelling for wide, sparse inputs such as encoded event, advertising, or text-derived features.
CatBoost: ordered statistics instead of naive encoding
CatBoost was designed around categorical variables. Rather than requiring global one-hot encoding, it constructs numeric statistics from categories and category combinations. Its documentation explicitly advises against preprocessing its categorical columns with one-hot encoding because doing so can hurt both speed and quality.
The important idea is ordered processing.
A naive target encoding can leak information from a row’s own label into its encoded feature. CatBoost calculates categorical statistics using ordered permutations so that an example’s representation is based on earlier examples rather than its own target. Ordered boosting applies a related principle while estimating residuals. These mechanisms were introduced to reduce prediction shift and target leakage.
That extra work explains CatBoost’s slow first fit:
-
It builds categorical statistics.
-
It may evaluate category combinations.
-
Its ordered machinery has more setup cost.
-
Symmetric trees can require evaluating splits across an entire level.
The payoff is a strong model without a separate encoding pipeline. In our benchmark, the first depth-6 configuration was already the overall AUC winner.
Reproduce the experiment
The following compact generator creates the same type of mixed retail dataset. It avoids external data downloads and keeps all categorical columns as pandas categories.
import numpy as np import pandas as pd from sklearn.model_selection import train_test_split SEED = 20260731 rng = np.random.default_rng(SEED) n = 80_000 X = pd.DataFrame({ "order_value": rng.lognormal(3.5, 0.75, n), "discount_pct": rng.beta(2, 8, n), "account_age_days": rng.gamma(2.2, 180, n), "page_views": rng.poisson(6, n), "shipping_days": rng.integers(1, 9, n), "prior_orders": rng.poisson(3, n), "returns_rate": rng.beta(1.5, 12, n), "ad_score": rng.normal(0, 1, n), "hour": rng.integers(0, 24, n), "stock_ratio": rng.beta(5, 2, n), }) cardinalities = { "channel": 8, "region": 24, "device": 5, "campaign": 120, "category": 40, "segment": 7, "merchant": 350, } category_values = {} category_effects = {} for name, size in cardinalities.items(): values = rng.integers(0, size, n) category_values[name] = values category_effects[name] = rng.normal(0, 0.35, size) X[name] = pd.Categorical(values.astype(str)) logit = ( -2.0 + 0.015 * np.minimum(X["order_value"], 180) + 2.2 * X["discount_pct"] + 0.06 * np.sqrt(X["account_age_days"]) + 0.09 * X["page_views"] - 0.12 * X["shipping_days"] + 0.12 * np.log1p(X["prior_orders"]) - 1.8 * X["returns_rate"] + 0.35 * X["ad_score"] + 0.8 * (X["stock_ratio"] > 0.8) ) for name, values in category_values.items(): logit += category_effects[name][values] logit += 0.9 * ( (category_values["channel"] == 2) & (category_values["device"] == 0) ) logit += 0.8 * ( category_values["campaign"] % 17 == category_values["region"] % 17 ) logit += rng.normal(0, 1.25, n) probability = 1.0 / (1.0 + np.exp(-logit)) y = rng.binomial(1, probability) X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=0.40, stratify=y, random_state=SEED, ) X_valid, X_test, y_valid, y_test = train_test_split( X_temp, y_temp, test_size=0.50, stratify=y_temp, random_state=SEED, )
These are representative winning model definitions using current APIs:
from catboost import CatBoostClassifier from lightgbm import LGBMClassifier from xgboost import XGBClassifier xgboost_model = XGBClassifier( n_estimators=1200, learning_rate=0.05, max_depth=6, min_child_weight=20, reg_lambda=16, subsample=0.85, colsample_bytree=0.85, tree_method="hist", enable_categorical=True, max_cat_to_onehot=8, eval_metric="auc", early_stopping_rounds=40, n_jobs=8, random_state=SEED, ) lightgbm_model = LGBMClassifier( n_estimators=1200, learning_rate=0.05, num_leaves=31, min_child_samples=120, reg_lambda=8, cat_smooth=40, subsample=0.85, subsample_freq=1, colsample_bytree=0.85, n_jobs=8, random_state=SEED, verbosity=-1, ) catboost_model = CatBoostClassifier( iterations=1200, learning_rate=0.05, depth=6, l2_leaf_reg=5, random_strength=1.0, loss_function="Logloss", eval_metric="AUC", early_stopping_rounds=40, thread_count=8, random_seed=SEED, allow_writing_files=False, verbose=False, )
The cherry on the cake
CatBoost was the slowest library per fit, yet it was the only one to cross validation AUC 0.730 within the budget.
-
XGBoost: 14 attempts, no crossing
-
LightGBM: 4 attempts, no crossing
-
CatBoost: 1 attempt, crossing after 14.89 seconds
This is the difference between fit time and time-to-a-good-model. Teams often benchmark only a single training call, then ignore the engineer-hours and compute spent finding acceptable parameters.
CatBoost lost the stopwatch race and won the development race.
A practical chooser rubric
Choose XGBoost when:
-
You need a dependable general-purpose baseline.
-
Training and tuning speed both matter.
-
Your team wants fine control over regularization.
-
Numeric features dominate or categories are already well managed.
Choose LightGBM when:
-
The dataset is very large, wide, or sparse.
-
Memory pressure is a primary constraint.
-
You can validate leaf-size and depth controls carefully.
-
You understand that its aggressive growth can overfit small data.
Choose CatBoost when:
-
Raw categorical columns carry important signal.
-
Cardinalities are moderate or high.
-
You want to avoid maintaining a target-encoding pipeline.
-
A slower initial fit is acceptable in exchange for less tuning.
For an unfamiliar dataset, do not choose from reputation. Give all three the same split, metric, thread count, and tuning budget.
Your next step
Run this benchmark on your own table using at least three seeds or time-based folds. Record validation quality, final test quality, fit time, batch latency, peak memory, and total tuning time—then choose the library that reaches your production target with the least total engineering cost.