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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## [19.4.0-rc.1](https://github.com/sequential-parameter-optimization/spotforecast2-safe/compare/v19.3.0...v19.4.0-rc.1) (2026-06-08)


### Features

* **configurator:** mirror new feature flags onto ConfigEntsoe (parity) ([a211271](https://github.com/sequential-parameter-optimization/spotforecast2-safe/commit/a211271021926b02aa89235d7c421cf7a2797361))
* **forecaster:** quantile-LightGBM probabilistic head factory ([d44093f](https://github.com/sequential-parameter-optimization/spotforecast2-safe/commit/d44093f0e60489d121f636d92bcb80d6431b1a38)), closes [#3](https://github.com/sequential-parameter-optimization/spotforecast2-safe/issues/3)

## [19.3.0](https://github.com/sequential-parameter-optimization/spotforecast2-safe/compare/v19.2.0...v19.3.0) (2026-06-08)


Expand Down
8 changes: 4 additions & 4 deletions MODEL_CARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This card describes what spotforecast2-safe is, how to use it safely, the condit
| Field | Value |
| --- | --- |
| Name | spotforecast2-safe |
| Version | 19.3.0 |
| Version | 19.4.0-rc.1 |
| Type | Deterministic Python library for time series feature engineering and recursive multi-step forecasting. It performs no training of its own. |
| Developed by | Thomas Bartz-Beielstein, ORCID [0000-0002-5938-5158](https://orcid.org/0000-0002-5938-5158) |
| Distributed by | the `sequential-parameter-optimization` GitHub organization |
Expand All @@ -18,7 +18,7 @@ This card describes what spotforecast2-safe is, how to use it safely, the condit

The library depends only on numpy, pandas, scikit-learn, lightgbm, numba, pyarrow, requests, feature-engine, holidays, astral, and tqdm. It deliberately excludes plotly, matplotlib, spotoptim, optuna, torch, and tensorflow, so no plotting or automated-tuning code ships in this package.

Two Common Platform Enumeration (CPE) identifiers let vulnerability-tracking and software bill of materials (SBOM) tools recognize the package. The wildcard identifier `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:*:*:*:*:*:*:*:*` matches any release; the current release is `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:19.3.0:*:*:*:*:*:*:*`.
Two Common Platform Enumeration (CPE) identifiers let vulnerability-tracking and software bill of materials (SBOM) tools recognize the package. The wildcard identifier `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:*:*:*:*:*:*:*:*` matches any release; the current release is `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:19.4.0-rc.1:*:*:*:*:*:*:*`.

The library itself is a low-risk component: it is deterministic, its source is fully inspectable, and it fails safe on invalid input. It is built to support high-risk AI systems in the sense of the EU AI Act, but it is not itself such a system. When it is embedded in a high-risk deployment, the duties that attach to that system fall on the integrator, not on the library.

Expand All @@ -30,7 +30,7 @@ Responsibilities are divided as follows.
| Distribution | sequential-parameter-optimization on GitHub | repository issue tracker |
| Deployment, operation, and audit | the system integrator | defined per deployment |

The current release is 19.3.0, with a stable public interface pinned in `spotforecast2_safe.__init__.__all__`. The full version history, including release dates, is recorded in `CHANGELOG.md` and on the GitHub Releases page; it is maintained automatically by the release pipeline and is not repeated here.
The current release is 19.4.0-rc.1, with a stable public interface pinned in `spotforecast2_safe.__init__.__all__`. The full version history, including release dates, is recorded in `CHANGELOG.md` and on the GitHub Releases page; it is maintained automatically by the release pipeline and is not repeated here.

## 2. Intended Use and Scope

Expand Down Expand Up @@ -216,7 +216,7 @@ Maintainer: Thomas Bartz-Beielstein, ORCID [0000-0002-5938-5158](https://orcid.o
}
```

Or as a formatted reference: Bartz-Beielstein, T. (2026). *spotforecast2-safe: Safety-critical subset of spotforecast2* (Version 19.3.0) [Computer software]. https://github.com/sequential-parameter-optimization/spotforecast2-safe
Or as a formatted reference: Bartz-Beielstein, T. (2026). *spotforecast2-safe: Safety-critical subset of spotforecast2* (Version 19.4.0-rc.1) [Computer software]. https://github.com/sequential-parameter-optimization/spotforecast2-safe

The technical report (`bart26h/index.qmd`) is the long-form reference for design rationale, compliance mapping, and evaluation protocol.

Expand Down
2 changes: 2 additions & 0 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,8 @@ quartodoc:
- multitask.predict.PredictTask
- multitask.clean.CleanTask
- multitask.factories.default_lgbm_forecaster_factory
- multitask.factories.quantile_lgbm_forecaster_factory
- multitask.factories.predict_quantile_band
- multitask.strategies.TrainingStrategy
- multitask.strategies.LazyStrategy
- multitask.strategies.DefaultsStrategy
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/configurator.config_entsoe.ConfigEntsoe.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ configurator.config_entsoe.ConfigEntsoe(
include_weather_windows=False,
include_holiday_features=False,
include_holiday_adjacency_features=False,
use_population_weighted_weather=False,
include_degree_hours=False,
include_apparent_temperature=False,
degree_hours_base_heating=15.0,
degree_hours_base_cooling=22.0,
include_ephemeris_features=False,
include_day_type_features=False,
poly_features_degree=1,
max_poly_features=10,
poly_mi_n_jobs=-1,
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ strategies, and a one-call runner.
| [multitask.predict.PredictTask](multitask.predict.PredictTask.qmd#spotforecast2_safe.multitask.predict.PredictTask) | Task 5 — Predict-only using previously saved models. |
| [multitask.clean.CleanTask](multitask.clean.CleanTask.qmd#spotforecast2_safe.multitask.clean.CleanTask) | Cache-cleaning task — removes all cached data from the pipeline cache. |
| [multitask.factories.default_lgbm_forecaster_factory](multitask.factories.default_lgbm_forecaster_factory.qmd#spotforecast2_safe.multitask.factories.default_lgbm_forecaster_factory) | Return a fresh, unfitted LightGBM ``ForecasterRecursive``. |
| [multitask.factories.quantile_lgbm_forecaster_factory](multitask.factories.quantile_lgbm_forecaster_factory.qmd#spotforecast2_safe.multitask.factories.quantile_lgbm_forecaster_factory) | Return one quantile-regression LightGBM ``ForecasterRecursive`` per quantile. |
| [multitask.factories.predict_quantile_band](multitask.factories.predict_quantile_band.qmd#spotforecast2_safe.multitask.factories.predict_quantile_band) | Assemble per-quantile forecasts into one non-crossing band. |
| [multitask.strategies.TrainingStrategy](multitask.strategies.TrainingStrategy.qmd#spotforecast2_safe.multitask.strategies.TrainingStrategy) | Strategy interface for preparing a forecaster before the final fit. |
| [multitask.strategies.LazyStrategy](multitask.strategies.LazyStrategy.qmd#spotforecast2_safe.multitask.strategies.LazyStrategy) | Approach 1 — Lazy fitting with optional cached tuning. |
| [multitask.strategies.DefaultsStrategy](multitask.strategies.DefaultsStrategy.qmd#spotforecast2_safe.multitask.strategies.DefaultsStrategy) | Approach 2 — Train with defaults, no tuning, no cached params. |
Expand Down
73 changes: 73 additions & 0 deletions docs/reference/multitask.factories.predict_quantile_band.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# multitask.factories.predict_quantile_band { #spotforecast2_safe.multitask.factories.predict_quantile_band }

```python
multitask.factories.predict_quantile_band(
forecasters,
steps,
*,
last_window=None,
exog=None,
enforce_monotonic=True,
)
```

Assemble per-quantile forecasts into one non-crossing band.

Calls ``predict`` on each fitted forecaster from
:func:`quantile_lgbm_forecaster_factory` and stacks the results into columns
named ``q_<level>`` (e.g. ``q_0.1``), in ascending quantile order. Because the
quantile heads are fitted independently they can *cross* (a higher quantile
predicting below a lower one); with ``enforce_monotonic`` the rows are sorted
ascending (the Chernozhukov rearrangement), a deterministic post-hoc fix that
restores monotonicity without changing the marginal quantile levels.

## Parameters {.doc-section .doc-section-parameters}

| Name | Type | Description | Default |
|-------------------|---------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|------------|
| forecasters | [Mapping](`typing.Mapping`)\[[float](`float`), [ForecasterRecursive](`spotforecast2_safe.forecaster.recursive.ForecasterRecursive`)\] | Map from quantile level to a **fitted** ``ForecasterRecursive`` (as returned by :func:`quantile_lgbm_forecaster_factory`, after fit). | _required_ |
| steps | [int](`int`) | Forecast horizon passed to each ``predict``. | _required_ |
| last_window | [Optional](`typing.Optional`)\[[Any](`typing.Any`)\] | Optional last-window override forwarded to ``predict``. | `None` |
| exog | [Optional](`typing.Optional`)\[[Any](`typing.Any`)\] | Optional exogenous frame forwarded to ``predict``. | `None` |
| enforce_monotonic | [bool](`bool`) | When ``True`` (default), sort each row ascending so the band never crosses. | `True` |

## Returns {.doc-section .doc-section-returns}

| Name | Type | Description |
|--------|------------------------------------------------|-----------------------------------------------------------------------|
| | [pd](`pandas`).[DataFrame](`pandas.DataFrame`) | pd.DataFrame: One column per quantile (``q_<level>``), indexed by the |
| | [pd](`pandas`).[DataFrame](`pandas.DataFrame`) | forecast horizon. |

## Raises {.doc-section .doc-section-raises}

| Name | Type | Description |
|--------|----------------------------|------------------------------------------------------|
| | [ValueError](`ValueError`) | If *forecasters* is empty or its levels are invalid. |

## Examples {.doc-section .doc-section-examples}

```{python}
import numpy as np
import pandas as pd
import types
from spotforecast2_safe.multitask.factories import (
quantile_lgbm_forecaster_factory,
predict_quantile_band,
)

idx = pd.date_range("2023-01-01", periods=300, freq="h")
y = pd.Series(
50 + 10 * np.sin(np.arange(300) * 2 * np.pi / 24), index=idx, name="y"
)
config = types.SimpleNamespace(
random_state=0, lags_consider=[1, 24], window_size=24
)
heads = quantile_lgbm_forecaster_factory(config, quantiles=[0.1, 0.5, 0.9])
for fc in heads.values():
fc.fit(y=y)
band = predict_quantile_band(heads, steps=6)
print(band.columns.tolist())
# Non-crossing after rearrangement.
assert (band["q_0.1"] <= band["q_0.5"] + 1e-9).all()
assert (band["q_0.5"] <= band["q_0.9"] + 1e-9).all()
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# multitask.factories.quantile_lgbm_forecaster_factory { #spotforecast2_safe.multitask.factories.quantile_lgbm_forecaster_factory }

```python
multitask.factories.quantile_lgbm_forecaster_factory(
config,
*,
quantiles=DEFAULT_QUANTILES,
weight_func=None,
target=None,
)
```

Return one quantile-regression LightGBM ``ForecasterRecursive`` per quantile.

Each forecaster uses ``LGBMRegressor(objective="quantile", alpha=q)`` and the
same lag/rolling configuration as :func:`default_lgbm_forecaster_factory`, so a
caller can fit the lower/median/upper heads independently and assemble a band
with :func:`predict_quantile_band`. Deterministic given ``config.random_state``;
sf2-safe (LightGBM only, no torch/optuna). Refs ``hong16b``, ``roma19a``.

## Parameters {.doc-section .doc-section-parameters}

| Name | Type | Description | Default |
|-------------|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|---------------------|
| config | [Any](`typing.Any`) | Object satisfying the ``PipelineConfig`` protocol; reads ``random_state``, ``lags_consider``, ``window_size``. | _required_ |
| quantiles | [Sequence](`typing.Sequence`)\[[float](`float`)\] | Quantile levels in the open interval ``(0, 1)``. Defaults to ``(0.1, 0.5, 0.9)``. | `DEFAULT_QUANTILES` |
| weight_func | [Optional](`typing.Optional`)\[[Any](`typing.Any`)\] | Optional per-sample weight function. | `None` |
| target | [Optional](`typing.Optional`)\[[str](`str`)\] | Accepted and ignored (parity with the default factory). | `None` |

## Returns {.doc-section .doc-section-returns}

| Name | Type | Description |
|--------|---------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| | [Dict](`typing.Dict`)\[[float](`float`), [ForecasterRecursive](`spotforecast2_safe.forecaster.recursive.ForecasterRecursive`)\] | Dict[float, ForecasterRecursive]: Map from quantile level to a fresh, |
| | [Dict](`typing.Dict`)\[[float](`float`), [ForecasterRecursive](`spotforecast2_safe.forecaster.recursive.ForecasterRecursive`)\] | unfitted forecaster, in ascending quantile order. |

## Raises {.doc-section .doc-section-raises}

| Name | Type | Description |
|--------|----------------------------|----------------------------------------------------------------|
| | [ValueError](`ValueError`) | If *quantiles* is empty, out of ``(0, 1)``, or has duplicates. |

## Examples {.doc-section .doc-section-examples}

```{python}
import types
from spotforecast2_safe.multitask.factories import (
quantile_lgbm_forecaster_factory,
)

config = types.SimpleNamespace(
random_state=42, lags_consider=[1, 2, 3], window_size=3
)
heads = quantile_lgbm_forecaster_factory(config, quantiles=[0.1, 0.5, 0.9])
print(sorted(heads))
print(heads[0.1].regressor.get_params()["objective"])
print(heads[0.1].regressor.get_params()["alpha"])
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "spotforecast2-safe"
version = "19.3.0"
version = "19.4.0-rc.1"
description = "spotforecast2-safe (Core): Safety-critical time series forecasting for production"
readme = "README.md"
license = { text = "AGPL-3.0-or-later" }
Expand Down
17 changes: 17 additions & 0 deletions src/spotforecast2_safe/configurator/config_entsoe.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,23 @@ class ConfigEntsoe:
include_weather_windows: bool = False
include_holiday_features: bool = False
include_holiday_adjacency_features: bool = False
# Global / derived weather and calendar refinements (parity with ConfigMulti;
# consumed by spotforecast2.multitask.base.build_exogenous_features). All
# default off → byte-identical to the single-point baseline.
# ``use_population_weighted_weather`` samples the fixed German load-centre
# registry and combines cities by population weight;
# ``include_degree_hours`` adds heating/cooling degree-hours;
# ``include_apparent_temperature`` adds apparent temperature + dew point;
# ``include_ephemeris_features`` adds continuous solar geometry
# (solar_elevation, daylight_duration_h, signed sunrise/sunset-relative time);
# ``include_day_type_features`` adds is_workday + a day_type class.
use_population_weighted_weather: bool = False
include_degree_hours: bool = False
include_apparent_temperature: bool = False
degree_hours_base_heating: float = 15.0
degree_hours_base_cooling: float = 22.0
include_ephemeris_features: bool = False
include_day_type_features: bool = False
poly_features_degree: int = 1
max_poly_features: int = 10
poly_mi_n_jobs: Optional[int] = -1
Expand Down
Loading