"""
Base Forecasting Model
----------------------
A forecasting model captures the future values of a time series as a function of the past as follows:
.. math:: y_{t+1} = f(y_t, y_{t-1}, ..., y_1),
where :math:`y_t` represents the time series' value(s) at time :math:`t`.
The main functions are `fit()` and `predict()`. `fit()` learns the function `f()`, over the history of
one or several time series. The function `predict()` applies `f()` on one or several time series in order
to obtain forecasts for a desired number of time stamps into the future.
"""
import copy
import datetime
import inspect
import io
import os
import pickle
import sys
import time
from abc import ABC, ABCMeta, abstractmethod
from collections import OrderedDict
from collections.abc import Callable, Sequence
from itertools import product
from random import sample
from typing import Any, BinaryIO, Literal
from darts.metrics import CLASSIFICATION_METRICS
from darts.metrics.utils import _PARAM_LABEL_REDUCTION, _PARAM_LABELS
from darts.typing import TimeSeriesLike
from darts.utils.likelihood_models.base import (
Likelihood,
likelihood_component_names,
quantile_interval_names,
quantile_names,
)
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
import numpy as np
import pandas as pd
from darts import TimeSeries, metrics
from darts.dataprocessing.encoders import SequentialEncoder
from darts.dataprocessing.pipeline import Pipeline
from darts.dataprocessing.transformers import BaseDataTransformer
from darts.logging import get_logger, raise_log
from darts.metrics.utils import METRIC_OUTPUT_TYPE, METRIC_TYPE
from darts.typing import TimeIndex
from darts.utils import _build_tqdm_iterator, _parallel_apply, _with_sanity_checks
from darts.utils.historical_forecasts.utils import (
_apply_data_transformers,
_apply_inverse_data_transformers,
_convert_data_transformers,
_extend_series_for_overlap_end,
_get_historical_forecasts_setup,
_historical_forecasts_general_checks,
_pack_series_in_list,
_process_historical_forecast_for_backtest,
_slice_intersect_series,
)
from darts.utils.timeseries_generation import (
_build_forecast_series,
_generate_new_dates,
)
from darts.utils.ts_utils import (
SeriesType,
get_series_seq_type,
get_single_series,
series2seq,
)
from darts.utils.utils import (
generate_index,
)
logger = get_logger(__name__)
class ModelMeta(ABCMeta):
"""Meta class to store parameters used at model creation.
When creating a model instance, the parameters are extracted as follows:
1) Get the model's __init__ signature and store all arg and kwarg
names as well as default values (empty for args) in an ordered
dict `all_params`.
2) Replace the arg values from `all_params` with the positional
args used at model creation.
3) Remove args from `all_params` that were not passed as positional
args at model creation. This will enforce that an error is raised
if not all positional args were passed. If all positional args
were passed, no parameter will be removed.
4) Update `all_params` kwargs with optional kwargs from model creation.
5) Save `all_params` to the model.
6) Call (create) the model with `all_params`.
"""
def __call__(cls, *args, **kwargs):
# 1) get all default values from class' __init__ signature
sig = inspect.signature(cls.__init__)
all_params = OrderedDict([
(p.name, p.default) for p in sig.parameters.values() if not p.name == "self"
])
# 2) fill params with positional args
for param, arg in zip(all_params, args):
all_params[param] = arg
# 3) remove args which were not set (and are per default empty)
remove_params = []
for param, val in all_params.items():
if val is sig.parameters[param].empty:
remove_params.append(param)
for param in remove_params:
all_params.pop(param)
# 4) update defaults with actual model call parameters and store
all_params.update(kwargs)
# 5) save parameters in model
cls._model_call = all_params
# 6) call model
return super().__call__(**all_params)
class ForecastingModel(ABC, metaclass=ModelMeta):
"""The base class for forecasting models. It defines the *minimal* behavior that all forecasting models have to
support. The signatures in this base class are for "local" models handling only one univariate series and no
covariates. Sub-classes can handle more complex cases.
"""
@abstractmethod
def __init__(self, *args, **kwargs):
# The series, and covariates used for training the model through the `fit()` function.
# This is only used if the model has been fit on one time series.
self.training_series: TimeSeries | None = None
self.past_covariate_series: TimeSeries | None = None
self.future_covariate_series: TimeSeries | None = None
self.static_covariates: pd.DataFrame | None = None
self._expect_past_covariates, self._uses_past_covariates = False, False
self._expect_future_covariates, self._uses_future_covariates = False, False
# for static covariates there is the option to consider static covariates or ignore them
self._considers_static_covariates = False
self._expect_static_covariates, self._uses_static_covariates = False, False
# state; whether the model has been fit (on a single time series)
self._fit_called = False
# extract and store sub class model creation parameters
self._model_params = self._extract_model_creation_params()
if "add_encoders" not in kwargs:
raise_log(
NotImplementedError(
"Model subclass must pass the `add_encoders` parameter to base class."
),
)
# by default models do not use encoders
self.add_encoders = kwargs["add_encoders"]
self.encoders = self.initialize_encoders(default=True)
@abstractmethod
def fit(
self, series: TimeSeries, verbose: bool | None = None
) -> "ForecastingModel":
"""Fit/train the model on the provided series.
Parameters
----------
series
A target time series. The model will be trained to forecast this time series.
verbose
Optionally, set the fit verbosity. Not effective for all models.
Returns
-------
self
Fitted model.
"""
if not isinstance(series, TimeSeries):
raise_log(
ValueError("Train `series` must be a single `TimeSeries`."),
)
if not len(series) >= self.min_train_series_length:
raise_log(
ValueError(
f"Train series only contains {len(series)} elements"
f" but {str(self)} model requires at least {self.min_train_series_length} entries"
),
)
self.training_series = series
self._fit_called = True
if series.has_range_index:
self._supports_range_index
return self
@property
def _supports_range_index(self) -> bool:
"""Checks if the forecasting model supports a range index.
Some models may not support this, if for instance they rely on underlying dates.
By default, returns True. Needs to be overwritten by models that do not support
range indexing and raise meaningful exception.
"""
return True
@property
def likelihood(self) -> Likelihood | None:
"""Returns the likelihood (if any) that the model uses for probabilistic forecasts."""
return None
@property
def supports_probabilistic_prediction(self) -> bool:
"""
Checks if the forecasting model with this configuration supports probabilistic predictions.
By default, returns False. Needs to be overwritten by models that do support
probabilistic predictions.
"""
return False
@property
def _supports_non_retrainable_historical_forecasts(self) -> bool:
"""
Checks if the forecasting model supports historical forecasts without retraining
the model.
By default, returns False. Needs to be overwritten by models that do
support historical forecasts without retraining.
"""
return False
@property
def _supports_val_series(self) -> bool:
"""
Whether the model supports evaluation series passed to `fit()`.
"""
return False
@property
@abstractmethod
def supports_multivariate(self) -> bool:
"""
Whether the model considers more than one variate in the time series.
"""
@property
def supports_past_covariates(self) -> bool:
"""
Whether model supports past covariates
"""
return False
@property
def supports_future_covariates(self) -> bool:
"""
Whether model supports future covariates
"""
return False
@property
def supports_static_covariates(self) -> bool:
"""
Whether model supports static covariates
"""
return False
@property
def supports_sample_weight(self) -> bool:
"""
Whether model supports sample weight for training.
"""
return False
@property
def supports_likelihood_parameter_prediction(self) -> bool:
"""
Whether model instance supports direct prediction of likelihood parameters
"""
return self.likelihood is not None
@property
@abstractmethod
def supports_transferable_series_prediction(self) -> bool:
"""
Whether the model supports prediction for any input `series`.
"""
@property
def uses_past_covariates(self) -> bool:
"""
Whether the model uses past covariates, once fitted.
"""
return self._uses_past_covariates
@property
def uses_future_covariates(self) -> bool:
"""
Whether the model uses future covariates, once fitted.
"""
return self._uses_future_covariates
@property
def uses_static_covariates(self) -> bool:
"""
Whether the model uses static covariates, once fitted.
"""
return self._uses_static_covariates
@property
def considers_static_covariates(self) -> bool:
"""
Whether the model considers static covariates, if there are any.
"""
return self._considers_static_covariates
@property
def supports_optimized_historical_forecasts(self) -> bool:
"""
Whether the model supports optimized historical forecasts
"""
return False
@property
def output_chunk_length(self) -> int | None:
"""
Number of time steps predicted at once by the model, not defined for statistical models.
"""
return None
@property
def output_chunk_shift(self) -> int:
"""
Number of time steps that the output/prediction starts after the end of the input.
"""
return 0
@abstractmethod
def predict(
self,
n: int,
num_samples: int = 1,
verbose: bool | None = None,
show_warnings: bool = True,
random_state: int | None = None,
) -> TimeSeries:
"""Forecasts values for `n` time steps after the end of the training series.
Parameters
----------
n
Forecast horizon - the number of time steps after the end of the series for which to produce predictions.
num_samples
Number of times a prediction is sampled from a probabilistic model. Must be `1` for deterministic models.
verbose
Optionally, set the prediction verbosity. Not effective for all models.
show_warnings
Optionally, control whether warnings are shown. Not effective for all models.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
TimeSeries
A time series containing the `n` next points after then end of the training series.
"""
if not self._fit_called:
raise_log(
ValueError(
"The model must be fit before calling predict(). "
"For global models, if predict() is called without specifying a series, "
"the model must have been fit on a single training series."
),
)
is_autoregression = (
False
if self.output_chunk_length is None
else (n > self.output_chunk_length)
)
if self.output_chunk_shift and is_autoregression:
raise_log(
ValueError(
"Cannot perform auto-regression `(n > output_chunk_length)` with a model that uses a "
"shifted output chunk `(output_chunk_shift > 0)`."
),
)
if not self.supports_probabilistic_prediction and num_samples > 1:
raise_log(
ValueError(
"`num_samples > 1` is only supported for probabilistic models."
),
)
def _fit_wrapper(
self,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
sample_weight: TimeSeriesLike | None = None,
val_series: TimeSeriesLike | None = None,
**kwargs,
):
add_kwargs = {}
# optionally, add validation series if supported by the model
if val_series is not None and self._supports_val_series:
add_kwargs["val_series"] = val_series
# handle past and future covariates based on model support
for series_, series_name in zip(
[past_covariates, future_covariates, sample_weight],
["past_covariates", "future_covariates", "sample_weight"],
):
if getattr(self, f"supports_{series_name}"):
add_kwargs[series_name] = series_
if val_series is not None and self._supports_val_series:
add_kwargs[f"val_{series_name}"] = series_
elif series_ is not None:
raise_log(
ValueError(f"Model cannot be fit/trained with `{series_name}`."),
)
return self.fit(series=series, **add_kwargs, **kwargs)
def _predict_wrapper(
self,
n: int,
series: TimeSeries | None = None,
past_covariates: TimeSeries | None = None,
future_covariates: TimeSeries | None = None,
predict_likelihood_parameters: bool = False,
random_state: int | None = None,
**kwargs,
) -> TimeSeriesLike:
add_kwargs = {}
# not all models supports input `series` at inference
if self.supports_transferable_series_prediction:
add_kwargs["series"] = series
# even if predict() accepts covariates, the model might not support them at inference
for covs, name in zip(
[past_covariates, future_covariates],
["past_covariates", "future_covariates"],
):
if getattr(self, f"supports_{name}"):
add_kwargs[name] = covs
elif covs is not None:
raise_log(
ValueError(
f"Model prediction does not support `{name}`, either because it "
f"does not support `{name}` in general, or because it was fit/trained "
f"without using `{name}`."
),
)
if self.supports_likelihood_parameter_prediction:
add_kwargs["predict_likelihood_parameters"] = predict_likelihood_parameters
return self.predict(n=n, random_state=random_state, **add_kwargs, **kwargs)
@property
def min_train_series_length(self) -> int:
"""
The minimum required length for the training series.
"""
return sum(self._target_window_lengths) + (self.min_train_samples - 1)
@property
@abstractmethod
def min_train_samples(self) -> int:
"""
The minimum number of samples for training the model.
"""
@property
@abstractmethod
def _target_window_lengths(self) -> tuple[int, int]:
"""
The input and output target series window lengths consumed (input + output for training, input for prediction)
or generated (output for prediction) by the model.
"""
@property
def _requires_training(self):
"""Whether the model is trained when calling a `fit*` method."""
return True
@property
@abstractmethod
def extreme_lags(
self,
) -> tuple[
int | None,
int | None,
int | None,
int | None,
int | None,
int | None,
int,
]:
"""
A seven element tuple containing in order:
- min target lag
- max target lag
- min past covariate lag
- max past covariate lag
- min future covariate lag
- max future covariate lag
- output shift
If `0` is the index of the first prediction point, then all lags are relative to this index.
See examples below.
If the model wasn't fitted with:
- target (concerning SKLearnModels only): then the first element should be `None`.
- past covariates: then the third and fourth elements should be `None`.
- future covariates: then the fifth and sixth elements should be `None`.
Should be overridden by models that use past or future covariates, and/or for model that have minimum target
lag and maximum target lags potentially different from -1 and 0.
Notes
-----
maximum target lag (second value) cannot be `None` and is always larger than or equal to 0.
Examples
--------
>>> model = LinearRegressionModel(lags=3, output_chunk_length=2)
>>> model.fit(train_series)
>>> model.extreme_lags
(-3, 1, None, None, None, None, 0)
>>> model = LinearRegressionModel(lags=3, output_chunk_length=2, output_chunk_shift=2)
>>> model.fit(train_series)
>>> model.extreme_lags
(-3, 1, None, None, None, None, 2)
>>> model = LinearRegressionModel(lags=[-3, -5], lags_past_covariates = 4, output_chunk_length=7)
>>> model.fit(train_series, past_covariates=past_covariates)
>>> model.extreme_lags
(-5, 6, -4, -1, None, None, 0)
>>> model = LinearRegressionModel(lags=[3, 5], lags_future_covariates = [4, 6], output_chunk_length=7)
>>> model.fit(train_series, future_covariates=future_covariates)
>>> model.extreme_lags
(-5, 6, None, None, 4, 6, 0)
>>> model = NBEATSModel(input_chunk_length=10, output_chunk_length=7)
>>> model.fit(train_series)
>>> model.extreme_lags
(-10, 6, None, None, None, None, 0)
>>> model = NBEATSModel(input_chunk_length=10, output_chunk_length=7, lags_future_covariates=[4, 6])
>>> model.fit(train_series, future_covariates)
>>> model.extreme_lags
(-10, 6, None, None, 4, 6, 0)
"""
def _generate_new_dates(
self, n: int, input_series: TimeSeries | None = None
) -> TimeIndex:
"""
Generates `n` new dates after the end of the specified series
"""
input_series = (
input_series if input_series is not None else self.training_series
)
return _generate_new_dates(n=n, input_series=input_series)
def _build_forecast_series(
self,
points_preds: np.ndarray | Sequence[np.ndarray],
input_series: TimeSeries | None = None,
custom_components: list[str] | None = None,
with_static_covs: bool = True,
with_hierarchy: bool = True,
pred_start: pd.Timestamp | int | None = None,
) -> TimeSeries:
"""
Builds a forecast time series starting after the end of the training time series, with the
correct time index (or after the end of the input series, if specified).
Parameters
----------
points_preds
Forecasted values, can be either the target(s) or parameters of the likelihood model
input_series
TimeSeries used as input for the prediction
custom_components
New names for the forecast TimeSeries components, used when the number of components changes
with_static_covs
If set to `False`, do not copy the input_series `static_covariates` attribute
with_hierarchy
If set to `False`, do not copy the input_series `hierarchy` attribute
pred_start
Optionally, give a custom prediction start point.
Returns
-------
TimeSeries
New TimeSeries instance starting after the input series
"""
input_series = (
input_series if input_series is not None else self.training_series
)
return _build_forecast_series(
points_preds=points_preds,
input_series=input_series,
custom_columns=custom_components,
with_static_covs=with_static_covs,
with_hierarchy=with_hierarchy,
pred_start=pred_start,
copy=False,
)
def _historical_forecasts_sanity_checks(self, *args: Any, **kwargs: Any) -> None:
"""Sanity checks for the historical_forecasts function
Parameters
----------
args
The args parameter(s) provided to the historical_forecasts function.
kwargs
The kwargs parameter(s) provided to the historical_forecasts function.
Raises
------
ValueError
when a check on the parameter does not pass.
"""
# parse args and kwargs
series = args[0]
is_conformal = kwargs.get("is_conformal", False)
_historical_forecasts_general_checks(
self, series, kwargs, is_conformal=is_conformal
)
def _check_optimizable_historical_forecasts(
self,
retrain: bool | int | Callable[..., bool],
) -> bool:
"""By default, historical forecasts cannot be optimized"""
return False
@_with_sanity_checks("_historical_forecasts_sanity_checks")
def historical_forecasts(
self,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
forecast_horizon: int = 1,
num_samples: int = 1,
train_length: int | None = None,
val_length: int = 0,
start: pd.Timestamp | float | int | Literal["end"] | None = None,
start_format: Literal["position", "value"] = "value",
stride: int = 1,
retrain: bool | int | Callable[..., bool] = True,
apply_globally: bool = False,
overlap_end: bool = False,
last_points_only: bool = True,
verbose: bool = False,
show_warnings: bool = True,
predict_likelihood_parameters: bool = False,
enable_optimization: bool = True,
data_transformers: dict[str, BaseDataTransformer | Pipeline] | None = None,
fit_kwargs: dict[str, Any] | None = None,
predict_kwargs: dict[str, Any] | None = None,
sample_weight: TimeSeriesLike | str | None = None,
random_state: int | None = None,
) -> TimeSeries | list[TimeSeries] | list[list[TimeSeries]]:
"""Generates historical forecasts by simulating predictions at various points in time throughout the history of
the provided (potentially multiple) `series`. This process involves retrospectively applying the model to
different time steps, as if the forecasts were made in real-time at those specific moments. This allows for an
evaluation of the model's performance over the entire duration of the series, providing insights into its
predictive accuracy and robustness across different historical periods.
There are two main modes for this method:
- Re-training Mode (Default, `retrain=True`): The model is re-trained at each step of the simulation, and
generates a forecast using the updated model. In case of multiple series, the model can be either re-trained
on each series independently with `apply_globally=False`, or on all series jointly with `apply_globally=True`
(only effective for global models). If `apply_globally=True`, historical forecasts are computed only on the
time intersection of all series.
- Pre-trained Mode (`retrain=False`): The forecasts are generated at each step of the simulation without
re-training. It is only supported for pre-trained global forecasting models. This mode is significantly
faster as it skips the re-training step.
By choosing the appropriate mode, you can balance between computational efficiency and the need for up-to-date
model training.
**Re-training Mode:** This mode repeatedly builds a training set by either expanding from the beginning of
the `series` or by using a fixed-length `train_length` (the start point can also be configured with `start`
and `start_format`). The model is then trained on this training set, and a forecast of length `forecast_horizon`
is generated. Subsequently, the end of the training set is moved forward by `stride` time steps, and the process
is repeated.
**Pre-trained Mode:** This mode is only supported for pre-trained global forecasting models. It uses the same
simulation steps as in the *Re-training Mode* (ignoring `train_length`), but generates the forecasts directly
without re-training.
By default, with `last_points_only=True`, this method returns a single time series (or a sequence of time
series) composed of the last point from each historical forecast. This time series will thus have a frequency of
`series.freq * stride`.
If `last_points_only=False`, it will instead return a list (or a sequence of lists) of the full historical
forecast series each with frequency `series.freq`.
Parameters
----------
series
A (sequence of) target time series used to successively train (if `retrain` is not ``False``) and compute
the historical forecasts.
past_covariates
Optionally, a (sequence of) past-observed covariate time series for every input time series in `series`.
This applies only if the model supports past covariates.
future_covariates
Optionally, a (sequence of) future-known covariate time series for every input time series in `series`.
This applies only if the model supports future covariates.
forecast_horizon
The forecast horizon for the predictions.
num_samples
Number of times a prediction is sampled from a probabilistic model. Use values ``>1`` only for probabilistic
models.
train_length
Optionally, use a fixed length / number of time steps for every constructed training set (rolling window
mode). Only effective when `retrain` is not ``False``. The default is ``None``, where it uses all time
steps up until the prediction time (expanding window mode). If larger than the number of available time
steps, uses the expanding mode. Needs to be at least `min_train_series_length`.
val_length
Use a fixed length / number of time steps after every constructed training set as validation set. This set
will be passed as `val_series` and `val_*_covariates` to the fit method of the underlying forecasting model
(if it supports it). Otherwise, the time frame will be included in the training set. Only effective when
`retrain` is not ``False``. If given, cannot pass any `val_*` series in ``fit_kwargs``. The default is
``0``, which does not construct a validation set. Needs to be at least the length of one model output
(``output_chunk_length + output_chunk_shift`` for global models and ``0`` for local models).
start
Optionally, the first point in time at which a prediction is computed. This parameter supports:
``float``, ``int``, ``pandas.Timestamp``, ``'end'``, and ``None``.
If a ``float``, it is the proportion of the time series that should lie before the first prediction point.
If an ``int``, it is either the index position of the first prediction point for `series` with a
`pd.DatetimeIndex`, or the index value for `series` with a `pd.RangeIndex`. The latter can be changed to
the index position with `start_format="position"`.
If a ``pandas.Timestamp``, it is the time stamp of the first prediction point.
If the string ``'end'``, generates a single forecast per series starting one step after the end of each
series (future-only forecasting). Forecasts will extend beyond the series end regardless ``overlap_end``.
If ``None``, the first prediction point will automatically be set to:
- the first predictable point if `retrain` is ``False``, or `retrain` is a Callable and the first
predictable point is earlier than the first trainable point.
- the first trainable point if `retrain` is ``True`` or ``int`` (given `train_length`),
or `retrain` is a ``Callable`` and the first trainable point is earlier than the first predictable point.
- the first trainable point (given `train_length`) otherwise
Note: If `start` is not within the trainable / forecastable points, uses the closest valid start point that
is a round multiple of `stride` ahead of `start`. Raises a `ValueError`, if no valid start point exists.
Note: If the model uses a shifted output (`output_chunk_shift > 0`), then the first predicted point is also
shifted by `output_chunk_shift` points into the future.
Note: If `start` is outside the possible historical forecasting times, will ignore the parameter
(default behavior with ``None``) and start at the first trainable/predictable point.
start_format
Defines the `start` format. Ignored when ``start='end'``.
If set to ``'position'``, `start` corresponds to the index position of the first predicted point and can
range from `(-len(series), len(series) - 1)`.
If set to ``'value'``, `start` corresponds to the index value/label of the first predicted point. Will raise
an error if the value is not in `series`' index. Default: ``'value'``.
stride
The number of time steps between two consecutive predictions.
retrain
Whether and/or on which condition to retrain the model before predicting.
This parameter supports 3 different types: ``bool``, (positive) ``int``, and ``Callable`` (returning a
``bool``).
In the case of ``bool``: retrain the model at each step (`True`), or never retrain the model (`False`).
In the case of ``int``: the model is retrained every `retrain` iterations.
In the case of ``Callable``: the model is retrained whenever callable returns `True`.
The callable must have the following positional arguments:
- `counter` (int): current `retrain` iteration
- `pred_time` (pd.Timestamp or int): timestamp of forecast time (end of the training series)
- `train_series` (TimeSeries): train series up to `pred_time`
- `past_covariates` (TimeSeries): past_covariates series up to `pred_time`
- `future_covariates` (TimeSeries): future_covariates series up to `min(pred_time + series.freq *
forecast_horizon, series.end_time())`
Note: if any optional `*_covariates` are not passed to `historical_forecast`, ``None`` will be passed
to the corresponding retrain function argument.
Note: some models require being retrained every time and do not support anything other than
`retrain=True`.
Note: also controls the retraining of the `data_transformers`.
apply_globally
Whether to apply historical forecasts globally on the time intersection all series, or independently on
each series. This includes global model- and data transformer fitting. Only really effective for global
forecasting models, but can also be used with local models to generate forecasts on the same time frame. If
`True`, considers only the time intersection of all series for historical forecasting. If `False`,
considers the entire extent of each individual series for historical forecasting.
overlap_end
Whether the returned forecasts can go beyond the series' end or not.
last_points_only
Whether to return only the last point of each historical forecast. If set to ``True``, the method returns a
single ``TimeSeries`` (for each time series in `series`) containing the successive point forecasts.
Otherwise, returns a list of historical ``TimeSeries`` forecasts.
verbose
Whether to print the progress.
show_warnings
Whether to show warnings related to historical forecasts optimization, or parameters `start` and
`train_length`.
predict_likelihood_parameters
If set to `True`, the model predicts the parameters of its `likelihood` instead of the target. Only
supported for probabilistic models with a likelihood, `num_samples = 1` and `n<=output_chunk_length`.
Default: ``False``.
enable_optimization
Whether to use the optimized version of `historical_forecasts` when supported and available.
Default: ``True``.
data_transformers
Optionally, a dictionary of `BaseDataTransformer` or `Pipeline` to apply to the corresponding series
(possibles keys; "series", "past_covariates", "future_covariates"). If provided, all input series must be
in the un-transformed space. For fittable transformer / pipeline:
- if `retrain=True`, the data transformer re-fit on the training data at each historical forecast step.
- if `retrain=False`, the data transformer transforms the series once before all the forecasts.
The fitted transformer is used to transform the input during both training and prediction.
If the transformation is invertible, the forecasts will be inverse-transformed.
fit_kwargs
Optionally, some additional arguments passed to the model `fit()` method.
predict_kwargs
Optionally, some additional arguments passed to the model `predict()` method.
sample_weight
Optionally, some sample weights to apply to the target `series` labels for training. Only effective when
`retrain` is not ``False``. They are applied per observation, per label (each step in
`output_chunk_length`), and per component.
If a series or sequence of series, then those weights are used. If the weight series only have a single
component / column, then the weights are applied globally to all components in `series`. Otherwise, for
component-specific weights, the number of components must match those of `series`.
If a string, then the weights are generated using built-in weighting functions. The available options are
`"linear"` or `"exponential"` decay - the further in the past, the lower the weight. The weights are
computed per time `series`.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
TimeSeries
A single historical forecast for a single `series` and `last_points_only=True`: it contains only the
predictions at step `forecast_horizon` from all historical forecasts.
List[TimeSeries]
A list of historical forecasts for:
- a sequence (list) of `series` and `last_points_only=True`: for each series, it contains only the
predictions at step `forecast_horizon` from all historical forecasts.
- a single `series` and `last_points_only=False`: for each historical forecast, it contains the entire
horizon `forecast_horizon`.
List[List[TimeSeries]]
A list of lists of historical forecasts for a sequence of `series` and `last_points_only=False`. For each
series, and historical forecast, it contains the entire horizon `forecast_horizon`. The outer list
is over the series provided in the input sequence, and the inner lists contain the historical forecasts for
each series.
"""
# note: decorator already sanity-checked the parameters
model: ForecastingModel = self
fit_kwargs = fit_kwargs or {}
predict_kwargs = predict_kwargs or {}
# convert retrain to a function (if not already)
if isinstance(retrain, bool) or (isinstance(retrain, int) and retrain >= 0):
def retrain_func(
counter, pred_time, train_series, past_covariates, future_covariates
):
return counter % int(retrain) == 0 if retrain else False
else:
retrain_func = retrain
sequence_type_in = get_series_seq_type(series)
series = series2seq(series)
past_covariates = series2seq(past_covariates)
future_covariates = series2seq(future_covariates)
sample_weight = (
sample_weight
if isinstance(sample_weight, str)
else series2seq(sample_weight)
)
data_transformers = _convert_data_transformers(
data_transformers=data_transformers, copy=True
)
using_prefitted_transformers = False
# data transformer already fitted and can be directly applied to all the series
if data_transformers and not retrain:
using_prefitted_transformers = True
series, _, past_covariates, future_covariates = _apply_data_transformers(
series=series,
pred_series=None,
past_covariates=past_covariates,
future_covariates=future_covariates,
data_transformers=data_transformers,
max_future_cov_lag=model.extreme_lags[5],
fit_transformers=False,
)
if apply_globally:
series, past_covariates, future_covariates, sample_weight = (
_slice_intersect_series(
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
sample_weight=sample_weight,
)
)
if not isinstance(self, GlobalForecastingModel):
apply_globally = False
if (
enable_optimization
and model.supports_optimized_historical_forecasts
and model._check_optimizable_historical_forecasts(retrain)
):
forecasts = model._optimized_historical_forecasts(
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
num_samples=num_samples,
start=start,
start_format=start_format,
forecast_horizon=forecast_horizon,
stride=stride,
overlap_end=overlap_end,
last_points_only=last_points_only,
verbose=verbose,
show_warnings=show_warnings,
predict_likelihood_parameters=predict_likelihood_parameters,
random_state=random_state,
predict_kwargs=predict_kwargs,
)
return _apply_inverse_data_transformers(
series=series2seq(series, seq_type_out=sequence_type_in),
forecasts=series2seq(forecasts, seq_type_out=sequence_type_in),
data_transformers=data_transformers,
)
forecasts_list = [[] for _ in range(len(series))]
if apply_globally:
# for global hfc, we wrap the input in a list to run the inner loop on all series at once;
# the progress bar will be on the inner loop
series, past_covariates, future_covariates, sample_weight = (
_pack_series_in_list(
series, past_covariates, future_covariates, sample_weight
)
)
outer_iterator = series
elif len(series) == 1:
# the progress bar will be on the inner loop
outer_iterator = series
else:
# for multiple series and local hfc, the progress bar will be on the outer loop
outer_iterator = _build_tqdm_iterator(
series, verbose, total=len(series), desc="historical forecasts"
)
# deactivate the warning after displaying it once if show_warnings is True
show_predict_warnings = show_warnings
for idx, series_ in enumerate(outer_iterator):
# get input as Sequence[TimeSeries]:
# - local hfc: sequence will contain only a single TimeSeries
# - global hfc: sequence will contain all TimeSeries with identical time index
past_covariates_ = past_covariates[idx] if past_covariates else None
future_covariates_ = future_covariates[idx] if future_covariates else None
if isinstance(sample_weight, str):
sample_weight_ = sample_weight
else:
sample_weight_ = sample_weight[idx] if sample_weight else None
if not apply_globally:
# pack in list to apply downstream functions on same input type
series_, past_covariates_, future_covariates_, sample_weight_ = (
_pack_series_in_list(
series_,
past_covariates_,
future_covariates_,
sample_weight_,
)
)
# get a single TimeSeries to compute the hfc bounds;
# for global hfc, the series were intersected and share the same time index
series_0 = get_single_series(series_)
past_covariates_0 = get_single_series(past_covariates_)
future_covariates_0 = get_single_series(future_covariates_)
# get the historical forecast bounds, and potentially adjusted series, train and val lengths
(
historical_forecasts_time_index,
series_adjusted,
train_length_,
val_length_,
) = _get_historical_forecasts_setup(
model=model,
series=series_0,
past_covariates=past_covariates_0,
future_covariates=future_covariates_0,
series_idx=idx,
forecast_horizon=forecast_horizon,
start=start,
start_format=start_format,
stride=stride,
overlap_end=overlap_end,
retrain=retrain,
train_length=train_length,
val_length=val_length,
show_warnings=show_warnings,
)
# series time frame might have changed, slice all series accordingly
if not get_single_series(series_).has_same_time_as(series_adjusted):
series_ = [s.slice_intersect(series_adjusted) for s in series_]
series_0 = get_single_series(series_)
# generate time index for the iteration
historical_forecasts_time_index = generate_index(
start=historical_forecasts_time_index[0],
end=historical_forecasts_time_index[-1],
freq=stride * series_0.freq,
)
if len(series) == 1:
# Only use tqdm if there's no outer loop
iterator = _build_tqdm_iterator(
historical_forecasts_time_index,
verbose,
total=len(historical_forecasts_time_index),
desc="historical forecasts",
)
else:
iterator = historical_forecasts_time_index
# iterate and forecast
_counter_train = 0
for _counter, pred_time in enumerate(iterator):
# get current prediction input; drop everything after `pred_time`
if pred_time <= series_0.end_time():
pred_series_ = [
s.drop_after(pred_time, keep_point=False) for s in series_
]
else:
pred_series_ = series_
pred_series_0 = get_single_series(pred_series_)
# get current training input (already account for potential validation set);
if train_length_ and len(pred_series_0) > train_length_:
# moving training window with potential validation window
train_series_ = [
s[-(train_length_ + val_length_) : -val_length_ or None]
for s in pred_series_
]
elif val_length_:
# expanding training window with validation window
train_series_ = [s[:-val_length_] for s in pred_series_]
else:
# expanding training window
train_series_ = pred_series_
train_series_0 = get_single_series(train_series_)
# check if model must be re-trained
apply_retrain = False
if retrain:
# retrain_func processes the series that would be used for training
apply_retrain = retrain_func(
counter=_counter_train,
pred_time=pred_time,
train_series=train_series_0,
past_covariates=past_covariates_0,
future_covariates=future_covariates_0,
)
if (
not apply_retrain
and not _counter_train
and not model._fit_called
):
# untrained model was not trained on the first trainable timestamp
raise_log(
ValueError(
f"`retrain` is `False` in the first train iteration at prediction point (in time) "
f"`{pred_time}` and the model has not been fit before. Either call `fit()` before "
f"`historical_forecasts()`, use a different `retrain` value or modify the function "
f"to return `True` at or before this timestamp."
),
)
_counter_train += 1
# apply data transformers; only fit transformers if it is a retraining iteration (`apply_retrain=True`)
# when `retrain=False`, transformers were already applied to all the series at the beginning
if data_transformers and retrain:
(
train_series_tf,
pred_series_tf,
past_covariates_tf,
future_covariates_tf,
) = _apply_data_transformers(
series=train_series_,
pred_series=pred_series_,
past_covariates=past_covariates_,
future_covariates=future_covariates_,
data_transformers=data_transformers,
max_future_cov_lag=model.extreme_lags[5],
fit_transformers=apply_retrain,
)
else:
train_series_tf = train_series_
pred_series_tf = pred_series_
past_covariates_tf = past_covariates_
future_covariates_tf = future_covariates_
sample_weight_tf = sample_weight_
# get current validation input from transformed series;
if apply_retrain and val_length_:
# include one model input window to allow direct evaluation after the training set
input_length = model._target_window_lengths[0]
val_series_tf = [
s[-(val_length_ + input_length) :] for s in pred_series_tf
]
else:
val_series_tf = None
# for regression models with lags=None, lags_past_covariates=None and min(lags_future_covariates)>=0,
# the first predictable timestamp is the first timestamp of the series, a dummy ts must be created
# to support `predict()`
if len(pred_series_0) == 0:
pred_series_tf = [
ps.with_times_and_values(
times=generate_index(
start=pred_time - 1 * s.freq,
length=1,
freq=s.freq,
name=s._time_index.name,
),
values=np.array([np.nan]),
)
for s, ps in zip(series_, pred_series_tf)
]
if not apply_globally:
# single series for local models
pred_series_tf = get_single_series(pred_series_tf)
train_series_tf = get_single_series(train_series_tf)
val_series_tf = get_single_series(val_series_tf)
past_covariates_tf = get_single_series(past_covariates_tf)
future_covariates_tf = get_single_series(future_covariates_tf)
if isinstance(sample_weight_tf, str):
sample_weight_tf = sample_weight_tf
else:
sample_weight_tf = get_single_series(sample_weight_tf)
if apply_retrain:
# fit a new instance of the model
model = model.untrained_model()
model._fit_wrapper(
series=train_series_tf,
past_covariates=past_covariates_tf,
future_covariates=future_covariates_tf,
sample_weight=sample_weight_tf,
val_series=val_series_tf,
**fit_kwargs,
)
# forecast:
# - local hfc returns a single TimeSeries
# - global hfc returns a list of TimeSeries
forecast_tf = model._predict_wrapper(
n=forecast_horizon,
series=pred_series_tf,
past_covariates=past_covariates_tf,
future_covariates=future_covariates_tf,
num_samples=num_samples,
predict_likelihood_parameters=predict_likelihood_parameters,
show_warnings=show_predict_warnings,
random_state=random_state,
**predict_kwargs,
)
if using_prefitted_transformers and not apply_globally:
series_idx = idx
else:
series_idx = None
forecast = _apply_inverse_data_transformers(
series=pred_series_tf,
forecasts=forecast_tf,
data_transformers=data_transformers,
series_idx=series_idx,
)
show_predict_warnings = False
if not apply_globally:
forecasts_list[idx].append(forecast)
else:
for s_idx, fc in enumerate(forecast):
forecasts_list[s_idx].append(fc)
forecasts = []
if last_points_only:
# extract only the last points from the forecasts
for fc_series in forecasts_list:
fc_series_0 = get_single_series(fc_series)
time_index = generate_index(
start=fc_series_0.end_time(),
length=len(fc_series),
freq=fc_series_0.freq * stride,
name=fc_series_0._time_index.name,
)
values = np.concatenate(
[fc.all_values(copy=False)[-1:] for fc in fc_series], axis=0
)
forecasts.append(
fc_series_0.with_times_and_values(times=time_index, values=values)
)
else:
forecasts = forecasts_list
return series2seq(forecasts, seq_type_out=sequence_type_in)
def backtest(
self,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
historical_forecasts: TimeSeries
| Sequence[TimeSeries]
| Sequence[Sequence[TimeSeries]]
| None = None,
forecast_horizon: int = 1,
num_samples: int = 1,
train_length: int | None = None,
val_length: int = 0,
start: pd.Timestamp | float | int | Literal["end"] | None = None,
start_format: Literal["position", "value"] = "value",
stride: int = 1,
retrain: bool | int | Callable[..., bool] = True,
apply_globally: bool = False,
overlap_end: bool = False,
last_points_only: bool = False,
metric: METRIC_TYPE | list[METRIC_TYPE] = metrics.mape,
reduction: Callable[..., float] | None = np.nanmean,
verbose: bool = False,
show_warnings: bool = True,
predict_likelihood_parameters: bool = False,
enable_optimization: bool = True,
data_transformers: dict[str, BaseDataTransformer | Pipeline] | None = None,
metric_kwargs: dict[str, Any] | list[dict[str, Any]] | None = None,
fit_kwargs: dict[str, Any] | None = None,
predict_kwargs: dict[str, Any] | None = None,
sample_weight: TimeSeriesLike | str | None = None,
random_state: int | None = None,
) -> float | np.ndarray | list[float] | list[np.ndarray]:
r"""Compute error values that the model produced for historical forecasts on (potentially multiple) `series`.
If `historical_forecasts` are provided, the metric(s) (given by the `metric` function) is evaluated directly on
all forecasts and actual values. The same `series` and `last_points_only` value must be passed that were used
to generate the historical forecasts. Finally, the method returns an optional `reduction` (the mean by default)
of all these metric scores.
If `historical_forecasts` is ``None``, it first generates the historical forecasts with the parameters given
below (see :meth:`ForecastingModel.historical_forecasts()
<darts.models.forecasting.forecasting_model.ForecastingModel.historical_forecasts>` for more info) and then
evaluates as described above.
The metric(s) can be further customized `metric_kwargs` (e.g. control the aggregation over components, time
steps, multiple series, other required arguments such as `q` for quantile metrics, ...).
Parameters
----------
series
A (sequence of) target time series used to successively train (if `retrain` is not ``False``) and compute
the historical forecasts.
past_covariates
Optionally, a (sequence of) past-observed covariate time series for every input time series in `series`.
This applies only if the model supports past covariates.
future_covariates
Optionally, a (sequence of) future-known covariate time series for every input time series in `series`.
This applies only if the model supports future covariates.
historical_forecasts
Optionally, the (or a sequence of / a sequence of sequences of) historical forecasts time series to be
evaluated. Corresponds to the output of :meth:`historical_forecasts()
<darts.models.forecasting.forecasting_model.ForecastingModel.historical_forecasts>`. The same `series` and
`last_points_only` values must be passed that were used to generate the historical forecasts. If provided,
will skip historical forecasting and ignore all parameters except `series`, `last_points_only`, `metric`,
and `reduction`.
forecast_horizon
The forecast horizon for the predictions.
num_samples
Number of times a prediction is sampled from a probabilistic model. Use values ``>1`` only for probabilistic
models.
train_length
Optionally, use a fixed length / number of time steps for every constructed training set (rolling window
mode). Only effective when `retrain` is not ``False``. The default is ``None``, where it uses all time
steps up until the prediction time (expanding window mode). If larger than the number of available time
steps, uses the expanding mode. Needs to be at least `min_train_series_length`.
val_length
Use a fixed length / number of time steps after every constructed training set as validation set. This set
will be passed as `val_series` and `val_*_covariates` to the fit method of the underlying forecasting model
(if it supports it). Otherwise, the time frame will be included in the training set. Only effective when
`retrain` is not ``False``. If given, cannot pass any `val_*` series in ``fit_kwargs``. The default is
``0``, which does not construct a validation set. Needs to be at least the length of one model output
(``output_chunk_length + output_chunk_shift`` for global models and ``0`` for local models).
start
Optionally, the first point in time at which a prediction is computed. This parameter supports:
``float``, ``int``, ``pandas.Timestamp``, ``'end'``, and ``None``.
If a ``float``, it is the proportion of the time series that should lie before the first prediction point.
If an ``int``, it is either the index position of the first prediction point for `series` with a
`pd.DatetimeIndex`, or the index value for `series` with a `pd.RangeIndex`. The latter can be changed to
the index position with `start_format="position"`.
If a ``pandas.Timestamp``, it is the time stamp of the first prediction point.
If the string ``'end'``, generates a single forecast per series starting one step after the end of each
series (future-only forecasting). Forecasts will extend beyond the series end regardless ``overlap_end``.
If ``None``, the first prediction point will automatically be set to:
- the first predictable point if `retrain` is ``False``, or `retrain` is a Callable and the first
predictable point is earlier than the first trainable point.
- the first trainable point if `retrain` is ``True`` or ``int`` (given `train_length`),
or `retrain` is a ``Callable`` and the first trainable point is earlier than the first predictable point.
- the first trainable point (given `train_length`) otherwise
There are additional rules for some edge cases:
- If `start` is before the trainable / forecastable points, uses the closest valid start point that is a
round multiple of `stride` ahead of `start`. Raises a `ValueError`, if no valid start point exists.
- If `start` is after the trainable / forecastable points, will ignore the parameter (default behavior with
``None``) and start at the first trainable / predictable point.
- If the model uses a shifted output (`output_chunk_shift > 0`), then the first predicted point is
also shifted by `output_chunk_shift` points into the future.
start_format
Defines the `start` format. Ignored when ``start='end'``.
If set to ``'position'``, `start` corresponds to the index position of the first predicted point and can
range from `(-len(series), len(series) - 1)`.
If set to ``'value'``, `start` corresponds to the index value/label of the first predicted point. Will raise
an error if the value is not in `series`' index. Default: ``'value'``.
stride
The number of time steps between two consecutive predictions.
retrain
Whether and/or on which condition to retrain the model before predicting.
This parameter supports 3 different types: ``bool``, (positive) ``int``, and ``Callable`` (returning a
``bool``).
In the case of ``bool``: retrain the model at each step (`True`), or never retrain the model (`False`).
In the case of ``int``: the model is retrained every `retrain` iterations.
In the case of ``Callable``: the model is retrained whenever callable returns `True`.
The callable must have the following positional arguments:
- `counter` (int): current `retrain` iteration
- `pred_time` (pd.Timestamp or int): timestamp of forecast time (end of the training series)
- `train_series` (TimeSeries): train series up to `pred_time`
- `past_covariates` (TimeSeries): past_covariates series up to `pred_time`
- `future_covariates` (TimeSeries): future_covariates series up to `min(pred_time + series.freq *
forecast_horizon, series.end_time())`
Note: if any optional `*_covariates` are not passed to `historical_forecast`, ``None`` will be passed
to the corresponding retrain function argument.
Note: some models require being retrained every time and do not support anything other than
`retrain=True`.
Note: also controls the retraining of the `data_transformers`.
apply_globally
Whether to apply historical forecasts globally on the time intersection all series, or independently on
each series. This includes global model- and data transformer fitting. Only really effective for global
forecasting models, but can also be used with local models to generate forecasts on the same time frame. If
`True`, considers only the time intersection of all series for historical forecasting. If `False`,
considers the entire extent of each individual series for historical forecasting.
overlap_end
Whether the returned forecasts can go beyond the series' end or not.
last_points_only
Whether to return only the last point of each historical forecast. If set to ``True``, the method returns a
single ``TimeSeries`` (for each time series in `series`) containing the successive point forecasts.
Otherwise, returns a list of historical ``TimeSeries`` forecasts.
metric
A metric function or a list of metric functions. Each metric must either be a Darts metric (see `here
<https://unit8co.github.io/darts/generated_api/darts.metrics.html>`__), or a custom metric that has an
identical signature as Darts' metrics, uses decorators :func:`~darts.metrics.metrics.multi_ts_support` and
:func:`~darts.metrics.metrics.multi_ts_support`, and returns the metric score.
reduction
A function used to combine the individual error scores obtained when `last_points_only` is set to `False`.
When providing several metric functions, the function will receive the argument `axis = 1` to obtain single
value for each metric function.
If explicitly set to `None`, the method will return a list of the individual error scores instead.
Set to ``np.mean`` by default.
verbose
Whether to print the progress.
show_warnings
Whether to show warnings related to historical forecasts optimization, or parameters `start` and
`train_length`.
predict_likelihood_parameters
If set to `True`, the model predicts the parameters of its `likelihood` instead of the target. Only
supported for probabilistic models with a likelihood, `num_samples = 1` and `n<=output_chunk_length`.
Default: ``False``.
enable_optimization
Whether to use the optimized version of `historical_forecasts` when supported and available.
Default: ``True``.
data_transformers
Optionally, a dictionary of `BaseDataTransformer` or `Pipeline` to apply to the corresponding series
(possibles keys; "series", "past_covariates", "future_covariates"). If provided, all input series must be
in the un-transformed space. For fittable transformer / pipeline:
- if `retrain=True`, the data transformer re-fit on the training data at each historical forecast step.
- if `retrain=False`, the data transformer transforms the series once before all the forecasts.
The fitted transformer is used to transform the input during both training and prediction.
If the transformation is invertible, the forecasts will be inverse-transformed.
Only effective when `historical_forecasts=None`.
metric_kwargs
Additional arguments passed to `metric()`, such as `'n_jobs'` for parallelization, `'component_reduction'`
for reducing the component wise metrics, seasonality `'m'` for scaled metrics, etc. Will pass arguments to
each metric separately and only if they are present in the corresponding metric signature. Parameter
`'insample'` for scaled metrics (e.g. mase`, `rmsse`, ...) is ignored, as it is handled internally.
fit_kwargs
Optionally, some additional arguments passed to the model `fit()` method.
predict_kwargs
Optionally, some additional arguments passed to the model `predict()` method.
sample_weight
Optionally, some sample weights to apply to the target `series` labels for training. Only effective when
`retrain` is not ``False``. They are applied per observation, per label (each step in
`output_chunk_length`), and per component.
If a series or sequence of series, then those weights are used. If the weight series only have a single
component / column, then the weights are applied globally to all components in `series`. Otherwise, for
component-specific weights, the number of components must match those of `series`.
If a string, then the weights are generated using built-in weighting functions. The available options are
`"linear"` or `"exponential"` decay - the further in the past, the lower the weight. The weights are
computed per time `series`.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
float
A single backtest score for single uni/multivariate series, a single `metric` function and:
- `historical_forecasts` generated with `last_points_only=True`
- `historical_forecasts` generated with `last_points_only=False` and using a backtest `reduction`
np.ndarray
An numpy array of backtest scores. For single series and one of:
- a single `metric` function, `historical_forecasts` generated with `last_points_only=False`
and backtest `reduction=None`. The output has shape (n forecasts, \*).
- multiple `metric` functions and `historical_forecasts` generated with `last_points_only=False`.
The output has shape (\*, n metrics) when using a backtest `reduction`, and (n forecasts, \*, n metrics)
when `reduction=None`
- multiple uni/multivariate series including `series_reduction` and at least one of
`component_reduction=None` or `time_reduction=None` for "per time step metrics"
List[float]
Same as for type `float` but for a sequence of series. The returned metric list has length
`len(series)` with the `float` metric for each input `series`.
List[np.ndarray]
Same as for type `np.ndarray` but for a sequence of series. The returned metric list has length
`len(series)` with the `np.ndarray` metrics for each input `series`.
"""
metric_kwargs = metric_kwargs or dict()
if not isinstance(metric_kwargs, list):
metric_kwargs = [metric_kwargs]
if not isinstance(metric, list):
metric = [metric]
if len(metric_kwargs) > 1 and len(metric_kwargs) != len(metric):
raise_log(
ValueError(
f"Mismatch between number of metric-specific `metric_kwargs` "
f"({len(metric_kwargs)}) and number of metrics in `metric` ({len(metric)}). "
f"For `metric_kwargs`, either give a list of dicts of length `{len(metric)}` "
f"with metric-specific kwargs, or a single dict that is applied to all metrics."
),
)
if len(metric_kwargs) != len(metric):
metric_kwargs = [metric_kwargs[0] for _ in range(len(metric))]
hfc_precomputed = historical_forecasts is not None
historical_forecasts = historical_forecasts or self.historical_forecasts(
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
num_samples=num_samples,
train_length=train_length,
start=start,
start_format=start_format,
forecast_horizon=forecast_horizon,
stride=stride,
retrain=retrain,
apply_globally=apply_globally,
overlap_end=overlap_end,
last_points_only=last_points_only,
verbose=verbose,
show_warnings=show_warnings,
predict_likelihood_parameters=predict_likelihood_parameters,
enable_optimization=enable_optimization,
data_transformers=data_transformers,
fit_kwargs=fit_kwargs,
predict_kwargs=predict_kwargs,
sample_weight=sample_weight,
random_state=random_state,
)
# remember input series type
series_seq_type = get_series_seq_type(series)
# validate historical forecasts and convert to multiple series with multiple forecasts case
series, historical_forecasts = _process_historical_forecast_for_backtest(
series=series,
historical_forecasts=historical_forecasts,
last_points_only=last_points_only,
)
# when pre-computed historical forecasts are supplied, extend each series with NaN
# where any forecast goes beyond the series end, so that per-time-step metrics
# produce arrays of consistent shape across all forecasts
if hfc_precomputed or overlap_end:
series = _extend_series_for_overlap_end(
series=series, historical_forecasts=historical_forecasts
)
# we have multiple forecasts per series: rearrange forecasts to call each metric only once;
# flatten historical forecasts, get matching target series index, remember cumulative target lengths
# for later reshaping back to original
series_idx = []
cum_len = [0]
forecasts_list = []
for idx, fc_list in enumerate(historical_forecasts):
series_idx += [idx] * len(fc_list)
cum_len.append(cum_len[-1] + len(fc_list))
forecasts_list.extend(fc_list)
class SeriesGenerator(Sequence):
"""Yields the target `series` corresponding the historical forecast at index `i`.
Allows lazy loading of target `series` in case it is a Sequence.
"""
def __len__(self):
return len(forecasts_list)
def __getitem__(self, index) -> TimeSeries:
return series[series_idx[index]]
# extract metrics per metric and series, and optionally reduce
# errors shape `(n metrics, n total historical forecasts)`
series_gen = SeriesGenerator()
errors = []
for metric_f, metric_f_kwargs in zip(metric, metric_kwargs):
# add user supplied metric kwargs
kwargs = {k: v for k, v in metric_f_kwargs.items()}
metric_params = inspect.signature(metric_f).parameters
if metric_f in CLASSIFICATION_METRICS:
has_labels_p = _PARAM_LABELS in metric_params
has_label_reduction_p = _PARAM_LABEL_REDUCTION in metric_params
if (
has_labels_p
and kwargs.get(_PARAM_LABELS, None) is None
and (
(
has_label_reduction_p
and kwargs.get(_PARAM_LABEL_REDUCTION, False) is None
)
or metric_f.__name__ == "confusion_matrix"
)
):
logger.warning(
f"Parameter `{_PARAM_LABELS}` is not set for classification metric "
f"`{metric_f.__name__}()` that does not reduce the label-specific scores "
f"(either `{_PARAM_LABEL_REDUCTION}=None` or using the "
f"`confusion_matrix` metric). This may result in inconsistent or failing backtest."
)
# scaled metrics require `insample` series
if "insample" in metric_params:
kwargs["insample"] = series_gen
errors.append(metric_f(series_gen, forecasts_list, **kwargs))
try:
# multiple series can result in different number of forecasts; try if we can run it efficiently
errors = np.array(errors)
is_arr = True
except ValueError:
# otherwise, compute array later
is_arr = False
# get errors for each input `series`
backtest_list = []
for i in range(len(cum_len) - 1):
# errors_series with shape `(n metrics, n series specific historical forecasts, *)`
if is_arr:
errors_series = errors[:, cum_len[i] : cum_len[i + 1]]
else:
errors_series = np.array([
errors_[cum_len[i] : cum_len[i + 1]] for errors_ in errors
])
if reduction is not None:
# shape `(n metrics, n forecasts, *)` -> `(n metrics, *)`
errors_series = reduction(errors_series, axis=1)
elif last_points_only:
# shape `(n metrics, n forecasts = 1, *)` -> `(n metrics, *)`
errors_series = errors_series[:, 0]
if len(metric) == 1:
# shape `(n metrics, *)` -> `(*,)`
errors_series = errors_series[0]
else:
# shape `(n metrics, *)` -> `(*, n metrics)`
errors_series = errors_series.transpose(
tuple(i for i in range(1, errors_series.ndim)) + (0,)
)
backtest_list.append(errors_series)
return (
backtest_list if series_seq_type > SeriesType.SINGLE else backtest_list[0]
)
@classmethod
def gridsearch(
model_class,
parameters: dict,
series: TimeSeries,
past_covariates: TimeSeries | None = None,
future_covariates: TimeSeries | None = None,
forecast_horizon: int | None = None,
stride: int = 1,
start: pd.Timestamp | float | int | Literal["end"] | None = None,
start_format: Literal["position", "value"] = "value",
last_points_only: bool = False,
show_warnings: bool = True,
val_series: TimeSeries | None = None,
use_fitted_values: bool = False,
metric: Callable[[TimeSeries, TimeSeries], METRIC_OUTPUT_TYPE] = metrics.mape,
reduction: Callable[[np.ndarray], float] = np.mean,
verbose=False,
n_jobs: int = 1,
n_random_samples: int | float | None = None,
data_transformers: dict[str, BaseDataTransformer | Pipeline] | None = None,
fit_kwargs: dict[str, Any] | None = None,
predict_kwargs: dict[str, Any] | None = None,
sample_weight: TimeSeries | str | None = None,
random_state: int | None = None,
) -> tuple["ForecastingModel", dict[str, Any], float]:
"""
Find the best hyper-parameters among a given set using a grid search.
This function has 3 modes of operation: Expanding window mode, split mode and fitted value mode.
The three modes of operation evaluate every possible combination of hyper-parameter values
provided in the `parameters` dictionary by instantiating the `model_class` subclass
of ForecastingModel with each combination, and returning the best-performing model with regard
to the `metric` function. The `metric` function is expected to return an error value,
thus the model resulting in the smallest `metric` output will be chosen.
The relationship of the training data and test data depends on the mode of operation.
Expanding window mode (activated when `forecast_horizon` is passed):
For every hyperparameter combination, the model is repeatedly trained and evaluated on different
splits of `series`. This process is accomplished by using
the :func:`backtest` function as a subroutine to produce historic forecasts starting from `start`
that are compared against the ground truth values of `series`.
Note that the model is retrained for every single prediction, thus this mode is slower.
Split window mode (activated when `val_series` is passed):
This mode will be used when the `val_series` argument is passed.
For every hyper-parameter combination, the model is trained on `series` and
evaluated on `val_series`.
Fitted value mode (activated when `use_fitted_values` is set to `True`):
For every hyper-parameter combination, the model is trained on `series`
and evaluated on the resulting fitted values.
Not all models have fitted values, and this method raises an error if the model doesn't have a `fitted_values`
member. The fitted values are the result of the fit of the model on `series`. Comparing with the
fitted values can be a quick way to assess the model, but one cannot see if the model is overfitting the series.
Derived classes must ensure that a single instance of a model will not share parameters with the other
instances, e.g., saving models in the same path. Otherwise, an unexpected behavior can arise while running
several models in parallel (when ``n_jobs != 1``). If this cannot be avoided, then gridsearch
should be redefined, forcing ``n_jobs = 1``.
Currently this method only supports deterministic predictions (i.e. when models' predictions
have only 1 sample).
Parameters
----------
model_class
The ForecastingModel subclass to be tuned for 'series'.
parameters
A dictionary containing as keys hyperparameter names, and as values lists of values for the
respective hyperparameter.
series
The target series used as input and target for training.
past_covariates
Optionally, a past-observed covariate series. This applies only if the model supports past covariates.
future_covariates
Optionally, a future-known covariate series. This applies only if the model supports future covariates.
forecast_horizon
The integer value of the forecasting horizon. Activates expanding window mode.
stride
Only used in expanding window mode. The number of time steps between two consecutive predictions.
start
Only used in expanding window mode. Optionally, the first point in time at which a prediction is computed.
This parameter supports: ``float``, ``int``, ``pandas.Timestamp``, and ``None``.
If a ``float``, it is the proportion of the time series that should lie before the first prediction point.
If an ``int``, it is either the index position of the first prediction point for `series` with a
`pd.DatetimeIndex`, or the index value for `series` with a `pd.RangeIndex`. The latter can be changed to
the index position with `start_format="position"`.
If a ``pandas.Timestamp``, it is the time stamp of the first prediction point.
If ``None``, the first prediction point will automatically be set to:
- the first predictable point if `retrain` is ``False``, or `retrain` is a Callable and the first
predictable point is earlier than the first trainable point.
- the first trainable point if `retrain` is ``True`` or ``int`` (given `train_length`),
or `retrain` is a Callable and the first trainable point is earlier than the first predictable point.
- the first trainable point (given `train_length`) otherwise
Note: If `start` is not within the trainable / forecastable points, uses the closest valid start point that
is a round multiple of `stride` ahead of `start`. Raises a `ValueError`, if no valid start point exists.
Note: If the model uses a shifted output (`output_chunk_shift > 0`), then the first predicted point is also
shifted by `output_chunk_shift` points into the future.
Note: If `start` is outside the possible historical forecasting times, will ignore the parameter
(default behavior with ``None``) and start at the first trainable/predictable point.
start_format
Only used in expanding window mode. Defines the `start` format. Only effective when `start` is an integer
and `series` is indexed with a `pd.RangeIndex`.
If set to 'position', `start` corresponds to the index position of the first predicted point and can range
from `(-len(series), len(series) - 1)`.
If set to 'value', `start` corresponds to the index value/label of the first predicted point. Will raise
an error if the value is not in `series`' index. Default: ``'value'``
last_points_only
Only used in expanding window mode. Whether to use the whole forecasts or only the last point of each
forecast to compute the error.
show_warnings
Only used in expanding window mode. Whether to show warnings related to the `start` parameter.
val_series
The TimeSeries instance used for validation in split mode. If provided, this series must start right after
the end of `series`; so that a proper comparison of the forecast can be made.
use_fitted_values
If `True`, uses the comparison with the fitted values.
Raises an error if ``fitted_values`` is not an attribute of `model_class`.
metric
A metric function that returns the error between two `TimeSeries` as a float value . Must either be one of
Darts' "aggregated over time" metrics (see `here
<https://unit8co.github.io/darts/generated_api/darts.metrics.html>`__), or a custom metric that as input two
`TimeSeries` and returns the error
reduction
A reduction function (mapping array to float) describing how to aggregate the errors obtained
on the different validation series when backtesting. By default it'll compute the mean of errors.
verbose
Whether to print the progress.
n_jobs
The number of jobs to run in parallel. Parallel jobs are created only when there are two or more parameters
combinations to evaluate. Each job will instantiate, train, and evaluate a different instance of the model.
Defaults to `1` (sequential). Setting the parameter to `-1` means using all the available cores.
n_random_samples
The number/ratio of hyperparameter combinations to select from the full parameter grid. This will perform
a random search instead of using the full grid.
If an integer, `n_random_samples` is the number of parameter combinations selected from the full grid and
must be between `0` and the total number of parameter combinations.
If a float, `n_random_samples` is the ratio of parameter combinations selected from the full grid and must
be between `0` and `1`. Defaults to `None`, for which random selection will be ignored.
data_transformers
Optionally, a dictionary of `BaseDataTransformer` or `Pipeline` to apply to the corresponding series
(possibles keys; "series", "past_covariates", "future_covariates"). If provided, all input series must be
in the un-transformed space. For fittable transformer / pipeline:
- if `retrain=True`, the data transformer re-fit on the training data at each historical forecast step.
- if `retrain=False`, the data transformer transforms the series once before all the forecasts.
The fitted transformer is used to transform the input during both training and prediction.
If the transformation is invertible, the forecasts will be inverse-transformed.
fit_kwargs
Additional arguments passed to the model `fit()` method.
predict_kwargs
Additional arguments passed to the model `predict()` method.
sample_weight
Optionally, some sample weights to apply to the target `series` labels for training. Only effective when
`retrain` is not ``False``. They are applied per observation, per label (each step in
`output_chunk_length`), and per component.
If a series, then those weights are used. If the weight series only have a single component / column, then
the weights are applied globally to all components in `series`. Otherwise, for component-specific weights,
the number of components must match those of `series`.
If a string, then the weights are generated using built-in weighting functions. The available options are
`"linear"` or `"exponential"` decay - the further in the past, the lower the weight.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
ForecastingModel, Dict, float
A tuple containing an untrained `model_class` instance created from the best-performing hyper-parameters,
along with a dictionary containing these best hyper-parameters,
and metric score for the best hyper-parameters.
"""
if (forecast_horizon is not None) + (
val_series is not None
) + use_fitted_values != 1:
raise_log(
ValueError(
"Please pass exactly one of the arguments 'forecast_horizon', "
"'val_series' or 'use_fitted_values'."
),
)
if not isinstance(parameters, dict):
raise_log(
ValueError(
f"`parameters` should be a dictionary, received a: {type(parameters)}."
)
)
if not all(
isinstance(params, list | np.ndarray) for params in parameters.values()
):
raise_log(
ValueError(
"Every value in the `parameters` dictionary should be a list or a np.ndarray."
),
)
if use_fitted_values:
if not hasattr(
model_class(**{k: v[0] for k, v in parameters.items()}),
"fitted_values",
):
raise_log(
ValueError(
"The model must have a fitted_values attribute to compare with the train "
"TimeSeries (local models)."
),
)
elif val_series is not None:
if series.width != val_series.width:
raise_log(
ValueError(
"Training and validation series require the same number of components."
),
)
data_transformers = _convert_data_transformers(
data_transformers=data_transformers, copy=True
)
if fit_kwargs is None:
fit_kwargs = dict()
if predict_kwargs is None:
predict_kwargs = dict()
# compute all hyperparameter combinations from selection
params_cross_product = list(product(*parameters.values()))
# If n_random_samples has been set, randomly select a subset of the full parameter cross product to search with
if n_random_samples is not None:
params_cross_product = model_class._sample_params(
params_cross_product, n_random_samples
)
# iterate through all combinations of the provided parameters and choose the best one
iterator = _build_tqdm_iterator(
zip(params_cross_product),
verbose,
total=len(params_cross_product),
desc="gridsearch",
)
def _evaluate_combination(param_combination) -> float:
param_combination_dict = dict(
list(zip(parameters.keys(), param_combination))
)
if param_combination_dict.get("model_name", None):
current_time = time.strftime("%Y-%m-%d_%H.%M.%S.%f", time.localtime())
param_combination_dict["model_name"] = (
f"{current_time}_{param_combination_dict['model_name']}"
)
model = model_class(**param_combination_dict)
if use_fitted_values: # fitted value mode
if data_transformers:
series_, _, past_covariates_, future_covariates_ = (
_apply_data_transformers(
series=series,
pred_series=None,
past_covariates=past_covariates,
future_covariates=future_covariates,
data_transformers=data_transformers,
max_future_cov_lag=model.extreme_lags[5],
fit_transformers=True,
)
)
else:
series_ = series
past_covariates_ = past_covariates
future_covariates_ = future_covariates
model._fit_wrapper(
series=series_,
past_covariates=past_covariates_,
future_covariates=future_covariates_,
sample_weight=sample_weight,
**fit_kwargs,
)
fitted_values = TimeSeries(
times=series.time_index,
values=model.fitted_values,
copy=False,
)
if data_transformers and "series" in data_transformers:
fitted_values = _apply_inverse_data_transformers(
series=series_,
forecasts=fitted_values,
data_transformers=data_transformers,
series_idx=None,
pass_insample=False,
)
error = metric(series, fitted_values)
elif val_series is None: # expanding window mode
error = model.backtest(
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
num_samples=1,
start=start,
start_format=start_format,
forecast_horizon=forecast_horizon,
stride=stride,
metric=metric,
reduction=reduction,
last_points_only=last_points_only,
verbose=verbose,
show_warnings=show_warnings,
data_transformers=data_transformers,
fit_kwargs=fit_kwargs,
predict_kwargs=predict_kwargs,
sample_weight=sample_weight,
random_state=random_state,
)
else: # split mode
if data_transformers:
series_, _, past_covariates_, future_covariates_ = (
_apply_data_transformers(
series=series,
pred_series=None,
past_covariates=past_covariates,
future_covariates=future_covariates,
data_transformers=data_transformers,
max_future_cov_lag=model.extreme_lags[5],
fit_transformers=True,
)
)
else:
series_ = series
past_covariates_ = past_covariates
future_covariates_ = future_covariates
model._fit_wrapper(
series=series_,
past_covariates=past_covariates_,
future_covariates=future_covariates_,
sample_weight=sample_weight,
**fit_kwargs,
)
pred = model._predict_wrapper(
n=len(val_series),
series=series_,
past_covariates=past_covariates_,
future_covariates=future_covariates_,
num_samples=1,
verbose=verbose,
random_state=random_state,
**predict_kwargs,
)
pred = _apply_inverse_data_transformers(
series=series_,
forecasts=pred,
data_transformers=data_transformers,
)
error = metric(val_series, pred)
return float(error)
errors: list[float] = _parallel_apply(
iterator, _evaluate_combination, n_jobs, {}, {}
)
min_error = min(errors)
best_param_combination = dict(
list(zip(parameters.keys(), params_cross_product[errors.index(min_error)]))
)
logger.info("Chosen parameters: " + str(best_param_combination))
return model_class(**best_param_combination), best_param_combination, min_error
def residuals(
self,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
historical_forecasts: TimeSeries
| Sequence[TimeSeries]
| Sequence[Sequence[TimeSeries]]
| None = None,
forecast_horizon: int = 1,
num_samples: int = 1,
train_length: int | None = None,
val_length: int = 0,
start: pd.Timestamp | float | int | Literal["end"] | None = None,
start_format: Literal["position", "value"] = "value",
stride: int = 1,
retrain: bool | int | Callable[..., bool] = True,
apply_globally: bool = False,
overlap_end: bool = False,
last_points_only: bool = True,
metric: METRIC_TYPE = metrics.err,
verbose: bool = False,
show_warnings: bool = True,
predict_likelihood_parameters: bool = False,
enable_optimization: bool = True,
data_transformers: dict[str, BaseDataTransformer | Pipeline] | None = None,
metric_kwargs: dict[str, Any] | None = None,
fit_kwargs: dict[str, Any] | None = None,
predict_kwargs: dict[str, Any] | None = None,
sample_weight: TimeSeriesLike | str | None = None,
values_only: bool = False,
random_state: int | None = None,
) -> TimeSeries | list[TimeSeries] | list[list[TimeSeries]]:
"""Compute the residuals that the model produced for historical forecasts on (potentially multiple) `series`.
This function computes the difference (or one of Darts' "per time step" metrics) between the actual
observations from `series` and the fitted values obtained by training the model on `series` (or using a
pre-trained model with `retrain=False`). Not all models support fitted values, so we use historical forecasts
as an approximation for them.
In sequence this method performs:
- use pre-computed `historical_forecasts` or compute historical forecasts for each series (see
:meth:`~darts.models.forecasting.forecasting_model.ForecastingModel.historical_forecasts` for more details).
How the historical forecasts are generated can be configured with parameters `num_samples`, `train_length`,
`start`, `start_format`, `forecast_horizon`, `stride`, `retrain`, `last_points_only`, `fit_kwargs`, and
`predict_kwargs`.
- compute a backtest using a "per time step" `metric` between the historical forecasts and `series` per
component/column and time step (see
:meth:`~darts.models.forecasting.forecasting_model.ForecastingModel.backtest` for more details). By default,
uses the residuals :func:`~darts.metrics.metrics.err` (error) as a `metric`.
- create and return `TimeSeries` (or simply a np.ndarray with `values_only=True`) with the time index from
historical forecasts, and values from the metrics per component and time step.
This method works for single or multiple univariate or multivariate series.
It uses the median prediction (when dealing with stochastic forecasts).
Parameters
----------
series
A (sequence of) target time series used to successively train (if `retrain` is not ``False``) and compute
the historical forecasts.
past_covariates
Optionally, a (sequence of) past-observed covariate time series for every input time series in `series`.
This applies only if the model supports past covariates.
future_covariates
Optionally, a (sequence of) future-known covariate time series for every input time series in `series`.
This applies only if the model supports future covariates.
historical_forecasts
Optionally, the (or a sequence of / a sequence of sequences of) historical forecasts time series to be
evaluated. Corresponds to the output of :meth:`historical_forecasts()
<darts.models.forecasting.forecasting_model.ForecastingModel.historical_forecasts>`. The same `series` and
`last_points_only` values must be passed that were used to generate the historical forecasts. If provided,
will skip historical forecasting and ignore all parameters except `series`, `last_points_only`, `metric`,
and `reduction`.
forecast_horizon
The forecast horizon for the predictions.
num_samples
Number of times a prediction is sampled from a probabilistic model. Use values ``>1`` only for probabilistic
models.
train_length
Optionally, use a fixed length / number of time steps for every constructed training set (rolling window
mode). Only effective when `retrain` is not ``False``. The default is ``None``, where it uses all time
steps up until the prediction time (expanding window mode). If larger than the number of available time
steps, uses the expanding mode. Needs to be at least `min_train_series_length`.
val_length
Use a fixed length / number of time steps after every constructed training set as validation set. This set
will be passed as `val_series` and `val_*_covariates` to the fit method of the underlying forecasting model
(if it supports it). Otherwise, the time frame will be included in the training set. Only effective when
`retrain` is not ``False``. If given, cannot pass any `val_*` series in ``fit_kwargs``. The default is
``0``, which does not construct a validation set. Needs to be at least the length of one model output
(``output_chunk_length + output_chunk_shift`` for global models and ``0`` for local models).
start
Optionally, the first point in time at which a prediction is computed. This parameter supports:
``float``, ``int``, ``pandas.Timestamp``, ``'end'``, and ``None``.
If a ``float``, it is the proportion of the time series that should lie before the first prediction point.
If an ``int``, it is either the index position of the first prediction point for `series` with a
`pd.DatetimeIndex`, or the index value for `series` with a `pd.RangeIndex`. The latter can be changed to
the index position with `start_format="position"`.
If a ``pandas.Timestamp``, it is the time stamp of the first prediction point.
If the string ``'end'``, generates a single forecast per series starting one step after the end of each
series (future-only forecasting). Forecasts will extend beyond the series end regardless ``overlap_end``.
If ``None``, the first prediction point will automatically be set to:
- the first predictable point if `retrain` is ``False``, or `retrain` is a Callable and the first
predictable point is earlier than the first trainable point.
- the first trainable point if `retrain` is ``True`` or ``int`` (given `train_length`),
or `retrain` is a ``Callable`` and the first trainable point is earlier than the first predictable point.
- the first trainable point (given `train_length`) otherwise
Note: If `start` is not within the trainable / forecastable points, uses the closest valid start point that
is a round multiple of `stride` ahead of `start`. Raises a `ValueError`, if no valid start point exists.
Note: If the model uses a shifted output (`output_chunk_shift > 0`), then the first predicted point is also
shifted by `output_chunk_shift` points into the future.
Note: If `start` is outside the possible historical forecasting times, will ignore the parameter
(default behavior with ``None``) and start at the first trainable/predictable point.
start_format
Defines the `start` format.
If set to ``'position'``, `start` corresponds to the index position of the first predicted point and can
range from `(-len(series), len(series) - 1)`.
If set to ``'value'``, `start` corresponds to the index value/label of the first predicted point. Will raise
an error if the value is not in `series`' index. Default: ``'value'``.
stride
The number of time steps between two consecutive predictions.
retrain
Whether and/or on which condition to retrain the model before predicting.
This parameter supports 3 different types: ``bool``, (positive) ``int``, and ``Callable`` (returning a
``bool``).
In the case of ``bool``: retrain the model at each step (`True`), or never retrain the model (`False`).
In the case of ``int``: the model is retrained every `retrain` iterations.
In the case of ``Callable``: the model is retrained whenever callable returns `True`.
The callable must have the following positional arguments:
- `counter` (int): current `retrain` iteration
- `pred_time` (pd.Timestamp or int): timestamp of forecast time (end of the training series)
- `train_series` (TimeSeries): train series up to `pred_time`
- `past_covariates` (TimeSeries): past_covariates series up to `pred_time`
- `future_covariates` (TimeSeries): future_covariates series up to `min(pred_time + series.freq *
forecast_horizon, series.end_time())`
Note: if any optional `*_covariates` are not passed to `historical_forecast`, ``None`` will be passed
to the corresponding retrain function argument.
Note: some models require being retrained every time and do not support anything other than
`retrain=True`.
Note: also controls the retraining of the `data_transformers`.
apply_globally
Whether to apply historical forecasts globally on the time intersection all series, or independently on
each series. This includes global model- and data transformer fitting. Only really effective for global
forecasting models, but can also be used with local models to generate forecasts on the same time frame. If
`True`, considers only the time intersection of all series for historical forecasting. If `False`,
considers the entire extent of each individual series for historical forecasting.
overlap_end
Whether the returned forecasts can go beyond the series' end or not.
last_points_only
Whether to return only the last point of each historical forecast. If set to ``True``, the method returns a
single ``TimeSeries`` (for each time series in `series`) containing the successive point forecasts.
Otherwise, returns a list of historical ``TimeSeries`` forecasts.
metric
Either one of Darts' "per time step" metrics (see `here
<https://unit8co.github.io/darts/generated_api/darts.metrics.html>`__), or a custom metric that has an
identical signature as Darts' "per time step" metrics, uses decorators
:func:`~darts.metrics.metrics.multi_ts_support` and :func:`~darts.metrics.metrics.multi_ts_support`,
and returns one value per time step.
verbose
Whether to print the progress.
show_warnings
Whether to show warnings related to historical forecasts optimization, or parameters `start` and
`train_length`.
predict_likelihood_parameters
If set to `True`, the model predicts the parameters of its `likelihood` instead of the target. Only
supported for probabilistic models with a likelihood, `num_samples = 1` and `n<=output_chunk_length`.
Default: ``False``.
enable_optimization
Whether to use the optimized version of `historical_forecasts` when supported and available.
Default: ``True``.
data_transformers
Optionally, a dictionary of `BaseDataTransformer` or `Pipeline` to apply to the corresponding series
(possibles keys; "series", "past_covariates", "future_covariates"). If provided, all input series must be
in the un-transformed space. For fittable transformer / pipeline:
- if `retrain=True`, the data transformer re-fit on the training data at each historical forecast step.
- if `retrain=False`, the data transformer transforms the series once before all the forecasts.
The fitted transformer is used to transform the input during both training and prediction.
If the transformation is invertible, the forecasts will be inverse-transformed.
Only effective when `historical_forecasts=None`.
metric_kwargs
Additional arguments passed to `metric()`, such as `'n_jobs'` for parallelization, `'m'` for scaled
metrics, etc. Will pass arguments only if they are present in the corresponding metric signature. Ignores
reduction arguments `"series_reduction", "component_reduction", "time_reduction"`, and parameter
`'insample'` for scaled metrics (e.g. mase`, `rmsse`, ...), as they are handled internally.
fit_kwargs
Optionally, some additional arguments passed to the model `fit()` method.
predict_kwargs
Optionally, some additional arguments passed to the model `predict()` method.
sample_weight
Optionally, some sample weights to apply to the target `series` labels for training. Only effective when
`retrain` is not ``False``. They are applied per observation, per label (each step in
`output_chunk_length`), and per component.
If a series or sequence of series, then those weights are used. If the weight series only have a single
component / column, then the weights are applied globally to all components in `series`. Otherwise, for
component-specific weights, the number of components must match those of `series`.
If a string, then the weights are generated using built-in weighting functions. The available options are
`"linear"` or `"exponential"` decay - the further in the past, the lower the weight. The weights are
computed per time `series`.
values_only
Whether to return the residuals as `np.ndarray`. If `False`, returns residuals as `TimeSeries`.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
TimeSeries
Residual `TimeSeries` for a single `series` and `historical_forecasts` generated with
`last_points_only=True`.
List[TimeSeries]
A list of residual `TimeSeries` for a sequence (list) of `series` with `last_points_only=True`.
The residual list has length `len(series)`.
List[List[TimeSeries]]
A list of lists of residual `TimeSeries` for a sequence of `series` with `last_points_only=False`.
The outer residual list has length `len(series)`. The inner lists consist of the residuals from
all possible series-specific historical forecasts.
"""
# `residuals()` should return metrics per series, component and time step (no reduction)
metric_kwargs = copy.deepcopy(metric_kwargs) or {}
metric_kwargs["series_reduction"] = None
metric_kwargs["component_reduction"] = None
metric_kwargs["time_reduction"] = None
historical_forecasts = historical_forecasts or self.historical_forecasts(
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
num_samples=num_samples,
train_length=train_length,
start=start,
start_format=start_format,
forecast_horizon=forecast_horizon,
stride=stride,
retrain=retrain,
apply_globally=apply_globally,
last_points_only=last_points_only,
verbose=verbose,
show_warnings=show_warnings,
predict_likelihood_parameters=predict_likelihood_parameters,
enable_optimization=enable_optimization,
data_transformers=data_transformers,
fit_kwargs=fit_kwargs,
predict_kwargs=predict_kwargs,
overlap_end=overlap_end,
sample_weight=sample_weight,
random_state=random_state,
)
# remember input series type
series_seq_type = get_series_seq_type(series)
# validate historical forecasts and convert to multiple series with multiple forecasts case
series, historical_forecasts = _process_historical_forecast_for_backtest(
series=series,
historical_forecasts=historical_forecasts,
last_points_only=last_points_only,
)
residuals = self.backtest(
series=series,
historical_forecasts=historical_forecasts,
last_points_only=False,
metric=metric,
reduction=None,
data_transformers=data_transformers,
metric_kwargs=metric_kwargs,
random_state=random_state,
)
# sanity check residual output
q, q_interval = metric_kwargs.get("q"), metric_kwargs.get("q_interval")
try:
series_, res, fc = series[0], residuals[0][0], historical_forecasts[0][0]
_ = np.reshape(res, (len(fc), -1, 1))
except Exception as err:
raise_log(
ValueError(
f"`metric` function did not yield expected output. Make sure "
f"to use one of Darts 'per time step' metrics, or a similar "
f"custom metric. The following exception was raised: "
f"{type(err).__name__}('{err}')"
),
)
# process residuals
residuals_out = []
for series_, fc_list, res_list in zip(series, historical_forecasts, residuals):
res_list_out = []
if q is not None:
q = [q] if isinstance(q, float) else q
# multi-quantile metrics yield more components
comp_names = likelihood_component_names(
components=series_.components,
parameter_names=quantile_names(q),
)
# `q` and `q_interval` are mutually exclusive
elif q_interval is not None:
# multi-quantile metrics yield more components
q_interval = (
[q_interval] if isinstance(q_interval, tuple) else q_interval
)
comp_names = likelihood_component_names(
components=series_.components,
parameter_names=quantile_interval_names(q_interval),
)
else:
comp_names = None
for fc, res in zip(fc_list, res_list):
# make sure all residuals have shape (n time steps, n components * n quantiles, n samples=1)
if len(res.shape) != 3:
res = np.reshape(res, (len(fc), -1, 1))
if values_only:
res = res
elif (q is None and q_interval is None) and res.shape[
1
] == fc.n_components:
res = TimeSeries(
times=fc.time_index,
values=res,
components=fc.components,
copy=False,
**fc._attrs,
)
else:
# quantile (interval) metrics created different number of components;
# create new series with unknown components
res = TimeSeries(
times=fc.time_index,
values=res,
components=comp_names,
copy=False,
)
res_list_out.append(res)
residuals_out.append(res_list_out)
# if required, reduce to `series` input type
if series_seq_type == SeriesType.SINGLE:
return residuals_out[0][0] if last_points_only else residuals_out[0]
return (
[res for res_list in residuals_out for res in res_list]
if last_points_only
else residuals_out
)
def initialize_encoders(self, default=False) -> SequentialEncoder:
"""instantiates the SequentialEncoder object based on self._model_encoder_settings and parameter
``add_encoders`` used at model creation"""
if default:
return SequentialEncoder(add_encoders={})
(
input_chunk_length,
output_chunk_length,
takes_past_covariates,
takes_future_covariates,
lags_past_covariates,
lags_future_covariates,
) = self._model_encoder_settings
return SequentialEncoder(
add_encoders=self.add_encoders,
input_chunk_length=input_chunk_length,
output_chunk_length=output_chunk_length,
lags_past_covariates=lags_past_covariates,
lags_future_covariates=lags_future_covariates,
takes_past_covariates=takes_past_covariates,
takes_future_covariates=takes_future_covariates,
)
def generate_fit_encodings(
self,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
) -> tuple[TimeSeriesLike, TimeSeriesLike]:
"""Generates the covariate encodings that were used/generated for fitting the model and returns a tuple of
past, and future covariates series with the original and encoded covariates stacked together. The encodings are
generated by the encoders defined at model creation with parameter `add_encoders`. Pass the same `series`,
`past_covariates`, and `future_covariates` that you used to train/fit the model.
Parameters
----------
series
The series or sequence of series with the target values used when fitting the model.
past_covariates
Optionally, the series or sequence of series with the past-observed covariates used when fitting the model.
future_covariates
Optionally, the series or sequence of series with the future-known covariates used when fitting the model.
Returns
-------
Tuple[TimeSeriesLike, TimeSeriesLike]
A tuple of (past covariates, future covariates). Each covariate contains the original as well as the
encoded covariates.
"""
if self.encoders is None or not self.encoders.encoding_available:
raise_log(
ValueError(
"Encodings are not available. Consider adding parameter `add_encoders` at model creation "
"and fitting the model with `model.fit()` before."
),
)
return self.encoders.encode_train(
target=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
def generate_predict_encodings(
self,
n: int,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
) -> tuple[TimeSeriesLike, TimeSeriesLike]:
"""Generates covariate encodings for the inference/prediction set and returns a tuple of past, and future
covariates series with the original and encoded covariates stacked together. The encodings are generated by the
encoders defined at model creation with parameter `add_encoders`. Pass the same `series`, `past_covariates`,
and `future_covariates` that you intend to use for prediction.
Parameters
----------
n
The number of prediction time steps after the end of `series` intended to be used for prediction.
series
The series or sequence of series with target values intended to be used for prediction.
past_covariates
Optionally, the past-observed covariates series intended to be used for prediction. The dimensions must
match those of the covariates used for training.
future_covariates
Optionally, the future-known covariates series intended to be used for prediction. The dimensions must
match those of the covariates used for training.
Returns
-------
Tuple[TimeSeriesLike, TimeSeriesLike]
A tuple of (past covariates, future covariates). Each covariate contains the original as well as the
encoded covariates.
"""
if self.encoders is None or not self.encoders.encoding_available:
raise_log(
ValueError(
"Encodings are not available. Consider adding parameter `add_encoders` at model creation "
"and fitting the model with `model.fit()` before."
),
)
return self.encoders.encode_inference(
n=n,
target=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
def generate_fit_predict_encodings(
self,
n: int,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
) -> tuple[TimeSeriesLike, TimeSeriesLike]:
"""Generates covariate encodings for training and inference/prediction and returns a tuple of past, and future
covariates series with the original and encoded covariates stacked together. The encodings are generated by the
encoders defined at model creation with parameter `add_encoders`. Pass the same `series`, `past_covariates`,
and `future_covariates` that you intend to use for training and prediction.
Parameters
----------
n
The number of prediction time steps after the end of `series` intended to be used for prediction.
series
The series or sequence of series with target values intended to be used for training and prediction.
past_covariates
Optionally, the past-observed covariates series intended to be used for training and prediction. The
dimensions must match those of the covariates used for training.
future_covariates
Optionally, the future-known covariates series intended to be used for prediction. The dimensions must
match those of the covariates used for training.
Returns
-------
Tuple[TimeSeriesLike, TimeSeriesLike]
A tuple of (past covariates, future covariates). Each covariate contains the original as well as the
encoded covariates.
"""
if self.encoders is None or not self.encoders.encoding_available:
raise_log(
ValueError(
"Encodings are not available. Consider adding parameter `add_encoders` at model creation "
"and fitting the model with `model.fit()` before."
),
)
return self.encoders.encode_train_inference(
n=n,
target=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
def _process_validation_set(
self,
series: Sequence[TimeSeries],
past_covariates: Sequence[TimeSeries] | None,
future_covariates: Sequence[TimeSeries] | None,
val_series: Sequence[TimeSeries] | None,
val_past_covariates: Sequence[TimeSeries] | None,
val_future_covariates: Sequence[TimeSeries] | None,
) -> tuple[
Sequence[TimeSeries] | None,
Sequence[TimeSeries] | None,
Sequence[TimeSeries] | None,
]:
"""Validates the validation set and generates/adds the required encodings."""
if val_series is None:
return None, None, None
# generate encodings for the val set covariates
if self.encoders.encoding_available:
(
val_past_covariates,
val_future_covariates,
) = self.generate_fit_encodings(
series=val_series,
past_covariates=val_past_covariates,
future_covariates=val_future_covariates,
)
for idx in range(len(val_series)):
val_s = val_series[idx]
val_pc = (
val_past_covariates[idx] if val_past_covariates is not None else None
)
val_fc = (
val_future_covariates[idx]
if val_future_covariates is not None
else None
)
# check that val set has same number of features as train set
match_series = series[0].width == val_s.width
match_past_covariates = (
past_covariates[0].width if past_covariates is not None else None
) == (val_pc.width if val_pc is not None else None)
match_future_covariates = (
future_covariates[0].width if future_covariates is not None else None
) == (val_fc.width if val_fc is not None else None)
if self.uses_static_covariates:
self._verify_static_covariates(val_s.static_covariates)
match_static_covariates = (
series[0].static_covariates.shape
if series[0].static_covariates is not None
else None
) == (
val_s.static_covariates.shape
if val_s.static_covariates is not None
else None
)
else:
match_static_covariates = True
matches = [
match_series,
match_past_covariates,
match_future_covariates,
match_static_covariates,
]
if not all(matches):
invalid_series = [
name
for match, name in zip(
matches,
[
"`series`",
"`past_covariates`",
"`future_covariates`",
"`static_covariates`",
],
)
if not match
]
raise_log(
ValueError(
f"The dimensions of the ({', '.join(invalid_series)}) between the training and "
f"validation set "
f"{'' if len(val_series) == 1 else 'at sequence/list index `' + str(idx) + '` '}"
f"do not match."
),
)
return val_series, val_past_covariates, val_future_covariates
@property
@abstractmethod
def _model_encoder_settings(
self,
) -> tuple[
int | None,
int | None,
bool,
bool,
list[int] | None,
list[int] | None,
]:
"""Abstract property that returns model specific encoder settings that are used to initialize the encoders.
Must return Tuple (input_chunk_length, output_chunk_length, takes_past_covariates, takes_future_covariates,
lags_past_covariates, lags_future_covariates).
"""
@classmethod
def _sample_params(model_class, params, n_random_samples):
"""Select the absolute number of samples randomly if an integer has been supplied. If a float has been
supplied, select a fraction"""
if isinstance(n_random_samples, int):
if not ((n_random_samples > 0) and (n_random_samples <= len(params))):
raise_log(
ValueError(
"If supplied as an integer, n_random_samples must be greater than 0 and less"
"than or equal to the size of the cartesian product of the hyperparameters."
),
)
return sample(params, n_random_samples)
if isinstance(n_random_samples, float):
if not ((n_random_samples > 0.0) and (n_random_samples <= 1.0)):
raise_log(
ValueError(
"If supplied as a float, n_random_samples must be greater than 0.0 and less than 1.0."
),
)
return sample(params, int(n_random_samples * len(params)))
def _extract_model_creation_params(self):
"""extracts immutable model creation parameters from `ModelMeta` and deletes reference."""
model_params = copy.deepcopy(self._model_call)
del self.__class__._model_call
return model_params
def untrained_model(self):
"""Returns a new (untrained) model instance created with the same parameters."""
return self.__class__(**copy.deepcopy(self.model_params))
@property
def model_params(self) -> dict:
return (
self._model_params if hasattr(self, "_model_params") else self._model_call
)
@classmethod
def _default_save_path(cls) -> str:
return f"{cls.__name__}_{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}"
def _clean(self) -> Self:
"""Returns a cleaned instance of the model. Has no effect for local forecasting models."""
return self
def save(
self,
path: str | os.PathLike | BinaryIO | None = None,
clean: bool = False,
**pkl_kwargs,
) -> None:
"""
Saves the model under a given path or file handle.
Example for saving and loading a :class:`SKLearnModel`:
.. highlight:: python
.. code-block:: python
from darts.models import SKLearnModel
model = SKLearnModel(lags=4)
model.save("my_model.pkl")
model_loaded = SKLearnModel.load("my_model.pkl")
..
Parameters
----------
path
Path or file handle under which to save the model at its current state. If no path is specified, the model
is automatically saved under ``"{ModelClass}_{YYYY-mm-dd_HH_MM_SS}.pkl"``.
E.g., ``"SKLearnModel_2020-01-01_12_00_00.pkl"``.
clean
Whether to store a cleaned version of the model. Only effective for global forecasting models.
If `True`, the training series and covariates are removed.
Note: After loading a global forecasting model stored with `clean=True`, a `series` must be passed
'predict()', `historical_forecasts()` and other forecasting methods.
pkl_kwargs
Keyword arguments passed to `pickle.dump()`
"""
if path is None:
# default path
path = self._default_save_path() + ".pkl"
model_to_save = self._clean() if clean else self
if isinstance(path, str | os.PathLike):
# save the whole object using pickle
with open(path, "wb") as handle:
pickle.dump(obj=model_to_save, file=handle, **pkl_kwargs)
elif isinstance(path, io.BufferedWriter):
# save the whole object using pickle
pickle.dump(obj=model_to_save, file=path, **pkl_kwargs)
else:
raise_log(
ValueError(
"Argument 'path' has to be either 'str' or 'PathLike' (for a filepath) "
f"or 'BufferedWriter' (for an already opened file), but was '{path.__class__}'."
),
)
@staticmethod
def load(path: str | os.PathLike | BinaryIO) -> "ForecastingModel":
"""
Loads a model from a given path or file handle.
Parameters
----------
path
Path or file handle from which to load the model.
"""
if isinstance(path, str | os.PathLike):
if not os.path.exists(path):
raise_log(ValueError(f"The file {path} doesn't exist."))
with open(path, "rb") as handle:
model = pickle.load(file=handle)
elif isinstance(path, io.BufferedReader):
model = pickle.load(file=path)
else:
raise_log(
ValueError(
"Argument 'path' has to be either 'str' or 'PathLike' (for a filepath) "
f"or 'BufferedReader' (for an already opened file), but was '{path.__class__}'."
),
)
return model
def _assert_univariate(self, series: TimeSeries):
if not series.is_univariate:
raise_log(
ValueError(
f"Model `{self.__class__.__name__}` only supports univariate TimeSeries instances"
),
)
def _assert_multivariate(self, series: TimeSeries):
if series.is_univariate:
raise_log(
ValueError(
f"Model `{self.__class__.__name__}` only supports multivariate TimeSeries instances"
),
)
def __repr__(self):
"""
Get full description for this estimator (includes all params).
"""
return self._get_model_description_string(True)
def __str__(self):
"""
Get short description for this estimator (only includes params with non-default values).
"""
return self._get_model_description_string(False)
def _get_model_description_string(self, include_default_params):
"""
Get model description string of structure `model_name`(`model_param_key_value_pairs`).
Parameters
----------
include_default_params : bool,
If True, will include params with default values in the description.
Returns
-------
description : String
Model description.
"""
default_model_params = self._get_default_model_params()
changed_model_params = [
(k, v)
for k, v in self.model_params.items()
if include_default_params or np.any(v != default_model_params.get(k, None))
]
model_name = self.__class__.__name__
params_string = ", ".join([f"{k}={str(v)}" for k, v in changed_model_params])
return f"{model_name}({params_string})"
@classmethod
def _get_default_model_params(cls):
"""Get parameter key : default_value pairs for the estimator"""
init_signature = inspect.signature(cls.__init__)
# Consider the constructor parameters excluding 'self'
return {
p.name: p.default
for p in init_signature.parameters.values()
if p.name != "self"
}
def _verify_static_covariates(self, static_covariates: pd.DataFrame | None):
"""
Verify that all static covariates are numeric.
"""
if static_covariates is not None:
numeric_mask = static_covariates.columns.isin(
static_covariates.select_dtypes(include=np.number)
)
if sum(~numeric_mask):
raise_log(
ValueError(
f"{self.__class__.__name__} can only interpret numeric static covariate data. Consider "
"encoding/transforming categorical static covariates with "
"`darts.dataprocessing.transformers.static_covariates_transformer.StaticCovariatesTransformer` "
"or set `use_static_covariates=False` at model creation to ignore static covariates."
),
)
def _optimized_historical_forecasts(
self,
series: Sequence[TimeSeries],
past_covariates: Sequence[TimeSeries] | None = None,
future_covariates: Sequence[TimeSeries] | None = None,
num_samples: int = 1,
start: pd.Timestamp | float | int | Literal["end"] | None = None,
start_format: Literal["position", "value"] = "value",
forecast_horizon: int = 1,
stride: int = 1,
overlap_end: bool = False,
last_points_only: bool = True,
verbose: bool = False,
show_warnings: bool = True,
predict_likelihood_parameters: bool = False,
random_state: int | None = None,
predict_kwargs: dict[str, Any] | None = None,
) -> Sequence[TimeSeries] | Sequence[Sequence[TimeSeries]]:
logger.warning(
"`optimized historical forecasts is not available for this model, use `historical_forecasts` instead."
)
return []
def _sanity_check_predict_likelihood_parameters(
self, n: int, output_chunk_length: int | None, num_samples: int
):
"""Verify that the assumptions for likelihood parameters prediction are verified:
- Probabilistic models fitted with a likelihood
- `num_samples=1`
- `n <= output_chunk_length`
"""
if not self.supports_likelihood_parameter_prediction:
raise_log(
ValueError(
"`predict_likelihood_parameters=True` is only supported for probabilistic models fitted with "
"a likelihood."
),
)
if num_samples != 1:
raise_log(
ValueError(
f"`predict_likelihood_parameters=True` is only supported for `num_samples=1`, "
f"received {num_samples}."
),
)
if output_chunk_length is not None and n > output_chunk_length:
raise_log(
ValueError(
"`predict_likelihood_parameters=True` is only supported for `n` smaller than or equal to "
"`output_chunk_length`."
),
)
class LocalForecastingModel(ForecastingModel, ABC):
"""The base class for "local" forecasting models, handling only single univariate time series.
Local Forecasting Models (LFM) are models that can be trained on a single univariate target series only. In Darts,
most models in this category tend to be simpler statistical models (such as ETS or FFT). LFMs usually train on
the entire target series supplied when calling :func:`fit()` at once. They can also predict in one go with
:func:`predict()` for any number of predictions `n` after the end of the training series.
All implementations must implement the `fit()` and `predict()` methods.
"""
def __init__(self, add_encoders: dict | None = None):
super().__init__(add_encoders=add_encoders)
@property
def _model_encoder_settings(
self,
) -> tuple[
int | None,
int | None,
bool,
bool,
list[int] | None,
list[int] | None,
]:
return None, None, False, False, None, None
@abstractmethod
def fit(
self, series: TimeSeries, verbose: bool | None = None
) -> "LocalForecastingModel":
super().fit(series, verbose=verbose)
series._assert_deterministic()
return self
@property
def extreme_lags(
self,
) -> tuple[
int | None,
int | None,
int | None,
int | None,
int | None,
int | None,
int,
]:
# TODO: LocalForecastingModels do not yet handle extreme lags properly. Especially
# TransferableFutureCovariatesLocalForecastingModel, where there is a difference between fit and predict mode)
# do not yet. In general, Local models train on the entire series (input=output), different to Global models
# that use an input to predict an output.
return -self.min_train_series_length, -1, None, None, None, None, 0
@property
def supports_transferable_series_prediction(self) -> bool:
"""
Whether the model supports prediction for any input `series`.
"""
return False
@property
def min_train_samples(self) -> int:
# local models do not work with samples, so only one training sample is needed
return 1
@property
def _target_window_lengths(self) -> tuple[int, int]:
# local models do not work with samples, so the length of the training sample is not tied to lags
return 3, self.output_chunk_length or 0
class GlobalForecastingModel(ForecastingModel, ABC):
"""The base class for "global" forecasting models, handling several time series and optional covariates.
Global forecasting models expand upon the functionality of `ForecastingModel` in 4 ways:
1. Models can be fitted on many series (multivariate or univariate) with different indices.
2. The input series used by :func:`predict()` can be different from the series used to fit the model.
3. Covariates can be supported (multivariate or univariate).
4. They can allow for multivariate target series and covariates.
The name "global" stems from the fact that a training set of a forecasting model of this class is not constrained
to a temporally contiguous, "local", time series.
All implementations must implement the :func:`fit()` and :func:`predict()` methods.
The :func:`fit()` method is meant to train the model on one or several training time series, along with optional
covariates.
If :func:`fit()` has been called with only one training and covariate series as argument, then
calling :func:`predict()` will forecast the future of this series. Otherwise, the user has to
provide to :func:`predict()` the series they want to forecast, as well as covariates, if needed.
"""
def __init__(self, add_encoders: dict | None = None):
super().__init__(add_encoders=add_encoders)
@abstractmethod
def fit(
self,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
verbose: bool | None = None,
) -> "GlobalForecastingModel":
"""Fit/train the model on (potentially multiple) series.
Optionally, one or multiple past and/or future covariates series can be provided as well.
The number of covariates series must match the number of target series.
Parameters
----------
series
One or several target time series. The model will be trained to forecast these time series.
The series may or may not be multivariate, but if multiple series are provided
they must have the same number of components.
past_covariates
One or several past-observed covariate time series. These time series will not be forecast, but can
be used by some models as an input. The covariate(s) may or may not be multivariate, but if multiple
covariates are provided they must have the same number of components. If `past_covariates` is provided,
it must contain the same number of series as `series`.
future_covariates
One or several future-known covariate time series. These time series will not be forecast, but can
be used by some models as an input. The covariate(s) may or may not be multivariate, but if multiple
covariates are provided they must have the same number of components. If `future_covariates` is provided,
it must contain the same number of series as `series`.
verbose
Optionally, set the fit verbosity. Not effective for all models.
Returns
-------
self
Fitted model.
"""
if isinstance(series, TimeSeries):
# if only one series is provided, save it for prediction time (including covariates, if available)
self.training_series = series
if past_covariates is not None:
self.past_covariate_series = past_covariates
if future_covariates is not None:
self.future_covariate_series = future_covariates
if (
series.static_covariates is not None
and self.supports_static_covariates
and self.considers_static_covariates
):
self.static_covariates = series.static_covariates
else:
if past_covariates is not None:
self._expect_past_covariates = True
if future_covariates is not None:
self._expect_future_covariates = True
if (
get_single_series(series).static_covariates is not None
and self.supports_static_covariates
and self.considers_static_covariates
):
self.static_covariates = series[0].static_covariates
self._expect_static_covariates = True
if past_covariates is not None:
self._uses_past_covariates = True
if future_covariates is not None:
self._uses_future_covariates = True
if (
get_single_series(series).static_covariates is not None
and self.supports_static_covariates
and self.considers_static_covariates
):
self._uses_static_covariates = True
self._fit_called = True
return self
@abstractmethod
def predict(
self,
n: int,
series: TimeSeriesLike | None = None,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
num_samples: int = 1,
verbose: bool | None = None,
predict_likelihood_parameters: bool = False,
show_warnings: bool = True,
random_state: int | None = None,
) -> TimeSeriesLike:
"""Forecasts values for `n` time steps after the end of the series.
If :func:`fit()` has been called with only one ``TimeSeries`` as argument, then the `series` argument of
this function is optional, and it will simply produce the next `horizon` time steps forecast.
The `past_covariates` and `future_covariates` arguments also don't have to be provided again in this case.
If :func:`fit()` has been called with `series` specified as a ``Sequence[TimeSeries]`` (i.e., the model
has been trained on multiple time series), the `series` argument must be specified.
When the `series` argument is specified, this function will compute the next `n` time steps forecasts
for the simple series (or for each series in the sequence) given by `series`.
If multiple past or future covariates were specified during the training, some corresponding covariates must
also be specified here. For every input in `series` a matching (past and/or future) covariate time series
has to be provided.
Parameters
----------
n
Forecast horizon - the number of time steps after the end of the series for which to produce predictions.
series
The series whose future values will be predicted.
past_covariates
One past-observed covariate time series for every input time series in `series`. They must match the
past covariates that have been used with the :func:`fit()` function for training in terms of dimension.
future_covariates
One future-known covariate time series for every input time series in `series`. They must match the
past covariates that have been used with the :func:`fit()` function for training in terms of dimension.
num_samples
Number of times a prediction is sampled from a probabilistic model. Must be `1` for deterministic models.
verbose
Optionally, set the prediction verbosity. Not effective for all models.
predict_likelihood_parameters
If set to `True`, the model predicts the parameters of its `likelihood` instead of the target. Only
supported for probabilistic models with a likelihood, `num_samples = 1` and `n<=output_chunk_length`.
Default: ``False``
show_warnings
Whether to show warnings related auto-regression and past covariates usage.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
TimeSeriesLike
If `series` is not specified, this function returns a single time series containing the `n`
next points after then end of the training series.
If `series` is given and is a simple ``TimeSeries``, this function returns the `n` next points
after the end of `series`.
If `series` is given and is a sequence of several time series, this function returns
a sequence where each element contains the corresponding `n` points forecasts.
"""
super().predict(n, num_samples, verbose=verbose, random_state=random_state)
if predict_likelihood_parameters:
self._sanity_check_predict_likelihood_parameters(
n, self.output_chunk_length, num_samples
)
if self.uses_past_covariates and past_covariates is None:
raise_log(
ValueError(
"The model was trained with past covariates. Some matching past_covariates "
"must be passed to `predict()`."
)
)
if self.uses_future_covariates and future_covariates is None:
raise_log(
ValueError(
"The model was trained with future covariates. Some matching future_covariates "
"must be passed to `predict()`."
)
)
if (
self.uses_static_covariates
and get_single_series(series).static_covariates is None
):
raise_log(
ValueError(
"The model was trained with static covariates. Some matching static covariates "
"must be embedded in the target `series` passed to `predict()`."
)
)
if (
show_warnings
and self.uses_past_covariates
and self.output_chunk_length is not None
and n > self.output_chunk_length
):
logger.warning(
"`predict()` was called with `n > output_chunk_length`: using auto-regression to forecast "
"the values after `output_chunk_length` points. The model will access `(n - output_chunk_length)` "
"future values of your `past_covariates` (relative to the first predicted time step). "
"To hide this warning, set `show_warnings=False`."
)
def _clean(self) -> Self:
"""Returns a cleaned instance of the model by removing the training series and covariates."""
# a shallow copy is enough since we are only interested in removing pointers to the training data
cleaned_model = copy.copy(self)
cleaned_model.training_series = None
cleaned_model.past_covariate_series = None
cleaned_model.future_covariate_series = None
cleaned_model.static_covariates = None
return cleaned_model
@property
def _supports_non_retrainable_historical_forecasts(self) -> bool:
"""GlobalForecastingModel supports historical forecasts without retraining the model"""
return True
@property
def supports_optimized_historical_forecasts(self) -> bool:
"""
Whether the model supports optimized historical forecasts
"""
return True
@property
def supports_transferable_series_prediction(self) -> bool:
"""
Whether the model supports prediction for any input `series`.
"""
return True
@property
def supports_sample_weight(self) -> bool:
"""
Whether model supports sample weight for training.
"""
return True
class FutureCovariatesLocalForecastingModel(LocalForecastingModel, ABC):
"""The base class for future covariates "local" forecasting models, handling single uni- or multivariate target
and optional future covariates time series.
Future Covariates Local Forecasting Models (FC-LFM) are models that can be trained on a single uni- or multivariate
target and optional future covariates series. In Darts, most models in this category tend to be simpler statistical
models (such as ARIMA). FC-LFMs usually train on the entire target and future covariates series supplied when
calling :func:`fit()` at once. They can also predict in one go with :func:`predict()` for any number of predictions
`n` after the end of the training series. When using future covariates, the values for the future `n` prediction
steps must be given in the covariate series.
All implementations must implement the :func:`_fit()` and :func:`_predict()` methods.
"""
def fit(
self,
series: TimeSeries,
future_covariates: TimeSeries | None = None,
verbose: bool | None = None,
):
"""Fit/train the model on the (single) provided series.
Optionally, a future covariates series can be provided as well.
Parameters
----------
series
The model will be trained to forecast this time series. Can be multivariate if the model supports it.
future_covariates
A time series of future-known covariates. This time series will not be forecasted, but can be used by
some models as an input. It must contain at least the same time steps/indices as the target `series`.
If it is longer than necessary, it will be automatically trimmed.
verbose
Optionally, set the fit verbosity. Not effective for all models.
Returns
-------
self
Fitted model.
"""
self.encoders = self.initialize_encoders()
if self.encoders.encoding_available:
_, future_covariates = self.generate_fit_encodings(
series=series,
past_covariates=None,
future_covariates=future_covariates,
)
if future_covariates is not None:
future_covariates_copy = future_covariates
if not series.has_same_time_as(future_covariates):
# fit() expects future_covariates to have same time as the target, so we intersect it here
future_covariates = future_covariates.slice_intersect(series)
if len(future_covariates) != len(series):
raise_log(
ValueError(
"The provided `future_covariates` series must contain at least the same time steps/"
"indices as the target `series`."
),
)
self.future_covariate_series = future_covariates_copy
self._uses_future_covariates = True
super().fit(series, verbose=verbose)
return self._fit(series, future_covariates=future_covariates, verbose=verbose)
@abstractmethod
def _fit(
self,
series: TimeSeries,
future_covariates: TimeSeries | None = None,
verbose: bool | None = None,
):
"""Fits/trains the model on the provided series.
DualCovariatesModels must implement the fit logic in this method.
"""
def predict(
self,
n: int,
future_covariates: TimeSeries | None = None,
num_samples: int = 1,
predict_likelihood_parameters: bool = False,
verbose: bool | None = None,
show_warnings: bool = True,
random_state: int | None = None,
**kwargs,
) -> TimeSeries:
"""Forecasts values for `n` time steps after the end of the training series.
If some future covariates were specified during the training, they must also be specified here.
Parameters
----------
n
Forecast horizon - the number of time steps after the end of the series for which to produce predictions.
future_covariates
The time series of future-known covariates which can be fed as input to the model. It must correspond to
the covariate time series that has been used with the :func:`fit()` method for training, and it must
contain at least the next `n` time steps/indices after the end of the training target series.
num_samples
Number of times a prediction is sampled from a probabilistic model. Must be `1` for deterministic models.
predict_likelihood_parameters
If set to `True`, the model predicts the parameters of its `likelihood` instead of the target. Only
supported for probabilistic models with a likelihood, `num_samples = 1` and `n<=output_chunk_length`.
Default: ``False``.
verbose
Optionally, set the prediction verbosity. Not effective for all models.
show_warnings
Optionally, control whether warnings are shown. Not effective for all models.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
TimeSeries, a single time series containing the `n` next points after then end of the training series.
"""
super().predict(n, num_samples, verbose=verbose, random_state=random_state)
if predict_likelihood_parameters:
self._sanity_check_predict_likelihood_parameters(
n, self.output_chunk_length, num_samples
)
# avoid generating encodings again if subclass has already generated them
future_covariates = future_covariates or self.future_covariate_series
if not self._supress_generate_predict_encoding:
# stored future covariates already contain the encodings: this is not a problem as the
# encoders regenerate the encodings
if self.encoders is not None and self.encoders.encoding_available:
_, future_covariates = self.generate_predict_encodings(
n=n,
series=self.training_series,
past_covariates=None,
future_covariates=future_covariates,
)
self._verify_passed_predict_covariates(future_covariates)
if future_covariates is not None:
start = self.training_series.end_time() + self.training_series.freq
offset = n - 1 if future_covariates.has_datetime_index else n
future_covariates = future_covariates[
start : start + offset * self.training_series.freq
]
if len(future_covariates) != n:
raise_log(
ValueError(
f"For the given forecasting horizon `n={n}`, the provided `future_covariates` "
f"series must contain at least the next `n={n}` time steps/indices after the "
f"end of the target `series` that was used to train the model."
),
)
return self._predict(
n=n,
future_covariates=future_covariates,
num_samples=num_samples,
predict_likelihood_parameters=predict_likelihood_parameters,
random_state=random_state,
verbose=verbose,
**kwargs,
)
@abstractmethod
def _predict(
self,
n: int,
future_covariates: TimeSeries | None = None,
num_samples: int = 1,
predict_likelihood_parameters: bool = False,
verbose: bool = False,
random_state: int | None = None,
**kwargs,
) -> TimeSeries:
"""Forecasts values for a certain number of time steps after the end of the series.
DualCovariatesModels must implement the predict logic in this method.
"""
@property
def _model_encoder_settings(
self,
) -> tuple[
int | None,
int | None,
bool,
bool,
list[int] | None,
list[int] | None,
]:
return None, None, False, True, None, None
def _verify_passed_predict_covariates(self, future_covariates: TimeSeries):
"""Simple check if user supplied/did not supply covariates as done at fitting time."""
if self.uses_future_covariates:
# at this point future covariates are available, either passed from user or retrieved from
# covariates used during training
if (
future_covariates.n_components
!= self.future_covariate_series.n_components
):
n_expected = self.future_covariate_series.n_components
n_actual = future_covariates.n_components
if self.encoders is not None and self.encoders.encoding_available:
n_encodings = self.encoders.encoding_n_components[1]
n_expected -= n_encodings
n_actual -= n_encodings
raise_log(
ValueError(
f"The `future_covariates` passed to `predict()` must have the same number "
f"of components / columns as the `future_covariates` used to train the model. "
f"Received number of components `{n_actual}`, excepted {n_expected}."
)
)
elif future_covariates is not None:
raise_log(
ValueError(
"The model was trained without future covariates, but future `future_covariates` "
"were provided to `predict()`."
)
)
@property
def _supress_generate_predict_encoding(self) -> bool:
"""Controls whether encodings should be generated in :func:`FutureCovariatesLocalForecastingModel.predict()``"""
return False
@property
def extreme_lags(
self,
) -> tuple[
int | None,
int | None,
int | None,
int | None,
int | None,
int | None,
int,
]:
# TODO: LocalForecastingModels do not yet handle extreme lags properly. Especially
# TransferableFutureCovariatesLocalForecastingModel, where there is a difference between fit and predict mode)
# do not yet. In general, Local models train on the entire series (input=output), different to Global models
# that use an input to predict an output.
return -self.min_train_series_length, -1, None, None, 0, 0, 0
@property
def supports_future_covariates(self) -> bool:
return True
class TransferableFutureCovariatesLocalForecastingModel(
FutureCovariatesLocalForecastingModel, ABC
):
"""The base class for transferable future covariates "local" forecasting models, handling single uni- or
multivariate target and optional future covariates time series. Additionally, at prediction time, it can be
applied to new data unrelated to the original series used for fitting the model.
Transferable Future Covariates Local Forecasting Models (TFC-LFM) are models that can be trained on a single uni-
or multivariate target and optional future covariates series. Additionally, at prediction time, it can be applied
to new data unrelated to the original series used for fitting the model. Currently in Darts, all models in this
category wrap to statsmodel models such as VARIMA. TFC-LFMs usually train on the entire target and future covariates
series supplied when calling :func:`fit()` at once. They can also predict in one go with :func:`predict()`
for any number of predictions `n` after the end of the training series. When using future covariates, the values
for the future `n` prediction steps must be given in the covariate series.
All implementations must implement the :func:`_fit()` and :func:`_predict()` methods.
"""
def predict(
self,
n: int,
series: TimeSeries | None = None,
future_covariates: TimeSeries | None = None,
num_samples: int = 1,
predict_likelihood_parameters: bool = False,
verbose: bool | None = None,
show_warnings: bool = True,
random_state: int | None = None,
**kwargs,
) -> TimeSeries:
"""If the `series` parameter is not set, forecasts values for `n` time steps after the end of the training
series. If some future covariates were specified during the training, they must also be specified here.
If the `series` parameter is set, forecasts values for `n` time steps after the end of the new target
series. If some future covariates were specified during the training, they must also be specified here.
Parameters
----------
n
Forecast horizon - the number of time steps after the end of the series for which to produce predictions.
series
Optionally, a new target series whose future values will be predicted. Defaults to `None`, meaning that the
model will forecast the future value of the training series.
future_covariates
The time series of future-known covariates which can be fed as input to the model. It must correspond to
the covariate time series that has been used with the :func:`fit()` method for training.
If `series` is not set, it must contain at least the next `n` time steps/indices after the end of the
training target series. If `series` is set, it must contain at least the time steps/indices corresponding
to the new target series (historic future covariates), plus the next `n` time steps/indices after the end.
num_samples
Number of times a prediction is sampled from a probabilistic model. Must be `1` for deterministic models.
predict_likelihood_parameters
If set to `True`, the model predicts the parameters of its `likelihood` instead of the target. Only
supported for probabilistic models with a likelihood, `num_samples = 1` and `n<=output_chunk_length`.
Default: ``False``.
verbose
Optionally, set the prediction verbosity. Not effective for all models.
show_warnings
Optionally, control whether warnings are shown. Not effective for all models.
random_state
Controls the randomness of probabilistic predictions.
Returns
-------
TimeSeries, a single time series containing the `n` next points after then end of the training series.
"""
future_covariates = future_covariates or self.future_covariate_series
# stored future covariates already contain the encodings: this is not a problem as the
# encoders regenerate the encodings
if self.encoders is not None and self.encoders.encoding_available:
_, future_covariates = self.generate_predict_encodings(
n=n,
series=series if series is not None else self.training_series,
past_covariates=None,
future_covariates=future_covariates,
)
self._verify_passed_predict_covariates(future_covariates)
historic_future_covariates = None
if future_covariates is not None:
series_ = series or self.training_series
if (
future_covariates.start_time() > series_.start_time()
or future_covariates.end_time() < series_.end_time() + n * series_.freq
):
raise_log(
ValueError(
"The provided `future_covariates` related to the new target series must contain at "
"least the same timesteps/indices as the target `series` + `n`."
),
)
historic_future_covariates = future_covariates.slice_intersect(series_)
# FutureCovariatesLocalForecastingModel performs some checks on self.training_series. We temporary replace
# that with the new ts
if series is not None:
self._orig_training_series = self.training_series
self.training_series = series
result = super().predict(
n=n,
series=series,
historic_future_covariates=historic_future_covariates,
future_covariates=future_covariates,
num_samples=num_samples,
predict_likelihood_parameters=predict_likelihood_parameters,
verbose=verbose,
random_state=random_state,
**kwargs,
)
# restoring the original training ts
if series is not None:
self.training_series = self._orig_training_series
return result
def generate_predict_encodings(
self,
n: int,
series: TimeSeriesLike,
past_covariates: TimeSeriesLike | None = None,
future_covariates: TimeSeriesLike | None = None,
) -> tuple[TimeSeriesLike, TimeSeriesLike]:
if self.encoders is None or not self.encoders.encoding_available:
raise_log(
ValueError(
"Encodings are not available. Consider adding parameter "
"`add_encoders` at model creation and fitting the model "
"with `model.fit()` before."
),
)
return self.generate_fit_predict_encodings(
n=n,
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
@abstractmethod
def _predict(
self,
n: int,
series: TimeSeries | None = None,
historic_future_covariates: TimeSeries | None = None,
future_covariates: TimeSeries | None = None,
num_samples: int = 1,
predict_likelihood_parameters: bool = False,
verbose: bool = False,
random_state: int | None = None,
) -> TimeSeries:
"""Forecasts values for a certain number of time steps after the end of the series.
TransferableFutureCovariatesLocalForecastingModel must implement the predict logic in this method.
"""
@property
def supports_transferable_series_prediction(self) -> bool:
"""
Whether the model supports prediction for any input `series`.
"""
return True
@property
def _supports_non_retrainable_historical_forecasts(self) -> bool:
return True
@property
def _supress_generate_predict_encoding(self) -> bool:
return True