Full Distribution Forecast
A full distribution describes the complete probability law of a random variable rather than a point summary or a handful of quantiles. Any real-valued \(X\) is characterized by its cumulative distribution function \(F(x) = P(X \leq x)\); a continuous variable can also be written as a density \(f(x)\), and a discrete one as a mass function \(p(x) = P(X = x)\).
A full distribution forecast reports that object for a future outcome. From it you can derive, when the relevant quantities exist: the mean, the median and any other quantile, prediction intervals, event probabilities such as \(P(Y > 100)\), and risk measures such as value-at-risk and expected shortfall.
The contrast is with two weaker outputs. A point forecast gives one value and does not express its own uncertainty. A quantile forecast gives selected cut points — the 10th, 50th and 90th, say — and describes only part of the distribution. That phrase is usually left as a caveat, so it is worth making it exact.
What the whole object buys, and what three cut points lose
import numpy as np
from scipy.stats import lognorm
mu, sigma = 6.2, 0.3
F = lognorm(s=sigma, scale=np.exp(mu))
N = 2_000_000
u = (np.arange(N) + 0.5) / N
"""A full predictive distribution for next week's demand. Every number below
is read off this one object, by integrating its quantile function."""
print(f" LogNormal(mu={mu}, sigma={sigma}) on the log scale:")
print(f" mean {F.mean():>10.2f}")
print(f" median {F.median():>10.2f}")
print(f" mode {np.exp(mu - sigma ** 2):>10.2f}")
print(f" 90th percentile {F.ppf(0.90):>10.2f}")
print(f" P(demand > 700) {F.sf(700):>10.4f}")
print(f" 95% value at risk {F.ppf(0.95):>10.2f}")
print(f" expected shortfall {F.ppf(u[u > 0.95]).mean():>10.2f} (the mean beyond it)")
cut = 0.95
base = F.ppf(cut)
def heavy(u):
out = F.ppf(u)
t = u > cut
v = (u[t] - cut) / (1 - cut)
out[t] = base * (1 + 9 * v ** 3)
return out
"""A second distribution identical to the first below its 95th percentile
and heavier above it. Nothing a 10/50/90 forecast reports can see it."""
H = heavy(u)
G = F.ppf(u)
print(f"\n a second distribution, identical below the 95th percentile:")
print(f"{'':>22}{'10th':>9}{'50th':>9}{'90th':>9}{'P(>700)':>10}{'mean':>10}"
f"{'ES(95%)':>11}{'P(>2000)':>11}")
for name, s in (("the lognormal", G), ("the heavy-tailed one", H)):
print(f"{name:>22}{np.quantile(s, 0.10):>9.2f}{np.quantile(s, 0.50):>9.2f}"
f"{np.quantile(s, 0.90):>9.2f}{np.mean(s > 700):>10.4f}{s.mean():>10.2f}"
f"{s[s > np.quantile(s, 0.95)].mean():>11.2f}{np.mean(s > 2000):>11.4f}")
print(" the three quantiles agree to the second decimal, and so does P(>700).")
print(f" The mean differs by {H.mean() - G.mean():.2f} units, the expected shortfall by a factor"
f" of {H[H > np.quantile(H, 0.95)].mean() / G[G > np.quantile(G, 0.95)].mean():.2f},")
print(" and the lognormal probability past 2000 is positive but rounds to zero")
print(f"\n which is the point. From a 10/50/90 forecast you cannot compute:")
print(" the mean - it is an integral over the whole line")
print(" any tail risk - the region past the last quantile is unreported")
print(" P(Y > 900) - 900 is past the last cut point, so all the")
print(" forecast says is that the answer is under 0.10")
print(" From the distribution, all of them are one function call")
# LogNormal(mu=6.2, sigma=0.3) on the log scale:
# mean 515.43
# median 492.75
# mode 450.34
# 90th percentile 723.76
# P(demand > 700) 0.1209
# 95% value at risk 807.11
# expected shortfall 920.93 (the mean beyond it)
#
# a second distribution, identical below the 95th percentile:
# 10th 50th 90th P(>700) mean ES(95%) P(>2000)
# the lognormal 335.47 492.75 723.76 0.1209 515.43 920.93 0.0000
# the heavy-tailed one 335.47 492.75 723.76 0.1209 600.54 2623.10 0.0226
# the three quantiles agree to the second decimal, and so does P(>700).
# The mean differs by 85.11 units, the expected shortfall by a factor of 2.85,
# and the lognormal probability past 2000 is positive but rounds to zero
#
# which is the point. From a 10/50/90 forecast you cannot compute:
# the mean - it is an integral over the whole line
# any tail risk - the region past the last quantile is unreported
# P(Y > 900) - 900 is past the last cut point, so all the
# forecast says is that the answer is under 0.10
# From the distribution, all of them are one function call
Reading the two halves
The first block is the retail example in full. Next week’s demand is LogNormal with \(\mu = 6.2\) and \(\sigma = 0.3\) on the log scale, and from that one statement come a mean of about 516 units, a 90th percentile near 724, and \(P(\text{demand} > 700) \approx 0.12\) — none of which had to be forecast separately. The same holds for weather: “tomorrow’s temperature is Normal with \(\mu = 25\) and \(\sigma = 2\)” contains the point forecast of 25 °C and the 10th and 90th percentiles of roughly 22.4 and 27.6 as consequences.
The second block is what “only part of the distribution” means when you try to use it. Two distributions agreeing exactly below the 95th percentile report the same 10th, 50th and 90th, and the same \(P(Y > 700)\), while their means differ by 85 units and their expected shortfalls by a factor of 2.85. A quantile forecast is silent about everything past its last cut point, and tail risk lives entirely in that region.
The mean is the cleaner demonstration, because it is not a tail quantity at all. It is an integral over the whole line, so no finite set of quantiles determines it. Any pipeline that reports quantiles and then needs an expected value is silently assuming a shape.
How they are produced, and what they cost
Parametric models assume a family and forecast its parameters. Ensemble and simulation methods generate many plausible paths and use the empirical distribution of the results. Among neural approaches, DeepAR models the parameters of a chosen predictive likelihood, mixture density networks predict the parameters of a mixture, normalizing flows learn flexible densities directly, and temporal fusion transformers commonly produce quantile forecasts — a full predictive distribution only when paired with a distributional output layer.
Evaluation has to cover calibration and sharpness together, which is what CRPS and the logarithmic score do. Both are proper, so their expected values are optimized by reporting your true predictive distribution. Alongside them, check whether predicted intervals contain the outcome at their stated rates over many forecasts.
The cost is the modeling choice itself. Requiring a whole distribution demands a stronger assumption than requiring a mean, it is harder to estimate, and the part that matters most — the tail — is the part the data constrains least. The retail example also carries a modeling caveat worth keeping: if demand has to be whole units, a discrete count distribution fits the quantity better than a continuous LogNormal, however well the LogNormal describes the shape.
References
- SciPy documentation,
scipy.stats.lognorm. docs.scipy.org
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
