Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## [5.0.0-rc.1](https://github.com/sequential-parameter-optimization/spotforecast2/compare/v4.0.0...v5.0.0-rc.1) (2026-06-06)

### ⚠ BREAKING CHANGES

* **multitask:** spotforecast2 no longer reads start_download,
end_download, data_start, data_end, cov_start, cov_end, end_train_ts,
start_train_ts, or the resolved target list from the config; use
task.run_state. config.targets always holds the user input.

NOTE: CI stays red against PyPI spotforecast2-safe < 18; green requires
the 18.0.0 pin bump follow-up.

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

### Features

* **multitask:** read derived pipeline state from task.run_state ([1f36cec](https://github.com/sequential-parameter-optimization/spotforecast2/commit/1f36cec2335ec9feea19e9addf984017d3c5e814))

### Documentation

* migrate multitask tutorial off make_demo10_config to explicit ConfigMulti ([3399699](https://github.com/sequential-parameter-optimization/spotforecast2/commit/3399699188cb13a9088b24bbd9dfd3e0856a4f6a))

## [4.0.0](https://github.com/sequential-parameter-optimization/spotforecast2/compare/v3.9.0...v4.0.0) (2026-06-06)

### ⚠ BREAKING CHANGES
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "spotforecast2"
version = "4.0.0"
version = "5.0.0-rc.1"
description = "Forecasting with spot"
readme = "README.md"
license = { text = "AGPL-3.0-or-later" }
Expand Down Expand Up @@ -32,7 +32,7 @@ dependencies = [
"ruff>=0.15.6",
"scikit-learn>=1.8.0",
"shap>=0.49.1",
"spotforecast2-safe>=16.3.0,<17",
"spotforecast2-safe>=18.0.0,<19",
"spotoptim>=0.12.3",
"tqdm>=4.67.2",
]
Expand Down
19 changes: 9 additions & 10 deletions src/spotforecast2/multitask/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@
agg_predictor,
)

from spotforecast2.plots.plotter import (
make_plot,
plot_with_outliers as _plot_with_outliers,
)
from spotforecast2.plots.plotter import make_plot
from spotforecast2.plots.plotter import plot_with_outliers as _plot_with_outliers

__all__ = [
"SafeBaseTask",
Expand Down Expand Up @@ -63,6 +61,7 @@ def plot_with_outliers(self) -> None:
df_pipeline=self.df_pipeline, # type: ignore[attr-defined]
df_pipeline_original=self.df_pipeline_original, # type: ignore[attr-defined]
config=self.config, # type: ignore[attr-defined]
targets=self.run_state.targets, # type: ignore[attr-defined]
)

def _show_prediction_figure(
Expand Down Expand Up @@ -100,7 +99,7 @@ def _show_prediction_figure_agg(
agg_pkg,
title=(
f"Aggregated Forecast: Weighted Combination of "
f"Targets {self.config.targets} ({task_name})" # type: ignore[attr-defined]
f"Targets {self.run_state.targets} ({task_name})" # type: ignore[attr-defined]
),
save=False,
)
Expand Down Expand Up @@ -161,24 +160,24 @@ def _aggregate_and_show(
Returns:
Aggregated prediction package dict.
"""
if len(self.config.targets) == 1:
target = self.config.targets[0]
if len(self.run_state.targets) == 1:
target = self.run_state.targets[0]
agg_pkg = results[target]
self.agg_results[task_name] = agg_pkg
return agg_pkg

if self.config.agg_weights is not None:
active_weights = self.config.agg_weights[: len(self.config.targets)]
active_weights = self.config.agg_weights[: len(self.run_state.targets)]
else:
n = len(self.config.targets)
n = len(self.run_state.targets)
active_weights = [1.0 / n] * n
self.logger.info(
"No agg_weights configured — using equal weights (1/%d each).", n
)

agg_pkg = self.agg_predictor(
results=results,
targets=self.config.targets,
targets=self.run_state.targets,
weights=active_weights,
)
self.agg_results[task_name] = agg_pkg
Expand Down
34 changes: 23 additions & 11 deletions src/spotforecast2/plots/plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,10 @@ def plot_actual_vs_predicted(


def plot_with_outliers(
df_pipeline: pd.DataFrame, df_pipeline_original: pd.DataFrame, config: Any
df_pipeline: pd.DataFrame,
df_pipeline_original: pd.DataFrame,
config: Any,
targets: Optional[list[str]] = None,
) -> None:
"""Interactive time series plot with outliers and optional bounds.

Expand All @@ -637,15 +640,26 @@ def plot_with_outliers(
The plot title includes the percentage of outliers detected for each
target variable.

The resolved list of target column names must be passed explicitly via
*targets*. Callers inside the pipeline (e.g. ``PlottingMixin``) obtain
this list from ``task.run_state.targets`` after ``prepare_data`` has run.
A ``SimpleNamespace``/dict-like *config* that carries its own ``targets``
attribute is still accepted for backwards-compatible standalone usage —
the explicit *targets* argument takes precedence when provided.

Args:
df_pipeline (pd.DataFrame): The processed DataFrame from the pipeline,
which may contain NaN values where outliers have been detected and
removed.
df_pipeline_original (pd.DataFrame): The original DataFrame before
outlier removal.
config: Configuration object containing ``targets`` (list of column
names) and optionally ``bounds`` (list of ``(lower, upper)``
tuples, one per target, in the same order as ``targets``).
config: Configuration object carrying ``bounds`` (optional list of
``(lower, upper)`` tuples, one per target, in the same order as
*targets*). ``config.targets`` is used as a fallback when the
*targets* argument is ``None`` (legacy path).
targets: Resolved list of target column names. When ``None`` the
function falls back to ``config.targets`` (legacy callers that
pass a ``SimpleNamespace`` with ``targets`` set).

Returns:
None. Displays one interactive Plotly figure per target variable.
Expand All @@ -667,17 +681,15 @@ def plot_with_outliers(
data.loc[dates[20], "target2"] = 150 # Outlier in target2
df_pipeline = data.copy()
df_pipeline.loc[[dates[10], dates[20]], ["target1", "target2"]] = np.nan
# Config with bounds
config = SimpleNamespace(
targets=["target1", "target2"],
bounds=[(-10, 200), (0, 100)],
)
plot_with_outliers(df_pipeline, data, config)
# Config with bounds; targets passed explicitly
config = SimpleNamespace(bounds=[(-10, 200), (0, 100)])
plot_with_outliers(df_pipeline, data, config, targets=["target1", "target2"])
```
"""
bounds = getattr(config, "bounds", None)
_targets = targets if targets is not None else config.targets

for i, target in enumerate(config.targets):
for i, target in enumerate(_targets):
fig = go.Figure()

# Plot Regular Data (lightgrey)
Expand Down
8 changes: 4 additions & 4 deletions tests/test_agg_predictor.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ class TestAggregateAndShowAlwaysAggregates:

def _make_task_with_targets(self, targets):
task = MultiTask()
task.config.targets = targets
task.run_state.targets = targets
return task

def test_returns_dict_without_agg_weights(self):
Expand Down Expand Up @@ -356,13 +356,13 @@ def _inject_pipeline(task, n_rows=300):
df_test = df.iloc[-24:].copy().reset_index().rename(columns={"index": "DateTime"})
task.df_pipeline = df
task.df_test = df_test
task.config.targets = ["A", "B"]
task.run_state.targets = ["A", "B"]
task.config.agg_weights = [0.5, 0.5]
task.data_with_exog = None
task.exo_pred = None
task.exog_feature_names = []
task.config.end_train_ts = idx[-25]
task.config.start_train_ts = idx[0]
task.run_state.end_train_ts = idx[-25]
task.run_state.start_train_ts = idx[0]
return task


Expand Down
Loading