Back to Projects

FMCG Demand Forecast

Production Dual-Pipeline Demand & Sales Forecasting — In Production, Fully Automated

Role

AI Engineer

Timeline

2024 — 2026

Status

In production, fully automated

Languages
Python SQL
Tools & Frameworks
PyTorch NHITS / neuralforecast LightGBM Docker ClickHouse
Methodologies
Probabilistic Forecasting Time Series Bias Correction MLOps

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_oos flag 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.