← All posts

q50 is not the mean: how to read a probabilistic forecast correctly

Pierre N. · Founder, VecTime Cloud · 7 min read
A probabilistic forecast does not give you one number per future step. It gives you a distribution, and the API hands you eleven summaries of it: nine quantiles from q10 to q90, a point that is exactly q50, and a mean. The two mistakes people make are reading q50 as the average and reading q90 as the worst case. Neither is true, and on a real series the gap is visible.
The short versionq50 is the value the future is equally likely to land above or below. mean is the expected value, which sits off the median whenever the distribution is lopsided. q90 is the value the future lands under nine times in ten, not the ceiling. Plan inventory on the quantile that matches your cost of being wrong, and check the band on held-out data before you trust it.

What a quantile promises

The qk quantile at step h is the value that the outcome at step h should fall below with probability k%. That is the whole definition. q10 is not a lower bound and q90 is not an upper bound: one time in ten the real number is below the first, and one time in ten it is above the second. The pair together form an 80% interval, which is why the benchmark article measures coverage against 80% rather than against 100%.Because each quantile is defined per step, the band widens with the horizon on almost every series. That is not the model getting worse; it is the model being honest that twelve weeks out is less knowable than one.

A real response, read line by line

This is the response for the series that appears in every screenshot on this site — 96 weekly observations for one SKU, horizon 12, from a free-tier key. Only the arrays are abbreviated.
json
{
"forecasts": [{
"index": 0,
"point": [564.2, 550.2, 534.0, …, 588.8], // == quantiles.q50
"mean": [565.7, 554.4, 527.4, …, 581.9], // the distribution mean
"quantiles": {
"q10": [533.0, 512.7, 491.6, …, 507.2],
"q20": [544.3, …], "q30": […], "q40": […],
"q50": [564.2, 550.2, 534.0, …, 588.8],
"q60": […], "q70": […], "q80": […],
"q90": [595.0, 585.4, 575.5, …, 673.0]
}
}]
}
444559673forecast →last 12 observed+12 steps
Observedq50 (median)meanq10–q90, q20–q80, q30–q70, q40–q60
The nested bands are the decile pairs: q10–q90 outermost, q40–q60 innermost. The dashed line is the mean. Everything plotted is copied from the response above.

Mean versus median

On a symmetric distribution the two coincide, and on this series they nearly do for the first few steps. From step 8 on the mean sits below the median by 5 to 7 units — the model puts a longer tail on the downside than the upside. That is a small skew, and this article is not going to pretend it is dramatic. It is simply the reason the API returns both: if you sum q50 across 500 SKUs to get a total, you get the sum of medians, which is not the median of the total and is not its expectation either. Sum the mean for that.
Stepq10q50meanq90mean − q50
1533.0564.2565.7595.01.5
4459.8507.3506.5555.6-0.8
8460.3525.4519.1590.6-6.3
10497.2570.6569.6646.3-1.0
12507.2588.8581.9673.0-6.9
Rows are a sample of the twelve steps. Widths: the q10–q90 band is 62 units wide at step 1 and 166 wide at step 12.

Which line to plan on

The median is the right number to show on a chart. It is almost never the right number to order stock against, because the cost of being short and the cost of being over are not equal. The classic result — the newsvendor fractile — says the optimal quantile is cu / (cu + co), where cu is the cost of one unit short and co the cost of one unit over. Lost margin of 12 against holding cost of 3 puts you at the 80th percentile, and the API already returns it.
python
# Pick the quantile from the cost of being wrong in each direction.
# cu = cost of one unit short (lost margin, expedite fee)
# co = cost of one unit over (holding, markdown, waste)
cu, co = 12.0, 3.0
fractile = cu / (cu + co) # 0.80 → plan on q80
q = out["forecasts"][0]["quantiles"]
key = f"q{int(round(fractile * 10) * 10)}" # nearest decile the API returns
order_up_to = q[key] # one number per horizon step
The same logic runs the other way for alerts. An anomaly detector that pages when a metric crosses q10 or q90 will, by construction, page on roughly one in five clean points. Use q10 and q90 for a warning colour on a dashboard; for a page, widen the band or require two consecutive points outside it.

Checking the band is honest

A quantile is a promise, and promises can be checked. Hold out the last H points of a series, forecast them from the rest, and count how many land between q10 and q90. Over enough series the count should be close to 80%. On the 300 M4 series in our benchmark TimesFM landed at 82.5%, 86.8% and 75.8% by frequency; Prophet's 80% interval landed at 65.0%, 58.5% and 44.8%. A band that covers half of reality while claiming 80% is worse than no band, because it will be planned on.
python
# Hold out the last H points, forecast them, count how many land in the band.
hist, truth = series[:-H], series[-H:]
q = forecast(hist, horizon=H)["forecasts"][0]["quantiles"]
inside = sum(lo <= y <= hi for lo, y, hi in zip(q["q10"], truth, q["q90"]))
print(f"{inside}/{H} inside the 80% band") # aim for ~0.8·H over many series
On the single series in this article the last 12 points all fell inside the band — 12 of 12, against a promise of roughly 10. One series is an anecdote, not a calibration; run it on dozens before drawing a conclusion.

When this is the wrong tool

You need the joint distribution. The quantiles are marginal, per step. They do not tell you the probability that all twelve weeks come in under q90, and multiplying 0.9 by itself twelve times is wrong because the steps are correlated. If you need a distribution over the twelve-week total, sample paths, which this API does not return.You need finer tails than the deciles. The response stops at q10 and q90. A 99th-percentile capacity plan needs a model that returns it or a fitted tail; do not extrapolate the deciles.Your series has fewer than about thirty points. The band will be wide and the median flat, which is the model saying it does not know. That is honest, but it is not useful for planning; use a category-level series until the history is there.
Run it on your own series.
If you want to try this without hosting a model, VecTime Cloud's free tier is 300 forecasts a month and needs no card.
Pierre N.Founder, VecTime CloudBuilds VecTime Cloud — the Go API and its metering, the Python inference service, and the benchmarks published here, including the runs that go against us.