Custom forecast methods
This page explains how to add your own forecasting method to use with Interfere's workflows (fitting, prediction, and hyperparameter tuning with CrossValObjective and Optuna). We use a polynomial forecaster as the running example: it fits a polynomial in time to each variable and extrapolates, with a single hyperparameter degree.
What you need to implement
Subclass interfere.ForecastMethod and implement four pieces:
| Method | Purpose |
|---|---|
_fit(t, endog_states, exog_states) |
Train your model on the given time series. Store whatever you need for prediction. |
_predict(t, prior_endog_states, ...) |
Return predictions at times t, shape (len(t), n_variables). |
get_window_size() |
Return how many past observations the method needs (for CV and prediction). |
_get_optuna_params(trial, ...) |
Return a dict of constructor kwargs for Optuna to try (so CV can tune hyperparameters). |
You also need get_test_params() (static) if the method will be used in the package test suite; for a standalone example you can return a fixed dict, e.g. {"degree": 2}.
The polynomial example uses only endogenous data (no exogenous variables). If your method uses exogenous variables, you must handle exog_states in _fit and prediction_exog / prior_exog_states in _predict; the Prediction and built-in methods (e.g. SINDy, VAR) show the expected shapes.
Complete example
The code below is a full, runnable example. It defines a polynomial forecaster (fit a polynomial in time to each variable and extrapolate), then uses it with CrossValObjective and Optuna.
from typing import Any, Dict, Optional
import numpy as np
import optuna
import interfere
class PolynomialForecast(interfere.ForecastMethod):
"""Fits a polynomial (in time) to each variable and extrapolates. Hyperparameter: degree."""
def __init__(self, degree: int = 2):
self.degree = degree
self.coeffs_ = None # filled in by _fit
def _fit(
self,
t: np.ndarray,
endog_states: np.ndarray,
exog_states: Optional[np.ndarray] = None,
):
_, n_vars = endog_states.shape
self.coeffs_ = [
np.polyfit(t, endog_states[:, j], self.degree)
for j in range(n_vars)
]
def _predict(
self,
t: np.ndarray,
prior_endog_states: np.ndarray,
prior_exog_states: Optional[np.ndarray] = None,
prior_t: Optional[np.ndarray] = None,
prediction_exog: Optional[np.ndarray] = None,
rng=None,
) -> np.ndarray:
n_vars = len(self.coeffs_)
return np.column_stack([
np.polyval(self.coeffs_[j], t) for j in range(n_vars)
])
def get_window_size(self) -> int:
return self.degree + 1
@staticmethod
def get_test_params() -> Dict[str, Any]:
return {"degree": 2}
@staticmethod
def _get_optuna_params(
trial,
max_lags: Optional[int] = None,
max_horizon=None,
**kwargs,
) -> Dict[str, Any]:
max_degree = 10
if max_lags is not None and max_lags >= 2:
max_degree = min(max_degree, max_lags - 1)
max_degree = max(1, max_degree)
return {"degree": trial.suggest_int("degree", 1, max_degree)}
# --- Use it with the hyperparameter optimizer ---
# Simple 1D time series: quadratic in time + noise
t = np.linspace(0, 10, 100)
y = 0.5 * t**2 - 2 * t + 1 + np.random.RandomState(42).normal(0, 0.5, size=t.size)
data = y.reshape(-1, 1) # shape (n_timesteps, n_variables)
cv = interfere.CrossValObjective(
method_type=PolynomialForecast,
data=data,
times=t,
train_window_percent=0.6,
num_folds=4,
exog_idxs=[],
num_val_prior_states=10,
metric=interfere.metrics.rmse,
metric_direction="minimize",
)
study = optuna.create_study(direction="minimize")
study.optimize(cv, n_trials=8, show_progress_bar=True)
print("Best degree:", study.best_params["degree"])
print("Best CV RMSE:", round(study.best_value, 6))
In the example, data has shape (n_timesteps, n_variables) and times is 1D. CrossValObjective gets your class as method_type; it uses _get_optuna_params by default, so each Optuna trial builds PolynomialForecast(**params), runs sliding-window CV, and returns the metric. Use study.best_params to instantiate your method and call fit / predict (or simulate with interventions) as in the Quick Start and Optimization docs.