A demand forecaster that classifies every item by its demand pattern, routes each pattern to the forecasting method built for it, and proves the result by walking forward in time. It runs standalone on a CSV of weekly demand and also imports as a module into a larger network decision system.
The design goal was not a single clever model. It was a defensible system: each routing decision is justified, each method is matched to the demand it handles well, and the evaluation is honest about where the engine wins and where it does not.
- The problem, and why item statuses were replaced
- Architecture: a two-stage router
- How items are classified
- Why each bucket uses the method it does
- Features and how leakage is avoided
- Evaluation methodology and results
- Limitations
- Future work
- Using the engine
- Project structure
- References
Forecasting one method across an entire catalog fails because a catalog is not one thing. A staple that sells every week and a specialty item that sells a few times a quarter need different treatment, and a brand-new item with no history needs different treatment again.
An earlier version of this work routed items using a company's internal merchandising statuses (labels such as INTRO, CORE, and EXCLU). That approach has two problems. It does not transfer, because the next company has different labels or none at all, and it is only loosely correlated with the statistical behavior that actually decides which forecasting method works. So the statuses were replaced with buckets derived from sales history itself, which every company has.
| Old status (one company) | New, data-derived routing |
|---|---|
| INTRO and other new items | Cold-start gate, forecast from the analog group |
| CORE, IN STK, EXCLU | Established, classified into a demand-pattern bucket |
| PD (phasing down) | Surfaces as a declining trend, an optional lifecycle overlay |
The engine makes two decisions for every item.
First, a data-sufficiency gate. An item without enough observed history (here, fewer than 8 observed weeks or fewer than 3 demand events) cannot be forecast on its own and is sent to the cold-start path, where it borrows from its analog group.
Second, for items with enough history, a demand-pattern classification into one of four buckets, each routed to a different method.
item history
|
v
enough history? ---- no ----> cold start (shrink item's own early sales
| toward the analog group prior)
yes
|
v
classify by demand pattern (ADI, CV2)
|
+-- smooth -> global gradient-boosted model
+-- erratic -> global gradient-boosted model (separate from smooth)
+-- intermittent -> Croston / SBA
+-- lumpy -> Croston / SBA
The router is the architecture. The same catalog is served by four methods, each matched to the demand it is good at, instead of one method stretched across all of them.
Established items are classified by two statistics computed from sales history alone, so the scheme works for any company.
ADI, the average demand interval, is the number of periods divided by the number of periods with nonzero demand. It measures how often demand occurs. CV squared is the squared coefficient of variation of the nonzero demand sizes. It measures how much the size of a demand varies when it does occur.
The cut points, ADI of 1.32 and CV squared of 0.49, are from Syntetos, Boylan, and Croston (2005), who derived them as the boundaries where one forecasting method starts to outperform another in expected error. The four quadrants are:
- Smooth: demand is regular and steady. Low ADI, low CV squared.
- Erratic: demand is frequent but volatile in size. Low ADI, high CV squared.
- Intermittent: demand is sporadic but stable in size. High ADI, low CV squared.
- Lumpy: demand is sporadic and volatile, the hardest case. High ADI, high CV squared.
Missing periods and true zero-demand periods are kept distinct throughout. A missing week is one where the item was not observed; a zero week is one where it was observed to sell nothing. Conflating the two, for example by filling missing values with zero, biases intermittent items toward predicting no demand, which is a common and costly error.
The classifier reproduces the quadrant cleanly on the demo data:
This is the core model-selection rationale.
Smooth and erratic, regular enough to learn from, are routed to a global gradient-boosted model (XGBoost). A global model is trained jointly across many items rather than one model per item, which shares statistical strength across the catalog and is the approach that has won large-scale retail forecasting competitions, notably the M5 competition (Makridakis et al. 2022; Montero-Manso and Hyndman 2021). Smooth and erratic are trained as separate global models, not one pooled model, because the heavy right tail of erratic demand otherwise pulls the smooth forecasts upward. Pooling within a pattern helps; pooling across patterns hurts, and the evaluation below confirmed it.
Intermittent and lumpy are routed to Croston's method and its bias-corrected variant, the Syntetos-Boylan Approximation (SBA). Croston (1972) forecasts sporadic demand by smoothing the demand size and the interval between demands separately, then forecasting the per-period rate as size divided by interval. A standard regressor trained on these zero-heavy series learns to predict near zero and misses the occasional demand entirely, which is exactly the failure Croston was designed to avoid. SBA applies a correction that removes a known positive bias in Croston and is the recommended default for intermittent and lumpy demand (Syntetos and Boylan 2005).
New items are forecast by a cold-start that blends the item's own early sales toward an analog group prior, with the weight on the item's own data growing as it accrues history (a shrinkage estimator). The prior is built from the group's regular items, since a new item usually grows into a regular one, and averaging in the group's intermittent and zero-heavy items would bias the prior low.
The machine-learning route uses lags of the item's own demand, rolling mean and standard deviation, the group's mean demand in the prior week, calendar seasonality (week-of-year encoded as sine and cosine), a linear trend, item price, and category. Every feature for a target week is computed from information available strictly before that week. Nothing contemporaneous with the target enters the feature row.
This matters more than it sounds. The single largest defect found during development was a rolling-mean feature that was the model's most important input in training but resolved to a missing value at prediction time because of an index-alignment mistake, which silently collapsed forecasts to the population mean. Leakage-free feature construction and serve-time parity are treated as correctness requirements, not niceties, and are covered by tests.
Time series are evaluated by walking forward in time, never by a random split. The model is trained only on the past and scored on the next unseen week, the rolling- origin evaluation that out-of-sample forecasting practice recommends (Tashman 2000). A random split would let the model see the future and report accuracy it will never achieve in production.
Four metrics are reported, because one number hides too much (Hyndman and Koehler 2006):
- MAE, mean absolute error, in units of demand.
- WMAPE, weighted mean absolute percentage error, which is scale-free and, unlike MAPE, does not break on the zero-demand weeks that fill intermittent series.
- Bias, the mean signed error, which reveals systematic over or under forecasting.
- Skill versus naive, the ratio of the engine's MAE to a last-week naive forecast. Below 1 means the engine beats carrying last week forward.
Results on the synthetic demo (225 items, 52 weeks, four-week walk-forward horizon). These are illustrative, generated by the included data generator, not a claim about any real dataset:
| bucket | n | MAE | WMAPE | bias | skill vs naive |
|---|---|---|---|---|---|
| smooth | 200 | 5.73 | 0.149 | 0.66 | 0.832 |
| erratic | 160 | 32.07 | 0.742 | 1.56 | 0.831 |
| intermittent | 213 | 6.26 | 1.295 | 0.11 | 0.956 |
| lumpy | 147 | 6.75 | 1.258 | -0.78 | 0.763 |
| new | 180 | 5.62 | 0.154 | -1.75 | 1.430 |
| all | 900 | 10.68 | 0.419 | -0.03 | 0.871 |
Reading the table honestly. The engine beats the naive baseline overall (skill 0.871) and on four of five buckets, with overall bias near zero. Erratic has a high MAE because the demand itself is large and volatile, yet it still beats naive, which is the right bar for a hard pattern. The new bucket is the one place the engine trails naive, explained in limitations.
The new-item cold-start trails the naive baseline on the demo (skill 1.43). This is expected and was left as-is rather than overfit. A naive carry-forward is hard to beat once an item already has several weeks of history, which the demo's new items do. The cold-start's real value is the genuine zero-history case, where naive is undefined and the analog prior is the only signal available. The small remaining negative bias reflects the group prior sitting slightly below the new items' true level.
The demo runs on synthetic data so the project is inspectable without proprietary inputs. The synthetic generator assigns a latent pattern, but the engine never sees it; classification is derived from the data, so the demo tests the classifier rather than assuming it. Real data will be messier, and the thresholds and feature set should be revalidated on it.
The intermittent and lumpy buckets show high WMAPE. This is inherent to percentage error on sparse, zero-heavy demand, not a defect of the method, and is why MAE, bias, and skill are reported alongside it.
- A direct multi-horizon mode, forecasting weeks one through four as separate targets rather than one step at a time.
- Prediction intervals, not just point forecasts, which matter most for the lumpy bucket where the point forecast is least trustworthy.
- A learned cold-start that trains a hierarchical or global model on the group rather than a shrinkage heuristic.
- A lifecycle overlay that flags declining items by trend, recovering the intent of the old phasing-down status without depending on a manual label.
- Automatic alpha selection for Croston and SBA per item, rather than a fixed smoothing constant.
- Backtesting across multiple origins and a longer history for more stable metrics than a four-week window provides.
Standalone, on the included synthetic data:
pip install -r requirements.txt
python scripts/run_demo.pyAs a module inside another system, for example the network decision system this was built to feed:
from demand_engine import DemandForecaster, walk_forward, metrics
forecaster = DemandForecaster().fit(demand_history, items)
forecast = forecaster.predict_week(demand_history, items, week=52)
# or evaluate a horizon honestly
results = metrics(walk_forward(demand_history, items, horizon=4))demand_history is a long-format frame of item_id, week_index, date, demand, with
missing weeks left as missing rather than zero. items carries item_id, group,
category, and price. The classifier and metrics are exported as well, so the host
system can read an item's bucket or score its own forecasts.
demand-forecasting-engine/
README.md this document
requirements.txt
src/demand_engine/
classify.py ADI, CV2, the SBC quadrant, the sufficiency gate
data.py synthetic demand generator
features.py leakage-free feature construction
models.py Croston, SBA, and the global ML model
pipeline.py the two-stage router: fit and predict
evaluate.py walk-forward backtest and metrics
scripts/run_demo.py end-to-end demo, produces the figures and metrics
tests/ classification, methods, leakage, routing, skill
assets/ generated figures and metrics
Croston, J. D. (1972). Forecasting and stock control for intermittent demands. Operational Research Quarterly, 23(3), 289-303.
Hyndman, R. J., and Koehler, A. B. (2006). Another look at measures of forecast accuracy. International Journal of Forecasting, 22(4), 679-688.
Makridakis, S., Spiliotis, E., and Assimakopoulos, V. (2022). The M5 accuracy competition: Results, findings, and conclusions. International Journal of Forecasting, 38(4), 1346-1364.
Montero-Manso, P., and Hyndman, R. J. (2021). Principles and algorithms for forecasting groups of time series: Locality and globality. International Journal of Forecasting, 37(4), 1632-1653.
Syntetos, A. A., and Boylan, J. E. (2005). The accuracy of intermittent demand estimates. International Journal of Forecasting, 21(2), 303-314.
Syntetos, A. A., Boylan, J. E., and Croston, J. D. (2005). On the categorization of demand patterns. Journal of the Operational Research Society, 56(5), 495-503.
Tashman, L. J. (2000). Out-of-sample tests of forecasting accuracy: an analysis and review. International Journal of Forecasting, 16(4), 437-450.

