Files
kemal 18b9a29ffa Earn the blend weights and the intervals from forecasts, not from refits
fit() called learn(), and learn() updated three things: the RLS, the conformal
calibrator and the Hedge weights. Only the first belongs to a refit. The comment
above that loop already said so, and was wrong about what the code did.

Measured on 8.2 days of the live station, the 15 minute head had taken 977,078
Hedge updates from 758 distinct supervised pairs, a factor of 1,289, and the
1 day head 296,715 from 12 pairs, a factor of 24,726. A refit is not an outcome.
It is the same week of weather being read again, once every seven minutes.

Hedge is multiplicative, so an edge far too small to be real compounds to
certainty: twelve of twelve temperature and humidity heads had collapsed onto
climatology at a weight of 0.991 or above, while their own member_mae said the
members were within a few percent of each other. The ACI integrator moves by
gamma per observation, so it had likewise pinned against its clips, leaving the
6 hour temperature band (1.571 C) narrower than the 3 hour one (2.258 C), and
pressure at 1 day covering 3 of 7 with alpha jammed at the 0.005 floor.

Three changes, because fixing only the first would freeze the weights forever:

  - fit() calls refit_step(), which touches the regression and nothing else.
    The climatology and setpoint members were evaluated in that loop purely to
    feed the Hedge update, so fit() no longer needs a climatology or a
    setpoint_fn at all.
  - verify() feeds observe_outcome() with the member predictions the forecast
    was actually blended from. These are now written to the forecasts table at
    issue time, because the learned member cannot be recovered afterwards: the
    RLS has moved on.
  - a matured forecast teaches exactly once. It stays readable for an hour so
    the scorecard can aggregate a rolling window, which meant verify() was
    feeding the calibrator the same outcome about twelve times.

The Hedge weights additionally decline an outcome that overlaps the last one
they took, which is the stride rule from fit() applied on the scoring side.
Forecasts are issued every retrain tick, so at the 1 day horizon roughly two
hundred a day resolve against very nearly the same outcome. The conformal window
absorbs that, a quantile over duplicated scores being merely overconfident about
its sample size, but exponentiated gradient cannot.

Walk-forward over the full 8.2 day record, against the current code:

  mean MAE                0.856   (0.938 over the second half alone)
  heads improved          17/18
  beats persistence       7/18 -> 12/18
  coverage |dev from .90| 0.188 -> 0.060,  second half 0.112 -> 0.041

Decimating the conformal feed as well was measured and rejected. It reads better
(coverage |dev| 0.023) and is not: three heads fall below MIN_SCORES, drop to
1.645*sigma, and "cover" with a median band of +/- 107% relative humidity. At
h/4 and h/8 it never starves and lands within noise of not decimating at all, so
the simpler rule wins. Honest regressions: humidity at 12 hours is 19% worse,
and pressure past 6 hours is still under-covered, because at 1 day the point
forecast is genuinely poor and ACI can only widen so far.

A state file from before this change has its weights, member_mae, n_scored and
alpha reset on load. They are products of the replay, they are not evidence, and
they do not decay on their own: Hedge needs about twenty independent outcomes to
climb off its 1e-4 floor and the 1 d head sees one a day. The conformal scores
are kept, being residuals of roughly the right size, and the window refreshes
within about two days.

Schema migration verified against a pristine copy of the live database: 1,437
forecast rows preserved, five columns added, idempotent across restarts.
2026-08-24 00:38:18 +01:00
..