WAPE (Weighted Absolute Percentage Error)

WAPE divides the total absolute error by the total of the actuals:

\[ \text{WAPE} = \frac{\sum_i |y_i - \hat{y}_i|}{\sum_i |y_i|} \]

It is often described as a volume-weighted MAPE, and that identity holds when every actual is nonzero.

The weights, made explicit

import numpy as np

actual = np.array([100.0, 200.0, 700.0])
forecast = np.array([90.0, 220.0, 650.0])
abs_err = np.abs(actual - forecast)
print(f"  totals: absolute error {abs_err.sum():.0f}, actual {actual.sum():.0f}")
print(f"  WAPE = {abs_err.sum():.0f} / {actual.sum():.0f} = {100 * abs_err.sum() / actual.sum():.4f}%")
print(f"  MAPE = mean of {np.round(abs_err / actual, 4).tolist()} = "
      f"{100 * np.mean(abs_err / actual):.4f}%")
"""Same data, two numbers. WAPE weights each item by its actual volume;
   MAPE gives every item one vote regardless of size."""

slow = np.concatenate([np.full(40, 1.0), np.array([500.0])])
pred = np.concatenate([np.full(40, 2.0), np.array([510.0])])
err = np.abs(slow - pred)
print(f"    MAPE = {100 * np.mean(err / slow):.2f}%   WAPE = {100 * err.sum() / slow.sum():.4f}%")
print(f"    the forty tiny items are {100 * 40 / 41:.1f}% of the rows and "
      f"{100 * 40 / slow.sum():.1f}% of the volume")

weights = actual / actual.sum()
print(f"    weights {np.round(weights, 4).tolist()}")
print(f"    weighted mean of the percentage errors = "
      f"{100 * float(np.sum(weights * (abs_err / actual))):.4f}%")
print(f"    which is the WAPE computed above: {100 * abs_err.sum() / actual.sum():.4f}%")

for name, a, f in (("all error on the big item", np.array([100.0, 200.0, 700.0]), np.array([100.0, 200.0, 620.0])),
                   ("all error on the small item", np.array([100.0, 200.0, 700.0]), np.array([20.0, 200.0, 700.0]))):
    e = np.abs(a - f)
    print(f"    {name:>28}: WAPE {100 * e.sum() / a.sum():.4f}%"
          f"   worst single item {100 * (e / a).max():.1f}%")
#   totals: absolute error 80, actual 1000
#   WAPE = 80 / 1000 = 8.0000%
#   MAPE = mean of [0.1, 0.1, 0.0714] = 9.0476%
#     MAPE = 97.61%   WAPE = 9.2593%
#     the forty tiny items are 97.6% of the rows and 7.4% of the volume
#     weights [0.1, 0.2, 0.7]
#     weighted mean of the percentage errors = 8.0000%
#     which is the WAPE computed above: 8.0000%
#        all error on the big item: WAPE 8.0000%   worst single item 11.4%
#      all error on the small item: WAPE 8.0000%   worst single item 80.0%

The third block does the algebra. Weighting each item's percentage error by that item's share of total volume — 0.1, 0.2, 0.7 — and averaging gives 8.0000%, which is the WAPE computed from the totals. For these positive actuals, WAPE equals MAPE with volume weights. If an actual is zero, its MAPE term is undefined; use the ratio-of-sums definition of WAPE directly.

That framing settles the usual argument about which is right. MAPE gives every row one vote; WAPE gives every unit of demand one vote. Neither is a correction of the other, and the second block shows how far apart they can land: forty items selling one unit each, forecast at two, plus one item at 500 forecast at 510, gives MAPE 97.61% and WAPE 9.2593%. The forty tiny rows are 97.6% of the rows and 7.4% of the volume.

Both numbers are true. MAPE reports that the model cannot forecast slow movers; WAPE reports that this does not matter much to the warehouse. Which you want depends on whether the cost of an error scales with units or with SKUs.

What it fixes and what it does not

WAPE's main claim is robustness to small actuals, and it holds. MAPE divides by each \(y_i\), so a single actual near zero sends one term toward infinity and the average with it. WAPE divides once, by a total that small values barely move, so a tiny individual denominator cannot cause divergence. A sufficiently large absolute prediction error on one row can still dominate the numerator. It is also defined when some actuals are zero, provided the total is not.

The last block is the cost of that robustness. Two forecasts with identical 8.0000% WAPE: one misses the large item by 11.4%, the other forecasts a small item at a fifth of its demand. Aggregation is what makes WAPE stable, and aggregation is what makes it blind. A single WAPE over a catalog cannot tell you whether the error is spread evenly or concentrated in one line that is about to stock out.

The practical response is not to pick a better single number. It is to report WAPE by segment — by category, by velocity band, by location — so the aggregation happens at a level where the items really are interchangeable.

Reading the number

One property is worth stating because it is often assumed rather than checked: WAPE is unit-free, so it compares a model against another model on the same data, but comparing WAPE across different series compares their volatility as much as the models. A series with stable demand will show a lower WAPE than a spiky one under equally good forecasting.

A changing demand level creates another comparison problem. If total absolute error stays at 10 units while total actual demand grows from 100 to 200, WAPE falls from 10% to 5% without any decrease in absolute error. An upward trend can create this pattern across evaluation periods. Compare models over the same population and period, and report the error total alongside the actual-value denominator.

For those reasons a WAPE is most useful next to a baseline's WAPE on the same series — MASE instead scales test errors by an in-sample naive error. And because WAPE is built from absolute errors, minimizing it aims at the median rather than the mean, the same property discussed under MAE.

References

  • NumPy documentation, numpy.abs and array reductions. numpy.org
  • Rob J. Hyndman, WAPE: Weighted Absolute Percentage Error. Source

Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.