restockiq / README.md
RV302001's picture
RestockIQ v1: M5 quantile forecasting + reorder decision dashboard
3d4c9ec
|
Raw
History Blame Contribute Delete
4.65 kB
---
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).