TL;DR — Probabilistic Forecasts for 600+ Products, Retraining Itself Weekly
A production forecasting system for an Indonesian FMCG distributor that predicts daily demand (units, 30 days ahead) and sales revenue (90 days ahead) across 600+ products and 4,000+ product-warehouse series on ~8 years of live data. It ships P10/P50/P90 probabilistic output, runs the full pipeline in ~7.4 min on an RTX 5090, and retrains itself unattended every week. A disciplined 9-round A/B trail produced the headline win: a pooled bias correction that halved chronic under-forecast (−28.7% → −13.0%) without regressing accuracy.
DEMAND ACCURACY — raw vs bias-corrected (v9 rolling-origin backtest)
Raw Corrected
WAPE 48.7% ──▶ 46.0% ✓ lower error
Bias −28.7% ──▶ −13.0% ✓ halved toward 0
Naive Skill 0.007 ──▶ 0.056 ✓ beats "repeat last week" 8×
CRPS 3.844 ──▶ 3.713 ✓ sharper distribution
Ship gate: "drive Bias → 0 without WAPE/CRPS regressing" → PASSED, on by default
The Problem
FMCG demand is brutally hard to forecast: a distributor stocks thousands of SKUs across multiple darkstores, most with sparse, intermittent sales. Over-forecast and capital rots in dead stock; under-forecast and shelves go empty during the exact festive spikes (Lebaran drives 300%+ surges) that matter most. Two structural traps compounded it: 52% of product-warehouse pairs have under 90 days of history (too little for any sequence model), and demand grew ~46% over two years, so any history-anchored model chronically under-forecasts a rising series.
The Approach — Match the Model to the Data, Then Quantify Everything
Two pipelines (demand in units, sales in revenue) share one calendar-aware feature layer. The core idea: don't force one global model onto wildly uneven data — tier it.
DATA DENSITY (15,896 product-warehouse pairs) TIER → MODEL
<90 days ████████████████████████████ 52.2% ─▶ T3 DOW-naive
90–365 days ██████████ 20.0% ─▶ T2 LightGBM
1–2 years █████ 10.9% ─▶ T1 NHITS
2+ years ████████ 16.9% ─▶ T1 NHITS
- Tier 1 — NHITS (data-rich series): a global neural model with full calendar/weather/static exogenous channels and direct multi-horizon quantile output. Trains the entire tier in ~45s (replaced a Temporal Fusion Transformer that was data-loading bound).
- Tier 2 — LightGBM (medium history): tree model on expanding-window features — no need for continuous sequences, leakage-safe lags.
- Tier 3 — DOW-weighted naive (sparse): an honest day-of-week baseline instead of false precision.
- Out-of-stock censoring: an
is_oosflag stops the model from learning stock-out zeros as real demand (a major under-forecast driver). - Indonesian calendar features: Ramadan, Lebaran peak week, THR payout, payday window, rainy season — the single biggest accuracy lever for FMCG.
- Evidence-driven shipping: every change passed (or failed) a rolling-origin backtest gate before merging — no change ships on vibes.
THE EXPERIMENT TRAIL (why each decision shipped)
v4 single-holdout bias fix .... WAPE/CRPS ↑ → REJECTED (unstable estimator)
v6 scaler=standard A/B ......... bias worse → REJECTED (kept robust scaler)
v7 365-day history trim ........ variance 3× tighter → KEPT for speed
v8 pooled + shrinkage bias fix . Bias −28.7→−13.0%, all metrics ↑ → SHIPPED
v9 reproduction + persistence .. corrected metrics in registry → BASELINE
Results
- Demand: all four headline metrics improved when bias correction was switched on — so it ships enabled by default. Zero quantile-ordering violations, zero negative forecasts.
- Sales: per-darkstore daily revenue forecasts land within ~7–13% of the actual recent run-rate, biased slightly high (safer for a distributor than running out), with P10–P90 bands calibrated to ~80% coverage.
- Structural-break handling: when one darkstore absorbed another and nearly doubled overnight, the model tracked the new level (+9% on-level) instead of the −24% under-forecast a naive history-follower would produce.
- From forecast to action: the revenue forecast is converted to cost (COGS) and redistributed around closed days into a daily purchase plan a buyer can order against.
- Operational: full pipeline runs in ~7.4 min on an RTX 5090 and retrains unattended weekly via Task Scheduler, writing probabilistic results back for the reporting dashboards.
Portfolio note: this describes the production system built for an Indonesian FMCG distributor. A companion public repository reproduces the full architecture on a synthetic data generator; figures shown here are model-performance metrics, with client revenue and infrastructure details omitted.