diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f86780ed7..930ea37690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ but cannot always guarantee backwards compatibility. Changes that may **break co ### For users of the library: +**Improved** +- `TimeSeries` with a `RangeIndex` starting in the negative start are now supported by `historical_forecasts`. [#1866](https://github.com/unit8co/darts/pull/1866) by [Antoine Madrona](https://github.com/madtoinou). +- Added a new argument `start_format` to `historical_forecasts()`, `backtest()` and `gridsearch` that allows to use an integer `start` either as the index position or index value/label for `series` indexed with a `pd.RangeIndex`. [#1866](https://github.com/unit8co/darts/pull/1866) by [Antoine Madrona](https://github.com/madtoinou). + **Fixed** - Fixed a bug in `TimeSeries.from_dataframe()` when using a pandas.DataFrame with `df.columns.name != None`. [#1938](https://github.com/unit8co/darts/pull/1938) by [Antoine Madrona](https://github.com/madtoinou). - Fixed a bug in `RegressionEnsembleModel.extreme_lags` when the forecasting models have only covariates lags. [#1942](https://github.com/unit8co/darts/pull/1942) by [Antoine Madrona](https://github.com/madtoinou). diff --git a/darts/models/forecasting/forecasting_model.py b/darts/models/forecasting/forecasting_model.py index 7d75e9c258..452d2368cd 100644 --- a/darts/models/forecasting/forecasting_model.py +++ b/darts/models/forecasting/forecasting_model.py @@ -24,6 +24,11 @@ from random import sample from typing import Any, BinaryIO, Callable, Dict, List, Optional, Sequence, Tuple, Union +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + import numpy as np import pandas as pd @@ -560,6 +565,7 @@ def historical_forecasts( num_samples: int = 1, train_length: Optional[int] = None, start: Optional[Union[pd.Timestamp, float, int]] = None, + start_format: Literal["position", "value"] = "value", forecast_horizon: int = 1, stride: int = 1, retrain: Union[bool, int, Callable[..., bool]] = True, @@ -609,15 +615,14 @@ def historical_forecasts( steps available, all steps up until prediction time are used, as in default case. Needs to be at least `min_train_series_length`. start - Optionally, the first point in time at which a prediction is computed for a future time. - This parameter supports: ``float``, ``int`` and ``pandas.Timestamp``, and ``None``. - If a ``float``, the parameter will be treated as the proportion of the time series - that should lie before the first prediction point. - If an ``int``, the parameter will be treated as an integer index to the time index of - `series` that will be used as first prediction time. - If a ``pandas.Timestamp``, the time stamp will be used to determine the first prediction time - directly. - If ``None``, the first prediction time will automatically be set to: + 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. @@ -628,6 +633,13 @@ def historical_forecasts( Note: Raises a ValueError if `start` yields a time outside the time index of `series`. 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. 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'`` forecast_horizon The forecast horizon for the predictions. stride @@ -798,6 +810,7 @@ def retrain_func( future_covariates=future_covariates, num_samples=num_samples, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, stride=stride, overlap_end=overlap_end, @@ -876,6 +889,7 @@ def retrain_func( forecast_horizon=forecast_horizon, overlap_end=overlap_end, start=start, + start_format=start_format, show_warnings=show_warnings, ) @@ -1030,6 +1044,7 @@ def backtest( num_samples: int = 1, train_length: Optional[int] = None, start: Optional[Union[pd.Timestamp, float, int]] = None, + start_format: Literal["position", "value"] = "value", forecast_horizon: int = 1, stride: int = 1, retrain: Union[bool, int, Callable[..., bool]] = True, @@ -1085,25 +1100,31 @@ def backtest( steps available, all steps up until prediction time are used, as in default case. Needs to be at least `min_train_series_length`. start - Optionally, the first point in time at which a prediction is computed for a future time. - This parameter supports: ``float``, ``int`` and ``pandas.Timestamp``, and ``None``. - If a ``float``, the parameter will be treated as the proportion of the time series - that should lie before the first prediction point. - If an ``int``, the parameter will be treated as an integer index to the time index of - `series` that will be used as first prediction time. - If a ``pandas.Timestamp``, the time stamp will be used to determine the first prediction time - directly. - If ``None``, the first prediction time 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 + 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: Raises a ValueError if `start` yields a time outside the time index of `series`. 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. 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'`` forecast_horizon The forecast horizon for the point predictions. stride @@ -1160,6 +1181,7 @@ def backtest( num_samples=num_samples, train_length=train_length, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, stride=stride, retrain=retrain, @@ -1210,6 +1232,7 @@ def gridsearch( forecast_horizon: Optional[int] = None, stride: int = 1, start: Union[pd.Timestamp, float, int] = 0.5, + start_format: Literal["position", "value"] = "value", last_points_only: bool = False, show_warnings: bool = True, val_series: Optional[TimeSeries] = None, @@ -1275,17 +1298,38 @@ def gridsearch( forecast_horizon The integer value of the forecasting horizon. Activates expanding window mode. stride - The number of time steps between two consecutive predictions. Only used in expanding window mode. + Only used in expanding window mode. The number of time steps between two consecutive predictions. start - The ``int``, ``float`` or ``pandas.Timestamp`` that represents the starting point in the time index - of `series` from which predictions will be made to evaluate the model. - For a detailed description of how the different data types are interpreted, please see the documentation - for `ForecastingModel.backtest`. Only used in expanding window mode. + 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: Raises a ValueError if `start` yields a time outside the time index of `series`. + 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 - Whether to use the whole forecasts or only the last point of each forecast to compute the error. Only used - in expanding window mode. + 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 - Whether to show warnings related to the `start` parameter. Only used in expanding window mode. + 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. @@ -1386,6 +1430,7 @@ def _evaluate_combination(param_combination) -> float: future_covariates=future_covariates, num_samples=1, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, stride=stride, metric=metric, @@ -1893,6 +1938,7 @@ def _optimized_historical_forecasts( future_covariates: Optional[Sequence[TimeSeries]] = None, num_samples: int = 1, start: Optional[Union[pd.Timestamp, float, int]] = None, + start_format: Literal["position", "value"] = "value", forecast_horizon: int = 1, stride: int = 1, overlap_end: bool = False, diff --git a/darts/models/forecasting/regression_model.py b/darts/models/forecasting/regression_model.py index 1849dd8d1c..f51c26e902 100644 --- a/darts/models/forecasting/regression_model.py +++ b/darts/models/forecasting/regression_model.py @@ -29,6 +29,11 @@ from collections import OrderedDict from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression @@ -897,6 +902,7 @@ def _optimized_historical_forecasts( future_covariates: Optional[Sequence[TimeSeries]] = None, num_samples: int = 1, start: Optional[Union[pd.Timestamp, float, int]] = None, + start_format: Literal["position", "value"] = "value", forecast_horizon: int = 1, stride: int = 1, overlap_end: bool = False, @@ -949,6 +955,7 @@ def _optimized_historical_forecasts( future_covariates=future_covariates, num_samples=num_samples, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, stride=stride, overlap_end=overlap_end, @@ -963,6 +970,7 @@ def _optimized_historical_forecasts( future_covariates=future_covariates, num_samples=num_samples, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, stride=stride, overlap_end=overlap_end, diff --git a/darts/tests/models/forecasting/test_historical_forecasts.py b/darts/tests/models/forecasting/test_historical_forecasts.py index b8488a363e..6258462077 100644 --- a/darts/tests/models/forecasting/test_historical_forecasts.py +++ b/darts/tests/models/forecasting/test_historical_forecasts.py @@ -374,6 +374,47 @@ def test_historical_forecasts_local_models(self): "LocalForecastingModel does not support historical forecasting with `retrain` set to `False`" ) + def test_historical_forecasts_position_start(self): + series = tg.sine_timeseries(length=10) + + model = LinearRegressionModel(lags=2) + model.fit(series[:8]) + + # negative index + forecasts_neg = model.historical_forecasts( + series=series, start=-2, start_format="position", retrain=False + ) + assert len(forecasts_neg) == 2 + assert (series.time_index[-2:] == forecasts_neg.time_index).all() + + # positive index + forecasts_pos = model.historical_forecasts( + series=series, start=8, start_format="position", retrain=False + ) + assert forecasts_pos == forecasts_neg + + def test_historical_forecasts_negative_rangeindex(self): + series = TimeSeries.from_times_and_values( + times=pd.RangeIndex(start=-5, stop=5, step=1), values=np.arange(10) + ) + + model = LinearRegressionModel(lags=2) + model.fit(series[:8]) + + # start as point + forecasts = model.historical_forecasts( + series=series, start=-2, start_format="value", retrain=False + ) + assert len(forecasts) == 7 + assert (series.time_index[-7:] == forecasts.time_index).all() + + # start as index + forecasts = model.historical_forecasts( + series=series, start=-2, start_format="position", retrain=False + ) + assert len(forecasts) == 2 + assert (series.time_index[-2:] == forecasts.time_index).all() + def test_historical_forecasts(self): train_length = 10 forecast_horizon = 8 @@ -551,18 +592,40 @@ def test_sanity_check_invalid_start(self): rangeidx_step1 = tg.linear_timeseries(start=0, length=10, freq=1) rangeidx_step2 = tg.linear_timeseries(start=0, length=10, freq=2) - # index too large + # label_index (int), too large with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts(timeidx_, start=11) assert str(msg.value).startswith("`start` index `11` is out of bounds") with pytest.raises(ValueError) as msg: - LinearRegressionModel(lags=1).historical_forecasts(rangeidx_step1, start=11) - assert str(msg.value).startswith("`start` index `11` is out of bounds") + LinearRegressionModel(lags=1).historical_forecasts( + rangeidx_step1, start=rangeidx_step1.end_time() + rangeidx_step1.freq + ) + assert str(msg.value).startswith( + "`start` index `10` is larger than the last index" + ) + with pytest.raises(ValueError) as msg: + LinearRegressionModel(lags=1).historical_forecasts( + rangeidx_step2, start=rangeidx_step2.end_time() + rangeidx_step2.freq + ) + assert str(msg.value).startswith( + "`start` index `20` is larger than the last index" + ) + + # label_index (timestamp) too high + with pytest.raises(ValueError) as msg: + LinearRegressionModel(lags=1).historical_forecasts( + timeidx_, start=timeidx_.end_time() + timeidx_.freq + ) + assert str(msg.value).startswith( + "`start` time `2000-01-11 00:00:00` is after the last timestamp `2000-01-10 00:00:00`" + ) + + # label_index, invalid with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts(rangeidx_step2, start=11) assert str(msg.value).startswith("The provided point is not a valid index") - # value too low + # label_index, too low with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts( timeidx_, start=timeidx_.start_time() - timeidx_.freq @@ -574,32 +637,43 @@ def test_sanity_check_invalid_start(self): LinearRegressionModel(lags=1).historical_forecasts( rangeidx_step1, start=rangeidx_step1.start_time() - rangeidx_step1.freq ) - assert str(msg.value).startswith("if `start` is an integer, must be `>= 0`") + assert str(msg.value).startswith( + "`start` index `-1` is smaller than the first index `0`" + ) with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts( rangeidx_step2, start=rangeidx_step2.start_time() - rangeidx_step2.freq ) - assert str(msg.value).startswith("if `start` is an integer, must be `>= 0`") + assert str(msg.value).startswith( + "`start` index `-2` is smaller than the first index `0`" + ) + + # positional_index, predicting only the last position + LinearRegressionModel(lags=1).historical_forecasts( + timeidx_, start=9, start_format="position" + ) - # value too high + # positional_index, predicting from the first position with retrain=True with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts( - timeidx_, start=timeidx_.end_time() + timeidx_.freq + timeidx_, start=-10, start_format="position" ) - assert str(msg.value).startswith( - "`start` time `2000-01-11 00:00:00` is after the last timestamp `2000-01-10 00:00:00`" - ) + assert str(msg.value).endswith(", resulting in an empty training set.") + + # positional_index, beyond boundaries with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts( - rangeidx_step1, start=rangeidx_step1.end_time() + rangeidx_step1.freq + timeidx_, start=10, start_format="position" ) - assert str(msg.value).startswith("`start` index `10` is out of bounds") + assert str(msg.value).startswith( + "`start` index `10` is out of bounds for series of length 10" + ) with pytest.raises(ValueError) as msg: LinearRegressionModel(lags=1).historical_forecasts( - rangeidx_step2, start=rangeidx_step2.end_time() + rangeidx_step2.freq + timeidx_, start=-11, start_format="position" ) assert str(msg.value).startswith( - "`start` index `20` is larger than the last index `18`" + "`start` index `-11` is out of bounds for series of length 10" ) def test_regression_auto_start_multiple_no_cov(self): diff --git a/darts/timeseries.py b/darts/timeseries.py index dbb4f61788..2c54268d2c 100644 --- a/darts/timeseries.py +++ b/darts/timeseries.py @@ -216,7 +216,7 @@ def __init__(self, xa: xr.DataArray): logger, ) else: - self._freq = self._time_index.step + self._freq: int = self._time_index.step self._freq_str = None # check static covariates @@ -2103,7 +2103,7 @@ def get_index_at_point( ) raise_if_not( 0 <= point_index < len(self), - "point (int) should be a valid index in series", + f"The index corresponding to the provided point ({point}) should be a valid index in series", logger, ) elif isinstance(point, pd.Timestamp): @@ -2142,8 +2142,8 @@ def get_timestamp_at_point( This parameter supports 3 different data types: `float`, `int` and `pandas.Timestamp`. In case of a `float`, the parameter will be treated as the proportion of the time series that should lie before the point. - In the case of `int`, the parameter will be treated as an integer index to the time index of - `series`. Will raise a ValueError if not a valid index in `series` + In case of `int`, the parameter will be treated as an integer index to the time index of + `series`. Will raise a ValueError if not a valid index in `series`. In case of a `pandas.Timestamp`, point will be returned as is provided that the timestamp is present in the series time index, otherwise will raise a ValueError. """ diff --git a/darts/utils/historical_forecasts/optimized_historical_forecasts.py b/darts/utils/historical_forecasts/optimized_historical_forecasts.py index 8b3a777471..50c1364f26 100644 --- a/darts/utils/historical_forecasts/optimized_historical_forecasts.py +++ b/darts/utils/historical_forecasts/optimized_historical_forecasts.py @@ -1,5 +1,10 @@ from typing import List, Optional, Sequence, Union +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + import numpy as np import pandas as pd from numpy.lib.stride_tricks import sliding_window_view @@ -20,6 +25,7 @@ def _optimized_historical_forecasts_regression_last_points_only( future_covariates: Optional[Sequence[TimeSeries]] = None, num_samples: int = 1, start: Optional[Union[pd.Timestamp, float, int]] = None, + start_format: Literal["position", "value"] = "value", forecast_horizon: int = 1, stride: int = 1, overlap_end: bool = False, @@ -63,6 +69,7 @@ def _optimized_historical_forecasts_regression_last_points_only( past_covariates=past_covariates_, future_covariates=future_covariates_, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, overlap_end=overlap_end, freq=freq, @@ -156,6 +163,7 @@ def _optimized_historical_forecasts_regression_all_points( future_covariates: Optional[Sequence[TimeSeries]] = None, num_samples: int = 1, start: Optional[Union[pd.Timestamp, float, int]] = None, + start_format: Literal["position", "value"] = "value", forecast_horizon: int = 1, stride: int = 1, overlap_end: bool = False, @@ -199,6 +207,7 @@ def _optimized_historical_forecasts_regression_all_points( past_covariates=past_covariates_, future_covariates=future_covariates_, start=start, + start_format=start_format, forecast_horizon=forecast_horizon, overlap_end=overlap_end, freq=freq, diff --git a/darts/utils/historical_forecasts/utils.py b/darts/utils/historical_forecasts/utils.py index 5b06de0400..ee35d4fd32 100644 --- a/darts/utils/historical_forecasts/utils.py +++ b/darts/utils/historical_forecasts/utils.py @@ -1,6 +1,11 @@ from types import SimpleNamespace from typing import Any, Callable, Optional, Tuple, Union +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + import numpy as np import pandas as pd @@ -58,17 +63,23 @@ def _historical_forecasts_general_checks(model, series, kwargs): if not isinstance(n.start, (float, int, np.int64, pd.Timestamp)): raise_log( TypeError( - "`start` needs to be either `float`, `int`, `pd.Timestamp` or `None`" + "`start` must be either `float`, `int`, `pd.Timestamp` or `None`" ), logger, ) - if isinstance(n.start, float): + + if n.start_format == "position": raise_if_not( - 0.0 <= n.start <= 1.0, "`start` should be between 0.0 and 1.0.", logger + isinstance(n.start, (int, np.int64)), + f"Since `start_format='position'`, `start` must be an integer, received {type(n.start)}", + logger, ) - elif isinstance(n.start, (int, np.int64)): + + if isinstance(n.start, float): raise_if_not( - n.start >= 0, "if `start` is an integer, must be `>= 0`.", logger + 0.0 <= n.start <= 1.0, + "if `start` is a float, must be between 0.0 and 1.0.", + logger, ) # verbose error messages @@ -96,20 +107,24 @@ def _historical_forecasts_general_checks(model, series, kwargs): logger, ) elif isinstance(n.start, (int, np.int64)): - if ( - series_.has_datetime_index - or (series_.has_range_index and series_.freq == 1) - ) and n.start >= len(series_): + out_of_bound_error = False + if n.start_format == "position": + if (n.start > 0 and n.start >= len(series_)) or ( + n.start < 0 and np.abs(n.start) > len(series_) + ): + out_of_bound_error = True + elif series_.has_datetime_index: + if n.start >= len(series_): + out_of_bound_error = True + elif n.start < series_.time_index[0]: raise_log( ValueError( - f"`start` index `{n.start}` is out of bounds for series of length {len(series_)} " - f"at index: {idx}." + f"`start` index `{n.start}` is smaller than the first index `{series_.time_index[0]}` " + f"for series at index: {idx}." ), logger, ) - elif ( - series_.has_range_index and series_.freq > 1 - ) and n.start > series_.time_index[-1]: + elif n.start > series_.time_index[-1]: raise_log( ValueError( f"`start` index `{n.start}` is larger than the last index `{series_.time_index[-1]}` " @@ -118,7 +133,20 @@ def _historical_forecasts_general_checks(model, series, kwargs): logger, ) - start = series_.get_timestamp_at_point(n.start) + if out_of_bound_error: + raise_log( + ValueError( + f"`start` index `{n.start}` is out of bounds for series of length {len(series_)} " + f"at index: {idx}." + ), + logger, + ) + + if n.start_format == "value": + start = series_.get_timestamp_at_point(n.start) + else: + start = series_.time_index[n.start] + if n.retrain is not False and start == series_.start_time(): raise_log( ValueError( @@ -368,6 +396,7 @@ def _adjust_historical_forecasts_time_index( forecast_horizon: int, overlap_end: bool, start: Optional[Union[pd.Timestamp, float, int]], + start_format: Literal["position", "value"], show_warnings: bool, ) -> TimeIndex: """ @@ -388,7 +417,10 @@ def _adjust_historical_forecasts_time_index( # when applicable, shift the start of the forecastable index based on `start` if start is not None: - start_time_ = series.get_timestamp_at_point(start) + if start_format == "value": + start_time_ = series.get_timestamp_at_point(start) + else: + start_time_ = series.time_index[start] # ignore user-defined `start` if ( not historical_forecasts_time_index[0] @@ -543,6 +575,7 @@ def _get_historical_forecast_boundaries( past_covariates: Optional[TimeSeries], future_covariates: Optional[TimeSeries], start: Optional[Union[pd.Timestamp, float, int]], + start_format: Literal["position", "value"], forecast_horizon: int, overlap_end: bool, freq: pd.DateOffset, @@ -570,6 +603,7 @@ def _get_historical_forecast_boundaries( forecast_horizon=forecast_horizon, overlap_end=overlap_end, start=start, + start_format=start_format, show_warnings=show_warnings, )