Skip to content

fix(load_ml): guard against zero-filled exogenous inputs, and report rollout accuracy - #4678

Merged
springfall2008 merged 2 commits into
mainfrom
fix/load-ml-exogenous-zero-fill-4673
Aug 24, 2026
Merged

fix(load_ml): guard against zero-filled exogenous inputs, and report rollout accuracy#4678
springfall2008 merged 2 commits into
mainfrom
fix/load-ml-exogenous-zero-fill-4673

Conversation

@springfall2008

@springfall2008 springfall2008 commented Aug 23, 2026

Copy link
Copy Markdown
Owner

What

Rates and temperature are 3 of the 5 input channels to the ML load model — 864 of 1446 features. Where a forward value was missing, predict() substituted 0.0, feeding 0 p/kWh and 0 °C into a network trained on ~33.6 p/kWh and ~7 °C. Nothing logged it.

This is a startup and failure-path guard, not a steady-state bug. A healthy system supplies the whole rollout: rate_replicate() extends rates past the forecast horizon, publish_rates() emits them out to minutes_now + forecast_minutes + 24h, and minute_data() returns them as forward keys covering all 576 steps. I verified that last step rather than assuming it. The gap only opens when the source entity is unavailable — before the first plan cycle publishes it, or after a failed rate fetch.

This carries the last known value forward there instead, and logs when it engages. PV keeps its 0.0 default deliberately: past the solar forecast there is no generation to assume, and holding a daytime value overnight would be worse.

Why it matters despite being an edge case

Measured by replaying a user's saved predbat_ml_model.npz against their own history:

Forward exog data +8h MAE (before) +8h MAE (after) 48h rollout peak (before → after)
fully supplied 0.28 kWh 0.28 kWh 10.5 kW → 10.5 kW
absent 2.43 kWh 0.40 kWh 1.9 kW → 9.9 kW

Their true peak is 10.5 kW. With the inputs zeroed the rollout flattens to 1.9 kW — the household's large daily event vanishes entirely, and the plan is built on it. The fully-supplied case is byte-identical before and after, so there is no behaviour change when data is present.

Also: multi-step accuracy is now reported

validation_mae / mae_kwh is teacher-forced one-step-ahead and stays small however badly the rollout behaves — it reads 0.0065 kWh on the model above. There was no published signal for multi-step quality at all.

_ar_rollout_diagnostic now also scores the day-of-week daily-pattern baseline it is blended against, and rollout_mae_kwh / pattern_mae_kwh are persisted with the model and published on sensor.<prefix>_load_ml_stats. If pattern_mae_kwh is the lower of the two, the network is not beating a simple historical average at range. Both are reporting only and do not change the forecast.

This is arguably the more useful half. The model retrains every two hours, and nothing currently reveals whether a given retrain produced a good long-range rollout or a collapsed one.

Relationship to #4673

Raised while investigating #4673, deliberately not marked as fixing it. Replaying that reporter's model with correctly supplied inputs reproduces no anomaly (+8h MAE 0.28 kWh), so this is not the cause of their chart and their report stays open.

Testing

New apps/predbat/tests/test_load_ml_rollout.py, registered as load_ml_rollout (fast, no training). Four tests: rate features are never zero-filled, the diagnostic reports the pattern baseline, the scores survive a save/load roundtrip, and the stats sensor exposes them.

Full suite: 264 tests pass. Pre-commit clean.

🤖 Generated with Claude Code

Rates and temperature are 3 of the 5 input channels to the ML load model
(864 of 1446 features). A 48-hour autoregressive rollout outruns both
forecasts, and predict() substituted 0.0 beyond their end - feeding
0 p/kWh and 0 degrees into a network trained on 33.6 p/kWh and 7 degrees.
Nothing logged it.

Measured by replaying a user's saved model and history: with no forward
rate data the 48-hour rollout peaks at 1.9 kW against a 10.5 kW true
peak, and the 8-hour-ahead forecast error goes from 0.28 kWh MAE to
2.6-2.8 kWh, running -10 kWh (daily event erased) or +11 kWh (load
invented) depending on which rate channel is missing.

Carry the last known value forward instead, and log when it engages.
That restores a 9.9 kW peak with no forward data at all, and leaves the
complete-data case byte-identical. PV keeps its 0.0 default: past the
solar forecast there is no generation to assume, and holding a daytime
value overnight would be worse.

Also report multi-step accuracy, which was previously invisible.
_ar_rollout_diagnostic now scores the day-of-week daily-pattern baseline
alongside the model rollout, and rollout_mae_kwh / pattern_mae_kwh are
persisted with the model and published on sensor.<prefix>_load_ml_stats.
The existing mae_kwh is teacher-forced and stays small however badly the
rollout behaves. Both new figures are reporting only and do not change
the forecast.

Raised while investigating #4673. That report is not confirmed to be
this bug: the effect needs the forward forecast to be missing within the
first few hours, and one reaching 12h ahead is unaffected at any horizon,
so a rate plan merely ending at the 48h horizon does not reproduce the
reported chart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are correctness issues in the new exogenous “ran out” counter/logging logic and in metadata persistence of validation_mae that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves the ML load model’s long-horizon autoregressive rollout by preventing missing forward rate/temperature forecasts from being implicitly treated as zero, and adds visibility into multi-step rollout quality versus the built-in daily-pattern baseline.

Changes:

  • Carry forward last-known temperature/import/export rate values when forward forecasts run out during rollout (PV remains zero-filled by design).
  • Persist and publish additional diagnostic metrics (rollout_mae_kwh, pattern_mae_kwh) alongside the existing teacher-forced MAE.
  • Add regression tests and documentation covering rollout accuracy and the new diagnostic signals.
File summaries
File Description
docs/load-ml.md Documents forward exogenous fallback behavior and adds a “Rollout Accuracy” section explaining new metrics.
apps/predbat/unit_test.py Registers the new load_ml_rollout test suite.
apps/predbat/tests/test_load_ml_rollout.py Adds regression tests for exogenous fallback and rollout-vs-pattern diagnostics/persistence/publication.
apps/predbat/load_predictor.py Implements exogenous carry-forward, adds rollout/pattern holdout metrics, persists them in model save/load, and logs diagnostic comparison.
apps/predbat/load_ml_component.py Publishes rollout_mae_kwh and pattern_mae_kwh on the load ML stats sensor.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/load_predictor.py
Comment thread apps/predbat/load_predictor.py
The previous commit message and comments claimed a 48-hour rollout always
outruns the rate and temperature forecasts. It does not: rate_replicate()
extends rates past the forecast horizon and publish_rates() emits them to
minutes_now + forecast_minutes + 24h, and minute_data() returns them as
forward keys covering the whole rollout. A healthy system supplies every
step.

The zero-fill is still worth guarding - it silently produces a garbage
forecast when the rates entity is unavailable, such as before the first
plan cycle publishes it or after a failed fetch - but it is a startup and
failure-path guard, not a steady-state bug, and it does not explain #4673.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 changed the title fix(load_ml): carry forward exogenous forecasts instead of zero-filling fix(load_ml): guard against zero-filled exogenous inputs, and report rollout accuracy Aug 23, 2026
@springfall2008
springfall2008 merged commit 5bf2fe1 into main Aug 24, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/load-ml-exogenous-zero-fill-4673 branch August 24, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants