Skip to content

Commit de43d47

Browse files
authored
Merge pull request #111 from openclimatefix/fix-integration-test
Fix integration test
2 parents 0131065 + cc5a22d commit de43d47

3 files changed

Lines changed: 107 additions & 9 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
status: "draft"
3+
version: "v1.0"
4+
task_type: "standard"
5+
requires_ml_review: false
6+
requires_data_engineer: false
7+
target_modules: ["packages/ml_core/src/ml_core/utils.py"]
8+
---
9+
# Implementation Plan: Extend Forecast Horizon in Evaluation
10+
11+
## Objective
12+
Modify `evaluate_and_save_model` to allow the forecast horizon to extend beyond `test_end` (e.g., by 14 days) for inference, while keeping the evaluation metrics restricted to the `[test_start, test_end]` period.
13+
14+
## Context
15+
Currently, all input data in `evaluate_and_save_model` is sliced to `test_end`. This limits the generated forecast to the test period (which is 1 day in integration tests). By extending the NWP slice to `test_end + 14 days`, the model can generate a 14-day forecast. The evaluation will naturally remain restricted to `test_end` because the actuals (`power_time_series`) will still be sliced to `test_end`, and the evaluation uses an `inner` join between predictions and actuals.
16+
17+
## Proposed Changes
18+
19+
### 1. Fix `time_col` determination for `power_time_series`
20+
In both `evaluate_and_save_model` and `train_and_log_model`, the `time_col` logic currently checks for `"power_flows" in key`. This fails to match the new `"power_time_series"` key, causing actuals to not be sliced at all.
21+
- **Change:** Update the condition to check for `"power" in key` (or explicitly `key in ["power_flows", "power_time_series"]`).
22+
- **Why:** Ensures `power_time_series` is correctly identified and sliced using `period_end_time`.
23+
24+
### 2. Extend `slice_end` for NWPs in `evaluate_and_save_model`
25+
Modify the temporal slicing loop in `evaluate_and_save_model` to extend the end date for NWP data.
26+
- **Change:** Introduce a `slice_end` variable that defaults to `test_end`.
27+
- **Change:** If `"nwps" in key`, set `slice_end = test_end + timedelta(days=getattr(config.model, "forecast_horizon_days", 14))`.
28+
- **Change:** Pass `slice_end` to `_slice_temporal_data` instead of `test_end`.
29+
- **Why:** This provides the model with future weather data, allowing it to generate predictions up to 14 days beyond `test_end`.
30+
31+
### 3. Verify Evaluation Restriction
32+
No changes are needed to the evaluation logic itself.
33+
- **Why:** Because `power_time_series` (actuals) will now be correctly sliced to `test_end`, the `inner` join between `results_lf` (which extends to `test_end + 14 days`) and `actuals_lf` will automatically drop the future predictions from the evaluation dataset (`eval_lf`). The returned dataframe will still contain the full 14-day forecast.
34+
35+
## Code Snippet (for `evaluate_and_save_model`)
36+
```python
37+
# 1. Universal Temporal Slicing for Test Set
38+
test_start = config.data_split.test_start
39+
test_end = config.data_split.test_end
40+
41+
sliced_data = {}
42+
for key, val in kwargs.items():
43+
if key == "time_series_metadata":
44+
sliced_data[key] = val
45+
continue
46+
47+
# Fix time_col to handle power_time_series
48+
time_col = "period_end_time" if "power" in key else "valid_time"
49+
50+
# Add a configurable lookback for autoregressive features
51+
slice_start = test_start
52+
if "power" in key or "nwps" in key:
53+
lookback = getattr(config.model, "required_lookback_days", 14)
54+
slice_start = test_start - timedelta(days=lookback)
55+
56+
# Extend slice_end for NWPs to allow forecasting beyond test_end
57+
slice_end = test_end
58+
if "nwps" in key:
59+
forecast_horizon = getattr(config.model, "forecast_horizon_days", 14)
60+
slice_end = test_end + timedelta(days=forecast_horizon)
61+
62+
sliced_data[key] = _slice_temporal_data(val, slice_start, slice_end, time_col)
63+
```
64+
65+
## Code Snippet (for `train_and_log_model`)
66+
```python
67+
# Fix time_col to handle power_time_series
68+
time_col = "period_end_time" if "power" in key else "valid_time"
69+
70+
# Add a configurable lookback for autoregressive features
71+
slice_start = train_start
72+
if "power" in key or "nwps" in key:
73+
lookback = getattr(config.model, "required_lookback_days", 14)
74+
slice_start = train_start - timedelta(days=lookback)
75+
```
76+
77+
## Coding Standards & Mandates
78+
- **Comments:** You must add explicit code comments explaining *why* `slice_end` is extended for NWPs (to allow forecasting into the future) and *why* the evaluation remains restricted (because the inner join with actuals naturally drops future predictions). Do not just describe *what* the code is doing.
79+
- **No FLAW IDs:** You are strictly forbidden from referencing any FLAW-XXX IDs in code comments.

