Homogenization
A forecast can have the right centre and the wrong width. Homogenization is an online correction for the width, learned from the forecaster's own track record rather than from the data directly. It is worth about 3.3% of pinball loss at the horizon where it helps most, and it never made things worse in any cell measured.
The motivation: a forecaster that grades itself
If a predictive distribution is correct, then pushing each realized value through its
own forecast CDF gives a uniform number, and through the normal quantile gives a
standard normal one. That transformed residual is what skaters calls the
parade z, and it is available online, for free, at every horizon.
So the forecaster can grade itself. If those z values have variance below one, the intervals were too wide; above one, too narrow. Either way the miscalibration is visible without any held-out data, because the grade only uses forecasts that have already matured.
Two kinds of structure are commonly left over. The first is a predictable shift in
conditional variance: quiet stretches and violent ones, which a single fixed scale
cannot serve. The second is unresolved heterogeneity: excess kurtosis that no
single time-varying variance explains, because the residual is really drawn from
several latent regimes at once. A Hermite expansion names both precisely, as the
H₂ and H₄ terms, but the correction it suggests is
not usable: it can go negative in the tails, so it is not a density at all.
The mechanism: a two-point scale mixture, hedged
The correction used instead is the simplest object with both effects built in and no way to go negative, a two-point Gaussian scale mixture:
g(z) = ½ N(0, v(1−δ)) + ½ N(0, v(1+δ))
Here v carries the variance level and δ splits one
variance into two nearby ones, which is exactly how excess kurtosis arises from purely
Gaussian pieces. It is a genuine density for any v > 0. The name comes
from that split: an unresolved mixture of latent regimes is replaced by an effective
aggregate description, without ever committing to which regime is active now.
A small fixed grid of candidates is run in parallel, each with its own filter on the
residual stream, and combined by discounted Bayesian weight. One candidate is the
identity, frozen at v = 1 and holding most of the prior
weight, which is what makes the whole thing safe: on a well-specified forecaster the
identity wins and the correction does nothing.
Results by horizon
One candidate pool per horizon, each resolved against its own matured forecast. FRED economic change-series, 792 paired series per horizon, one-step through twelve-step. Ratio below 1 means homogenization wins; the coverage target is 0.800, and implied sd of 1.000 would be perfect whitening.
| h | n | pinball ratio | wins | coverage raw | coverage corrected | implied residual sd |
|---|---|---|---|---|---|---|
| 1 | 792 | 0.9909 | 61.4% | 0.854 | 0.841 | 0.882 → 0.909 |
| 2 | 791 | 0.9938 | 56.8% | 0.849 | 0.834 | 0.893 → 0.926 |
| 3 | 791 | 0.9910 | 58.2% | 0.858 | 0.856 | 0.874 → 0.877 |
| 4 | 790 | 0.9723 | 63.4% | 0.871 | 0.829 | 0.844 → 0.937 |
| 5 | 792 | 0.9666 | 64.4% | 0.920 | 0.869 | 0.732 → 0.849 |
| 6 | 790 | 0.9676 | 64.7% | 0.902 | 0.859 | 0.773 → 0.871 |
| 7 | 792 | 0.9711 | 63.4% | 0.848 | 0.794 | 0.896 → 1.014 |
| 8 | 790 | 0.9764 | 62.7% | 0.894 | 0.849 | 0.793 → 0.893 |
| 9 | 792 | 0.9790 | 62.6% | 0.868 | 0.823 | 0.852 → 0.950 |
| 10 | 790 | 0.9760 | 62.9% | 0.900 | 0.863 | 0.779 → 0.863 |
| 11 | 792 | 0.9809 | 60.9% | 0.839 | 0.794 | 0.915 → 1.014 |
| 12 | 786 | 0.9672 | 65.8% | 0.856 | 0.802 | 0.877 → 0.994 |
Two things stand out. The gain grows with horizon: about 0.9% at one step, rising to 3.3% at h=5. And the defect being corrected is real and largest in the middle of the range, where raw laplace's implied residual sd falls to 0.732 at h=5 — intervals roughly 37% too wide. Coverage moves toward nominal at every horizon.
One step, held-out corpus
Independently on GIFT-Eval, at one step, across 21 datasets and 3,299 paired series: median ratio 0.9950, winning 57.7% of series (56.0% weighting every dataset equally), and positive on 15 of 21 datasets. So the effect is not confined to the corpus it was developed on.
Using it
from skaters import laplace
from skaters.homogenize import homogenize
f = homogenize(laplace(k=12), k=12) # one pool per horizon
state = None
for y in stream:
dists, state = f(y, state) # dists[h-1] is the corrected h-step predictive
It is deliberately opt-in rather than part of laplace's
default output. It costs roughly 10% more compute per step, and keeping raw laplace as
a stable baseline is what makes measuring the correction possible at all.
What did not work
- Sharing one pool across horizons. The obvious economy is a single correction for every horizon. It overshoots to an implied sd above 1, turning too-wide intervals into too-narrow ones, and loses 2–4% of pinball beyond one step. The horizons genuinely differ.
- Regularizing between horizons. Since long horizons resolve less often, shrinking their pools toward their neighbours should stabilise them. Two variants were tried, shrinking the candidate weights and smoothing the filter states along the horizon axis. Both were within noise of independent pools, and insensitive to the shrinkage strength, which says the quantities being pooled were already nearly equal. Independence is left in place because nothing beat it.
Caveats
- The multi-horizon table is FRED only so far. The held-out GIFT figure is one step, because that arm predates the multi-horizon version.
- Gains are small in absolute terms: about 1% of pinball at one step and 3% at the middle horizons. Reliable and cheap, not transformational.
- Coverage still exceeds nominal after correction at most horizons, so the width problem is reduced rather than solved.
- These are one-step-ahead and multi-step density forecasts of economic change series. Nothing here speaks to level forecasting or to seasonal data.
Reproducing this
PYTHONPATH=src:benchmarks python benchmarks/homogenization_page.py
homogenization_page.py — generates this page · homogenization_results.json — these numbers as data · homogenize.py — the implementation
Measured with skaters 0.16.0+ff9740f, written 2026-08-14. Every figure on this page is generated from the results store, not transcribed.