Week-2 provider diagnostic (2026-08-04)¶
Per-source error profile vs station truth in 24 h lead bins from
data/hourly_matrix_live.parquet (live archive through 2026-08-04), run to
decide [forecasts].max_lead_hours trim values before building on the
168-240h bucket. Script inline below; MAE/bias in variable units.
Horizon census¶
Only three sources reach the 168-240h bucket at all: visual_crossing (max lead 359 h), stormglass_sg (239 h), met_norway (231 h, ragged — n per week-2 bin is only ~100-170). nws/open_meteo/weatherapi cliff at 154-167 h, inside 96-168h; tomorrow_io at ~119 h.
temp_c — MAE (bias) by bin¶
| source | 96h | 120h | 144h | 168h | 192h | 216h | 240h | 288h | 336h |
|---|---|---|---|---|---|---|---|---|---|
| met_norway | 1.09 (−0.81) | 0.99 (−0.68) | 0.76 (−0.57) | 1.47 (−0.06) | 1.64 (+0.64) | 2.04 (+0.23) | |||
| visual_crossing | 2.09 (+0.67) | 2.16 (+1.28) | 2.28 (+1.74) | 4.07 (+3.82) | 6.78 (+6.78) | 6.50 (+6.50) | 6.49 (+6.49) | 4.45 (+4.36) | 3.63 (+3.26) |
| stormglass_sg | 3.39 (+1.45) | 5.70 (+5.68) | 6.57 (+6.57) | 7.19 (+7.19) | 6.79 (+6.79) | 6.68 (+6.68) |
humidity_pct — MAE (bias) by bin¶
| source | 96h | 144h | 168h | 192h | 216h | 240h | 288h |
|---|---|---|---|---|---|---|---|
| met_norway | 6.65 (−4.7) | 5.84 (−4.6) | 11.94 (−9.8) | 13.88 (−11.8) | 13.17 (−11.4) | ||
| visual_crossing | 5.99 (−2.6) | 5.50 (−1.1) | 10.82 (−9.2) | 18.06 (−17.7) | 17.17 (−16.9) | 15.23 (−14.0) | 9.05 (−2.3) |
| stormglass_sg | 9.07 (−8.2) | 9.00 (−7.7) | 14.72 (−14.7) | 18.71 (−18.7) | 15.92 (−15.9) |
Findings¶
- The horizon-cliff hypothesis is wrong. Week-2 error is not "last
hours before each provider's horizon are noisy." It is systematic
bias: for visual_crossing and stormglass, week-2 MAE ≈ |bias| in both
variables (+6.5 °C warm, −17 % dry at 192-240 h). The residual after
removing the offset is small — the anomaly signal out there is actually
decent; the level is broken. This is exactly the −17.9 % humidity bias
the served (ungrounded)
equal_weightblend inherits at 168-240h. - stormglass_sg is the one true trim case. Its warm bias switches on
at 120 h (+5.7) and stays 6-7 °C through its horizon. The provider is
also payment-lapsed in the collector (402 since 2026-08-03), so its
archive tail only pollutes ungrounded blends going forward.
→
[forecasts.max_lead_hours] stormglass_sg = 120. - visual_crossing should NOT be trimmed at 168 h despite the bias
hump (168-288 h, recovering by 312 h): capping it would leave the
168-240h bucket with only met_norway's ~150 rows per bin — coverage
would collapse below eligibility and the 240h+ product would vanish.
The bias is grounding-correctable signal, and the bias-vulnerable
serving choice (raw equal_weight) is now challenged by
damped_grounded_equal_weight(bias-free climatology component) andanalog_ensemble. Revisit after one promotion cycle. - met_norway is the only well-calibrated week-2 source (MAE 1.5-2.0, |bias| < 0.7 on temp) but too thin to carry the bucket alone.
- Why grounding hasn't already fixed this: the bias is an episode (visual_crossing's hump spans 168-288 h and shrinks again by 312 h — consistent with a summer regime the provider's long-lead product handles badly), while expanding-window grounding is dominated by the longer low-bias history. Drift-tracking methods (ewma/harmonic grounding) and the damped blend are the right responses, and the promotion-transparency report now shows what the gate is holding back.
Actions taken¶
config.toml:stormglass_sg = 120lead cap;exclude = ["weatherapi:dew_point_c", "weatherapi:pop"](dew point anti-correlates with truth, −0.42; pop throws Page-Hinkley residual excursions 166-259 in every bucket).- No visual_crossing cap (finding 3).
Script¶
frame = pl.read_parquet("data/hourly_matrix_live.parquet")
frame = frame.filter(pl.col("lead_hours") >= 96.0)
# per (variable, source): bin = (lead_hours // 24) * 24;
# err = fx__{source}__{variable} - t__{variable}__inst
# aggregate MAE = mean |err|, bias = mean err, n per bin