packages/ml_core/src/ml_core/utils.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ def train_and_log_model(
7171
sliced_data["time_series_metadata"] = val
7272
continue
7373

74-
time_col = "period_end_time" if "power_flows" in key else "valid_time"
74+
# Fix time_col to handle power_time_series
75+
time_col = "period_end_time" if "power" in key else "valid_time"
7576

7677
# Add a configurable lookback for autoregressive features
7778
slice_start = train_start
78-
if "power_flows" in key or "nwps" in key:
79+
if "power" in key or "nwps" in key:
7980
lookback = getattr(config.model, "required_lookback_days", 14)
8081
slice_start = train_start - timedelta(days=lookback)
8182

@@ -133,17 +134,26 @@ def evaluate_and_save_model(
133134
sliced_data[key] = val
134135
continue
135136

136-
# TODO: This function should never be called with `power_flows`. That's a hang-over from
137-
# when we used a `SubstationPowerFlows` data contract, instead of the new `PowerTimeSeries`.
138-
time_col = "period_end_time" if "power_flows" in key else "valid_time"
137+
# Fix time_col to handle power_time_series
138+
time_col = "period_end_time" if "power" in key else "valid_time"
139139

140140
# Add a configurable lookback for autoregressive features
141141
slice_start = test_start
142-
if "power_flows" in key or "nwps" in key:
142+
if "power" in key or "nwps" in key:
143143
lookback = getattr(config.model, "required_lookback_days", 14)
144144
slice_start = test_start - timedelta(days=lookback)
145145

146-
sliced_data[key] = _slice_temporal_data(val, slice_start, test_end, time_col)
146+
# Extend slice_end for NWPs to allow forecasting beyond test_end
147+
# We extend the NWP data slice to include future weather data, enabling the model
148+
# to generate forecasts beyond the test period. The evaluation remains restricted
149+
# to the test period because the actuals (power_time_series) are still sliced
150+
# to test_end, and the inner join with actuals naturally drops future predictions.
151+
slice_end = test_end
152+
if "nwps" in key:
153+
forecast_horizon = getattr(config.model, "forecast_horizon_days", 14)
154+
slice_end = test_end + timedelta(days=forecast_horizon)
155+
156+
sliced_data[key] = _slice_temporal_data(val, slice_start, slice_end, time_col)
147157

148158
# 2. Call the Model-Specific Inference
149159
# Extract the actual init_time from the provided nwps data

src/nged_substation_forecast/defs/plotting_assets.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,21 @@ def forecast_vs_actual_plot(
5050
)
5151

5252
# Keep actuals lazy and filter by substation first to avoid eager collection.
53+
# We need to ensure we have enough data for the 14-day plot.
54+
# We calculate the required start time based on the predictions.
55+
max_pred_time = predictions.get_column("valid_time").max()
56+
57+
# We need data from at least 14 days before the max_pred_time to ensure we have
58+
# enough actuals for the comparison.
59+
required_start_time = cast(datetime, max_pred_time) - timedelta(days=14)
60+
5361
cleaned_power_time_series_lazy = scan_delta_table(
5462
str(settings.nged_data_path / "delta" / "cleaned_power_time_series")
55-
)
63+
).filter(pl.col("period_end_time") >= required_start_time)
64+
5665
raw_power_lazy = scan_delta_table(
5766
str(settings.nged_data_path / "delta" / "raw_power_time_series")
58-
)
67+
).filter(pl.col("period_end_time") >= required_start_time)
5968

6069
# Filter actuals and raw by substation.
6170
actuals_30m = cast(

0 commit comments

Comments
 (0)