Predict-then-Optimize — Inventory

Python · LightGBM, NumPyro, PyTorch, PuLP  ·  Data: UCI Online Retail II (1.07M transactions; the raw workbook is 43 MB and is not stored here)

A Forecast Is Not A Decision

A forecast is not a decision. The chain that pays is forecast a distribution → feed it to an optimizer → measure the money, and each of those three steps fails in a different way: a forecast can be accurate and still useless, a decision rule can throw away most of what the forecast said, and a metric can flatter both. This example runs the chain end to end on real transaction data — the UCI Online Retail II file, 1,067,371 transactions from a UK gift wholesaler — and finishes with a backtest that puts a number on what the distribution was worth.

The data is not clean and the notebook does not pretend otherwise. Cancellations, negative quantities, non-product stock codes and zero prices are removed in a logged sequence that keeps 89.4% of rows: 954,729 transactions, 4,862 products over 604 active days. The top 200 products by volume become a dense daily panel of 147,800 rows, and even there 48% of product-days are zero.

That zero share is the whole problem, and the Syntetos–Boylan classification says so precisely. Sorting products on two statistics — ADI, the average interval between demands, and CV², the squared coefficient of variation of the non-zero quantities — gives four classes, split at ADI 1.32 and CV² 0.49. Smooth demand arrives regularly in steady sizes; intermittent arrives irregularly but in steady sizes; erratic arrives regularly in wildly varying sizes; lumpy is both irregular and variable, and is the hardest to forecast because neither timing nor size is predictable. This catalogue occupies only three of the four: across all 4,862 products, 0 smooth, 835 intermittent, 12 erratic, 4,015 lumpy. A gift retailer has essentially no product that sells steadily every day, and four in five sit in the hardest class — long dead spells broken by occasional wholesale orders.

Forecasting The Distribution

The forecast is therefore a distribution, not a mean. Nine quantiles are fit by gradient boosting under the pinball loss, ρτ(u)=u(τ1{u<0})\rho_\tau(u) = u(\tau - \mathbf{1}\{u<0\}), whose minimiser is the τ\tau-quantile, then sorted to prevent crossing. What matters is where the decision lives: coverage in the extreme upper tail is 0.977 against a nominal 0.975, and 0.99 against 0.99. In the mid-range it runs conservative, and below the zero share every quantile collapses onto zero — which is not miscalibration but the probability atom at zero that intermittent demand genuinely has.

nominal quantile0.750.900.950.9750.99
empirical coverage0.8370.9320.9630.9770.990

From Distribution To Decision

The optimizer is the newsvendor, whose solution is a quantile of the demand distribution and nothing else: order the τ\tau^\star-quantile where the critical fractile is

τ=CuCu+Co\tau^\star = \frac{C_u}{C_u + C_o}

Underage cost CuC_u is the lost margin on a stockout, overage cost CoC_o the cost of holding a unit that did not sell. Because real per-product prices drive it, the fractile spans 0.38 to 0.98 across the 200 products (median 0.86) — the same forecast produces a different order for a 12p pen-pot and a £12.75 storage jar. On the snapshot day the recommended order totals 17,148 units against a median-demand total of 5,318: +222% safety stock, which is what a 0.86 fractile on lumpy demand actually costs.

When the warehouse cannot hold every product's ideal order, allocation becomes a linear program and the interesting object is the shadow price — the marginal value of one more unit of capacity. It is strictly positive while capacity binds and falls to exactly zero once capacity passes the unconstrained total of about 17,000 units, because past that point the constraint is not doing anything.

What The Distribution Was Worth

The backtest is the point of the whole exercise: three folds of 28 days, 16,800 product-days scored, three policies charged the same real costs. Ordering the newsvendor quantile costs £273,949 against £327,126 for ordering the point forecast — a 16.3% reduction in realized cost, and cheapest in 3 of 3 folds. Ordering the median is worst at £359,802, which is the sharpest way to see the lesson: the median is the best point forecast under absolute error and still the wrong quantity to stock.

policywhat it ordersrealized cost, 16,800 product-days
newsvendorthe critical-fractile quantile£273,949
point forecastthe conditional mean£327,126 +19.4% vs newsvendor
medianthe 0.50 quantile£359,802 worst, despite minimising absolute error

The value came from the distribution, not the forecast accuracy. All three policies use the same fitted model; they differ only in which part of its output they act on. That is the thesis this whole section exists to state, and the reason the remaining notebooks vary the forecaster rather than the optimizer.

Varying The Forecaster

Three companions test how far that holds. Scaling to the full catalogue — 3,593,018 product-days, 86% of them zero — reproduces the class split at 82.6% lumpy. A hierarchical Bayesian model with per-product random effects beats the booster where data is scarcest (realized cost per unit 0.713 against 1.010 on the scarce stratum) and loses on the rich stratum (0.664 against 0.301), which is partial pooling behaving exactly as it should. A DeepAR-style neural forecaster over-covers at every quantile (0.967 at a nominal 0.90) and splits the strata with the booster rather than dominating it. None of the three overturns the opener; they establish where the extra machinery is worth paying for.

realized cost per unit demandscarcemediumrich
gradient boosting1.0100.3400.301
hierarchical Bayes0.7130.6350.664

Notebooks

References