fix(load_ml): guard against zero-filled exogenous inputs, and report rollout accuracy - #4678
Merged
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
🟡 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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()substituted0.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 tominutes_now + forecast_minutes + 24h, andminute_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.0default 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.npzagainst their own history: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_kwhis 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_diagnosticnow also scores the day-of-week daily-pattern baseline it is blended against, androllout_mae_kwh/pattern_mae_kwhare persisted with the model and published onsensor.<prefix>_load_ml_stats. Ifpattern_mae_kwhis 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 asload_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