Skip to content
Open
63 changes: 63 additions & 0 deletions examples/skforecast/skforecast_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Skforecast Integration Example - Hyperparameter Tuning for Time Series Forecasting

This example demonstrates how to use Hyperactive to tune hyperparameters of a
skforecast ForecasterRecursive model. It uses the SkforecastOptCV class which
provides a familiar sklearn-like API for integrating skforecast models with
Hyperactive's optimization algorithms.

Characteristics:
- Integration with skforecast's backtesting functionality
- Tuning of regressor hyperparameters (e.g., RandomForestRegressor)
- Uses HillClimbing optimizer (can be swapped for any Hyperactive optimizer)
- Time series cross-validation via backtesting
"""

import numpy as np
import pandas as pd
from skforecast.recursive import ForecasterRecursive
from sklearn.ensemble import RandomForestRegressor
from hyperactive.opt import HillClimbing
from hyperactive.integrations.skforecast import SkforecastOptCV

# Generate synthetic data
data = pd.Series(
np.random.randn(100),
index=pd.date_range(start="2020-01-01", periods=100, freq="D"),
name="y",
)

# Define forecaster
forecaster = ForecasterRecursive(
regressor=RandomForestRegressor(random_state=123), lags=5
)

# Define optimizer
optimizer = HillClimbing(
search_space={
"n_estimators": list(range(10, 100, 10)),
"max_depth": list(range(2, 10)),
},
n_iter=10,
)

# Define SkforecastOptCV
opt_cv = SkforecastOptCV(
forecaster=forecaster,
optimizer=optimizer,
steps=5,
metric="mean_squared_error",
initial_train_size=50,
verbose=True,
)

# Fit
print("Fitting...")
opt_cv.fit(y=data)

# Predict
print("Predicting...")
predictions = opt_cv.predict(steps=5)
print("Predictions:")
print(predictions)
print("Best params:", opt_cv.best_params_)
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ sktime-integration = [
"skpro",
'sktime; python_version < "3.14"',
]
skforecast-integration = [
"skforecast",
]
integrations = [
"scikit-learn <1.8.0",
"skpro",
'sktime; python_version < "3.14"',
"skforecast",
]
build = [
"setuptools",
"build",
Expand Down
4 changes: 4 additions & 0 deletions src/hyperactive/experiment/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from hyperactive.experiment.integrations.sktime_forecasting import (
SktimeForecastingExperiment,
)
from hyperactive.experiment.integrations.skforecast_forecasting import (
SkforecastExperiment,
)
from hyperactive.experiment.integrations.torch_lightning_experiment import (
TorchExperiment,
)
Expand All @@ -20,5 +23,6 @@
"SkproProbaRegExperiment",
"SktimeClassificationExperiment",
"SktimeForecastingExperiment",
"SkforecastExperiment",
"TorchExperiment",
]
146 changes: 146 additions & 0 deletions src/hyperactive/experiment/integrations/skforecast_forecasting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Experiment adapter for skforecast backtesting experiments."""
# copyright: hyperactive developers, MIT License (see LICENSE file)

import copy
import numpy as np
from hyperactive.base import BaseExperiment


class SkforecastExperiment(BaseExperiment):
"""Experiment adapter for skforecast backtesting experiments.

This class is used to perform backtesting experiments using a given
skforecast forecaster. It allows for hyperparameter tuning and evaluation of
the model's performance.

Parameters
----------
forecaster : skforecast forecaster
skforecast forecaster to benchmark

y : pandas Series
Target time series used in the evaluation experiment

steps : int
Number of steps to predict

metric : str or callable
Metric used to quantify the goodness of fit of the model

initial_train_size : int
Number of samples in the initial training set

exog : pandas Series or DataFrame, optional
Exogenous variable/s used in the evaluation experiment

refit : bool, optional
Whether to re-fit the forecaster in each iteration

fixed_train_size : bool, optional
If True, the train size doesn't increase but moves by `steps` in each iteration

gap : int, optional
Number of samples to exclude from the end of each training set and the start of the test set

allow_incomplete_fold : bool, optional
If True, the last fold is allowed to have fewer samples than `steps`

return_best : bool, optional
If True, the best model is returned

n_jobs : int or 'auto', optional
Number of jobs to run in parallel

verbose : bool, optional
Print summary figures

show_progress : bool, optional
Whether to show a progress bar
"""

def __init__(
self,
forecaster,
y,
steps,
metric,
initial_train_size,
exog=None,
refit=False,
fixed_train_size=False,
gap=0,
allow_incomplete_fold=True,
return_best=False,
n_jobs="auto",
verbose=False,
show_progress=False,
):
self.forecaster = forecaster
self.y = y
self.steps = steps
self.metric = metric
self.initial_train_size = initial_train_size
self.exog = exog
self.refit = refit
self.fixed_train_size = fixed_train_size
self.gap = gap
self.allow_incomplete_fold = allow_incomplete_fold
self.return_best = return_best
self.n_jobs = n_jobs
self.verbose = verbose
self.show_progress = show_progress

super().__init__()

def _evaluate(self, params):
"""Evaluate the parameters.

Parameters
----------
params : dict with string keys
Parameters to evaluate.

Returns
-------
float
The value of the parameters as per evaluation.
dict
Additional metadata about the search.
"""
from skforecast.model_selection import backtesting_forecaster
from skforecast.model_selection import TimeSeriesFold

forecaster = copy.deepcopy(self.forecaster)
forecaster.set_params(params)

cv = TimeSeriesFold(
steps=self.steps,
initial_train_size=self.initial_train_size,
refit=self.refit,
fixed_train_size=self.fixed_train_size,
gap=self.gap,
allow_incomplete_fold=self.allow_incomplete_fold,
)

results, _ = backtesting_forecaster(
forecaster=forecaster,
y=self.y,
cv=cv,
metric=self.metric,
exog=self.exog,
n_jobs=self.n_jobs,
verbose=self.verbose,
show_progress=self.show_progress,
)

if isinstance(self.metric, str):
metric_name = self.metric
else:
metric_name = (
self.metric.__name__ if hasattr(self.metric, "__name__") else "score"
)

# backtesting_forecaster returns a DataFrame
res_float = results[metric_name].iloc[0]

return res_float, {"results": results}
5 changes: 5 additions & 0 deletions src/hyperactive/integrations/skforecast/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# copyright: hyperactive developers, MIT License (see LICENSE file)

from .skforecast_opt_cv import SkforecastOptCV

__all__ = ["SkforecastOptCV"]
Loading
Loading