project-025 · MLOps and system design · retail
Weekly batch forecasting platform for tens of thousands of item-store series
A retailer's replenishment system needs 28-day item-store forecasts every Monday by 05:00 (a hypothetical SLA). Design a platform that is reproducible, auditable after the fact, safe to change, and degrades gracefully: which architecture, release gates and fallbacks would you defend in a design review?
At a glance
Data
M5 Forecasting - Accuracy (Kaggle); synthetic airline hierarchy as no-login fallback (public) · 3,049 products in 10 stores across 3 US states as reported in the M5 literature; 42,840 series across levels per the M5 results paper.
Details on dataBaseline
The common starting point: one notebook per analyst, per-series models refit ad hoc, forecasts overwritten in place, no record of what was issued.
Details on baselineValidation
Three stages: (1) offline rolling-origin backtest per hierarchy level (from project-019);
Details on validationMetric
Accuracy per hierarchy level (RMSSE), pipeline success rate, end-to-end runtime vs SLA, time to restore after a failed run, share of series on fallback.
Details on metric
Success criterion
A design document with at least two architecture decision records (ADRs), and a prototype on one state's stores that: rebuilds any past forecast exactly from its version ids; blocks a candidate model that is worse than the champion on any hierarchy level by more than a stated tolerance; produces a coherent fallback forecast when the model job fails; and finishes the subset run within a measured runtime budget extrapolated to the full catalogue.
Context
At this scale, the platform matters more than the model: data contracts, point-in-time features, a forecast vintage table (what was issued, when, by which version), release gates, monitoring and rollback. The M5 data provide a realistic hierarchy and volume. The modelling choices are covered in project-019; here they are inputs to an architecture decision.
Data
- Dataset
- M5 Forecasting - Accuracy (Kaggle); synthetic airline hierarchy as no-login fallback (public)
- Licence
- unknown - check before redistribution (governed by the Kaggle competition rules, not readable without login)
- Size
- 3,049 products in 10 stores across 3 US states as reported in the M5 literature; 42,840 series across levels per the M5 results paper.
- Verification
- identity verified
Access
Kaggle login and acceptance of the competition rules are required. Credentials stay outside the repository;
CI uses the synthetic fallback (network, market, route, flight series from capstone.airline_sim), labelled
synthetic. Raw M5 files are never committed.
Grain
Item x store x day sales; forecasts issued weekly for 28 daily horizons; vintage key = (issue_date, model_version, data_snapshot_id).
| Column | Meaning |
|---|---|
| item_id / store_id (and parent ids) | Series keys and hierarchy |
| daily unit sales | Target; arrives daily, may be restated |
| calendar events and SNAP flags | Known in advance; versioned like any other input |
| weekly sell price | Known up to the current week; future prices only if a price plan feed exists |
| forecast_vintage (platform table) | Every issued forecast with its issue date, model and data versions |
EDA and data-quality rules
Data contract per feed: schema, key uniqueness, expected series count, non-negative sales, calendar coverage for the next 28 days; a failed contract stops the run.
Restatement check: compare this week's history with last week's snapshot and report changed past values; large restatements trigger a review, not a silent overwrite.
New and discontinued series are counted every run; new series route to a cold-start rule.
Baseline
The common starting point: one notebook per analyst, per-series models refit ad hoc, forecasts overwritten in place, no record of what was issued. Its failure modes (non-reproducible numbers, no rollback, no audit) are the requirements the design must remove.
Features
Platform components: ingestion with contracts; point-in-time feature build keyed by issue date; training job writing to a model registry with data snapshot ids; batch scoring; reconciliation; forecast vintage table; monitoring on matured actuals; fallback generator (seasonal naive, always coherent).
Candidate methods
Design A: global LightGBM per horizon bucket plus reconciliation, retrained weekly
Few models to manage, shares information across series; risks: one bad training run affects every series, and runtime grows with the feature build.
Design B: per-series statistical models (statsforecast) by default, global ML as a challenger on high-volume series
Robust, embarrassingly parallel, easy fallback; ML only where it pays. Risks: two code paths to maintain and a routing rule to justify.
Validation design
Three stages: (1) offline rolling-origin backtest per hierarchy level (from project-019); (2) four-week shadow run where the candidate writes to the vintage table with a shadow flag and is scored on matured actuals; (3) staged rollout by store with automatic rollback when a monitored metric crosses its threshold.
Leakage checks
The feature build accepts only an issue date and reads snapshots as of that date; a test rebuilds last month's features and asserts they equal the stored ones (point-in-time reproducibility).
Bias and representativeness
Per-store and per-department bias in shadow mode; a model that improves the total but under-forecasts small stores shifts stock-outs onto them.
Robustness checks
Chaos tests: missing calendar rows, a late sales feed, a duplicated price week, a model job crash; each must produce the documented fallback and an alert.
Technical metrics
Accuracy per hierarchy level (RMSSE), pipeline success rate, end-to-end runtime vs SLA, time to restore after a failed run, share of series on fallback.
Business interpretation
Forecast value added relative to seasonal naive at the levels replenishment uses, and the cost of compute per weekly run (measured on the subset and extrapolated, with the extrapolation stated).
Error slices
Hierarchy level.
New vs established series.
Stores in shadow vs rolled-out groups.
Weeks with restated history.
Implementation steps
Write the requirements: SLA, scale, consumers per level, audit needs; mark hypothetical numbers.
Draft ADR-1 (Design A vs B) and ADR-2 (fallback and rollback strategy) with reconsideration triggers.
Prototype on one state: contracts, point-in-time feature build, registry entries (MLflow), vintage table, reconciliation.
Implement the release gate (per-level tolerance) and the fallback generator.
Run the shadow simulation over four historical weeks and the chaos tests.
Measure runtime and cost; extrapolate to the full catalogue and state assumptions.
Reproduction
Prototype repository with a synthetic-fallback CI path (no login) and a documented manual M5 path. Every run records the git commit, data snapshot id and model version in the vintage table.
Limitations
Runtime and cost extrapolations from a subset can understate costs of joins and reconciliation at full scale.
The SLA and consumers are hypothetical; a real design starts from interviewing those teams.
The M5 licence is not confirmed here, so the prototype cannot be published with real data.
Production and monitoring
Scheduled weekly run with retries and idempotent steps; alerts on contract failures, SLA risk and per-level degradation; runbook with rollback (registry alias switch) and fallback (seasonal naive); quarterly review of the ADR triggers (scale, accuracy gap, team capacity).
Interview questions
How would you make sure you can reproduce exactly the forecast that replenishment used three months ago?
One global model or thousands of local ones? Defend the choice for this retailer.
The training job failed on Sunday night. What does replenishment receive on Monday morning?
Postmortem and alternatives
Record the decision that was hardest to reverse (usually the vintage table schema or the routing rule), what the shadow run revealed that the backtest did not, and which chaos test failed first. Alternative: buy a managed forecasting service and keep only contracts, gates and the vintage table in house.
Reference implementation
Status: executed — outputs below are from the recorded run
r14 results - weekly batch forecasting platform prototype (project-025)
Recorded run of cd python && uv run python -m reference.r14_weekly_batch_forecast_platform.run (full mode). Every number
below is copied from the files in outputs/ written by that run (summary.json, panel_summary.csv, accuracy_by_level.csv,
accuracy_by_slice.csv, accuracy_restated_weeks.csv, release_gate.csv, replay_from_ids.csv,
replay_negative_controls.csv, chaos_suite.csv, scaling_test.csv, cost_time_budget.csv, training_jobs.csv,
vintage_index.csv, run_manifests.jsonl, success_criteria.csv, report.md). The panel is synthetic (thinned from the
academy airline simulator), not M5 and not a retailer; costs and the SLA window are hypothetical.
| Item | Value |
|---|---|
| Run date | 2026-10-12 |
| Seeds | database 42; panel 20241014; LightGBM random_state=42, deterministic=True, n_jobs=1 |
| Python / numpy / pandas / scipy / lightgbm | 3.12.13 / 2.5.3 / 3.0.6 / 1.18.1 / 4.7.0 |
| Machine | Apple M5, macOS arm64, CPU only, local files as the "object store" |
| Runtime of the recorded run | 73.5 s (production loop 33.0 s; other full runs during development took 73 to 130 s on a shared laptop). --smoke: 29 s. |
| Panel | 1,140 series, 19 flights, 11 routes, 3 markets, 1,096 days, 1,133,849 series-day cells, 39.0% zeros, median series mean 1.07 units per day, 4,981 ledger corrections, 116 new and 67 discontinued series |
| Simulated production | 26 weekly runs, issue dates 2024-06-10 to 2024-12-02, 28-day horizon, champion = statistical model, challenger = global LightGBM retrained every 4 weeks (7 training jobs), release decision at 2024-10-14 |
| Tests | uv run pytest tests/test_r14_weekly_batch_forecast_platform.py (see the end of this file) |
Findings
-
A rerun of the whole backfill changes nothing. The run key is a hash of issue date, requested model version, data snapshot id, code version and contract version. Re-running all 26 weeks skipped 26 of 26 (
skipped_idempotent), left the hash of every file undervintage/unchanged and reproduced all 26 forecast hashes; the rerun took 0.74 s against 33.0 s for the first production loop. A fallback run is not treated as done: the chaos suite reruns the same week without the fault and gets a normal success (rows ofchaos_suite.csv). -
A process killed during publish leaves nothing committed and the resume reproduces the clean backfill. Killed while publishing the 2024-09-09 vintage after 13 committed weeks: the killed week had no committed partition and no
CURRENTpointer, one staging directory was left behind and removed by the next run. The resume skipped 13 weeks and recomputed 13; all 26 forecast hashes and all data files (everything but the manifests, which carry timings) equal those of the clean backfill. The same kill is a row in the chaos suite. -
Every injected fault ends in a published, coherent, non-negative forecast, and 12 of 12 cases behaved as designed.
Case Outcome Series WAPE of the published forecast clean run success 0.438 2 transient scoring errors success after 3 attempts, identical forecast 0.438 3 transient errors (retries exhausted) fallback (seasonal naive) 0.453 persistent model job crash fallback 0.453 model artifact missing fallback 0.453 late feed (newest 2 days missing) blocked by contract, fallback 0.456 3% duplicated rows quarantined, identical forecast 0.438 2% negative unit rows blocked by contract, fallback 0.454 calendar missing for 10 horizon days blocked by contract, fallback 0.453 challenger crashes production unaffected, shadow failed0.438 +500 virtual seconds in scoring success, shadow shed, budget breach alerted 0.438 killed while publishing nothing committed, rerun succeeds 0.438 The fallback costs 3.4% to 4.3% more series-level WAPE than the clean champion run in these cases. After the artifact case the rerun without the fault still falls back, because the artifact stays missing; restoring it is a registry action, not a rerun (restore time 15.2 s of which 15 s is virtual back-off).
-
Forecasts are replayable from three ids. 26 of 26 vintages rebuilt from
(issue date, model version, data snapshot id)matched both the stored manifest hash and the hash of the values read back from the partition files (3 decimal places). The negative controls were detected: a wrong snapshot id is refused, a different model version gives a mismatch, and one value edited in a stored partition gives a mismatch. -
The release gate blocked the buggy candidate and the honest challenger; the challenger's later record is mixed. On the four matured vintages before 2024-10-14 (2024-08-26 to 2024-09-16) RMSSE relative to the champion was +3.9% (total), +1.3% (market), +0.9% (route), -0.4% (flight) and -2.0% (series). The tolerance of +2% at any level is exceeded at the total level and the series gain of 2% is not reached (-1.96%), so it was not promoted. A challenger whose calendar was joined 3 days off was blocked as well: +9.1% total and +4.4% market. Scored on all 26 vintages afterwards (shadow files), the challenger is slightly better at the top (total RMSSE 0.757 against 0.774) and at the series level (0.807 against 0.821), about 2% each, so the 4-vintage gate decision was within what the later record would call noise. The gate thresholds are hypothetical.
-
The champion is only a little better than the fallback. Pooled over 26 matured vintages, WAPE (statistical / challenger / seasonal naive): total 0.039 / 0.039 / 0.041, market 0.062 / 0.062 / 0.066, route 0.116 / 0.117 / 0.123, flight 0.152 / 0.152 / 0.162, series 0.453 / 0.455 / 0.475. The champion's improvement over the fallback is 4.3% (total), 5.6%, 6.3%, 6.2% and 4.6% (series). Series level WAPE is high because 39% of cells are zero and half of the series sell about one unit a day: bottom-third volume series have WAPE 1.34 for the champion (1.34 for seasonal naive), top third 0.34 (0.36). New series (under 90 days of history) are at 0.489 (0.467 challenger, 0.490 naive) against 0.449 for established series. Bias: +2.0% for the champion and +1.4% for the challenger (overforecast), +0.4% for the fallback.
-
A planted restatement is flagged for review but does not change the forecast. On 2024-09-16 the contract's restatement check reported 467 changed cells and a 26.1% change of ZX105 over the last 28 days, set
review_requiredand the alertreview_required:restatement, and the run proceeded. The 2024-09-16 vintage had series-level WAPE 0.461 against 0.452 on the other weeks for the champion (one week; no inference). The check only flags; one week of evidence cannot say whether the 2% review threshold is right. -
Cost and time are far inside the hypothetical window at this scale, and the extrapolation is only a trend. Median weekly run 0.19 s for 1,140 series (ingest 0.004 s, contract 0.009 s, score 0.004 s, shadow 0.04 s per run; the rest is writing partitions), training job 3.1 s on 310,642 rows. Run time over 285, 570, 1,140 and 2,280 series fits an exponent of 0.98. Linear extrapolation to 42,840 series gives 7.3 s per run and 118 s per training job, which is 0.05% of the subset budget (383 s, the 4-hour window scaled by series share) and a hypothetical 0.0005 USD per week at 0.05 USD per CPU hour.
Success criteria (record of project-025)
| Criterion | Result | Detail |
|---|---|---|
| rebuilds any past forecast exactly from its version ids | met | 26 of 26 replayed vintages identical; 3 of 3 negative controls detected |
| blocks a candidate worse than the champion on any level by more than the tolerance | met | bug candidate blocked; the honest candidate also failed the gate |
| produces a coherent fallback forecast when the model job fails | met | 12 of 12 chaos cases as designed; all coherent and non-negative |
| subset run finishes within a measured runtime budget extrapolated to the full catalogue | met (weak) | 0.19 s against 383 s; 7.3 s extrapolated; assumptions are listed in cost_time_budget.csv |
| design document with at least two ADRs | not met | not produced by this package; it is the learner's deliverable (this file records the two decisions the numbers support: the fallback is cheap and close in accuracy; the gate needs more than 4 vintages) |
What this does not prove
- A prototype of behaviour, not of scale or infrastructure. Local files stand in for object storage, a scheduler, a model
registry and a warehouse. Atomicity rests on
renameandos.replaceon one local file system; object stores and distributed schedulers have different failure modes (partial listings, concurrent writers, clock skew). There is no concurrency, no authentication and no real network. - The extrapolation to 42,840 series is a trend over four sizes. Memory, I/O, a real item-store hierarchy and a much heavier model are not exercised; the 0.19 s per run is dominated by file writes of a small panel. Training time at scale is a linear guess.
- The panel is much easier than retail data in one way and different in another. 1,140 series are thinned from 19 flights, so series within a flight share a common signal, far more than item-store series. The accuracy numbers say nothing about real hierarchical retail forecasting and the intermittent-demand behaviour is only partly realistic.
- The release gate has hypothetical thresholds and four vintages. A different tolerance, window or metric would give a different decision; the challenger's later record is nearly even. The deliberately buggy candidate is one bug.
- Matured actuals are post-correction values. Series are scored against the final values after late corrections, and the RMSSE scale uses the final values of the preceding year, not the snapshot held on the issue date. This is an evaluation convenience; it can move the denominators slightly and cannot change any forecast.
- Chaos cases are injected by this package. They show that the designed checks trip on the faults the designer thought of. They do not estimate the frequency or the variety of real incidents; late data of one day, a renamed column or silent unit changes are not all included.
- Calendar facts are assumed known in advance (holidays and announced events), and the fallback needs only weekday. A real platform would also face a wrong, not only a missing, calendar.
- Timings vary between runs. All committed timing columns come from this one run on a shared laptop; forecast values and hashes are deterministic and reproduce.
Reproduce
cd python
uv run python -m reference.r14_weekly_batch_forecast_platform.run # about 1.5 to 2 minutes, writes outputs/
uv run python -m reference.r14_weekly_batch_forecast_platform.run --smoke # about 30 seconds, temp directory
uv run pytest tests/test_r14_weekly_batch_forecast_platform.pyRubric3 criteria
| Criterion | Meets | Exceeds |
|---|---|---|
| Architecture decision | Two designs compared in ADRs with constraints, evidence and reconsideration triggers. | Quantifies the decisive trade-off on the prototype (runtime, accuracy gap, operating effort). |
| Reproducibility and audit | Vintage table and point-in-time rebuild test pass. | A reviewer reproduces a past forecast from ids alone. |
| Failure handling | Fallback and rollback documented and exercised in chaos tests. | Runbook timed end to end with a measured time to restore. |
References6
- Hidden Technical Debt in Machine Learning Systems
Glue code, pipeline jungles and configuration debt that platform design must prevent.
- The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction
Tests for data, model, infrastructure and monitoring as release criteria.
- MLflow Documentation
Model registry and experiment tracking for versioned releases.
- DVC (Data Version Control) documentation
Data snapshot versioning for reproducible rebuilds.
- M5 accuracy competition: Results, findings, and conclusions
Scale and hierarchy of the M5 data.
- Point-in-time joins (Feast documentation)
Point-in-time feature retrieval keyed by issue date.