Hierarchical Demand Forecasting

Predicting daily sales for 10 Walmart stores in California, Texas and Wisconsin, for the company as a whole, for each product category, for each store, and for each individual product in each store, and then adjusting those predictions so they add up correctly to each other, which separately made predictions never do on their own.

Data: the public M5 competition dataset (Walmart, 2011 to 2016). Units means individual items sold per day. Product names are anonymised in the source data, so products appear as IDs such as FOODS_1_001; the three categories and 10 store IDs are the real groupings the data ships with.


The Problem

A demand plan gets used at every altitude at once. Finance commits to a company wide total, category managers plan by category, and the replenishment team orders stock for one product in one store. If each level is forecast on its own, those numbers disagree: and someone ends up reconciling them in a spreadsheet by hand.

Why This Is Hard

There are two independent ways to slice this business, by product (product → department → category) and by geography (store → state). These two paths cross rather than nest: every category is sold in every store, so neither slicing sits inside the other. A single product in a store rolls up through two different chains, not one.

That rules out the simplest fix, taking the company total and splitting it downward, because there is no single path down. It forces methods that work on the whole structure at once.

Company Total ──┬── Product Category ── Department ──┐
                │                                    ├── Individual Product per Store
                └── Store ───────────────────────────┘        (30,490 combinations)

Markers sit at approximate state centres and are sized by store count. The dataset contains no store addresses or coordinates, so exact locations are not shown.


Forecast Accuracy By Level

Pick a level of the business to see how the three methods compare. Lower bars are better - the number is the average amount the forecast missed by, per day.

Error is not comparable between levels: the company total misses by thousands of units a day because it sums millions of units of sales, while a single product in a single store misses by about 2 units because it only sells a handful.

What If: Where Would You Rather Be Accurate?

Illustrative only. This slides linearly between two results that were already computed (MinT at 0%, the original forecast at 100%). It does not re run the optimisation, and the in between blends are not themselves coherent forecasts, they would not add up.

Every Level, Compared


Look Up One Product In One Store

Every product store combination has its own forecast. The list is ordered by how much it sells.

The two forecast lines will look identical, that is the real result, not a glitch. Reconciliation moves an individual product's forecast by about 0.05 units a day on average. Almost all of the adjustment lands on the aggregate levels; the bottom of the hierarchy barely moves. This page ships the highest selling combinations of the 30,490, the full set is in the Streamlit version.


Which Days Are Hardest To Forecast?

The same 28 days, grouped by day of the week. Retail demand is strongly weekly, so some days are consistently harder to predict than others, and reconciliation does not help them equally.


Company Wide Forecast Over The Test Period

The 28 days held back from the models. Actual sales are always shown; switch the forecasts on and off to compare them. Hover for exact values.

Both forecasts follow the weekly rhythm, sales peak at weekends, but under shoot the biggest peaks. The reconciled forecast sits closer to reality on most days.


Do The Forecasts Add Up?

If you add up every store's forecast for one day, you should get the company wide forecast for that day. Before reconciliation you do not. Use the toggle to compare.


The Short Version

What was built. Daily sales forecasts for 30,490 product store combinations across 10 Walmart stores, rolled up into five levels of the business, then adjusted so every level agrees with every other.

What reconciliation bought. Before it, adding up the individual forecasts missed the company wide forecast by as much as 3,449 units on a single day. After it, the levels agree to within a rounding error, a gap of about one unit in eleven trillion. That consistency is guaranteed by construction, not tuned for.

What it cost. Company wide accuracy improved 12.5% and category accuracy 4.6%, but department and store forecasts got 2.7 to 5.6% worse. Error was moved, not removed. At the individual product level almost nothing changed at all.

So what. Whether this is a good trade depends on one question: which level is the plan actually committed at? If finance signs up to a company wide number, reconciliation pays for itself. If the binding commitment is store level replenishment, it costs more than it returns. That is a business decision, and the numbers above are the input to it, not a model selection question with one right answer.


Glossary And Technical Details

Glossary
How The Forecasts Were Built

The raw M5 Walmart data is reshaped in DuckDB from a spreadsheet style file (one column per day) into 58.3 million rows, one per product, per store, per day across 5 years, then aggregated into the five levels.

Each level gets one global LightGBM model using day of week, month, and lagged and rolling average sales. Because the horizon is 28 days but the features reach back only 7, the model feeds its own predictions forward day by day rather than peeking at held out values, verified by replacing the test period with random numbers and confirming the forecasts did not change.

Why The Textbook Method Does Not Fit

The standard version of MinT needs a table comparing every product store combination with every other one. At 30,490 combinations that is a 30,490 × 30,490 matrix, about 7.4 GB, and the maths requires effectively inverting it.

The sparse version used here exploits the fact that of the 930 million possible entries in the summing table, only 152,450 are non zero (0.016%). It solves in 0.06 seconds.

One caveat: MinT produces 1,280 negative forecasts out of 854,308. Rounding them up to zero would break the adding up guarantee.

How This Page Is Built

This page is generated by src/build_static_dashboard.py from the same Parquet files the Streamlit app reads, so the two versions cannot drift. Every number is precomputed and embedded as JSON; the only runtime dependency is Plotly from a CDN. Regenerate with python3 src/build_static_dashboard.py.

Generated 2026-09-19 from the project's Parquet outputs · Source on GitHub