diff --git a/examples/tutorials/try_optuna.ipynb b/examples/tutorials/try_optuna.ipynb new file mode 100644 index 000000000..1a10ceb0f --- /dev/null +++ b/examples/tutorials/try_optuna.ipynb @@ -0,0 +1,1159 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "c25b63fd", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Setup: Logging and Display Configuration ---\n", + "# Configure logging to see training progress and plotly to render as PNG for VS Code compatibility\n", + "import logging\n", + "import pandas as pd\n", + "import plotly.io as pio\n", + "\n", + "pd.options.plotting.backend = \"plotly\"\n", + "pio.renderers.default = \"png\" # Use PNG for VS Code notebook compatibility\n", + "\n", + "logging.basicConfig(level=logging.INFO, format=\"[%(asctime)s][%(levelname)s] %(message)s\")\n", + "logger = logging.getLogger(__name__)\n", + "logging.getLogger(\"choreographer\").setLevel(logging.ERROR)\n", + "logging.getLogger(\"kaleido\").setLevel(logging.ERROR)\n", + "logging.getLogger(\"choreographer\").disabled = True\n", + "logging.getLogger(\"kaleido\").disabled = True" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "84299333", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/fleur.petit/projects/openstef/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning:\n", + "\n", + "IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading load_measurements/mv_feeder/OS Gorredijk.parquet...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/fleur.petit/projects/openstef/.venv/lib/python3.12/site-packages/huggingface_hub/utils/_validators.py:202: UserWarning:\n", + "\n", + "The `local_dir_use_symlinks` argument is deprecated and ignored in `hf_hub_download`. Downloading to a local directory does not use symlinks anymore.\n", + "\n", + "[2026-03-26 11:11:52,083][INFO] HTTP Request: HEAD https://huggingface.co/datasets/OpenSTEF/liander2024-energy-forecasting-benchmark/resolve/main/load_measurements/mv_feeder/OS%20Gorredijk.parquet \"HTTP/1.1 302 Found\"\n", + "[2026-03-26 11:11:52,244][INFO] HTTP Request: HEAD https://huggingface.co/datasets/OpenSTEF/liander2024-energy-forecasting-benchmark/resolve/main/weather_forecasts_versioned/mv_feeder/OS%20Gorredijk.parquet \"HTTP/1.1 302 Found\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ load_measurements/mv_feeder/OS Gorredijk.parquet downloaded\n", + "Downloading weather_forecasts_versioned/mv_feeder/OS Gorredijk.parquet...\n", + "✓ weather_forecasts_versioned/mv_feeder/OS Gorredijk.parquet downloaded\n", + "Downloading EPEX.parquet...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2026-03-26 11:11:52,424][INFO] HTTP Request: HEAD https://huggingface.co/datasets/OpenSTEF/liander2024-energy-forecasting-benchmark/resolve/main/EPEX.parquet \"HTTP/1.1 302 Found\"\n", + "Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n", + "[2026-03-26 11:11:52,425][WARNING] Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n", + "[2026-03-26 11:11:52,607][INFO] HTTP Request: HEAD https://huggingface.co/datasets/OpenSTEF/liander2024-energy-forecasting-benchmark/resolve/main/profiles.parquet \"HTTP/1.1 302 Found\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ EPEX.parquet downloaded\n", + "Downloading profiles.parquet...\n", + "✓ profiles.parquet downloaded\n", + "\n", + "✅ All files downloaded successfully!\n" + ] + } + ], + "source": [ + "# Download dataset from HuggingFace Hub\n", + "# The dataset is stored as parquet files for efficient loading\n", + "from huggingface_hub import hf_hub_download # pyright: ignore[reportUnknownVariableType]\n", + "from openstef_core.base_model import Path\n", + "\n", + "repo_id = \"OpenSTEF/liander2024-energy-forecasting-benchmark\" # Public benchmark dataset\n", + "local_dir = Path(\"./liander_dataset\")\n", + "target = \"mv_feeder/OS Gorredijk\" # Specific installation to focus on\n", + "\n", + "# Download required files: load measurements, weather, prices, and profiles\n", + "files_to_download = [\n", + " f\"load_measurements/{target}.parquet\", # Energy consumption data\n", + " f\"weather_forecasts_versioned/{target}.parquet\", # Weather features\n", + " \"EPEX.parquet\", # Electricity prices (optional feature)\n", + " \"profiles.parquet\" # Standard load profiles (optional feature)\n", + "]\n", + "\n", + "for filename in files_to_download:\n", + " print(f\"Downloading {filename}...\")\n", + " hf_hub_download(repo_id=repo_id, filename=filename, repo_type=\"dataset\",\n", + " local_dir=local_dir, local_dir_use_symlinks=False) # pyright: ignore[reportCallIssue]\n", + " print(f\"✓ {filename} downloaded\")\n", + "\n", + "print(\"\\n✅ All files downloaded successfully!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "524f65a1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2026-03-26 11:11:54,019][WARNING] Parquet file does not contain 'sample_interval' attribute. Using default value of 15 minutes.\n", + "[2026-03-26 11:11:54,040][WARNING] Parquet file does not contain 'sample_interval' attribute. Using default value of 15 minutes.\n", + "[2026-03-26 11:11:54,065][WARNING] Parquet file does not contain 'sample_interval' attribute. Using default value of 15 minutes.\n", + "[2026-03-26 11:11:54,075][WARNING] Parquet file does not contain 'sample_interval' attribute. Using default value of 15 minutes.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset shape: (35136, 28)\n", + "Date range: 2024-01-01 00:00:00+00:00 to 2024-12-31 23:45:00+00:00\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "timestamp", + "rawType": "datetime64[ns, UTC]", + "type": "unknown" + }, + { + "name": "load", + "rawType": "float64", + "type": "float" + }, + { + "name": "temperature_2m", + "rawType": "float32", + "type": "float" + }, + { + "name": "relative_humidity_2m", + "rawType": "float32", + "type": "float" + }, + { + "name": "surface_pressure", + "rawType": "float32", + "type": "float" + }, + { + "name": "cloud_cover", + "rawType": "float32", + "type": "float" + }, + { + "name": "wind_speed_10m", + "rawType": "float32", + "type": "float" + }, + { + "name": "wind_speed_80m", + "rawType": "float32", + "type": "float" + }, + { + "name": "wind_direction_10m", + "rawType": "float32", + "type": "float" + }, + { + "name": "shortwave_radiation", + "rawType": "float32", + "type": "float" + }, + { + "name": "direct_radiation", + "rawType": "float32", + "type": "float" + }, + { + "name": "diffuse_radiation", + "rawType": "float32", + "type": "float" + }, + { + "name": "direct_normal_irradiance", + "rawType": "float32", + "type": "float" + }, + { + "name": "EPEX_NL", + "rawType": "float64", + "type": "float" + }, + { + "name": "E1A_AZI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E1A_AMI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E1B_AZI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E1B_AMI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E1C_AZI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E1C_AMI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E2A_AZI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E2A_AMI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E2B_AZI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E2B_AMI_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E3A_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E3B_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E3C_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E3D_A", + "rawType": "float64", + "type": "float" + }, + { + "name": "E4A_A", + "rawType": "float64", + "type": "float" + } + ], + "ref": "eb7e1c92-b6c6-4dfc-8d57-1c88f21fdea1", + "rows": [ + [ + "2024-01-01 00:00:00+00:00", + "423333.3333333333", + "7.2435", + "85.02532", + "994.23645", + "100.0", + "28.185953", + "43.832863", + "204.92845", + "0.0", + "0.0", + "0.0", + "0.0", + "0.01", + "2.97e-05", + "4.031e-05", + "6.206e-05", + "7.815e-05", + "5.683e-05", + "6.431e-05", + "2.42e-05", + "3.404e-05", + "5.292e-05", + "6.425e-05", + "5.839e-05", + "5.839e-05", + "5.839e-05", + "5.839e-05", + "7.931e-05" + ], + [ + "2024-01-01 00:15:00+00:00", + "436666.6666666666", + "7.281", + "84.80853", + "994.1865", + "100.0", + "28.75338", + "44.97622", + "206.93102", + "0.0", + "0.0", + "0.0", + "0.0", + "0.01", + "2.91e-05", + "3.866e-05", + "6.079e-05", + "7.537e-05", + "5.572e-05", + "6.058e-05", + "2.395e-05", + "3.352e-05", + "5.236e-05", + "6.326e-05", + "5.826e-05", + "5.826e-05", + "5.826e-05", + "5.826e-05", + "7.931e-05" + ], + [ + "2024-01-01 00:30:00+00:00", + "410000.0", + "7.3185005", + "84.59174", + "994.1366", + "100.0", + "29.320807", + "46.11957", + "208.93356", + "0.0", + "0.0", + "0.0", + "0.0", + "0.01", + "2.794e-05", + "3.771e-05", + "5.799e-05", + "7.33e-05", + "5.451e-05", + "5.967e-05", + "2.338e-05", + "3.312e-05", + "5.113e-05", + "6.252e-05", + "5.782e-05", + "5.782e-05", + "5.782e-05", + "5.782e-05", + "7.931e-05" + ], + [ + "2024-01-01 00:45:00+00:00", + "403333.3333333333", + "7.3560004", + "84.374954", + "994.08673", + "100.0", + "29.888233", + "47.262924", + "210.93613", + "0.0", + "0.0", + "0.0", + "0.0", + "0.01", + "2.712e-05", + "3.649e-05", + "5.659e-05", + "7.12e-05", + "5.211e-05", + "5.708e-05", + "2.325e-05", + "3.219e-05", + "5.083e-05", + "6.076e-05", + "5.89e-05", + "5.89e-05", + "5.89e-05", + "5.89e-05", + "7.931e-05" + ], + [ + "2024-01-01 01:00:00+00:00", + "420000.0", + "7.3935003", + "84.158165", + "994.0368", + "100.0", + "30.45566", + "48.40628", + "212.93869", + "0.0", + "0.0", + "0.0", + "0.0", + "0.0", + "2.714e-05", + "3.44e-05", + "5.728e-05", + "6.668e-05", + "5.045e-05", + "5.493e-05", + "2.359e-05", + "3.198e-05", + "5.158e-05", + "6.036e-05", + "5.726e-05", + "5.726e-05", + "5.726e-05", + "5.726e-05", + "7.931e-05" + ] + ], + "shape": { + "columns": 28, + "rows": 5 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
loadtemperature_2mrelative_humidity_2msurface_pressurecloud_coverwind_speed_10mwind_speed_80mwind_direction_10mshortwave_radiationdirect_radiation...E1C_AMI_AE2A_AZI_AE2A_AMI_AE2B_AZI_AE2B_AMI_AE3A_AE3B_AE3C_AE3D_AE4A_A
timestamp
2024-01-01 00:00:00+00:00423333.3333337.24350085.025322994.236450100.028.18595343.832863204.9284520.00.0...0.0000640.0000240.0000340.0000530.0000640.0000580.0000580.0000580.0000580.000079
2024-01-01 00:15:00+00:00436666.6666677.28100084.808533994.186523100.028.75338044.976219206.9310150.00.0...0.0000610.0000240.0000340.0000520.0000630.0000580.0000580.0000580.0000580.000079
2024-01-01 00:30:00+00:00410000.0000007.31850184.591743994.136597100.029.32080746.119572208.9335630.00.0...0.0000600.0000230.0000330.0000510.0000630.0000580.0000580.0000580.0000580.000079
2024-01-01 00:45:00+00:00403333.3333337.35600084.374954994.086731100.029.88823347.262924210.9361270.00.0...0.0000570.0000230.0000320.0000510.0000610.0000590.0000590.0000590.0000590.000079
2024-01-01 01:00:00+00:00420000.0000007.39350084.158165994.036804100.030.45566048.406281212.9386900.00.0...0.0000550.0000240.0000320.0000520.0000600.0000570.0000570.0000570.0000570.000079
\n", + "

