Forecasting algorithm
LightGBM forecasting
Gradient-boosted quantile regression for a single time series. Trains a point prediction plus a calibrated uncertainty band for every period in the forecast horizon. Sub-second on tens of rows; scales to millions.
What it does
You point it at a DataSource and pick a date column, a target column, and (optionally) some drivers. It outputs a new DataSource with one row per future period, in this exact shape:
| Column | Type | Meaning |
|---|---|---|
<date_col> | date | Echoes the input date column name |
yhat | float | Point prediction (median quantile) |
yhat_lower | float | Lower bound at chosen interval |
yhat_upper | float | Upper bound at chosen interval |
The output DataSource is a first-class table the rest of the app can plot, pin to dashboards, or include in reports — there is no separate "forecast viewer".
How it works
Three quantile boosters
For each training run, three independent LightGBM models are fit, one per quantile derived from the chosen interval level:
| Interval level | Lower quantile | Median | Upper quantile |
|---|---|---|---|
| 80% (default) | 0.10 | 0.50 | 0.90 |
| 90% | 0.05 | 0.50 | 0.95 |
| 95% | 0.025 | 0.50 | 0.975 |
LightGBM trains each booster with objective="quantile" against its respective quantile parameter — no calibration trick, just three direct fits.
After prediction the three values are sorted per row so the output always satisfies yhat_lower ≤ yhat ≤ yhat_upper, even if the upper booster slightly under-predicts on a particular row (quantile crossing fix).
The raw quantile bands tend to under-cover at longer horizons, so the trainer applies Conformalized Quantile Regression (CQR) on top: a slice of the training data is held out, residuals max(yhat_lower − y, y − yhat_upper) are computed on it, and the predicted intervals are widened by the interval_level-quantile of those residuals. The "80% interval" label then carries its intended meaning (marginal coverage ≥ 80%) rather than being a hopeful gesture.
When the input is too short to carve a calibration slice, the trainer falls back to raw quantile bands and flags the run; the forecast viz appends "(uncalibrated)" to its title so the looser guarantee is visible.
Engineered features
The trainer adds three families of features automatically — you don't fill these in. The feature set is cadence-aware so the lags cover the dominant seasonality at each frequency without exploding:
| Cadence | Target lags | Rolling means | Exogenous lags |
|---|---|---|---|
| Daily | 1, 7, 14, 28 | 7, 28 | 1, 7 |
| Weekly | 1, 2, 4, 8 | 4, 8 | 1, 2 |
| Monthly | 1, 2, 3, 6, 12 | 3, 6 | 1, 3 |
| Quarterly | 1, 2, 4 | 2, 4 | 1, 2 |
| Yearly | 1, 2, 3 | 2, 3 | 1, 2 |
In addition, calendar features are derived from the date column: day-of-week, month, day-of-month, is_weekend. The rolling-mean window is shifted by one period before averaging so the model never peeks at its own current target during training.
Any driver columns you select are added as their own lagged copies (one entry per lag in the cadence's exog_lags row above). Drivers show up in the feature-importance panel alongside the engineered lag features.
Recursive multi-step forecast
Forecasting more than one period ahead is recursive: the median prediction for step t becomes part of the lag inputs for step t+1. The lower and upper bound predictions are produced the same way but with their respective booster.
This matches how real users want forecasts to behave ("show me the next 12 months") and keeps the feature engineering identical between training and prediction.
Configuration
The form maps directly to the spec — every input lands in either the model row or the spec, no UI-only flags:
| Form input | Stored as |
|---|---|
| Source | PredictionModel.source_id |
| Date column | spec.date_col |
| Target column | spec.target_col |
| Drivers (multi-select) | spec.exogenous_cols |
| Cadence | spec.frequency |
| Horizon | spec.horizon (in periods) |
| Interval level | spec.interval_level |
| Validation horizon | spec.validation_horizon |
| Seed | spec.seed |
algorithm, version, task, and hyperparams are server-defaulted. The seed is in the spec, not the run — same input + same spec ⇒ same output, deterministic by construction.
Metrics
A successful run writes these into PredictionRun.metrics:
| Metric | Meaning |
|---|---|
mae | Mean absolute error against the held-out backtest period |
mape | Mean absolute % error (rows with target=0 excluded) |
smape | Symmetric MAPE — robust to zero/near-zero targets |
pi_coverage | Fraction of backtest rows where actual fell inside band |
feature_importances | Gain-based, descending — what the model leaned on |
smape is the primary "is this any good" number. pi_coverage should be close to interval_level — see Limitations.
Good for
- Single time series with regular cadence and clear seasonal patterns. Daily retail metrics, weekly demand, monthly KPIs.
- Non-linear relationships and abrupt regime shifts. LightGBM handles step-changes, holidays, and threshold effects more gracefully than classical ARIMA.
- Small or large data. Trains in well under a second on a few hundred rows; scales to millions without changing the code path.
- Driver-informed forecasts. When you have leading indicators (marketing spend, weather, competitor pricing), adding them as drivers and reading the importance panel often pays off more than tuning hyperparameters.
Limitations
- Per-horizon coverage drift. CQR applies a uniform widening across the entire forecast horizon, so coverage averages out correctly but step-1 predictions get the same band as step-12. The real forecast variance grows recursively; a per-horizon adjustment (separate quantile per step) is a future refinement.
- No multi-series support. One time series in, one forecast out. Per-store / per-region forecasting is explicitly out of scope for V1 — split into one model per series in the meantime, or aggregate first via ETL.
- No categorical outcomes. Forecasting "which category wins next month" is a classification task and isn't supported in V1.
- Trailing-period edges. If your input data ends mid-period (e.g., the last week is truncated), the trainer doesn't drop the partial period and the final eval metric anchors against an incomplete observation. Trim before training.
- No SHAP / partial dependence. The importance panel shows LightGBM's native gain-based importance with the sign of correlation between feature and target. True SHAP is a separate sprint.
When to use other algorithms
autoarima-v1.md— the classical seasonal-ARIMA baseline. Reach for it on regular, stationary series, when you want model-derived (not conformally-patched) prediction intervals, or simply as a yardstick — running both and comparing is the single most useful forecasting habit.
Future entries (Prophet, theta, etc.) will appear in the Algorithm dropdown automatically when added to the backend registry.
Not sure which to pick?
Choosing a forecasting algorithmLightGBM forecasting vs AutoARIMA — when gradient-boosted forecasting wins, when the classical SARIMAX model is enough, and why it is worth running both.