skip to content
all projects
Tesco · ML Engineer · 2021–2022

Demand Forecasting at Scale

SKU-level demand forecasting across 10K+ products and thousands of stores — handling promotional uplift and seasonality to cut waste 22% and drive ~£2M annual savings.

status delivered
domain forecasting · MLOps
scale 10K+ SKUs · 50M+ records/day
stack Python · Prophet · XGBoost · PySpark · GCP Vertex · Airflow
OVERVIEW

What the platform does

The platform forecasts demand at SKU × store granularity across multi-horizon windows, feeding automated reordering. It fuses point-of-sale and stock data with external signals — weather, promotions, seasonality — through Spark feature pipelines and an ensemble forecast, orchestrated by Airflow.

ENGINEERING GOAL
Replace coarse forecasting that couldn't see local weather, events, or promotion cannibalisation with SKU-level, signal-aware forecasts accurate enough to drive automated reordering.
PROBLEM

Why coarse forecasting failed

Over-ordering perishables drives waste and margin loss; under-ordering drives stockouts. Coarse models couldn't attribute demand shifts to local drivers, so buyers over-corrected. The requirement was granular, explainable, signal-aware forecasting at scale.

ARCHITECTURE

System architecture

Spark pipelines process 50M+ daily transaction records into features; an ensemble of Prophet (seasonality/trend) and XGBoost (signal interactions) is stacked by a meta-learner; Airflow DAGs replace fragile manual workflows with monitored automation.

POS + Stock 50M+ records/day Weather + Promo external signals Feature Pipeline Spark Ensemble Prophet + XGBoost Stacking meta-learner Store Forecast SKU × store Auto Reorder weekly plan
Figure 1 · Tesco forecasting pipeline — signal fusion through a stacked Prophet + XGBoost ensemble into automated reordering.
METHODOLOGY

Forecasting methodology

Prophet captures trend and seasonality per SKU; XGBoost captures interactions between promotions, weather, and price. A stacking meta-learner combines them, and forecasts are backtested on held-out horizons before driving reorders.

stacked ensemble forecast (representative) representative pattern
def forecast(sku: str, horizon: int) -> Forecast:
    base = prophet_models[sku].predict(horizon)          # trend + seasonality
    signal = xgb.predict(feature_frame(sku, horizon))    # promo × weather × price
    # meta-learner blends base + signal, weighted by recent backtest error
    return meta_learner.combine(base, signal)
BACKTEST BEFORE AUTOMATION
Forecasts drive automated reordering, so they're validated on held-out horizons (MAPE ~8.2% on dairy) before going live — an incorrect forecast has direct cost, so accuracy is gated, not assumed.
DESIGN DECISIONS

Architecture decision records

ADR-01

Ensemble over a single model

accepted

Decision: combine Prophet and XGBoost via stacking rather than one model.

Consequence: Prophet handles seasonality it's strong at; XGBoost handles promotional interactions; the blend beats either alone on backtest.

ADR-02

Airflow-orchestrated, monitored pipelines

accepted

Decision: replace manual workflows with Airflow DAGs.

Consequence: retraining and validation are scheduled, monitored, and recoverable — production MLOps rather than fragile scripts.

OUTCOMES

Outcomes

SAVINGS
£2M/yr
SKUs
10K+
MAPE
8.2%
WASTE
−22%
ON THE FIGURES
Metrics reflect the delivered platform; code shown is a representative pattern, not proprietary implementation.