| --- |
| title: RestockIQ |
| emoji: π¦ |
| colorFrom: green |
| colorTo: blue |
| sdk: docker |
| app_port: 7860 |
| pinned: false |
| --- |
| |
| # π¦ RestockIQ |
|
|
| Probabilistic demand forecasting + reorder decisions for a multi-SKU retailer, built on |
| the **M5 Forecasting - Accuracy** dataset (Walmart via Kaggle): 30,490 daily series |
| (3,049 items x 10 stores, CA/TX/WI), ~5.4 years of history, real sell prices, calendar |
| events, and SNAP flags. |
|
|
| Three **global LightGBM quantile models** (P10 / P50 / P90, one model per quantile, |
| trained across all series at once) produce a forecast band per (store, item, day). The |
| band feeds classical inventory math β `demand_std β (P90 β P50) / 1.2816` β so uncertain |
| SKUs automatically get bigger safety stocks. A day-by-day simulation over the final 28 |
| days compares the quantile-driven reorder policy against a naive fixed-reorder baseline. |
|
|
| **No LLM anywhere.** Classical ML only: LightGBM + Prophet/seasonal-naive baselines. |
| Frontend is plain HTML/CSS/JS + Chart.js. Everything served here is precomputed offline; |
| the Space does fast lookups only. |
|
|
| ## Results (28-day holdout: d_1914βd_1941) |
|
|
| ### Forecast quality |
|
|
| | Model | Scope | WAPE | |
| |---|---|---| |
| | Seasonal-naive | all 30,490 series | 0.862 | |
| | Prophet (per-SKU) | 12 sampled series | 0.804 (vs 1.029 seasonal-naive on the same sample) | |
| | **Global LightGBM P50** | all 30,490 series | **0.679** | |
|
|
| P10βP90 empirical coverage: **80.6%** (nominal 80% β the band is well calibrated). |
|
|
| ### Policy backtest (the headline) |
|
|
| | Policy | Stockout rate | Avg holding units | |
| |---|---|---| |
| | Naive (historical mean x lead time, no buffer) | 15.52% | 2.93 | |
| | **Recommended (quantile band + safety stock)** | **12.42%** | 4.46 | |
|
|
| The quantile-driven policy trades ~1.5 extra units of average holding for a ~20% |
| relative reduction in stockout days β the safety stock is doing exactly what the |
| uncertainty band says it should. Series with volatile demand get bigger buffers; |
| stable ones don't pay for protection they don't need. |
|
|
| Both policies share identical simulation mechanics (daily review, 7-day lead time, |
| orders arrive after the lead time, unmet demand lost); only the reorder point differs. |
|
|
| ## How it works |
|
|
| ``` |
| M5 CSVs ββ> prepare_data.py ββ> 46.9M-row feature table (lags, rollings, price, events, SNAP) |
| β |
| ββ> train_baseline.py seasonal-naive + Prophet (WAPE/MAPE) |
| ββ> train_quantile_models.py 3 global LightGBM models (Ξ±=0.1/0.5/0.9) |
| β ββ> recursive 28-day P10/P50/P90 forecast, all series |
| ββ> backtest.py naive vs recommended policy simulation |
| ββ> FastAPI + Chart.js dashboard (this Space) |
| ``` |
|
|
| - **Time-based split, asserted in code**: train β€ d_1913, holdout d_1914βd_1941. Never a |
| random row split. |
| - **No leakage in the holdout**: multi-step forecasts are recursive β lag/rolling |
| features inside the holdout use the model's own P50 predictions, never actual holdout |
| sales. |
| - **Decision layer**: `safety_stock = zΒ·ΟΒ·βlead_time`, `reorder_point = P50 demand over |
| lead time + safety stock`, `order_qty = max(0, reorder_point β current_inventory)`. |
| Service level, lead time, and current inventory are configurable, clearly-labeled |
| assumptions (M5 has no inventory column). |
| |
| ## API |
| |
| | Route | Purpose | |
| |---|---| |
| | `GET /api/skus` | store/item lists for the dropdowns | |
| | `GET /api/forecast/{store}/{item}` | dates + p10/p50/p90 + actuals for charting | |
| | `GET /api/recommendation/{store}/{item}?current_inventory=0&service_level=0.95&lead_time_days=7` | reorder recommendation (all params optional) | |
| | `GET /api/backtest_summary` | naive-vs-recommended comparison, computed offline | |
| | `GET /health` | liveness | |
|
|
| ## Reproducing locally |
|
|
| 1. Download the M5 data (manual, no API token): go to |
| [the competition data page](https://www.kaggle.com/competitions/m5-forecasting-accuracy/data), |
| accept the rules, Download All, and place `calendar.csv`, `sell_prices.csv`, |
| `sales_train_evaluation.csv` under `data/raw/`. |
| 2. `pip install -r requirements.txt` (Python 3.13) |
| 3. Run the pipeline in order: |
| ``` |
| python pipeline/prepare_data.py |
| python pipeline/train_baseline.py |
| python pipeline/train_quantile_models.py |
| python pipeline/backtest.py |
| ``` |
| 4. `pytest eval/` β hand-checked tests for the inventory math. |
| 5. `uvicorn app.main:app --port 7860` and open http://localhost:7860. |
|
|
| Design decisions (dataset choice, quantile-vs-point reasoning, safety-stock derivation, |
| naive-policy definition, why no LLM) are logged in [DECISIONS.md](DECISIONS.md). |
|
|