Skip to content
OrionStats
Module 11 · Models · Regression and boosting

Regression, gradient boosting (XGBoost), model comparison and time-series forecasting

Fit and compare linear and gradient-boosted models, forecast with backtesting against seasonal baselines, interpret results for the business and prepare models for deployment.

Expert90 min
Learning objectives
  • Fit regression and gradient boosting models and compare them fairly
  • Explain how gradient boosting (XGBoost) works and its key hyperparameters
  • Build a monthly forecast with lag features and evaluate it by rolling-origin backtesting
  • Communicate model limitations and deployment requirements

Linear regression as a predictive model

Linear (and logistic) regression are strong baselines: fast, stable, interpretable. Add features (lags, dummies, interactions) to capture more structure; regularisation (ridge, lasso) controls overfitting when there are many features.

Gradient boosting and XGBoost

Gradient boosting builds many small decision trees sequentially; each new tree fits the errors (gradients) of the current ensemble. Predictions are the sum of all trees' contributions, scaled by a learning rate.

XGBoost is a widely used, efficient implementation (similar: LightGBM, CatBoost, scikit-learn's HistGradientBoosting). Key hyperparameters:

Parameter Effect
n_estimators number of trees — more fits more, risks overfitting
learning_rate step size — smaller needs more trees, generalises better
max_depth tree complexity / interaction depth
subsample, colsample_bytree randomness that reduces overfitting
min_child_weight, reg_lambda regularisation

Use early stopping on a validation set to choose the number of trees. In Python: xgboost.XGBRegressor(n_estimators=500, learning_rate=0.05, max_depth=4, subsample=0.8, early_stopping_rounds=50) with eval_set=[(X_val, y_val)]. The example below uses scikit-learn's HistGradientBoostingRegressor, which implements the same idea and is available everywhere scikit-learn is.

Forecasting monthly demand: a full backtest

Time-series terms: trend, seasonality, forecast horizon (how far ahead: 1–3 months), forecast accuracy (MAE, WAPE). We forecast monthly units per product category, horizon 1 month, with rolling-origin evaluation: for each month of 2025, train only on data before it.

import pandas as pd, numpy as np
from sklearn.linear_model import Ridge
from sklearn.ensemble import HistGradientBoostingRegressor

orders = pd.read_csv("retail/orders.csv", parse_dates=["order_date"])
items = pd.read_csv("retail/order_items.csv").merge(orders[["order_id", "order_date"]], on="order_id")
items = items.merge(pd.read_csv("retail/products.csv")[["product_id", "category"]], on="product_id")
items["month"] = items["order_date"].dt.to_period("M").dt.to_timestamp()
panel = items.groupby(["category", "month"])["quantity"].sum().rename("units").reset_index()
panel = panel.sort_values(["category", "month"])
g = panel.groupby("category")["units"]
panel["lag1"], panel["lag2"], panel["lag12"] = g.shift(1), g.shift(2), g.shift(12)
panel["roll3"] = g.shift(1).rolling(3).mean().reset_index(level=0, drop=True)
panel["month_no"] = panel["month"].dt.month
panel["t"] = (panel["month"].dt.year - 2023) * 12 + panel["month_no"]
data = pd.get_dummies(panel.dropna(), columns=["category"], dtype=float)
feats = [c for c in data.columns if c not in ("month", "units")]

res = []
for origin in pd.date_range("2025-01-01", "2025-12-01", freq="MS"):
    tr, te = data[data.month < origin], data[data.month == origin]
    preds = {
        "seasonal_naive": te["lag12"] * (tr.groupby("t")["units"].sum().iloc[-12:].sum() / tr.groupby("t")["lag12"].sum().iloc[-12:].sum()),
        "ridge": Ridge(alpha=1.0).fit(tr[feats], tr["units"]).predict(te[feats]),
        "gboost": HistGradientBoostingRegressor(max_depth=3, learning_rate=0.05, max_iter=300, random_state=0).fit(tr[feats], tr["units"]).predict(te[feats]),
    }
    for name, p in preds.items():
        res.append({"model": name, "abs_err": np.abs(te["units"].values - np.asarray(p)).sum(), "actual": te["units"].sum()})
r = pd.DataFrame(res).groupby("model")[["abs_err", "actual"]].sum()
r["WAPE_%"] = (100 * r["abs_err"] / r["actual"]).round(1)
print(r["WAPE_%"].sort_values())
model
seasonal_naive     7.8
ridge             10.7
gboost            20.0
Name: WAPE_%, dtype: float64

Reading the result. WAPE is the share of total units mis-forecast. Here the seasonal naive benchmark wins (≈8% WAPE), ridge regression is second and gradient boosting is clearly worse (≈20%). Category-level demand at Orion is dominated by a stable trend and yearly seasonality, which last year's same month × growth already captures; the boosted trees, trained on only a couple of hundred rows, overfit noise and cannot extrapolate the upward trend (trees never predict beyond the range they have seen). With only 6 categories and 36 months, a flexible model has little data to learn from — a common, honest outcome that should be reported, not hidden. Gains typically appear with more series (product-level), richer features (price, promotions, stock-outs) and longer history.

Model comparison done properly

  • Same splits, same target, same metric for every model.
  • Compare against baselines; report variability across folds/origins.
  • Prefer the simpler model when performance is similar (cheaper, more robust, easier to explain).
  • Look at errors by segment (ABC class, store, category) — an average can hide failure where it matters most.

Model limitations and business interpretation

Every model card should state: purpose, training data period, features, evaluation design and results vs baseline, known weaknesses (e.g. stock-out months understate demand; new products have no history), fairness considerations, and how predictions should be used ("order quantity suggestion reviewed by buyers", not "automatic purchase orders").

Translate accuracy into business terms: a 5-point WAPE improvement on 400k units is 20k fewer mis-forecast units — what does that mean in stock-outs avoided and inventory carried?

Deployment fundamentals

Step Practice
Packaging Save the fitted pipeline (preprocessing + model) with versioned code and data snapshot
Serving Batch scoring (nightly table) for most analytics use cases; API for real-time
Monitoring Input drift, prediction drift, accuracy once actuals arrive
Retraining Scheduled or triggered by degraded accuracy
Governance Owner, approval, documentation, rollback plan

Practice

How does gradient boosting build its ensemble?

Log in to answer and get feedback

Actual units: 100, 200, 300. Forecasts: 110, 180, 330. What is WAPE in %?

Log in to answer and get feedback

In a backtest, seasonal naive WAPE = 7.8%, ridge = 10.7%, gradient boosting = 20.0%. What should you recommend?

Log in to answer and get feedback

Summary

  • Linear models are strong, interpretable baselines; gradient boosting captures non-linearity and interactions with careful tuning and early stopping.
  • Evaluate forecasts by rolling-origin backtests against seasonal naive baselines, using WAPE/MAE.
  • Report honestly — including when complex models don't win — and document limitations.
  • Deploy pipelines, monitor drift and accuracy, and retrain deliberately.