diff --git a/.gitignore b/.gitignore index adba6ab4f..06d74c19e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist .vscode .idea *.gif +*.icloud *.csv */data/* *.parquet diff --git a/nbs/src/core/core.ipynb b/nbs/src/core/core.ipynb index 91d28c4f9..5c8308d38 100644 --- a/nbs/src/core/core.ipynb +++ b/nbs/src/core/core.ipynb @@ -166,14 +166,21 @@ " return False\n", " return np.allclose(self.data, other.data) and np.array_equal(self.indptr, other.indptr)\n", " \n", - " def fit(self, models):\n", + " def fit(self, models, fallback_model=None):\n", " fm = np.full((self.n_groups, len(models)), np.nan, dtype=object)\n", " for i, grp in enumerate(self):\n", " y = grp[:, 0] if grp.ndim == 2 else grp\n", " X = grp[:, 1:] if (grp.ndim == 2 and grp.shape[1] > 1) else None\n", " for i_model, model in enumerate(models):\n", - " new_model = model.new()\n", - " fm[i, i_model] = new_model.fit(y=y, X=X)\n", + " try:\n", + " new_model = model.new()\n", + " fm[i, i_model] = new_model.fit(y=y, X=X)\n", + " except Exception as error:\n", + " if fallback_model is not None:\n", + " new_fallback_model = fallback_model.new()\n", + " fm[i, i_model] = new_fallback_model.fit(y=y, X=X)\n", + " else:\n", + " raise error\n", " return fm\n", " \n", " def _get_cols(self, models, attr, h, X, level=tuple()):\n", @@ -354,9 +361,13 @@ " else:\n", " if i_window == 0:\n", " # for the first window we have to fit each model\n", - " model = model.fit(y=y_train, X=X_train)\n", - " if fallback_model is not None:\n", - " fallback_model = fallback_model.fit(y=y_train, X=X_train)\n", + " try:\n", + " model = model.fit(y=y_train, X=X_train)\n", + " except Exception as error:\n", + " if fallback_model is not None:\n", + " fallback_model = fallback_model.fit(y=y_train, X=X_train)\n", + " else:\n", + " raise error\n", " try:\n", " res_i = model.forward(h=h, y=y_train, X=X_train, \n", " X_future=X_future, fitted=fitted, **kwargs)\n", @@ -682,6 +693,47 @@ "np.testing.assert_array_equal(fcst_cv_f['fitted']['values'], fcst_cv_naive['fitted']['values'])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "e05f8b34", + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# test fallback model under failed fit for cross validation\n", + "class FailedFit:\n", + "\n", + " def __init__(self):\n", + " pass\n", + "\n", + " def forecast(self):\n", + " pass\n", + "\n", + " def fit(self, y, X):\n", + " raise Exception('Failed fit')\n", + "\n", + " def __repr__(self):\n", + " return \"FailedFit\"\n", + "\n", + "fcst_cv_f = ga.cross_validation(\n", + " models=[FailedFit()], \n", + " fallback_model=Naive(), h=2, \n", + " test_size=5,\n", + " refit=False,\n", + " fitted=True,\n", + ")\n", + "fcst_cv_naive = ga.cross_validation(\n", + " models=[Naive()], \n", + " h=2, \n", + " test_size=5,\n", + " refit=False,\n", + " fitted=True,\n", + ")\n", + "test_eq(fcst_cv_f['forecasts'], fcst_cv_naive['forecasts'])\n", + "np.testing.assert_array_equal(fcst_cv_f['fitted']['values'], fcst_cv_naive['fitted']['values'])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1333,7 +1385,7 @@ " self._set_prediction_intervals(prediction_intervals=prediction_intervals)\n", " self._prepare_fit(df, sort_df)\n", " if self.n_jobs == 1:\n", - " self.fitted_ = self.ga.fit(models=self.models)\n", + " self.fitted_ = self.ga.fit(models=self.models, fallback_model=self.fallback_model)\n", " else:\n", " self.fitted_ = self._fit_parallel()\n", " return self\n", @@ -1725,10 +1777,10 @@ " with Pool(self.n_jobs, **pool_kwargs) as executor:\n", " futures = []\n", " for ga in gas:\n", - " future = executor.apply_async(ga.fit, (self.models,))\n", + " future = executor.apply_async(ga.fit, (self.models, self.fallback_model))\n", " futures.append(future)\n", " fm = np.vstack([f.get() for f in futures])\n", - " return fm\n", + " return fm \n", " \n", " def _get_gas_Xs(self, X):\n", " gas = self.ga.split(self.n_jobs)\n", diff --git a/nbs/src/core/models.ipynb b/nbs/src/core/models.ipynb index 5f86d9215..5b2ad2dcf 100644 --- a/nbs/src/core/models.ipynb +++ b/nbs/src/core/models.ipynb @@ -5412,6 +5412,40 @@ " residuals = y - out[\"fitted\"]\n", " sigma = _calculate_sigma(residuals, len(residuals) - 1)\n", " res = _add_fitted_pi(res=res, se=sigma, level=level)\n", + " return res\n", + " \n", + " def forward(\n", + " self, \n", + " y: np.ndarray,\n", + " h: int,\n", + " X: Optional[np.ndarray] = None,\n", + " X_future: Optional[np.ndarray] = None,\n", + " level: Optional[List[int]] = None,\n", + " fitted: bool = False,\n", + " ):\n", + " \"\"\" Apply fitted model to an new/updated series.\n", + "\n", + " Parameters\n", + " ----------\n", + " y : numpy.array \n", + " Clean time series of shape (n,). \n", + " h: int\n", + " Forecast horizon.\n", + " X : array-like\n", + " Optional insample exogenous of shape (t, n_x). \n", + " X_future : array-like\n", + " Optional exogenous of shape (h, n_x). \n", + " level : List[float]\n", + " Confidence levels (0-100) for prediction intervals.\n", + " fitted : bool\n", + " Whether or not to return insample predictions.\n", + "\n", + " Returns\n", + " -------\n", + " forecasts : dict\n", + " Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions.\n", + " \"\"\"\n", + " res = self.forecast(y=y, h=h, X=X, X_future=X_future, level=level, fitted=fitted)\n", " return res" ] }, @@ -5472,6 +5506,23 @@ "_plot_insample_pi(fcst_naive)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# Unit test forward:=forecast\n", + "naive = Naive()\n", + "fcst_naive = naive.forward(ap,12,None,None,(80,95), True)\n", + "np.testing.assert_almost_equal(\n", + " fcst_naive['lo-80'],\n", + " np.array([388.7984, 370.9037, 357.1726, 345.5967, 335.3982, 326.1781, 317.6992, 309.8073, 302.3951, 295.3845, 288.7164, 282.3452]),\n", + " decimal=4\n", + ") # this is almost equal since Hyndman's forecasts are rounded up to 4 decimals" + ] + }, { "cell_type": "code", "execution_count": null, @@ -10880,6 +10931,40 @@ " if fitted:\n", " res[f'fitted-lo-{lv}'] = fitted_vals\n", " res[f'fitted-hi-{lv}'] = fitted_vals\n", + " return res\n", + " \n", + " def forward(\n", + " self,\n", + " y: np.ndarray,\n", + " h: int,\n", + " X: Optional[np.ndarray] = None,\n", + " X_future: Optional[np.ndarray] = None,\n", + " level: Optional[List[int]] = None,\n", + " fitted: bool = False,\n", + " ):\n", + " \"\"\"Apply Constant model predictions to a new/updated time series.\n", + "\n", + " Parameters\n", + " ----------\n", + " y : numpy.array \n", + " Clean time series of shape (n, ). \n", + " h : int \n", + " Forecast horizon.\n", + " X : array-like \n", + " Optional insample exogenous of shape (t, n_x). \n", + " X_future : array-like \n", + " Optional exogenous of shape (h, n_x). \n", + " level : List[float]\n", + " Confidence levels for prediction intervals.\n", + " fitted : bool \n", + " Whether or not returns insample predictions.\n", + "\n", + " Returns\n", + " -------\n", + " forecasts : dict \n", + " Dictionary with entries `constant` for point predictions and `level_*` for probabilistic predictions.\n", + " \"\"\"\n", + " res = self.forecast(y=y, h=h, X=X, X_future=X_future, level=level, fitted=fitted)\n", " return res" ] }, @@ -10904,6 +10989,16 @@ "constant_model.forecast(ap, 12, level=[90, 80])" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "constant_model.forward(ap, 12, level=[90, 80])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -10967,6 +11062,15 @@ "show_doc(ConstantModel.predict_in_sample, title_level=3)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_doc(ConstantModel.forward, title_level=3)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -11035,6 +11139,16 @@ "zero_model.forecast(ap, 12, level=[90, 80])" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "zero_model.forward(ap, 12, level=[90, 80])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -11098,6 +11212,15 @@ "show_doc(ZeroModel.predict_in_sample, title_level=3, name='ZeroModel.predict_in_sample')" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_doc(ZeroModel.forward, title_level=3, name='ZeroModel.forward')" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/statsforecast/_modidx.py b/statsforecast/_modidx.py index 30f335f72..91c4471ea 100644 --- a/statsforecast/_modidx.py +++ b/statsforecast/_modidx.py @@ -371,6 +371,8 @@ 'statsforecast/models.py'), 'statsforecast.models.ConstantModel.forecast': ( 'src/core/models.html#constantmodel.forecast', 'statsforecast/models.py'), + 'statsforecast.models.ConstantModel.forward': ( 'src/core/models.html#constantmodel.forward', + 'statsforecast/models.py'), 'statsforecast.models.ConstantModel.predict': ( 'src/core/models.html#constantmodel.predict', 'statsforecast/models.py'), 'statsforecast.models.ConstantModel.predict_in_sample': ( 'src/core/models.html#constantmodel.predict_in_sample', @@ -498,6 +500,8 @@ 'statsforecast.models.Naive.fit': ('src/core/models.html#naive.fit', 'statsforecast/models.py'), 'statsforecast.models.Naive.forecast': ( 'src/core/models.html#naive.forecast', 'statsforecast/models.py'), + 'statsforecast.models.Naive.forward': ( 'src/core/models.html#naive.forward', + 'statsforecast/models.py'), 'statsforecast.models.Naive.predict': ( 'src/core/models.html#naive.predict', 'statsforecast/models.py'), 'statsforecast.models.Naive.predict_in_sample': ( 'src/core/models.html#naive.predict_in_sample', diff --git a/statsforecast/core.py b/statsforecast/core.py index 44f1e70d7..1b9829190 100644 --- a/statsforecast/core.py +++ b/statsforecast/core.py @@ -61,14 +61,21 @@ def __eq__(self, other): self.indptr, other.indptr ) - def fit(self, models): + def fit(self, models, fallback_model=None): fm = np.full((self.n_groups, len(models)), np.nan, dtype=object) for i, grp in enumerate(self): y = grp[:, 0] if grp.ndim == 2 else grp X = grp[:, 1:] if (grp.ndim == 2 and grp.shape[1] > 1) else None for i_model, model in enumerate(models): - new_model = model.new() - fm[i, i_model] = new_model.fit(y=y, X=X) + try: + new_model = model.new() + fm[i, i_model] = new_model.fit(y=y, X=X) + except Exception as error: + if fallback_model is not None: + new_fallback_model = fallback_model.new() + fm[i, i_model] = new_fallback_model.fit(y=y, X=X) + else: + raise error return fm def _get_cols(self, models, attr, h, X, level=tuple()): @@ -331,11 +338,15 @@ def cross_validation( else: if i_window == 0: # for the first window we have to fit each model - model = model.fit(y=y_train, X=X_train) - if fallback_model is not None: - fallback_model = fallback_model.fit( - y=y_train, X=X_train - ) + try: + model = model.fit(y=y_train, X=X_train) + except Exception as error: + if fallback_model is not None: + fallback_model = fallback_model.fit( + y=y_train, X=X_train + ) + else: + raise error try: res_i = model.forward( h=h, @@ -401,7 +412,7 @@ def split_fm(self, fm, n_chunks): if x.size ] -# %% ../nbs/src/core/core.ipynb 22 +# %% ../nbs/src/core/core.ipynb 23 class DataFrameProcessing: """ A utility to process Pandas or Polars dataframes for time series forecasting. @@ -442,6 +453,7 @@ def __init__( sort_dataframe: bool, validate: Optional[bool] = True, ): + self.dataframe = dataframe self.sort_dataframe = sort_dataframe self.validate = validate @@ -692,7 +704,7 @@ def _check_datetime(self, arr: np.ndarray) -> Union[pd.DatetimeIndex, np.ndarray raise Exception(msg) from e return arr -# %% ../nbs/src/core/core.ipynb 25 +# %% ../nbs/src/core/core.ipynb 26 def _cv_dates(last_dates, freq, h, test_size, step_size=1): # assuming step_size = 1 if (test_size - h) % step_size: @@ -731,7 +743,7 @@ def _cv_dates(last_dates, freq, h, test_size, step_size=1): dates = dates.reset_index(drop=True) return dates -# %% ../nbs/src/core/core.ipynb 29 +# %% ../nbs/src/core/core.ipynb 30 def _get_n_jobs(n_groups, n_jobs): if n_jobs == -1 or (n_jobs is None): actual_n_jobs = cpu_count() @@ -739,7 +751,7 @@ def _get_n_jobs(n_groups, n_jobs): actual_n_jobs = n_jobs return min(n_groups, actual_n_jobs) -# %% ../nbs/src/core/core.ipynb 32 +# %% ../nbs/src/core/core.ipynb 33 def _parse_ds_type(df): dt_col = df["ds"] dt_check = pd.api.types.is_datetime64_any_dtype(dt_col) @@ -757,7 +769,7 @@ def _parse_ds_type(df): raise Exception(msg) from e return df -# %% ../nbs/src/core/core.ipynb 33 +# %% ../nbs/src/core/core.ipynb 34 class _StatsForecast: def __init__( self, @@ -871,7 +883,9 @@ def fit( self._set_prediction_intervals(prediction_intervals=prediction_intervals) self._prepare_fit(df, sort_df) if self.n_jobs == 1: - self.fitted_ = self.ga.fit(models=self.models) + self.fitted_ = self.ga.fit( + models=self.models, fallback_model=self.fallback_model + ) else: self.fitted_ = self._fit_parallel() return self @@ -1300,7 +1314,9 @@ def _fit_parallel(self): with Pool(self.n_jobs, **pool_kwargs) as executor: futures = [] for ga in gas: - future = executor.apply_async(ga.fit, (self.models,)) + future = executor.apply_async( + ga.fit, (self.models, self.fallback_model) + ) futures.append(future) fm = np.vstack([f.get() for f in futures]) return fm @@ -1533,7 +1549,7 @@ def plot( def __repr__(self): return f"StatsForecast(models=[{','.join(map(repr, self.models))}])" -# %% ../nbs/src/core/core.ipynb 34 +# %% ../nbs/src/core/core.ipynb 35 class ParallelBackend: def forecast(self, df, models, freq, fallback_model=None, **kwargs: Any) -> Any: model = _StatsForecast( @@ -1554,7 +1570,7 @@ def cross_validation( def make_backend(obj: Any, *args: Any, **kwargs: Any) -> ParallelBackend: return ParallelBackend() -# %% ../nbs/src/core/core.ipynb 35 +# %% ../nbs/src/core/core.ipynb 36 class StatsForecast(_StatsForecast): """Train statistical models. diff --git a/statsforecast/models.py b/statsforecast/models.py index 479757de2..8d7a81037 100644 --- a/statsforecast/models.py +++ b/statsforecast/models.py @@ -2550,6 +2550,7 @@ def __init__( alias: str = "Holt", prediction_intervals: Optional[ConformalIntervals] = None, ): + self.season_length = season_length self.error_type = error_type self.alias = alias @@ -2748,6 +2749,7 @@ def forecast( level: Optional[List[int]] = None, fitted: bool = False, ): + """Memory Efficient HistoricAverage predictions. This method avoids memory burden due from object storage. @@ -2969,7 +2971,43 @@ def forecast( res = _add_fitted_pi(res=res, se=sigma, level=level) return res -# %% ../nbs/src/core/models.ipynb 217 + def forward( + self, + y: np.ndarray, + h: int, + X: Optional[np.ndarray] = None, + X_future: Optional[np.ndarray] = None, + level: Optional[List[int]] = None, + fitted: bool = False, + ): + """Apply fitted model to an new/updated series. + + Parameters + ---------- + y : numpy.array + Clean time series of shape (n,). + h: int + Forecast horizon. + X : array-like + Optional insample exogenous of shape (t, n_x). + X_future : array-like + Optional exogenous of shape (h, n_x). + level : List[float] + Confidence levels (0-100) for prediction intervals. + fitted : bool + Whether or not to return insample predictions. + + Returns + ------- + forecasts : dict + Dictionary with entries `mean` for point predictions and `level_*` for probabilistic predictions. + """ + res = self.forecast( + y=y, h=h, X=X, X_future=X_future, level=level, fitted=fitted + ) + return res + +# %% ../nbs/src/core/models.ipynb 218 @njit(nogil=NOGIL, cache=CACHE) def _random_walk_with_drift( y: np.ndarray, # time series @@ -2989,7 +3027,7 @@ def _random_walk_with_drift( fcst["fitted"] = fitted_vals return fcst -# %% ../nbs/src/core/models.ipynb 218 +# %% ../nbs/src/core/models.ipynb 219 class RandomWalkWithDrift(_TS): def __init__( self, @@ -3166,7 +3204,7 @@ def forecast( return res -# %% ../nbs/src/core/models.ipynb 232 +# %% ../nbs/src/core/models.ipynb 233 class SeasonalNaive(_TS): def __init__( self, @@ -3355,7 +3393,7 @@ def forecast( return res -# %% ../nbs/src/core/models.ipynb 246 +# %% ../nbs/src/core/models.ipynb 247 @njit(nogil=NOGIL, cache=CACHE) def _window_average( y: np.ndarray, # time series @@ -3371,7 +3409,7 @@ def _window_average( mean = _repeat_val(val=wavg, h=h) return {"mean": mean} -# %% ../nbs/src/core/models.ipynb 247 +# %% ../nbs/src/core/models.ipynb 248 class WindowAverage(_TS): def __init__( self, @@ -3529,7 +3567,7 @@ def forecast( raise Exception("You must pass `prediction_intervals` to " "compute them.") return res -# %% ../nbs/src/core/models.ipynb 258 +# %% ../nbs/src/core/models.ipynb 259 @njit(nogil=NOGIL, cache=CACHE) def _seasonal_window_average( y: np.ndarray, @@ -3550,7 +3588,7 @@ def _seasonal_window_average( out = _repeat_val_seas(season_vals=season_avgs, h=h, season_length=season_length) return {"mean": out} -# %% ../nbs/src/core/models.ipynb 259 +# %% ../nbs/src/core/models.ipynb 260 class SeasonalWindowAverage(_TS): def __init__( self, @@ -3724,7 +3762,7 @@ def forecast( raise Exception("You must pass `prediction_intervals` to compute them.") return res -# %% ../nbs/src/core/models.ipynb 271 +# %% ../nbs/src/core/models.ipynb 272 def _adida( y: np.ndarray, # time series h: int, # forecasting horizon @@ -3745,7 +3783,7 @@ def _adida( mean = _repeat_val(val=forecast, h=h) return {"mean": mean} -# %% ../nbs/src/core/models.ipynb 272 +# %% ../nbs/src/core/models.ipynb 273 class ADIDA(_TS): def __init__( self, @@ -3907,7 +3945,7 @@ def forecast( ) return res -# %% ../nbs/src/core/models.ipynb 284 +# %% ../nbs/src/core/models.ipynb 285 @njit(nogil=NOGIL, cache=CACHE) def _croston_classic( y: np.ndarray, # time series @@ -3929,7 +3967,7 @@ def _croston_classic( mean = _repeat_val(val=mean, h=h) return {"mean": mean} -# %% ../nbs/src/core/models.ipynb 285 +# %% ../nbs/src/core/models.ipynb 286 class CrostonClassic(_TS): def __init__( self, @@ -4086,7 +4124,7 @@ def forecast( ) return res -# %% ../nbs/src/core/models.ipynb 296 +# %% ../nbs/src/core/models.ipynb 297 def _croston_optimized( y: np.ndarray, # time series h: int, # forecasting horizon @@ -4107,7 +4145,7 @@ def _croston_optimized( mean = _repeat_val(val=mean, h=h) return {"mean": mean} -# %% ../nbs/src/core/models.ipynb 297 +# %% ../nbs/src/core/models.ipynb 298 class CrostonOptimized(_TS): def __init__( self, @@ -4260,7 +4298,7 @@ def forecast( raise Exception("You must pass `prediction_intervals` to compute them.") return res -# %% ../nbs/src/core/models.ipynb 308 +# %% ../nbs/src/core/models.ipynb 309 @njit(nogil=NOGIL, cache=CACHE) def _croston_sba( y: np.ndarray, # time series @@ -4273,7 +4311,7 @@ def _croston_sba( mean["mean"] *= 0.95 return mean -# %% ../nbs/src/core/models.ipynb 309 +# %% ../nbs/src/core/models.ipynb 310 class CrostonSBA(_TS): def __init__( self, @@ -4432,7 +4470,7 @@ def forecast( ) return res -# %% ../nbs/src/core/models.ipynb 320 +# %% ../nbs/src/core/models.ipynb 321 def _imapa( y: np.ndarray, # time series h: int, # forecasting horizon @@ -4456,7 +4494,7 @@ def _imapa( mean = _repeat_val(val=forecast, h=h) return {"mean": mean} -# %% ../nbs/src/core/models.ipynb 321 +# %% ../nbs/src/core/models.ipynb 322 class IMAPA(_TS): def __init__( self, @@ -4612,7 +4650,7 @@ def forecast( ) return res -# %% ../nbs/src/core/models.ipynb 332 +# %% ../nbs/src/core/models.ipynb 333 @njit(nogil=NOGIL, cache=CACHE) def _tsb( y: np.ndarray, # time series @@ -4633,7 +4671,7 @@ def _tsb( mean = _repeat_val(val=forecast, h=h) return {"mean": mean} -# %% ../nbs/src/core/models.ipynb 333 +# %% ../nbs/src/core/models.ipynb 334 class TSB(_TS): def __init__( self, @@ -4800,7 +4838,7 @@ def forecast( raise Exception("You must pass `prediction_intervals` to compute them.") return res -# %% ../nbs/src/core/models.ipynb 345 +# %% ../nbs/src/core/models.ipynb 346 def _predict_mstl_seas(mstl_ob, h, season_length): seasoncolumns = mstl_ob.filter(regex="seasonal*").columns nseasons = len(seasoncolumns) @@ -4817,7 +4855,7 @@ def _predict_mstl_seas(mstl_ob, h, season_length): lastseas = seascomp.sum(axis=1) return lastseas -# %% ../nbs/src/core/models.ipynb 346 +# %% ../nbs/src/core/models.ipynb 347 class MSTL(_TS): """MSTL model. @@ -4853,6 +4891,7 @@ def __init__( alias: str = "MSTL", prediction_intervals: Optional[ConformalIntervals] = None, ): + # check ETS model doesnt have seasonality if repr(trend_forecaster) == "AutoETS": if trend_forecaster.model[2] != "N": @@ -5091,7 +5130,7 @@ def forward( } return res -# %% ../nbs/src/core/models.ipynb 362 +# %% ../nbs/src/core/models.ipynb 363 class Theta(AutoTheta): """Standard Theta Method. @@ -5127,7 +5166,7 @@ def __init__( prediction_intervals=prediction_intervals, ) -# %% ../nbs/src/core/models.ipynb 375 +# %% ../nbs/src/core/models.ipynb 376 class OptimizedTheta(AutoTheta): """Optimized Theta Method. @@ -5163,7 +5202,7 @@ def __init__( prediction_intervals=prediction_intervals, ) -# %% ../nbs/src/core/models.ipynb 388 +# %% ../nbs/src/core/models.ipynb 389 class DynamicTheta(AutoTheta): """Dynamic Standard Theta Method. @@ -5199,7 +5238,7 @@ def __init__( prediction_intervals=prediction_intervals, ) -# %% ../nbs/src/core/models.ipynb 401 +# %% ../nbs/src/core/models.ipynb 402 class DynamicOptimizedTheta(AutoTheta): """Dynamic Optimized Theta Method. @@ -5235,7 +5274,7 @@ def __init__( prediction_intervals=prediction_intervals, ) -# %% ../nbs/src/core/models.ipynb 415 +# %% ../nbs/src/core/models.ipynb 416 class GARCH(_TS): """Generalized Autoregressive Conditional Heteroskedasticity (GARCH) model. @@ -5429,7 +5468,7 @@ def forecast( res = _add_fitted_pi(res=res, se=se, level=level) return res -# %% ../nbs/src/core/models.ipynb 428 +# %% ../nbs/src/core/models.ipynb 429 class ARCH(GARCH): """Autoregressive Conditional Heteroskedasticity (ARCH) model. @@ -5475,7 +5514,7 @@ def __init__( def __repr__(self): return self.alias -# %% ../nbs/src/core/models.ipynb 439 +# %% ../nbs/src/core/models.ipynb 440 class ConstantModel(_TS): def __init__(self, constant: float, alias: str = "ConstantModel"): """Constant Model. @@ -5624,7 +5663,43 @@ def forecast( res[f"fitted-hi-{lv}"] = fitted_vals return res -# %% ../nbs/src/core/models.ipynb 450 + def forward( + self, + y: np.ndarray, + h: int, + X: Optional[np.ndarray] = None, + X_future: Optional[np.ndarray] = None, + level: Optional[List[int]] = None, + fitted: bool = False, + ): + """Apply Constant model predictions to a new/updated time series. + + Parameters + ---------- + y : numpy.array + Clean time series of shape (n, ). + h : int + Forecast horizon. + X : array-like + Optional insample exogenous of shape (t, n_x). + X_future : array-like + Optional exogenous of shape (h, n_x). + level : List[float] + Confidence levels for prediction intervals. + fitted : bool + Whether or not returns insample predictions. + + Returns + ------- + forecasts : dict + Dictionary with entries `constant` for point predictions and `level_*` for probabilistic predictions. + """ + res = self.forecast( + y=y, h=h, X=X, X_future=X_future, level=level, fitted=fitted + ) + return res + +# %% ../nbs/src/core/models.ipynb 453 class ZeroModel(ConstantModel): def __init__(self, alias: str = "ZeroModel"): """Returns Zero forecasts. @@ -5638,7 +5713,7 @@ def __init__(self, alias: str = "ZeroModel"): """ super().__init__(constant=0, alias=alias) -# %% ../nbs/src/core/models.ipynb 461 +# %% ../nbs/src/core/models.ipynb 466 class NaNModel(ConstantModel): def __init__(self, alias: str = "NaNModel"): """NaN Model.