--- 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).