Optimal reconciliation with MinT
Today let's learn how to make the forecasts of a whole hierarchy add up, using a method called MinT.
Take a chain of four bike shops. North has stores N1 and N2, and South has stores S1 and S2. Every month each store records the units it sold, so the sales also add up to a North total, a South total and a Total for the chain.
That makes 7 series in one hierarchy, and each series is called a node. The nodes directly below a node are its children, so N1 and N2 are the children of North.
Now suppose you forecast every node on its own, with its own model. Each result is a base forecast, and the table shows the base forecasts for January 2025. They do not agree with each other: Total is forecast at 1108.7 units, but the North and South forecasts add up to 1123.7. So two forecasts of the same quantity, the chain's sales, are 15.0 units apart.
Read the Gap column: a set of forecasts that adds up would show 0 in all three rows.
What coherent forecasts are, and the summing matrix S
To work on this we need sales data where we know exactly how every series was made. So the code below simulates 10 years of monthly units sold, January 2016 to December 2025, for the four stores. None of it is real store data.
Each store gets a trend (1.2, 0.15, 0.7 and 0.05 extra units every month), a seasonal wave that repeats every 12 months, and random noise with a standard deviation of 25 units. Total, North and South are then the sums of their stores.
The two stores of a region also share one extra shock, which makes them move together. That shock is an AR(1) series: each month's value is half of the previous month's value plus a fresh random draw with a standard deviation of 8.
We fit on the first 108 months, January 2016 to December 2024, and hold out the 12 months of 2025 to score the forecasts later. The training means show the size of each series. The Total averages 1,077 units a month, which is the four store means added up: 368 + 231 + 298 + 180.
Next come the base forecasts. The code fits one ETS model per node with ets(), which is exponential smoothing: each model is described by an error type, a trend type and a seasonal type, and each is additive (A), multiplicative (M) or absent (N). ets() picks that combination for every node on its own.
The nodes did not get the same model. Total, South, N1 and S1 have a trend, North and N2 have none, and S2 has a multiplicative error. With 7 models fitted separately, nothing forces their forecasts to add up.
Three objects come out of this block. fc holds the 12 forecast months for the 7 nodes. res holds the training residuals, which are the actual sales minus each model's forecast of that month, made from the months before it, so it is a 108 by 7 matrix of one-step forecast errors. And yhat is the January 2025 row of fc, rounded to 1 decimal so every number we print from here can be recomputed by hand.
Now the word for what is missing. A set of forecasts is coherent when every parent equals the sum of its children. To build a coherent set you only need the 4 store values, because everything above them is a sum. Write the store values as a vector b, and the full set of 7 as y, and the whole hierarchy is one line:
\[ y = S\, b \]
Here S is the summing matrix. It has 7 rows, one per node, and 4 columns, one per store. Each row says which stores add up to that node: the Total row is 1 1 1 1, North is 1 1 0 0, South is 0 0 1 1, and each store row picks out that store alone.
The last four rows of S are the 4 by 4 identity matrix, which has 1 on the diagonal and 0 everywhere else, so a set of 7 values is coherent exactly when it equals S times its own 4 store values. is_coherent() tests that with a small tolerance. The first month of the history passes, since every Total there is the exact sum of its stores. The base forecasts for January 2025 fail.
These are the three gaps from the cover: 1108.7 - 622.2 - 501.5 is -15.0. To close them, some of the 7 base forecasts have to change, and the question is which ones and by how much.