MLOps · Time series
ML demand forecasting in production
A weekly forecasting system covering thousands of product and store series, feeding replenishment directly.
- Repo
- mercaldas-forecast
- Context
- Mercaldas
- Role
- Sole engineer, design to operation
- Status
- In production since 2025
The problem
Mercaldas forecast demand with the black box bundled into its ERP. Nobody could see why a forecast missed, and there was no lever to make it better. Replenishment for every store ran on it anyway.
Stack
- Python
- LightGBM
- statsforecast
- Dagster
- Polars
- Delta Lake
- MLflow
- SQL Server
What it does
Double-digit %
WMAPE cut against the previous baseline, largest gains in fast-moving lines
Thousands
Product and store series forecast weekly, across 12+ stores
Feeds reordering
Output drives replenishment directly, not a report
Caught in production
A silent horizon failure found and root-caused by an audit I built
Try it yourself
InteractivePick a demand pattern
- What actually sold
- LightGBM forecast
- Prediction interval
Table view
| Week | What actually sold | LightGBM forecast |
|---|---|---|
| w-39 | 346 | n/a |
| w-38 | 371 | n/a |
| w-37 | 374 | n/a |
| w-36 | 423 | n/a |
| w-35 | 393 | n/a |
| w-34 | 434 | n/a |
| w-33 | 384 | n/a |
| w-32 | 417 | n/a |
| w-31 | 377 | n/a |
| w-30 | 383 | n/a |
| w-29 | 402 | n/a |
| w-28 | 417 | n/a |
| w-27 | 453 | n/a |
| w-26 | 442 | n/a |
| w-25 | 460 | n/a |
| w-24 | 416 | n/a |
| w-23 | 464 | n/a |
| w-22 | 469 | n/a |
| w-21 | 439 | n/a |
| w-20 | 483 | n/a |
| w-19 | 493 | n/a |
| w-18 | 445 | n/a |
| w-17 | 483 | n/a |
| w-16 | 486 | n/a |
| w-15 | 435 | n/a |
| w-14 | 449 | n/a |
| w-13 | 506 | n/a |
| w-12 | 429 | n/a |
| w-11 | 502 | n/a |
| w-10 | 473 | n/a |
| w-9 | 445 | n/a |
| w-8 | 453 | n/a |
| w-7 | 472 | n/a |
| w-6 | 453 | n/a |
| w-5 | 448 | n/a |
| w-4 | 381 | n/a |
| w-3 | 400 | n/a |
| w-2 | 419 | n/a |
| w-1 | 363 | n/a |
| w0 | 418 | 418 |
| +1 | 366 | 466 |
| +2 | 350 | 265 |
| +3 | 372 | 353 |
| +4 | 384 | 482 |
| +5 | 356 | 282 |
| +6 | 400 | 382 |
| +7 | 377 | 373 |
| +8 | 378 | 323 |
| +9 | 355 | 369 |
| +10 | 340 | 486 |
| +11 | 406 | 286 |
| +12 | 374 | 464 |
- Accuracy
- 82%100 minus WMAPE over the horizon
- Bias
- +1.6%Forecasts high on average
- Coverage
- 83%Weeks landing inside the interval
Model tournament click one to plot it
Why LightGBM wins here. Dense history and a stable rhythm is where gradient boosting is strongest. It picks up price, promotion and calendar effects that the smoothing models cannot see.
How that forecast is produced
- 01ERP extractsales · stock · master data
- 02Bronzeraw, append-only
- 03Silvercleaned + calendar features
- 04Goldtraining frames per cluster
- 05Tournamentscore, promote, log to MLflow
- 06Replenishmentreorder quantities
Orchestrated weekly in Dagster. Every run scores all four models on held-out weeks and promotes the winner per cluster, so the choice stays measured rather than assumed.
The real pipeline runs this every week over thousands of product and store series across 12+ stores, where the promoted model reaches 70% to 82% accuracy depending on the cluster. The three series here are generated in the browser and every score is measured from the chart beside it, so the figures sit in that same band rather than flattering it.
Context
A grocery chain: 12+ stores, tens of thousands of active items, one replenishment decision per combination every week. That decision came from the ERP's forecasting module, a black box that produced a number but never a reason.
Two constraints shaped the replacement. No cloud budget and no GPU, so it runs on a shared Windows server. No data team, just me. The second one matters more: it rules out any design whose failure mode is "someone notices". Whatever I built had to fail loudly on its own.
Architecture
Medallion layout on Delta Lake, local filesystem. Bronze is raw and append-only, so a bad transform is always recoverable. Silver holds cleaned series with calendar and promotional features. Gold holds training frames. The boundaries exist because that is where the assertions go.
A tournament, not a model. Series in a grocery catalogue do not behave alike. K-Means segments them, LightGBM handles the bulk per cluster, and intermittent demand goes to Croston and SBA. A tree model on a mostly-zero series learns to predict zero: technically right, operationally useless.
The switching rule is code. A champion/challenger setup where challengers get promoted because they "looked better" becomes the engineer's taste within two quarters. The criterion is defined in the pipeline, measured on held-out horizons, applied by the job. MLflow keeps the evidence.
The hard part
Months in, I audited the pipeline. Nothing was on fire. That was the point: "everything reports success" is exactly the state where a silent failure hides.
The audit measured usable horizon, how many weeks ahead the system could still forecast from the data it actually had. That number had been shrinking for weeks.
The cause was structural. The daily ingest had stopped running. The weekly forecast declared a dependency on it but did not enforce one, so Dagster happily materialised a forecast from whatever was already in the lake. The job ran, succeeded, and produced a forecast from data that got staler every week.
Every green checkmark was accurate. The system was still wrong.
The immediate fix made the dependency real. The durable fix was the audit itself: horizon health is now measured independently, never inferred from job status. A pipeline's own success reporting cannot be the evidence that it works. The failure modes that survive longest are precisely the ones the reporting does not cover.
Trade-offs
Reliability before sophistication. Hierarchical reconciliation is not implemented. It is a real gap and the wrong next investment while the base pipeline's failure modes are not fully instrumented.
Local filesystem over cloud storage. Zero infrastructure cost and no vendor lock-in, against horizontal scale and an easy disaster-recovery story. Right at this volume, and the first decision I would revisit.
Weekly, not daily. Replenishment operates weekly. Matching the model's cadence to the decision's cadence also keeps compute inside the window a shared server can give it.
Results
Double-digit percentage WMAPE reduction against the ERP baseline across several categories, largest in fast-moving lines. Thousands of series forecast weekly across 12+ stores, feeding merchandise reordering directly. Over 350 tests, green before deploy.
Business figures are published as ranges. The company is named, the exact numbers are not mine to publish.
Technical metrics
350+
Automated tests, green before every deploy
Medallion
Bronze, Silver and Gold layers on a local data lake
Tournament
Multi-model selection with measurable switching criteria
Mercaldas is named as the employer, but business figures on this page are published as ranges, orders of magnitude or relative percentages. Exact revenue, margin and volume figures, source code and screenshots of real operating data are not published.