5 rows × 28 columns

\n", + "
" + ], + "text/plain": [ + " load temperature_2m \\\n", + "timestamp \n", + "2024-01-01 00:00:00+00:00 423333.333333 7.243500 \n", + "2024-01-01 00:15:00+00:00 436666.666667 7.281000 \n", + "2024-01-01 00:30:00+00:00 410000.000000 7.318501 \n", + "2024-01-01 00:45:00+00:00 403333.333333 7.356000 \n", + "2024-01-01 01:00:00+00:00 420000.000000 7.393500 \n", + "\n", + " relative_humidity_2m surface_pressure \\\n", + "timestamp \n", + "2024-01-01 00:00:00+00:00 85.025322 994.236450 \n", + "2024-01-01 00:15:00+00:00 84.808533 994.186523 \n", + "2024-01-01 00:30:00+00:00 84.591743 994.136597 \n", + "2024-01-01 00:45:00+00:00 84.374954 994.086731 \n", + "2024-01-01 01:00:00+00:00 84.158165 994.036804 \n", + "\n", + " cloud_cover wind_speed_10m wind_speed_80m \\\n", + "timestamp \n", + "2024-01-01 00:00:00+00:00 100.0 28.185953 43.832863 \n", + "2024-01-01 00:15:00+00:00 100.0 28.753380 44.976219 \n", + "2024-01-01 00:30:00+00:00 100.0 29.320807 46.119572 \n", + "2024-01-01 00:45:00+00:00 100.0 29.888233 47.262924 \n", + "2024-01-01 01:00:00+00:00 100.0 30.455660 48.406281 \n", + "\n", + " wind_direction_10m shortwave_radiation \\\n", + "timestamp \n", + "2024-01-01 00:00:00+00:00 204.928452 0.0 \n", + "2024-01-01 00:15:00+00:00 206.931015 0.0 \n", + "2024-01-01 00:30:00+00:00 208.933563 0.0 \n", + "2024-01-01 00:45:00+00:00 210.936127 0.0 \n", + "2024-01-01 01:00:00+00:00 212.938690 0.0 \n", + "\n", + " direct_radiation ... E1C_AMI_A E2A_AZI_A \\\n", + "timestamp ... \n", + "2024-01-01 00:00:00+00:00 0.0 ... 0.000064 0.000024 \n", + "2024-01-01 00:15:00+00:00 0.0 ... 0.000061 0.000024 \n", + "2024-01-01 00:30:00+00:00 0.0 ... 0.000060 0.000023 \n", + "2024-01-01 00:45:00+00:00 0.0 ... 0.000057 0.000023 \n", + "2024-01-01 01:00:00+00:00 0.0 ... 0.000055 0.000024 \n", + "\n", + " E2A_AMI_A E2B_AZI_A E2B_AMI_A E3A_A \\\n", + "timestamp \n", + "2024-01-01 00:00:00+00:00 0.000034 0.000053 0.000064 0.000058 \n", + "2024-01-01 00:15:00+00:00 0.000034 0.000052 0.000063 0.000058 \n", + "2024-01-01 00:30:00+00:00 0.000033 0.000051 0.000063 0.000058 \n", + "2024-01-01 00:45:00+00:00 0.000032 0.000051 0.000061 0.000059 \n", + "2024-01-01 01:00:00+00:00 0.000032 0.000052 0.000060 0.000057 \n", + "\n", + " E3B_A E3C_A E3D_A E4A_A \n", + "timestamp \n", + "2024-01-01 00:00:00+00:00 0.000058 0.000058 0.000058 0.000079 \n", + "2024-01-01 00:15:00+00:00 0.000058 0.000058 0.000058 0.000079 \n", + "2024-01-01 00:30:00+00:00 0.000058 0.000058 0.000058 0.000079 \n", + "2024-01-01 00:45:00+00:00 0.000059 0.000059 0.000059 0.000079 \n", + "2024-01-01 01:00:00+00:00 0.000057 0.000057 0.000057 0.000079 \n", + "\n", + "[5 rows x 28 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load datasets using OpenSTEF's VersionedTimeSeriesDataset\n", + "# This class handles versioned data where each value has an \"available_at\" timestamp\n", + "from openstef_core.datasets import VersionedTimeSeriesDataset\n", + "\n", + "# Load each data source from parquet files\n", + "load_dataset = VersionedTimeSeriesDataset.read_parquet(\n", + " local_dir / f\"load_measurements/{target}.parquet\"\n", + ")\n", + "weather_dataset = VersionedTimeSeriesDataset.read_parquet(\n", + " local_dir / f\"weather_forecasts_versioned/{target}.parquet\"\n", + ")\n", + "epex_dataset = VersionedTimeSeriesDataset.read_parquet(local_dir / \"EPEX.parquet\")\n", + "profiles_dataset = VersionedTimeSeriesDataset.read_parquet(local_dir / \"profiles.parquet\")\n", + "\n", + "# Combine all datasets using left join (keep all load timestamps, match features where available)\n", + "# select_version() materializes the lazy dataset into a concrete TimeSeriesDataset\n", + "dataset = VersionedTimeSeriesDataset.concat(\n", + " [load_dataset, weather_dataset, epex_dataset, profiles_dataset], \n", + " mode=\"left\" # Left join keeps all timestamps from the first dataset\n", + ").select_version()\n", + "\n", + "# Preview the combined dataset\n", + "print(f\"Dataset shape: {dataset.data.shape}\")\n", + "print(f\"Date range: {dataset.data.index.min()} to {dataset.data.index.max()}\")\n", + "dataset.data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2a64bbb5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "📈 Training period: 2024-03-01 to 2024-05-30 (8640 samples)\n", + "🔮 Forecast period: 2024-05-30 to 2024-06-13 (1344 samples)\n" + ] + } + ], + "source": [ + "# Define training and forecast time periods\n", + "from datetime import datetime, timedelta\n", + "\n", + "# Training period: 90 days of historical data\n", + "train_start = datetime.fromisoformat(\"2024-03-01T00:00:00Z\")\n", + "train_end = train_start + timedelta(days=90)\n", + "\n", + "# Forecast period: 14 days after training (this is where we'll predict)\n", + "forecast_start = train_end\n", + "forecast_end = forecast_start + timedelta(days=14)\n", + "\n", + "# Split the dataset using time-based filtering\n", + "train_dataset = dataset.filter_by_range(start=train_start, end=train_end)\n", + "forecast_dataset = dataset.filter_by_range(start=forecast_start, end=forecast_end)\n", + "\n", + "print(f\"📈 Training period: {train_start.date()} to {train_end.date()} ({len(train_dataset.data)} samples)\")\n", + "print(f\"🔮 Forecast period: {forecast_start.date()} to {forecast_end.date()} ({len(forecast_dataset.data)} samples)\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bfe2a41f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize the training data\n", + "# The plot shows the 'load' column (energy consumption in MW) over time\n", + "fig = train_dataset.data[[\"load\"]].plot(title=\"Training Data: Energy Load over Time\")\n", + "fig.update_layout(yaxis_title=\"Load (MW)\", xaxis_title=\"Time\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12d8302c", + "metadata": {}, + "source": [ + "## Define a base config with inline search space\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a001a0f0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/fleur.petit/projects/openstef/.venv/lib/python3.12/site-packages/mlflow/__init__.py:41: UserWarning:\n", + "\n", + "Versions of mlflow (3.9.0) and child packages mlflow-skinny (3.6.0) are different. This may lead to unexpected behavior. Please install the same version of all MLflow packages.\n", + "\n" + ] + } + ], + "source": [ + "from openstef_core.types import LeadTime, Q\n", + "from openstef_models.models.forecasting.xgboost_forecaster import XGBoostHyperParams\n", + "from openstef_models.presets import ForecastingWorkflowConfig, fit_with_tuning\n", + "from openstef_models.presets.forecasting_workflow import create_forecasting_workflow\n", + "from openstef_models.utils.tuning import FloatRange, IntRange\n", + "\n", + "config = ForecastingWorkflowConfig(\n", + " model_id=\"xgboost_optuna_demo\",\n", + " model=\"xgboost\",\n", + "\n", + " horizons=[LeadTime.from_string(\"PT36H\")],\n", + " quantiles=[Q(0.5), Q(0.1), Q(0.9)],\n", + "\n", + " target_column=\"load\",\n", + " temperature_column=\"temperature_2m\",\n", + " relative_humidity_column=\"relative_humidity_2m\",\n", + " wind_speed_column=\"wind_speed_10m\",\n", + " radiation_column=\"shortwave_radiation\",\n", + " pressure_column=\"surface_pressure\",\n", + "\n", + " # Pass TuningRange objects directly as field values — tune=True marks them for Optuna.\n", + " # None values for low/high fall back to the class-level defaults in XGBoostHyperParams.\n", + " xgboost_hyperparams=XGBoostHyperParams(\n", + " n_estimators=200,\n", + " learning_rate=FloatRange(None, None, log=True, tune=True), # class default range: 0.01 to 0.5\n", + " max_depth=IntRange(5, 15, tune=True),\n", + " min_child_weight=FloatRange(None, None, tune=True), # class default range: 1.0 to 10.0\n", + " reg_alpha=FloatRange(None, None, log=True, tune=True), # class default range: 1e-8 to 10.0\n", + " reg_lambda=FloatRange(None, None, log=True, tune=True), # class default range: 1e-8 to 10.0\n", + " ),\n", + " optuna_n_trials=20,\n", + " optuna_seed=42,\n", + "\n", + " mlflow_storage=None, # Disable MLflow during tuning\n", + " verbosity=0,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "9b47824a", + "metadata": {}, + "source": [ + "## Inspect the resolved search space\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65a76a06", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resolved search space:\n", + " learning_rate : FloatRange [0.01 — 0.5] [log]\n", + " max_depth : IntRange [5 — 15]\n", + " min_child_weight : FloatRange [1.0 — 10.0]\n", + " reg_alpha : FloatRange [1e-08 — 10.0] [log]\n", + " reg_lambda : FloatRange [1e-08 — 10.0] [log]\n" + ] + } + ], + "source": [ + "from openstef_models.utils.tuning import FloatRange, IntRange, get_search_space\n", + "\n", + "# Merge custom annotated hyperparams with class-level annotated defaults (filling in any None bounds).\n", + "resolved_space = get_search_space(config.xgboost_hyperparams)\n", + "\n", + "print(\"Resolved search space:\")\n", + "for name, param in resolved_space.items():\n", + " if isinstance(param, (FloatRange, IntRange)):\n", + " scale = \" [log]\" if param.log else \"\"\n", + " print(f\" {name:25s}: {type(param).__name__} [{param.low} — {param.high}]{scale}\")\n", + " else:\n", + " print(f\" {name:25s}: CategoricalRange {param.choices}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "08768e60", + "metadata": {}, + "source": [ + "## Run the Optuna study with `fit_with_tuning`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8601c179", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 0%| | 0/20 [00:00 list[Transform[TimeSeriesDataset, TimeSeriesDataset]]: diff --git a/packages/openstef-models/src/openstef_models/models/forecasting/gblinear_forecaster.py b/packages/openstef-models/src/openstef_models/models/forecasting/gblinear_forecaster.py index b181b7bd5..ad20bbd39 100644 --- a/packages/openstef-models/src/openstef_models/models/forecasting/gblinear_forecaster.py +++ b/packages/openstef-models/src/openstef_models/models/forecasting/gblinear_forecaster.py @@ -11,7 +11,7 @@ to predict values outside the range of the training data. """ -from typing import ClassVar, Literal, override +from typing import Annotated, ClassVar, Literal, override import numpy as np import pandas as pd @@ -22,7 +22,6 @@ from openstef_core.datasets.mixins import LeadTime from openstef_core.datasets.validated_datasets import ForecastDataset, ForecastInputDataset from openstef_core.exceptions import InputValidationError, MissingExtraError, NotFittedError -from openstef_core.mixins.predictor import HyperParams from openstef_core.utils.pandas import normalize_to_unit_sum from openstef_models.explainability.mixins import ContributionsMixin, ExplainableForecaster from openstef_models.models.forecasting.forecaster import Forecaster @@ -32,6 +31,7 @@ get_objective_function, xgb_prepare_target_for_objective, ) +from openstef_models.utils.tuning import CategoricalRange, FloatRange, IntRange, TunableHyperParams try: import xgboost as xgb @@ -39,19 +39,19 @@ raise MissingExtraError("xgboost", "openstef-models") from e -class GBLinearHyperParams(HyperParams): +class GBLinearHyperParams(TunableHyperParams): """Hyperparameter configuration for GBLinear forecaster.""" # Learning Parameters - n_steps: int = Field( + n_steps: Annotated[int, IntRange(50, 1000)] = Field( default=500, description="Number for steps (boosting rounds) to train the GBLinear model.", ) - updater: str = Field( + updater: Annotated[str, CategoricalRange(("shotgun", "coord_descent"))] = Field( default="shotgun", description="The updater to use for the GBLinear booster.", ) - learning_rate: float = Field( + learning_rate: Annotated[float, FloatRange(0.01, 0.5, log=True)] = Field( default=0.15, description="Step size shrinkage used to prevent overfitting. Range: [0,1]. Lower values require more boosting " "rounds.", @@ -68,15 +68,15 @@ class GBLinearHyperParams(HyperParams): ) # Regularization - reg_alpha: float = Field( + reg_alpha: Annotated[float, FloatRange(1e-8, 1.0, log=True)] = Field( default=0.0001, description="L1 regularization on weights. Higher values increase regularization. Range: [0,∞]" ) - reg_lambda: float = Field( + reg_lambda: Annotated[float, FloatRange(1e-8, 1.0, log=True)] = Field( default=0.1, description="L2 regularization on weights. Higher values increase regularization. Range: [0,∞]" ) # Feature selection - feature_selector: str = Field( + feature_selector: Annotated[str, CategoricalRange(("cyclic", "shuffle", "random", "greedy", "thrifty"))] = Field( default="shuffle", description="Feature selection method.", ) diff --git a/packages/openstef-models/src/openstef_models/models/forecasting/xgboost_forecaster.py b/packages/openstef-models/src/openstef_models/models/forecasting/xgboost_forecaster.py index 87a956378..8717db795 100644 --- a/packages/openstef-models/src/openstef_models/models/forecasting/xgboost_forecaster.py +++ b/packages/openstef-models/src/openstef_models/models/forecasting/xgboost_forecaster.py @@ -9,7 +9,7 @@ comprehensive hyperparameter control for production forecasting workflows. """ -from typing import ClassVar, Literal, override +from typing import Annotated, ClassVar, Literal, override import numpy as np import pandas as pd @@ -18,7 +18,6 @@ from openstef_core.datasets import ForecastDataset, ForecastInputDataset, TimeSeriesDataset from openstef_core.exceptions import MissingExtraError, NotFittedError -from openstef_core.mixins import HyperParams from openstef_core.utils.pandas import normalize_to_unit_sum from openstef_models.explainability.mixins import ContributionsMixin, ExplainableForecaster from openstef_models.models.forecasting.forecaster import Forecaster @@ -28,6 +27,7 @@ get_objective_function, xgb_prepare_target_for_objective, ) +from openstef_models.utils.tuning import CategoricalRange, FloatRange, IntRange, TunableHyperParams try: import xgboost as xgb @@ -35,7 +35,7 @@ raise MissingExtraError("xgboost", "openstef-models") from e -class XGBoostHyperParams(HyperParams): +class XGBoostHyperParams(TunableHyperParams): """XGBoost hyperparameters for gradient boosting tree models. Configures tree-specific parameters for XGBoost gbtree booster. Provides @@ -65,28 +65,28 @@ class XGBoostHyperParams(HyperParams): """ # Core Tree Boosting Parameters - n_estimators: int = Field( + n_estimators: Annotated[int, IntRange(50, 500)] = Field( default=100, description="Number of boosting rounds/trees to fit. Higher values may improve performance but " "increase training time and risk overfitting.", ) - learning_rate: float = Field( + learning_rate: Annotated[float, FloatRange(0.01, 0.5, log=True)] = Field( default=0.3, alias="eta", description="Step size shrinkage used to prevent overfitting. Range: [0,1]. Lower values require " "more boosting rounds.", ) - max_depth: int = Field( + max_depth: Annotated[int, IntRange(1, 15)] = Field( default=6, description="Maximum depth of trees. Higher values capture more complex patterns but risk " "overfitting. Range: [1,∞]", ) - min_child_weight: float = Field( + min_child_weight: Annotated[float, FloatRange(1.0, 10.0)] = Field( default=1, description="Minimum sum of instance weight (hessian) needed in a child. Higher values prevent " "overfitting. Range: [0,∞]", ) - gamma: float = Field( + gamma: Annotated[float, FloatRange(0.0, 5.0)] = Field( default=0, alias="min_split_loss", description="Minimum loss reduction required to make a split. Higher values make algorithm more " @@ -103,10 +103,10 @@ class XGBoostHyperParams(HyperParams): ) # Regularization - reg_alpha: float = Field( + reg_alpha: Annotated[float, FloatRange(1e-8, 10.0, log=True)] = Field( default=0, description="L1 regularization on leaf weights. Higher values increase regularization. Range: [0,∞]" ) - reg_lambda: float = Field( + reg_lambda: Annotated[float, FloatRange(1e-8, 10.0, log=True)] = Field( default=1, description="L2 regularization on leaf weights. Higher values increase regularization. Range: [0,∞]" ) max_delta_step: float = Field( @@ -119,7 +119,7 @@ class XGBoostHyperParams(HyperParams): max_leaves: int = Field( default=0, description="Maximum number of leaves. 0 means no limit. Only relevant when grow_policy='lossguide'." ) - grow_policy: Literal["depthwise", "lossguide"] = Field( + grow_policy: Annotated[Literal["depthwise", "lossguide"], CategoricalRange(("depthwise", "lossguide"))] = Field( default="depthwise", description="Controls how new nodes are added. 'depthwise' grows level by level, 'lossguide' adds leaves " "with highest loss reduction.", @@ -136,11 +136,11 @@ class XGBoostHyperParams(HyperParams): ) # Subsampling Parameters - subsample: float = Field( + subsample: Annotated[float, FloatRange(0.5, 1.0)] = Field( default=1.0, description="Fraction of training samples used for each tree. Lower values prevent overfitting. Range: (0,1]", ) - colsample_bytree: float = Field( + colsample_bytree: Annotated[float, FloatRange(0.5, 1.0)] = Field( default=1.0, description="Fraction of features used when constructing each tree. Range: (0,1]" ) colsample_bylevel: float = Field( @@ -151,7 +151,10 @@ class XGBoostHyperParams(HyperParams): ) # Tree Construction Method - tree_method: Literal["auto", "exact", "hist", "approx", "gpu_hist"] = Field( + tree_method: Annotated[ + Literal["auto", "exact", "hist", "approx", "gpu_hist"], + CategoricalRange(("auto", "hist", "approx")), + ] = Field( default="auto", description="Tree construction algorithm. 'hist' is fastest for large datasets, 'exact' for small " "datasets, 'approx' is deprecated.", diff --git a/packages/openstef-models/src/openstef_models/presets/__init__.py b/packages/openstef-models/src/openstef_models/presets/__init__.py index 0615a0ea4..9e75c4bb9 100644 --- a/packages/openstef-models/src/openstef_models/presets/__init__.py +++ b/packages/openstef-models/src/openstef_models/presets/__init__.py @@ -7,9 +7,17 @@ Provides configurations and utilities for setting up forecasting workflows. """ -from .forecasting_workflow import ForecastingWorkflowConfig, create_forecasting_workflow +from openstef_models.utils.tuning import TuningResult, fit_with_tuning, tune + +from .forecasting_workflow import ( + ForecastingWorkflowConfig, + create_forecasting_workflow, +) __all__ = [ "ForecastingWorkflowConfig", + "TuningResult", "create_forecasting_workflow", + "fit_with_tuning", + "tune", ] diff --git a/packages/openstef-models/src/openstef_models/presets/forecasting_workflow.py b/packages/openstef-models/src/openstef_models/presets/forecasting_workflow.py index 028302c1b..e51653b83 100644 --- a/packages/openstef-models/src/openstef_models/presets/forecasting_workflow.py +++ b/packages/openstef-models/src/openstef_models/presets/forecasting_workflow.py @@ -63,6 +63,7 @@ ) from openstef_models.utils.data_split import DataSplitter from openstef_models.utils.feature_selection import Exclude, FeatureSelection, Include +from openstef_models.utils.tuning import TuningConfigMixin from openstef_models.workflows.custom_forecasting_workflow import ( CustomForecastingWorkflow, ForecastingCallback, @@ -100,7 +101,7 @@ def tags(self) -> dict[str, str]: } -class ForecastingWorkflowConfig(BaseConfig): # PredictionJob +class ForecastingWorkflowConfig(TuningConfigMixin, BaseConfig): # PredictionJob """Configuration for forecasting workflows. Defines all parameters needed to set up a forecasting model, including model type, @@ -278,6 +279,16 @@ class ForecastingWorkflowConfig(BaseConfig): # PredictionJob default=0, description="Verbosity level. 0=silent, 1=warning, 2=info, 3=debug" ) + # Hyperparameter tuning (Optuna) + optuna_n_trials: int = Field( + default=20, + description="Number of Optuna trials to run when any search-space field has tune=True.", + ) + optuna_seed: int | None = Field( + default=42, + description="Random seed for the Optuna TPE sampler. Set to None to disable seeding.", + ) + # Metadata tags: dict[str, str] = Field( default_factory=dict, diff --git a/packages/openstef-models/src/openstef_models/utils/tuning.py b/packages/openstef-models/src/openstef_models/utils/tuning.py new file mode 100644 index 000000000..22baf54ac --- /dev/null +++ b/packages/openstef-models/src/openstef_models/utils/tuning.py @@ -0,0 +1,665 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project +# +# SPDX-License-Identifier: MPL-2.0 +"""Hyperparameter tuning utilities for OpenSTEF models. + +Provides dataclasses for describing hyperparameter search spaces, helper functions to +extract and merge search spaces from annotated HyperParams classes, and a thin wrapper +around Optuna for running Bayesian hyperparameter optimisation studies. +""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Protocol, Self, cast, runtime_checkable + +import optuna +from pydantic import BaseModel, PrivateAttr, model_validator + +from openstef_core.mixins import HyperParams + +if TYPE_CHECKING: + from collections.abc import Callable + + from pydantic.fields import FieldInfo + + from openstef_core.datasets import TimeSeriesDataset + from openstef_core.types import QuantileOrGlobal + from openstef_models.mixins.model_serializer import ModelIdentifier + from openstef_models.models.forecasting_model import ModelFitResult + from openstef_models.workflows.custom_forecasting_workflow import CustomForecastingWorkflow + + +@dataclass(frozen=True) +class FloatRange: + """Search space metadata for continuous float hyperparameters. + + Attach to a ``HyperParams`` field via ``Annotated`` to declare the + range that a hyperparameter tuner should explore. + + Args: + low: Lower bound of the search interval (inclusive). + high: Upper bound of the search interval (inclusive). + log: When ``True`` the sampler draws on a log scale, which is + recommended for parameters like learning rates and + regularisation coefficients. + + Example: + >>> learning_rate: Annotated[float, FloatRange(0.01, 0.5, log=True)] = 0.3 + """ + + low: float | None + high: float | None + log: bool = False + tune: bool = False + + +@dataclass(frozen=True) +class IntRange: + """Search space metadata for discrete integer hyperparameters. + + Attach to a ``HyperParams`` field via ``Annotated`` to declare the + integer range that a hyperparameter tuner should explore. + + Args: + low: Minimum integer value (inclusive). + high: Maximum integer value (inclusive). + log: When ``True`` the sampler draws on a log scale. + + Example: + >>> n_estimators: Annotated[int, IntRange(50, 500)] = 100 + """ + + low: int | None + high: int | None + log: bool = False + tune: bool = False + + +@dataclass(frozen=True) +class CategoricalRange: + """Search space metadata for categorical hyperparameters. + + Attach to a ``HyperParams`` field via ``Annotated`` to list the + discrete choices that a hyperparameter tuner should explore. + + Args: + choices: Tuple of allowed values for the parameter. + + Example: + >>> tree_method: Annotated[str, CategoricalRange(("hist", "approx"))] = "hist" + """ + + choices: tuple[Any, ...] | None + tune: bool = False + + +#: Union alias for any single-parameter search space descriptor. +TuningRange = FloatRange | IntRange | CategoricalRange + + +class TunableHyperParams(HyperParams): + """HyperParams subclass that accepts ``TuningRange`` objects as field values. + + Pass a :class:`FloatRange`, :class:`IntRange`, or :class:`CategoricalRange` as the + value for any field during construction. The range is stored in the private + ``_instance_ranges`` attribute and the field itself keeps its declared default value. + ``None`` for ``low`` / ``high`` / ``choices`` falls back to the class-level + ``Annotated`` metadata when the search space is resolved. + + This means the tuning search space lives **on the HyperParams instance itself** — no + separate dict is needed. + + Example:: + + hp = XGBoostHyperParams( + n_estimators=IntRange(100, 800, tune=True), + learning_rate=FloatRange(None, None, log=True, tune=True), # → class default [0.01, 0.5] + ) + # hp.n_estimators == 100 (the class default; the IntRange was extracted) + # get_search_space(hp) → {'n_estimators': IntRange(100, 800), 'learning_rate': FloatRange(0.01, 0.5)} + """ + + _instance_ranges: dict[str, TuningRange] = PrivateAttr( # pyright: ignore[reportUnknownVariableType] + default_factory=dict + ) + + @property + def instance_ranges(self) -> dict[str, TuningRange]: + """Public view of the per-instance tuning ranges extracted at construction.""" + return self._instance_ranges + + @model_validator(mode="wrap") + @classmethod + def _extract_tuning_ranges( + cls, + data: dict[str, object] | object, + handler: Callable[[dict[str, object] | object], TunableHyperParams], + ) -> TunableHyperParams: + """Strip TuningRange values from the input dict and store them as instance metadata. + + Returns: + A new :class:`TunableHyperParams` instance with TuningRange values removed + from the fields and stored in the private ``_instance_ranges`` attribute. + """ + instance_ranges: dict[str, TuningRange] = {} + if isinstance(data, dict): + cleaned: dict[str, Any] = {} + for key, value in cast("dict[str, object]", data).items(): + if isinstance(value, (FloatRange, IntRange, CategoricalRange)): + instance_ranges[key] = value + # Keep the key absent: Pydantic uses the declared field default + else: + cleaned[key] = value + data = cleaned + result: TunableHyperParams = handler(data) + if instance_ranges and result.__pydantic_private__ is not None: + result._instance_ranges = instance_ranges + return result + + +@dataclass(frozen=True) +class ModelTuningInfo: + """Dataclass for model specific hyperparameter info. + + Ensures that search_space cannot be empty. + + Attributes: + model_hyperparams_field_name: Name of the field on the config object + (e.g. ``"xgboost_hyperparams"``). + tunable_hyperparams: The ``TunableHyperParams`` instance to update with + trial suggestions. + search_space: Pre-computed, non-empty mapping of + parameter name → :class:`TuningRange`. + """ + + model_hyperparams_field_name: str + tunable_hyperparams: TunableHyperParams + search_space: dict[str, TuningRange] + + def __post_init__(self) -> None: + """Validate that search_space is non-empty. + + Raises: + ValueError: If ``search_space`` is empty. + """ + if not self.search_space: + msg = ( + f"search_space for '{self.model_hyperparams_field_name}' must not be empty. " + "Pass TuningRange(tune=True) objects in the HyperParams constructor." + ) + raise ValueError(msg) + + +@runtime_checkable +class TunableWorkflowConfig(Protocol): + """Structural requirements for workflow configs for tuning. + + This protocol is used for type checking of different configs cross-package, for example + ForecastingWorkflowConfig and EnsembleForecastingWorkflowConfig. + """ + + model_id: ModelIdentifier + optuna_n_trials: int + optuna_seed: int | None + + @property + def model_selection_metric(self) -> tuple[QuantileOrGlobal, str, Any]: + """Metric used to select the best trial: (quantile, metric_name, direction).""" + ... + + def get_model_tuning_info(self) -> list[ModelTuningInfo]: + """Return TunableField with model_hyperparams_field_name, hyperparams_instance and search_space for tuning. + + Can be inherited from TuningConfigMixin. + """ + ... + + def model_copy(self, *, update: dict[str, Any]) -> Self: + """Return a copy of the config with the given fields updated.""" + ... + + +def _get_class_range(field_info: FieldInfo) -> TuningRange | None: + """Return the first TuningRange found in a Pydantic FieldInfo's metadata.""" + for meta in field_info.metadata: + if isinstance(meta, (FloatRange, IntRange, CategoricalRange)): + return meta + return None + + +def _merge_numerical_range[T: (FloatRange, IntRange)](override: T, class_range: TuningRange | None) -> T: + """Merge a FloatRange or IntRange override with the class-level default. + + Returns: + A new instance of the same type as *override* with ``None`` bounds filled + from *class_range*. + """ + cr = class_range if isinstance(class_range, type(override)) else None + low = override.low if override.low is not None else (cr.low if cr is not None else None) + high = override.high if override.high is not None else (cr.high if cr is not None else None) + return replace(override, low=low, high=high) + + +def _merge_categorical_range(override: CategoricalRange, class_range: TuningRange | None) -> CategoricalRange: + """Merge a CategoricalRange override with the class-level CategoricalRange default. + + Returns: + A new :class:`CategoricalRange` with ``None`` choices filled from *class_range*. + """ + cr = class_range if isinstance(class_range, CategoricalRange) else None + cr_choices = cr.choices if cr is not None else None + choices = override.choices if override.choices is not None else cr_choices + return CategoricalRange(choices=choices, tune=override.tune) + + +def _merge_range(override: TuningRange, class_range: TuningRange | None) -> TuningRange: + """Merge *override* with *class_range*, filling ``None`` from the class defaults. + + For ``FloatRange`` / ``IntRange``, ``None`` values for ``low`` or ``high`` are filled + in from *class_range*. For ``CategoricalRange``, ``None`` for ``choices`` falls back + to *class_range*. The ``tune`` flag always comes from *override*. + + Returns: + A new :class:`TuningRange` with ``None`` bounds merged from *class_range*. + """ + if isinstance(override, FloatRange): + return _merge_numerical_range(override, class_range) + if isinstance(override, IntRange): + return _merge_numerical_range(override, class_range) + return _merge_categorical_range(override, class_range) + + +def get_search_space( + hyperparams: BaseModel, + include: set[str] | None = None, +) -> dict[str, TuningRange]: + """Extract the effective tunable search space from a *HyperParams* instance. + + Reads per-instance ``TuningRange`` objects stored in ``_instance_ranges`` + (set by passing ranges directly in the constructor of a + :class:`TunableHyperParams` subclass) and merges them with the class-level + ``Annotated`` metadata. ``None`` bounds fall back to the class-level defaults. + Only fields where the resulting ``tune`` flag is ``True`` are included. + + Args: + hyperparams: A :class:`TunableHyperParams` (or plain ``HyperParams``) instance. + include: If given, restrict the output to exactly these field names. A + ``KeyError`` is raised immediately for any name that is absent or has no + ``tune=True`` annotation (catches typos early). + + Returns: + Mapping of hyperparam field-name → effective :class:`TuningRange` for all tunable fields. + + Raises: + KeyError: If ``include`` is specified and any requested field name is not + present in the tunable search space. + + Example:: + + hp = XGBoostHyperParams( + n_estimators=IntRange(100, 800, tune=True), + learning_rate=FloatRange(None, None, log=True, tune=True), + ) + space = get_search_space(hp) + # {'n_estimators': IntRange(100, 800), 'learning_rate': FloatRange(0.01, 0.5, log=True)} + """ + # Per-instance ranges take precedence over class-level annotations + instance_ranges: dict[str, TuningRange] = {} + if isinstance(hyperparams, TunableHyperParams): + instance_ranges = hyperparams.instance_ranges + + result: dict[str, TuningRange] = {} + for hyperparam_name, field_info in type(hyperparams).model_fields.items(): + class_range = _get_class_range(field_info) + override = instance_ranges.get(hyperparam_name) + + if override is not None: + if not override.tune: + continue + result[hyperparam_name] = _merge_range(override, class_range) + elif class_range is not None and class_range.tune: + result[hyperparam_name] = class_range + + if include is not None: + missing = include - result.keys() + if missing: + msg = ( + f"Fields {sorted(missing)!r} not found in the tunable search space. " + "Check that they exist on the HyperParams class and were passed as " + "TuningRange(tune=True) in the constructor." + ) + raise KeyError(msg) + result = {k: result[k] for k in include} + + return result + + +def apply_trial_suggestions[HP: BaseModel]( + trial: optuna.Trial, + space: dict[str, TuningRange], + current: HP, +) -> HP: + """Create an updated *HyperParams* using Optuna trial suggestions. + + Args: + trial: Optuna trial object for suggesting values. + space: Search space returned by :func:`get_search_space`. + current: Current ``HyperParams`` instance to copy-and-update. + + Returns: + A new ``HyperParams`` instance with the suggested values applied. + """ + updates: dict[str, Any] = {} + for hyperparam_name, tuning_range in space.items(): + value = _suggest_hyperparam_value(trial, hyperparam_name, tuning_range) + if value is not None: + updates[hyperparam_name] = value + return current.model_copy(update=updates) + + +def run_optuna_study( + objective: Callable[[optuna.Trial], float], + n_trials: int, + seed: int | None = 42, + direction: Literal["maximize", "minimize"] = "maximize", + study_name: str = "hyperparameter_tuning", +) -> optuna.Study: + """Run a Bayesian hyperparameter optimisation study using Optuna. + + Args: + objective: Callable that receives an :class:`optuna.Trial` and returns a + ``float`` score to optimise. + n_trials: Number of trials to evaluate. + seed: Random seed for the TPE sampler (``None`` disables seeding). + direction: ``"maximize"`` or ``"minimize"``. + study_name: Human-readable label for the study. + + Returns: + Completed :class:`optuna.Study` with all trial results. + """ + study = optuna.create_study( + direction=direction, + sampler=optuna.samplers.TPESampler(seed=seed), + pruner=optuna.pruners.MedianPruner(n_startup_trials=5), + study_name=study_name, + ) + study.optimize(objective, n_trials=n_trials, show_progress_bar=True) + return study + + +class TuningConfigMixin: + """Mixin for get_model_tuning_info for workflow configs. + + Discovers tunable fields by reflecting over model_fields and returning a TunableField for every field whose value + is a TunableHyperParams instance with a non-empty search space. + """ + + def get_model_tuning_info(self) -> list[ModelTuningInfo]: + """Return one ModelTuningInfo per active tunable hyperparameter group for a model.""" + result: list[ModelTuningInfo] = [] + model_fields: dict[str, Any] = cast(dict[str, Any], getattr(type(self), "model_fields", {})) + for field_name in model_fields: + value = getattr(self, field_name) + if isinstance(value, TunableHyperParams): # checks if the config field contains tunable hyperparams + space = get_search_space(value) + if space: + result.append( + ModelTuningInfo( + model_hyperparams_field_name=field_name, + tunable_hyperparams=value, + search_space=space, + ) + ) + return result + + +@dataclass(repr=False) +class TuningResult: + """Result of a :func:`fit_with_tuning` call. + + Attributes: + workflow: The fitted :class:`CustomForecastingWorkflow`. + fit_result: The :class:`ModelFitResult` from the final training run, or + ``None`` if fitting was skipped (e.g. by an MLflow callback). + study: The completed :class:`optuna.Study`. Raw best parameter values + are available via ``study.best_params``. + best_config: The workflow config updated with the best hyperparameters + found during tuning. + """ + + workflow: CustomForecastingWorkflow + fit_result: ModelFitResult | None + study: optuna.Study + best_config: TunableWorkflowConfig + + def __repr__(self) -> str: + """Return a string representation of the TuningResult.""" + n = len(self.study.best_params) + return f"TuningResult({n} params tuned)" if n else "TuningResult(no tuning)" + + +class _TrialEntry(NamedTuple): + """One entry in the combined search space, keyed by Optuna trial-key. + + Attributes: + model_hyperparams_field_name: Field on the config holding the hyperparams + group for a specific model, e.g. ``"xgboost_hyperparams"``. + hyperparam_name: Individual parameter within that group, e.g. ``"n_estimators"``. + tuning_range: Defines the search space for this parameter. + """ + + model_hyperparams_field_name: str + hyperparam_name: str + tuning_range: TuningRange + + +def _suggest_hyperparam_value( + trial: optuna.Trial, + trial_key: str, + tuning_range: TuningRange, +) -> bool | int | float | str | None: + """Suggest a value for *trial_key* using the appropriate Optuna API. + + Returns ``None`` when the range is incomplete (missing bounds or choices) + so the caller can skip updating that parameter. + + Returns: + The suggested value, or ``None`` if the range has no usable bounds. + """ + if isinstance(tuning_range, FloatRange) and tuning_range.low is not None and tuning_range.high is not None: + return trial.suggest_float(trial_key, tuning_range.low, tuning_range.high, log=tuning_range.log) + if isinstance(tuning_range, IntRange) and tuning_range.low is not None and tuning_range.high is not None: + return trial.suggest_int(trial_key, tuning_range.low, tuning_range.high, log=tuning_range.log) + if isinstance(tuning_range, CategoricalRange) and tuning_range.choices is not None: + return trial.suggest_categorical(trial_key, list(tuning_range.choices)) + return None + + +def _build_hp_updates( + model_tuning_info: list[ModelTuningInfo], + per_field: dict[str, dict[str, Any]], +) -> dict[str, Any]: + """Build a config-level update dict by applying *per_field* values to each HP group. + + Returns: + Mapping of config field name → updated :class:`TunableHyperParams` instance. + """ + return { + tf.model_hyperparams_field_name: tf.tunable_hyperparams.model_copy( + update=per_field[tf.model_hyperparams_field_name] + ) + for tf in model_tuning_info + if tf.model_hyperparams_field_name in per_field + } + + +class _TuningObjective: + """Callable Optuna objective that encapsulates the context for a tuning run.""" + + def __init__( + self, + combined_space: dict[str, _TrialEntry], + model_tuning_info: list[ModelTuningInfo], + config: TunableWorkflowConfig, + train_dataset: TimeSeriesDataset, + create_workflow: Callable[..., CustomForecastingWorkflow], + target_quantile: QuantileOrGlobal, + metric_name: str, + ) -> None: + """Store the tuning context.""" + self._combined_space = combined_space + self._model_tuning_info = model_tuning_info + self._config = config + self._train_dataset = train_dataset + self._create_workflow = create_workflow + self._target_quantile: QuantileOrGlobal = target_quantile + self._metric_name = metric_name + + def __call__(self, trial: optuna.Trial) -> float: + """Evaluate a single Optuna trial. + + Returns: + Score to maximise, or ``-inf`` on failure. + """ + per_field: dict[str, dict[str, Any]] = {} + for trial_key, trial_entry in self._combined_space.items(): + value = _suggest_hyperparam_value(trial, trial_key, trial_entry.tuning_range) + if value is not None: + per_field.setdefault(trial_entry.model_hyperparams_field_name, {})[trial_entry.hyperparam_name] = value + + tuned_config = self._config.model_copy(update=_build_hp_updates(self._model_tuning_info, per_field)) + + trial_workflow = self._create_workflow(tuned_config) + trial_result = trial_workflow.fit(self._train_dataset) + if trial_result is None: + return float("-inf") + metrics = trial_result.metrics_val if trial_result.metrics_val is not None else trial_result.metrics_train + score = metrics.get_metric(quantile=self._target_quantile, metric_name=self._metric_name) + return float(score) if score is not None else float("-inf") + + +def tune[ConfigT: TunableWorkflowConfig]( + config: ConfigT, + train_dataset: TimeSeriesDataset, + create_workflow: Callable[[ConfigT], CustomForecastingWorkflow], +) -> tuple[ConfigT, optuna.Study, dict[str, Any]]: + """Generic hyperparameter tuning for any TunableWorkflowConfig. + + Args: + config: Any config implementing TunableWorkflowConfig. + train_dataset: Dataset used for all trial fit calls. + create_workflow: Factory that builds a CustomForecastingWorkflow from config. + + Returns: + (best_config, study, best_params) + + Raises: + ValueError: If no hyperparameter field has tune=True ranges. + """ + model_tuning_info = config.get_model_tuning_info() + if not model_tuning_info: + msg = ( + f"No tunable hyperparameters found on config '{config.model_id}'. " + "Pass TuningRange(tune=True) objects as field values in the hyperparams constructor." + ) + raise ValueError(msg) + + target_quantile, metric_name, _ = config.model_selection_metric + + # Aggregate search spaces across tunable hyperparam fields. + # Use prefixes to avoid collisions. + multi = len(model_tuning_info) > 1 + combined_space: dict[ + str, _TrialEntry + ] = {} # trial_key -> (model_hyperparams_field_name, hyperparam_name, tuning_range) + for tf in model_tuning_info: + for hyperparam_name, tuning_range in tf.search_space.items(): + trial_key = f"{tf.model_hyperparams_field_name}.{hyperparam_name}" if multi else hyperparam_name + combined_space[trial_key] = _TrialEntry(tf.model_hyperparams_field_name, hyperparam_name, tuning_range) + + # Build and run the Optuna study + objective = _TuningObjective( + combined_space=combined_space, + model_tuning_info=model_tuning_info, + config=config, + train_dataset=train_dataset, + create_workflow=create_workflow, + target_quantile=target_quantile, + metric_name=metric_name, + ) + study = run_optuna_study( + objective=objective, + n_trials=config.optuna_n_trials, + seed=config.optuna_seed, + study_name=f"tuning_{config.model_id}", + ) + + # Reconstruct the best config by applying the best parameters per field + best_config = _reconstruct_best_config(config, model_tuning_info, study) + return best_config, study, study.best_params + + +def _reconstruct_best_config[ConfigT: TunableWorkflowConfig]( + config: ConfigT, + model_tuning_info_list: list[ModelTuningInfo], + study: optuna.Study, +) -> ConfigT: + """Returns the best config using the optuna study results for all tunable fields. + + Args: + config: Any config implementing TunableWorkflowConfig. + model_tuning_info_list: list of :class: ModelTuningInfo per model, + study: :class:`optuna.Study` completed with trial results + + Returns: + :class:`TunableWorkflowConfig` with the best best hyperparameter values. + """ + multi = len(model_tuning_info_list) > 1 + per_field_best: dict[str, dict[str, Any]] = {} + for trial_key, value in study.best_params.items(): + if multi and "." in trial_key: + model_hyperparams_field_name, hyperparam_name = trial_key.split(".", 1) + else: + model_hyperparams_field_name = model_tuning_info_list[0].model_hyperparams_field_name + hyperparam_name = trial_key + per_field_best.setdefault(model_hyperparams_field_name, {})[hyperparam_name] = value + + return config.model_copy(update=_build_hp_updates(model_tuning_info_list, per_field_best)) + + +def fit_with_tuning[ConfigT: TunableWorkflowConfig]( + config: ConfigT, train_dataset: TimeSeriesDataset, create_workflow: Callable[[ConfigT], CustomForecastingWorkflow] +) -> TuningResult: + """Create, tune and fit. + + Args: + config: Any config implementing TunableWorkflowConfig. + train_dataset: Dataset used for fit. + create_workflow: Factory that builds a CustomForecastingWorkflow from config. + + Returns: + :class:`TuningResult` with the fitted workflow, completed study, and best config. + """ + best_config, study, _ = tune(config, train_dataset, create_workflow) + workflow = create_workflow(best_config) + result = workflow.fit(train_dataset) + return TuningResult(workflow=workflow, fit_result=result, study=study, best_config=best_config) + + +__all__ = [ + "CategoricalRange", + "FloatRange", + "IntRange", + "ModelTuningInfo", + "TunableHyperParams", + "TunableWorkflowConfig", + "TuningConfigMixin", + "TuningRange", + "TuningResult", + "apply_trial_suggestions", + "fit_with_tuning", + "get_search_space", + "run_optuna_study", + "tune", +] diff --git a/packages/openstef-models/tests/unit/utils/test_tuning.py b/packages/openstef-models/tests/unit/utils/test_tuning.py new file mode 100644 index 000000000..291569208 --- /dev/null +++ b/packages/openstef-models/tests/unit/utils/test_tuning.py @@ -0,0 +1,477 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project +# +# SPDX-License-Identifier: MPL-2.0 + +"""Unit tests for the hyperparameter tuning utilities.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock + +import optuna +import pytest + +from openstef_models.models.forecasting.xgboost_forecaster import XGBoostHyperParams +from openstef_models.presets.forecasting_workflow import ForecastingWorkflowConfig +from openstef_models.utils.tuning import ( + CategoricalRange, + FloatRange, + IntRange, + ModelTuningInfo, + TuningResult, + _merge_range, # noqa: PLC2701 + _reconstruct_best_config, # noqa: PLC2701 + _suggest_hyperparam_value, # noqa: PLC2701 + apply_trial_suggestions, + fit_with_tuning, + get_search_space, + run_optuna_study, + tune, +) + +# Suppress Optuna progress output during tests +optuna.logging.set_verbosity(optuna.logging.WARNING) + + +def _config(**kwargs: Any) -> ForecastingWorkflowConfig: + """Minimal ForecastingWorkflowConfig for tuning tests.""" + return ForecastingWorkflowConfig( + model_id="test", + model="xgboost", + optuna_n_trials=2, + optuna_seed=0, + **kwargs, + ) + + +# Helper + + +def _make_mock_workflow(score: float = 0.8) -> MagicMock: + """Return a mock workflow whose fit() reports a fixed metric score.""" + mock_metrics = MagicMock() + mock_metrics.get_metric.return_value = score + mock_fit_result = MagicMock() + mock_fit_result.metrics_val = mock_metrics + mock_workflow = MagicMock() + mock_workflow.fit.return_value = mock_fit_result + return mock_workflow + + +# TunableHyperParams + + +def test_tunable_hyperparams__range_extracted_and_field_keeps_default() -> None: + """A TuningRange passed at construction is stored in instance_ranges; the field keeps its default.""" + hp = XGBoostHyperParams(n_estimators=IntRange(100, 800, tune=True)) + + assert hp.instance_ranges["n_estimators"] == IntRange(100, 800, tune=True) + assert hp.n_estimators == 100 # default preserved + + +def test_tunable_hyperparams__multiple_ranges_all_extracted() -> None: + """All TuningRange values passed at construction appear in instance_ranges.""" + hp = XGBoostHyperParams( + n_estimators=IntRange(100, 800, tune=True), + learning_rate=FloatRange(0.01, 0.5, log=True, tune=True), + grow_policy=CategoricalRange(("depthwise",), tune=True), + ) + + assert set(hp.instance_ranges.keys()) == {"n_estimators", "learning_rate", "grow_policy"} + + +def test_tunable_hyperparams__scalar_values_not_extracted() -> None: + """Scalar field values are stored normally and never appear in instance_ranges.""" + hp = XGBoostHyperParams(n_estimators=200) + + assert hp.n_estimators == 200 + assert hp.instance_ranges == {} + + +# ModelTuningInfo + + +def test_model_tuning_info__raises_on_empty_search_space() -> None: + """ModelTuningInfo raises ValueError when search_space is empty.""" + with pytest.raises(ValueError, match=r"search_space.*must not be empty"): + ModelTuningInfo( + model_hyperparams_field_name="xgboost_hyperparams", + tunable_hyperparams=XGBoostHyperParams(), + search_space={}, + ) + + +# _merge_range + + +@pytest.mark.parametrize( + ("override", "class_range", "expected"), + [ + pytest.param( + FloatRange(None, None, tune=True), + FloatRange(0.01, 0.5, log=True), + FloatRange(0.01, 0.5, log=False, tune=True), + id="float-none-bounds-fallback", + ), + pytest.param( + FloatRange(0.2, 0.8, tune=True), + FloatRange(0.01, 0.5), + FloatRange(0.2, 0.8, log=False, tune=True), + id="float-override-bounds-preserved", + ), + pytest.param( + FloatRange(None, 0.8, tune=True), + None, + FloatRange(None, 0.8, log=False, tune=True), + id="float-none-stays-without-class-range", + ), + pytest.param( + IntRange(None, None, tune=True), + IntRange(10, 100), + IntRange(10, 100, log=False, tune=True), + id="int-none-bounds-fallback", + ), + pytest.param( + IntRange(20, 80, tune=True), + IntRange(10, 100), + IntRange(20, 80, log=False, tune=True), + id="int-override-bounds-preserved", + ), + pytest.param( + CategoricalRange(None, tune=True), + CategoricalRange(("x", "y")), + CategoricalRange(("x", "y"), tune=True), + id="categorical-none-choices-fallback", + ), + pytest.param( + CategoricalRange(("a",), tune=True), + CategoricalRange(("x", "y")), + CategoricalRange(("a",), tune=True), + id="categorical-override-choices-preserved", + ), + ], +) +def test_merge_range__fills_none_bounds_from_class_range( + override: FloatRange | IntRange | CategoricalRange, + class_range: FloatRange | IntRange | CategoricalRange | None, + expected: FloatRange | IntRange | CategoricalRange, +) -> None: + """_merge_range fills None bounds/choices from class_range; tune always comes from override.""" + assert _merge_range(override, class_range) == expected + + +# =========================================================================== +# get_search_space +# =========================================================================== + + +def test_get_search_space__returns_instance_ranges_with_tune_true() -> None: + """get_search_space includes instance ranges where tune=True.""" + space = get_search_space(XGBoostHyperParams(n_estimators=IntRange(100, 800, tune=True))) + + assert space["n_estimators"] == IntRange(100, 800, tune=True) + + +def test_get_search_space__excludes_instance_ranges_with_tune_false() -> None: + """get_search_space excludes instance ranges where tune=False.""" + assert "n_estimators" not in get_search_space(XGBoostHyperParams(n_estimators=IntRange(100, 800, tune=False))) + + +def test_get_search_space__returns_empty_when_no_tune_true_annotations() -> None: + """get_search_space returns empty dict when no field has tune=True (XGBoostHyperParams class defaults).""" + assert get_search_space(XGBoostHyperParams()) == {} + + +def test_get_search_space__merges_none_bounds_from_class_annotation() -> None: + """Instance ranges with None bounds are filled from the class-level Annotated defaults.""" + # XGBoostHyperParams.learning_rate has FloatRange(0.01, 0.5, log=True) as its class annotation + space = get_search_space(XGBoostHyperParams(learning_rate=FloatRange(None, None, log=True, tune=True))) + + result = space["learning_rate"] + assert isinstance(result, FloatRange) + assert result.low == pytest.approx(0.01) + assert result.high == pytest.approx(0.5) + assert result.log is True + + +def test_get_search_space__include_restricts_to_requested_fields() -> None: + """get_search_space with include returns only the requested field names.""" + hp = XGBoostHyperParams(n_estimators=IntRange(100, 800, tune=True), max_depth=IntRange(1, 10, tune=True)) + assert set(get_search_space(hp, include={"n_estimators"}).keys()) == {"n_estimators"} + + +@pytest.mark.parametrize( + "bad_include", + [ + pytest.param({"nonexistent"}, id="field-missing-from-class"), + pytest.param({"objective"}, id="field-has-no-tune-annotation"), + ], +) +def test_get_search_space__raises_key_error_for_invalid_include(bad_include: set[str]) -> None: + """get_search_space raises KeyError when include contains an absent or non-tunable field.""" + with pytest.raises(KeyError): + get_search_space(XGBoostHyperParams(n_estimators=IntRange(100, 800, tune=True)), include=bad_include) + + +# _suggest_hyperparam_value + + +@pytest.mark.parametrize( + ("tuning_range", "suggest_method", "call_args", "call_kwargs"), + [ + (FloatRange(0.1, 0.9), "suggest_float", ("param", 0.1, 0.9), {"log": False}), + (FloatRange(0.01, 0.5, log=True), "suggest_float", ("param", 0.01, 0.5), {"log": True}), + (IntRange(10, 100), "suggest_int", ("param", 10, 100), {"log": False}), + (CategoricalRange(("a", "b")), "suggest_categorical", ("param", ["a", "b"]), {}), + ], +) +def test_suggest_hyperparam_value__calls_correct_optuna_api( + tuning_range: FloatRange | IntRange | CategoricalRange, + suggest_method: str, + call_args: tuple[object, ...], + call_kwargs: dict[str, object], +) -> None: + """_suggest_hyperparam_value calls the correct optuna.Trial method for each TuningRange type.""" + # Arrange + trial = MagicMock(spec=optuna.Trial) + getattr(trial, suggest_method).return_value = 0.5 + + # Act + _suggest_hyperparam_value(trial, "param", tuning_range) + + # Assert + getattr(trial, suggest_method).assert_called_once_with(*call_args, **call_kwargs) + + +@pytest.mark.parametrize( + "incomplete_range", + [ + FloatRange(None, 0.9), + FloatRange(0.1, None), + IntRange(None, 100), + CategoricalRange(None), + ], +) +def test_suggest_hyperparam_value__returns_none_for_incomplete_range( + incomplete_range: FloatRange | IntRange | CategoricalRange, +) -> None: + """_suggest_hyperparam_value returns None when bounds or choices are missing.""" + # Arrange + trial = MagicMock(spec=optuna.Trial) + + # Act + result = _suggest_hyperparam_value(trial, "param", incomplete_range) + + # Assert + assert result is None + trial.suggest_float.assert_not_called() + trial.suggest_int.assert_not_called() + trial.suggest_categorical.assert_not_called() + + +# apply_trial_suggestions + + +def test_apply_trial_suggestions__updates_all_fields_from_trial() -> None: + """apply_trial_suggestions applies all trial suggestions and returns an updated HyperParams.""" + # Arrange + hp = XGBoostHyperParams() + trial = MagicMock(spec=optuna.Trial) + trial.suggest_float.return_value = 0.1 + trial.suggest_int.return_value = 200 + trial.suggest_categorical.return_value = "lossguide" + space: dict[str, Any] = { + "learning_rate": FloatRange(0.01, 0.5, log=True), + "n_estimators": IntRange(50, 500), + "grow_policy": CategoricalRange(("depthwise", "lossguide")), + } + + # Act + result = apply_trial_suggestions(trial, space, hp) + + # Assert + assert result.learning_rate == pytest.approx(0.1) + assert result.n_estimators == 200 + assert result.grow_policy == "lossguide" + trial.suggest_float.assert_called_once_with("learning_rate", 0.01, 0.5, log=True) + trial.suggest_int.assert_called_once_with("n_estimators", 50, 500, log=False) + trial.suggest_categorical.assert_called_once_with("grow_policy", ["depthwise", "lossguide"]) + + +def test_apply_trial_suggestions__skips_fields_with_none_bounds() -> None: + """apply_trial_suggestions leaves fields unchanged when their range has None bounds.""" + hp = XGBoostHyperParams() + trial = MagicMock(spec=optuna.Trial) + + result = apply_trial_suggestions(trial, {"n_estimators": IntRange(None, None)}, hp) + + assert result.n_estimators == hp.n_estimators + trial.suggest_int.assert_not_called() + + +# TuningConfigMixin + + +def test_tuning_config_mixin__discovers_all_tunable_fields() -> None: + """get_model_tuning_info returns one ModelTuningInfo per TunableHyperParams field with a non-empty search space.""" + config = _config(xgboost_hyperparams=XGBoostHyperParams(n_estimators=IntRange(50, 500, tune=True))) + tunable = config.get_model_tuning_info() + + assert len(tunable) == 1 + assert tunable[0].model_hyperparams_field_name == "xgboost_hyperparams" + assert "n_estimators" in tunable[0].search_space + + +def test_tuning_config_mixin__returns_empty_when_no_tunable_ranges() -> None: + """get_model_tuning_info returns an empty list when no TunableHyperParams field has tune=True.""" + assert _config().get_model_tuning_info() == [] + + +# TuningResult + + +@pytest.mark.parametrize( + ("best_params", "expected_repr"), + [ + ({"alpha": 0.5, "n_iter": 42}, "TuningResult(2 params tuned)"), + ({}, "TuningResult(no tuning)"), + ], +) +def test_tuning_result__repr_reflects_param_count(best_params: dict[str, Any], expected_repr: str) -> None: + """TuningResult.__repr__ summarises the count of tuned parameters.""" + # Arrange + study = MagicMock(spec=optuna.Study) + study.best_params = best_params + result = TuningResult( + workflow=MagicMock(), + fit_result=None, + study=study, + best_config=MagicMock(), + ) + + # Act & Assert + assert repr(result) == expected_repr + + +# run_optuna_study + + +@pytest.mark.parametrize("n_trials", [1, 3]) +def test_run_optuna_study__runs_exactly_n_trials(n_trials: int) -> None: + """run_optuna_study produces a study containing exactly n_trials completed trials.""" + + # Arrange + def objective(trial: optuna.Trial) -> float: + return trial.suggest_float("x", 0.0, 1.0) + + # Act + study = run_optuna_study(objective, n_trials=n_trials, seed=0) + + # Assert + assert isinstance(study, optuna.Study) + assert len(study.trials) == n_trials + + +@pytest.mark.parametrize( + ("direction", "expected_direction_name"), + [ + ("maximize", "MAXIMIZE"), + ("minimize", "MINIMIZE"), + ], +) +def test_run_optuna_study__respects_direction(direction: str, expected_direction_name: str) -> None: + """run_optuna_study creates a study with the specified optimisation direction.""" + + # Arrange + def objective(trial: optuna.Trial) -> float: + return trial.suggest_float("x", 0.0, 1.0) + + # Act + study = run_optuna_study(objective, n_trials=1, seed=0, direction=direction) # type: ignore[arg-type] + + # Assert + assert study.direction.name == expected_direction_name + + +# _reconstruct_best_config + + +def test_reconstruct_best_config__single_model_applies_best_params() -> None: + """_reconstruct_best_config updates the single hyperparams field with best_params values.""" + config = _config() + info = ModelTuningInfo( + model_hyperparams_field_name="xgboost_hyperparams", + tunable_hyperparams=config.xgboost_hyperparams, + search_space={"n_estimators": IntRange(50, 500, tune=True)}, + ) + study = MagicMock(spec=optuna.Study) + study.best_params = {"n_estimators": 300} + + best_config = _reconstruct_best_config(config, [info], study) + + assert best_config.xgboost_hyperparams.n_estimators == 300 + assert best_config.xgboost_hyperparams.max_depth == config.xgboost_hyperparams.max_depth # unchanged + + +def test_reconstruct_best_config__multi_model_parses_dotted_trial_keys() -> None: + """_reconstruct_best_config routes 'field.param' keys to the correct HP group.""" + config = _config() + info_xgb = ModelTuningInfo( + model_hyperparams_field_name="xgboost_hyperparams", + tunable_hyperparams=config.xgboost_hyperparams, + search_space={"n_estimators": IntRange(50, 500, tune=True)}, + ) + info_gblinear = ModelTuningInfo( + model_hyperparams_field_name="gblinear_hyperparams", + tunable_hyperparams=config.gblinear_hyperparams, + search_space={"n_estimators": IntRange(50, 500, tune=True)}, + ) + study = MagicMock(spec=optuna.Study) + study.best_params = {"xgboost_hyperparams.n_estimators": 300, "gblinear_hyperparams.n_estimators": 150} + + best_config = _reconstruct_best_config(config, [info_xgb, info_gblinear], study) + + assert best_config.xgboost_hyperparams.n_estimators == 300 + assert best_config.gblinear_hyperparams.n_estimators == 150 + + +# tune + + +def test_tune__raises_when_no_tunable_hyperparams() -> None: + """tune raises ValueError when the config exposes no tune=True hyperparameter ranges.""" + with pytest.raises(ValueError, match="No tunable hyperparameters"): + tune(_config(), MagicMock(), MagicMock()) + + +def test_tune__returns_best_config_study_and_params() -> None: + """tune returns a best_config, completed study, and best_params dict after n_trials trials.""" + config = _config(xgboost_hyperparams=XGBoostHyperParams(n_estimators=IntRange(50, 500, tune=True))) + create_workflow = MagicMock(return_value=_make_mock_workflow()) + + best_config, study, best_params = tune(config, MagicMock(), create_workflow) + + assert isinstance(best_config, ForecastingWorkflowConfig) + assert isinstance(study, optuna.Study) + assert len(study.trials) == 2 + assert "n_estimators" in best_params + assert 50 <= best_params["n_estimators"] <= 500 + + +# fit_with_tuning + + +def test_fit_with_tuning__returns_tuning_result_with_best_config_and_study() -> None: + """fit_with_tuning returns a TuningResult holding the best config, study, and fitted workflow.""" + config = _config(xgboost_hyperparams=XGBoostHyperParams(n_estimators=IntRange(50, 500, tune=True))) + create_workflow = MagicMock(return_value=_make_mock_workflow()) + + result = fit_with_tuning(config, MagicMock(), create_workflow) + + assert isinstance(result, TuningResult) + assert isinstance(result.study, optuna.Study) + assert isinstance(result.best_config, ForecastingWorkflowConfig) + assert result.workflow is not None + # n_trials training runs during tuning + 1 final fit with the best config + assert create_workflow.call_count == 2 + 1 diff --git a/pyproject.toml b/pyproject.toml index 15a43c8cc..b50e3d487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ dependencies = [ "openstef-core", "openstef-models[xgb-cpu]", + "optuna>=4.7", ] optional-dependencies.all = [ diff --git a/uv.lock b/uv.lock index d6360eecc..a8c24e62d 100644 --- a/uv.lock +++ b/uv.lock @@ -172,6 +172,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "alembic" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/41/ab8f624929847b49f84955c594b165855efd829b0c271e1a8cac694138e5/alembic-1.18.3.tar.gz", hash = "sha256:1212aa3778626f2b0f0aa6dd4e99a5f99b94bd25a0c1ac0bba3be65e081e50b0", size = 2052564, upload-time = "2026-01-29T20:24:15.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/8e/d79281f323e7469b060f15bd229e48d7cdd219559e67e71c013720a88340/alembic-1.18.3-py3-none-any.whl", hash = "sha256:12a0359bfc068a4ecbb9b3b02cf77856033abfdb59e4a5aca08b7eacd7b74ddd", size = 262282, upload-time = "2026-01-29T20:24:17.488Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -632,6 +646,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + [[package]] name = "comm" version = "0.2.3" @@ -1236,6 +1262,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" }, ] +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -2055,6 +2120,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + [[package]] name = "markdown" version = "3.10" @@ -2685,6 +2762,7 @@ source = { editable = "." } dependencies = [ { name = "openstef-core" }, { name = "openstef-models", extra = ["xgb-cpu"] }, + { name = "optuna" }, ] [package.optional-dependencies] @@ -2739,6 +2817,7 @@ requires-dist = [ { name = "openstef-models", extras = ["xgb-cpu"], editable = "packages/openstef-models" }, { name = "openstef-models", extras = ["xgb-cpu"], marker = "extra == 'all'", editable = "packages/openstef-models" }, { name = "openstef-models", extras = ["xgb-cpu"], marker = "extra == 'models'", editable = "packages/openstef-models" }, + { name = "optuna", specifier = ">=4.7.0" }, ] provides-extras = ["all", "beam", "meta", "models"] @@ -3010,6 +3089,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, ] +[[package]] +name = "optuna" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "colorlog" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/b2/b5e12de7b4486556fe2257611b55dbabf30d0300bdb031831aa943ad20e4/optuna-4.7.0.tar.gz", hash = "sha256:d91817e2079825557bd2e97de2e8c9ae260bfc99b32712502aef8a5095b2d2c0", size = 479740, upload-time = "2026-01-19T05:45:52.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d1/6c8a4fbb38a9e3565f5c36b871262a85ecab3da48120af036b1e4937a15c/optuna-4.7.0-py3-none-any.whl", hash = "sha256:e41ec84018cecc10eabf28143573b1f0bde0ba56dba8151631a590ecbebc1186", size = 413894, upload-time = "2026-01-19T05:45:50.815Z" }, +] + [[package]] name = "optype" version = "0.14.0" @@ -4774,6 +4871,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +] + [[package]] name = "sqlparse" version = "0.5.3"