back to projects
2026 Multi-Agent

Foretell Vista — Retail Demand Forecasting & Inventory Optimisation

A multi-tenant retail analytics platform: a 45-feature gradient-boosting demand model with conformal prediction intervals, PSI-based drift monitoring, a three-estimator causal ensemble that warns when its own methods disagree, a signed product-substitution graph, and an inventory simulator with an RL baseline — on a 73,100-row retail dataset.

Foretell Vista — Retail Demand Forecasting & Inventory Optimisation cover image

// outcome

MAE
71.6
RMSE
92.0
dataset rows
73,100
inventory cost
−12.8%

// problem

Demand forecasting in retail has to balance product-level signal, cross-product elasticity, and the privacy constraints of each stakeholder's silo — while explaining which campaign is actually driving the lift, and noticing when the model has quietly stopped fitting.

// approach

Normalise messy vendor CSVs through an alias-resolving registry, forecast with a gradient-boosted model over 45 lag, rolling, calendar, price and competitor features, bound the uncertainty with split-conformal intervals, and triangulate promotion effects with three independent estimators instead of trusting one. The replenishment layer is a SimPy simulator with an RL policy and a heuristic baseline measured against each other.

// results

  • Forecast MAE 71.6 and RMSE 92.0 on a held-out validation window, against a naive lag-1 baseline MAE of 89.2
  • Causal ensemble that reports its own disagreement — 187% spread across three estimators on the same estimate
  • Signed product-substitution graph: complements and substitutes as edges, SVD embeddings, anchored to a fixed seed
  • PSI drift monitoring over six features on a 60-day baseline against a 14-day window, with severity thresholds
  • 12.8% lower total inventory cost for the RL policy against the (s,S) baseline, measured over 8 series

// screens

Foretell Vista — Retail Demand Forecasting & Inventory Optimisation — screen 2
Foretell Vista — Retail Demand Forecasting & Inventory Optimisation — screen 3

// in depth

Ingestion is half the problem

Retail data arrives in whatever shape the vendor had. The dataset registry resolves it: a column-alias map folds sales, units_sold, qty and demand onto one field, the frame is re-aggregated to (store_id, product_id, date), and fields the file did not carry are derived — competitor_price from price, seasonality from the month, inventory_level as seven days of sales when it is missing. CSV, JSON and XLSX all go in.

The underlying dataset is 73,100 rows across five stores and twenty products over two years, of which 14,611 rows form the grocery subset and 100 store-product series.

Zero-demand is censored data, not zero

The subtlest thing in the forecasting code is what it does with a row where inventory was at or below zero and nothing was sold. That is not a demand of zero — it is a demand that was never observable, because the shelf was empty. Those rows are set to missing and interpolated with limit_direction="both", rather than being trained on as zeros.

Uncertainty, and when it goes missing

Prediction intervals come from split-conformal regression where MAPIE is available, and fall back to a Gaussian interval built from the validation residual spread otherwise. The committed model reports a validation residual standard deviation of 65.8 alongside its MAE and RMSE.

Drift is watched separately. A hand-rolled Population Stability Index over ten bins on six numeric features compares a 60-day baseline against a recent 14-day window, with severity thresholds at PSI 0.1 / 0.25 / 0.5 and mean-shift thresholds at 15 / 30 / 50%. A scan history keeps the last hundred readings.

Causal, and honest about its own uncertainty

Most forecasters answer “what will demand be.” Foretell Vista also answers “what moved it” — and is careful about the answer. Three independent estimators are run and combined by inverse-variance proxy weighting:

EstimatorApproach
PSMPropensity score from a logistic model, matched by nearest neighbour
DMLRandom-forest nuisance estimates, residual regression on a logistic model
DiDTreated and control store × product groups differenced over time

When those three disagree, the platform says so. On the shipped data the spread between them is 187% — and the response carries a warning rather than a single confident number. Feature attribution runs through SHAP where it is available, with a permutation-importance fallback, and the causal factor breakdown reports price, month, season and weather contributions directly.

The product graph

Complements and substitutes are the same correlation with opposite signs, so the graph is signed: a positive correlation is an edge from a complement, a negative one a substitute. Nodes are the top SKUs by rank, tiered into anchor, core, growth and niche by rank ratio, with a fixed spring-layout seed so the picture does not move between renders. There is a fallback that keeps the strongest edges when the correlation threshold would otherwise produce an empty graph.

Embeddings come from an SVD of the signed adjacency matrix — spectral vectors scaled by singular value, then combined with the feature matrix. It is not message passing, and it is not a learned GNN; it is a cheap way to get the graph structure into the same feature space as the demand features.

Replenishment, measured against a baseline

Inventory is simulated in SimPy, and a PPO policy trained with Stable-Baselines3 is compared against a fixed heuristic on the same episodes. Over eight series the RL policy reaches a 12.8% lower total inventory cost than the (s,S) baseline, with average inventory down from 1,842 to 1,607 units. The service level is held at 100% for both, so the saving is not bought with stockouts.

The trained policy is evaluated against a served heuristic in production, and the comparison is what the platform reports — the number on the dashboard is the difference between two policies on the same world model, not a score in isolation.

Scope

The federated layer simulates rounds of cross-silo aggregation over the store partitions — five rounds, four participants per round — and the project’s own task list records that it should be replaced with true cross-client training. That is stated here rather than dressed up: the current implementation approximates the privacy-preserving workflow, and the replacement is the next piece of work.

// stack

Python 3.11FastAPIscikit-learnNetworkXStable-Baselines3GymnasiumSimPyMAPIESHAPReact 18ViteRecharts