spotforecast2 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. spotforecast2/.DS_Store +0 -0
  2. spotforecast2/__init__.py +2 -0
  3. spotforecast2/data/__init__.py +0 -0
  4. spotforecast2/data/data.py +130 -0
  5. spotforecast2/data/fetch_data.py +209 -0
  6. spotforecast2/exceptions.py +681 -0
  7. spotforecast2/forecaster/.DS_Store +0 -0
  8. spotforecast2/forecaster/__init__.py +7 -0
  9. spotforecast2/forecaster/base.py +448 -0
  10. spotforecast2/forecaster/metrics.py +527 -0
  11. spotforecast2/forecaster/recursive/__init__.py +4 -0
  12. spotforecast2/forecaster/recursive/_forecaster_equivalent_date.py +1075 -0
  13. spotforecast2/forecaster/recursive/_forecaster_recursive.py +939 -0
  14. spotforecast2/forecaster/recursive/_warnings.py +15 -0
  15. spotforecast2/forecaster/utils.py +954 -0
  16. spotforecast2/model_selection/__init__.py +5 -0
  17. spotforecast2/model_selection/bayesian_search.py +453 -0
  18. spotforecast2/model_selection/grid_search.py +314 -0
  19. spotforecast2/model_selection/random_search.py +151 -0
  20. spotforecast2/model_selection/split_base.py +357 -0
  21. spotforecast2/model_selection/split_one_step.py +245 -0
  22. spotforecast2/model_selection/split_ts_cv.py +634 -0
  23. spotforecast2/model_selection/utils_common.py +718 -0
  24. spotforecast2/model_selection/utils_metrics.py +103 -0
  25. spotforecast2/model_selection/validation.py +685 -0
  26. spotforecast2/preprocessing/__init__.py +30 -0
  27. spotforecast2/preprocessing/_binner.py +378 -0
  28. spotforecast2/preprocessing/_common.py +123 -0
  29. spotforecast2/preprocessing/_differentiator.py +123 -0
  30. spotforecast2/preprocessing/_rolling.py +136 -0
  31. spotforecast2/preprocessing/curate_data.py +254 -0
  32. spotforecast2/preprocessing/imputation.py +92 -0
  33. spotforecast2/preprocessing/outlier.py +114 -0
  34. spotforecast2/preprocessing/split.py +139 -0
  35. spotforecast2/py.typed +0 -0
  36. spotforecast2/utils/__init__.py +43 -0
  37. spotforecast2/utils/convert_to_utc.py +44 -0
  38. spotforecast2/utils/data_transform.py +208 -0
  39. spotforecast2/utils/forecaster_config.py +344 -0
  40. spotforecast2/utils/generate_holiday.py +70 -0
  41. spotforecast2/utils/validation.py +569 -0
  42. spotforecast2/weather/__init__.py +0 -0
  43. spotforecast2/weather/weather_client.py +288 -0
  44. spotforecast2-0.0.1.dist-info/METADATA +47 -0
  45. spotforecast2-0.0.1.dist-info/RECORD +46 -0
  46. spotforecast2-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,685 @@
1
+ from __future__ import annotations
2
+ from typing import Callable
3
+ from copy import deepcopy
4
+ import warnings
5
+ import numpy as np
6
+ import pandas as pd
7
+ from joblib import Parallel, delayed, cpu_count
8
+ from tqdm.auto import tqdm
9
+
10
+ from spotforecast2.forecaster.metrics import add_y_train_argument, _get_metric
11
+ from spotforecast2.exceptions import (
12
+ LongTrainingWarning,
13
+ IgnoredArgumentWarning,
14
+ )
15
+ from spotforecast2.model_selection.split_ts_cv import TimeSeriesFold
16
+ from spotforecast2.model_selection.utils_common import (
17
+ check_backtesting_input,
18
+ select_n_jobs_backtesting,
19
+ )
20
+ from spotforecast2.forecaster.utils import set_skforecast_warnings
21
+
22
+
23
+ def _backtesting_forecaster(
24
+ forecaster: object,
25
+ y: pd.Series,
26
+ cv: TimeSeriesFold,
27
+ metric: str | Callable | list[str | Callable],
28
+ exog: pd.Series | pd.DataFrame | None = None,
29
+ interval: float | list[float] | tuple[float] | str | object | None = None,
30
+ interval_method: str = "bootstrapping",
31
+ n_boot: int = 250,
32
+ use_in_sample_residuals: bool = True,
33
+ use_binned_residuals: bool = True,
34
+ random_state: int = 123,
35
+ return_predictors: bool = False,
36
+ n_jobs: int | str = "auto",
37
+ verbose: bool = False,
38
+ show_progress: bool = True,
39
+ suppress_warnings: bool = False,
40
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
41
+ """
42
+ Backtesting of forecaster model following the folds generated by the TimeSeriesFold
43
+ class and using the metric(s) provided.
44
+
45
+ If `forecaster` is already trained and `initial_train_size` is set to `None` in the
46
+ TimeSeriesFold class, no initial train will be done and all data will be used
47
+ to evaluate the model. However, the first `len(forecaster.last_window)` observations
48
+ are needed to create the initial predictors, so no predictions are calculated for
49
+ them.
50
+
51
+ A copy of the original forecaster is created so that it is not modified during the
52
+ process.
53
+
54
+ Args:
55
+ forecaster (ForecasterRecursive, ForecasterDirect, ForecasterEquivalentDate):
56
+ Forecaster model.
57
+ y (pd.Series): Training time series.
58
+ cv (TimeSeriesFold): TimeSeriesFold object with the information needed to
59
+ split the data into folds.
60
+ metric (str | Callable | list): Metric used to quantify the goodness of fit
61
+ of the model.
62
+
63
+ - If `str`: {'mean_squared_error', 'mean_absolute_error',
64
+ 'mean_absolute_percentage_error', 'mean_squared_log_error',
65
+ 'mean_absolute_scaled_error', 'root_mean_squared_scaled_error'}
66
+ - If `Callable`: Function with arguments `y_true`, `y_pred` and `y_train`
67
+ (Optional) that returns a float.
68
+ - If `list`: List containing multiple strings and/or Callables.
69
+ exog (pd.Series | pd.DataFrame, optional): Exogenous variable/s included as
70
+ predictor/s. Must have the same number of observations as `y` and should
71
+ be aligned so that y[i] is regressed on exog[i]. Defaults to None.
72
+ interval (float | list | tuple | str | object, optional): Specifies whether
73
+ probabilistic predictions should be estimated and the method to use.
74
+ The following options are supported:
75
+
76
+ - If `float`, represents the nominal (expected) coverage (between 0 and 1).
77
+ For instance, `interval=0.95` corresponds to `[2.5, 97.5]` percentiles.
78
+ - If `list` or `tuple`: Sequence of percentiles to compute, each value must
79
+ be between 0 and 100 inclusive. For example, a 95% confidence interval can
80
+ be specified as `interval = [2.5, 97.5]` or multiple percentiles (e.g. 10,
81
+ 50 and 90) as `interval = [10, 50, 90]`.
82
+ - If 'bootstrapping' (str): `n_boot` bootstrapping predictions will be
83
+ generated.
84
+ - If scipy.stats distribution object, the distribution parameters will
85
+ be estimated for each prediction.
86
+ - If None, no probabilistic predictions are estimated.
87
+ Defaults to None.
88
+ interval_method (str, optional): Technique used to estimate prediction
89
+ intervals. Available options:
90
+
91
+ - 'bootstrapping': Bootstrapping is used to generate prediction intervals.
92
+ - 'conformal': Employs the conformal prediction split method for
93
+ interval estimation.
94
+ Defaults to 'bootstrapping'.
95
+ n_boot (int, optional): Number of bootstrapping iterations to perform when
96
+ estimating prediction intervals. Defaults to 250.
97
+ use_in_sample_residuals (bool, optional): If `True`, residuals from the
98
+ training data are used as proxy of prediction error to create predictions.
99
+ If `False`, out of sample residuals (calibration) are used.
100
+ Out-of-sample residuals must be precomputed using Forecaster's
101
+ `set_out_sample_residuals()` method. Defaults to True.
102
+ use_binned_residuals (bool, optional): If `True`, residuals are selected
103
+ based on the predicted values (binned selection).
104
+ If `False`, residuals are selected randomly. Defaults to True.
105
+ random_state (int, optional): Seed for the random number generator to
106
+ ensure reproducibility. Defaults to 123.
107
+ return_predictors (bool, optional): If `True`, the predictors used to make
108
+ the predictions are also returned. Defaults to False.
109
+ n_jobs (int | str, optional): The number of jobs to run in parallel. If `-1`,
110
+ then the number of jobs is set to the number of cores. If 'auto', `n_jobs`
111
+ is set using the function `skforecast.utils.select_n_jobs_backtesting`.
112
+ Defaults to 'auto'.
113
+ verbose (bool, optional): Print number of folds and index of training and
114
+ validation sets used for backtesting. Defaults to False.
115
+ show_progress (bool, optional): Whether to show a progress bar.
116
+ Defaults to True.
117
+ suppress_warnings (bool, optional): If `True`, spotforecast warnings will be
118
+ suppressed during the backtesting process. See
119
+ `spotforecast.exceptions.warn_skforecast_categories` for more information.
120
+ Defaults to False.
121
+
122
+ Returns:
123
+ tuple (pd.DataFrame, pd.DataFrame):
124
+ - metric_values: Value(s) of the metric(s).
125
+ - backtest_predictions: Value of predictions. The DataFrame includes
126
+ the following columns:
127
+
128
+ - fold: Indicates the fold number where the prediction was made.
129
+ - pred: Predicted values for the corresponding series and time steps.
130
+
131
+ If `interval` is not `None`, additional columns are included depending
132
+ on the method:
133
+
134
+ - For `float`: Columns `lower_bound` and `upper_bound`.
135
+ - For `list` or `tuple` of 2 elements: Columns `lower_bound` and
136
+ `upper_bound`.
137
+ - For `list` or `tuple` with multiple percentiles: One column per
138
+ percentile (e.g., `p_10`, `p_50`, `p_90`).
139
+ - For `'bootstrapping'`: One column per bootstrapping iteration
140
+ (e.g., `pred_boot_0`, `pred_boot_1`, ..., `pred_boot_n`).
141
+ - For `scipy.stats` distribution objects: One column for each
142
+ estimated parameter of the distribution (e.g., `loc`, `scale`).
143
+
144
+ If `return_predictors` is `True`, one column per predictor is created.
145
+
146
+ Depending on the relation between `steps` and `fold_stride`, the output
147
+ may include repeated indexes (if `fold_stride < steps`) or gaps
148
+ (if `fold_stride > steps`). See Notes below for more details.
149
+
150
+ Notes:
151
+ Note on `fold_stride` vs. `steps`:
152
+
153
+ - If `fold_stride == steps`, test sets are placed back-to-back without overlap.
154
+ Each observation appears only once in the output DataFrame, so the
155
+ index is unique.
156
+ - If `fold_stride < steps`, test sets overlap. Multiple forecasts are
157
+ generated for the same observations and, therefore, the output
158
+ DataFrame contains repeated indexes.
159
+ - If `fold_stride > steps`, there are gaps between consecutive test sets.
160
+ Some observations in the series will not have associated predictions,
161
+ so the output DataFrame has non-contiguous indexes.
162
+
163
+ References:
164
+ .. [1] Forecasting: Principles and Practice (3rd ed) Rob J Hyndman and
165
+ George Athanasopoulos. https://otexts.com/fpp3/prediction-intervals.html
166
+ .. [2] MAPIE - Model Agnostic Prediction Interval Estimator.
167
+ https://mapie.readthedocs.io/en/stable/theoretical_description_regression.html#the-split-method
168
+ """
169
+
170
+ set_skforecast_warnings(suppress_warnings, action="ignore")
171
+
172
+ forecaster = deepcopy(forecaster)
173
+ is_regression = forecaster.__spotforecast_tags__["forecaster_task"] == "regression"
174
+ cv = deepcopy(cv)
175
+
176
+ cv.set_params(
177
+ {
178
+ "window_size": forecaster.window_size,
179
+ "differentiation": forecaster.differentiation_max,
180
+ "return_all_indexes": False,
181
+ "verbose": verbose,
182
+ }
183
+ )
184
+
185
+ refit = cv.refit
186
+ overlapping_folds = cv.overlapping_folds
187
+
188
+ if n_jobs == "auto":
189
+ n_jobs = select_n_jobs_backtesting(forecaster=forecaster, refit=refit)
190
+ elif not isinstance(refit, bool) and refit != 1 and n_jobs != 1:
191
+ warnings.warn(
192
+ "If `refit` is an integer other than 1 (intermittent refit). `n_jobs` "
193
+ "is set to 1 to avoid unexpected results during parallelization.",
194
+ IgnoredArgumentWarning,
195
+ )
196
+ n_jobs = 1
197
+ else:
198
+ n_jobs = n_jobs if n_jobs > 0 else cpu_count()
199
+
200
+ if not isinstance(metric, list):
201
+ metrics = [
202
+ (
203
+ _get_metric(metric=metric)
204
+ if isinstance(metric, str)
205
+ else add_y_train_argument(metric)
206
+ )
207
+ ]
208
+ else:
209
+ metrics = [
210
+ _get_metric(metric=m) if isinstance(m, str) else add_y_train_argument(m)
211
+ for m in metric
212
+ ]
213
+
214
+ store_in_sample_residuals = True if use_in_sample_residuals else False
215
+ if interval is None:
216
+ forecaster._probabilistic_mode = False
217
+ elif use_binned_residuals:
218
+ forecaster._probabilistic_mode = "binned"
219
+ else:
220
+ forecaster._probabilistic_mode = "no_binned"
221
+
222
+ folds = cv.split(X=y, as_pandas=False)
223
+ initial_train_size = cv.initial_train_size
224
+ window_size = cv.window_size
225
+ gap = cv.gap
226
+
227
+ if initial_train_size is not None:
228
+ # NOTE: This allows for parallelization when `refit` is `False`. The initial
229
+ # Forecaster fit occurs outside of the auxiliary function.
230
+ exog_train = exog.iloc[:initial_train_size,] if exog is not None else None
231
+ forecaster.fit(
232
+ y=y.iloc[:initial_train_size,],
233
+ exog=exog_train,
234
+ store_in_sample_residuals=store_in_sample_residuals,
235
+ )
236
+ folds[0][5] = False
237
+
238
+ if refit:
239
+ n_of_fits = int(len(folds) / refit)
240
+ if type(forecaster).__name__ != "ForecasterDirect" and n_of_fits > 50:
241
+ warnings.warn(
242
+ f"The forecaster will be fit {n_of_fits} times. This can take substantial"
243
+ f" amounts of time. If not feasible, try with `refit = False`.\n",
244
+ LongTrainingWarning,
245
+ )
246
+ elif (
247
+ type(forecaster).__name__ == "ForecasterDirect"
248
+ and n_of_fits * forecaster.max_step > 50
249
+ ):
250
+ warnings.warn(
251
+ f"The forecaster will be fit {n_of_fits * forecaster.max_step} times "
252
+ f"({n_of_fits} folds * {forecaster.max_step} estimators). This can take "
253
+ f"substantial amounts of time. If not feasible, try with `refit = False`.\n",
254
+ LongTrainingWarning,
255
+ )
256
+
257
+ if show_progress:
258
+ folds = tqdm(folds)
259
+
260
+ def _fit_predict_forecaster(
261
+ fold,
262
+ forecaster,
263
+ y,
264
+ exog,
265
+ store_in_sample_residuals,
266
+ gap,
267
+ interval,
268
+ interval_method,
269
+ n_boot,
270
+ use_in_sample_residuals,
271
+ use_binned_residuals,
272
+ random_state,
273
+ return_predictors,
274
+ is_regression,
275
+ ) -> pd.DataFrame:
276
+ """
277
+ Fit the forecaster and predict `steps` ahead. This is an auxiliary
278
+ function used to parallelize the backtesting_forecaster function.
279
+ """
280
+
281
+ train_iloc_start = fold[1][0]
282
+ train_iloc_end = fold[1][1]
283
+ last_window_iloc_start = fold[2][0]
284
+ last_window_iloc_end = fold[2][1]
285
+ test_iloc_start = fold[3][0]
286
+ test_iloc_end = fold[3][1]
287
+
288
+ if fold[5] is False:
289
+ # When the model is not fitted, last_window must be updated to include
290
+ # the data needed to make predictions.
291
+ last_window_y = y.iloc[last_window_iloc_start:last_window_iloc_end]
292
+ else:
293
+ # The model is fitted before making predictions. If `fixed_train_size`
294
+ # the train size doesn't increase but moves by `steps` in each iteration.
295
+ # If `False` the train size increases by `steps` in each iteration.
296
+ y_train = y.iloc[train_iloc_start:train_iloc_end,]
297
+ exog_train = (
298
+ exog.iloc[train_iloc_start:train_iloc_end,]
299
+ if exog is not None
300
+ else None
301
+ )
302
+ last_window_y = None
303
+ forecaster.fit(
304
+ y=y_train,
305
+ exog=exog_train,
306
+ store_in_sample_residuals=store_in_sample_residuals,
307
+ )
308
+
309
+ next_window_exog = (
310
+ exog.iloc[test_iloc_start:test_iloc_end,] if exog is not None else None
311
+ )
312
+
313
+ steps = len(range(test_iloc_start, test_iloc_end))
314
+ if type(forecaster).__name__ == "ForecasterDirect" and gap > 0:
315
+ # Select only the steps that need to be predicted if gap > 0
316
+ test_no_gap_iloc_start = fold[4][0]
317
+ test_no_gap_iloc_end = fold[4][1]
318
+ n_steps = test_no_gap_iloc_end - test_no_gap_iloc_start
319
+ steps = list(range(gap + 1, gap + 1 + n_steps))
320
+
321
+ preds = []
322
+ if is_regression:
323
+ if interval is not None:
324
+ kwargs_interval = {
325
+ "steps": steps,
326
+ "last_window": last_window_y,
327
+ "exog": next_window_exog,
328
+ "n_boot": n_boot,
329
+ "use_in_sample_residuals": use_in_sample_residuals,
330
+ "use_binned_residuals": use_binned_residuals,
331
+ "random_state": random_state,
332
+ }
333
+ if interval_method == "bootstrapping":
334
+ if interval == "bootstrapping":
335
+ pred = forecaster.predict_bootstrapping(**kwargs_interval)
336
+ elif isinstance(interval, (list, tuple)):
337
+ quantiles = [q / 100 for q in interval]
338
+ pred = forecaster.predict_quantiles(
339
+ quantiles=quantiles, **kwargs_interval
340
+ )
341
+ if len(interval) == 2:
342
+ pred.columns = ["lower_bound", "upper_bound"]
343
+ else:
344
+ pred.columns = [f"p_{p}" for p in interval]
345
+ else:
346
+ pred = forecaster.predict_dist(
347
+ distribution=interval, **kwargs_interval
348
+ )
349
+
350
+ preds.append(pred)
351
+ else:
352
+ pred = forecaster.predict_interval(
353
+ method="conformal", interval=interval, **kwargs_interval
354
+ )
355
+ preds.append(pred)
356
+
357
+ # NOTE: This is done after probabilistic predictions to avoid repeating
358
+ # the same checks.
359
+ if interval is None or interval_method != "conformal":
360
+ pred = forecaster.predict(
361
+ steps=steps,
362
+ last_window=last_window_y,
363
+ exog=next_window_exog,
364
+ check_inputs=True if interval is None else False,
365
+ )
366
+ preds.insert(0, pred)
367
+ else:
368
+ pred = forecaster.predict_proba(
369
+ steps=steps, last_window=last_window_y, exog=next_window_exog
370
+ )
371
+ preds.append(pred)
372
+
373
+ if return_predictors:
374
+ pred = forecaster.create_predict_X(
375
+ steps=steps,
376
+ last_window=last_window_y,
377
+ exog=next_window_exog,
378
+ check_inputs=False,
379
+ )
380
+ preds.append(pred)
381
+
382
+ if len(preds) == 1:
383
+ pred = preds[0]
384
+ else:
385
+ pred = pd.concat(preds, axis=1)
386
+
387
+ if type(forecaster).__name__ != "ForecasterDirect" and gap > 0:
388
+ pred = pred.iloc[gap:,]
389
+
390
+ return pred
391
+
392
+ kwargs_fit_predict_forecaster = {
393
+ "forecaster": forecaster,
394
+ "y": y,
395
+ "exog": exog,
396
+ "store_in_sample_residuals": store_in_sample_residuals,
397
+ "gap": gap,
398
+ "interval": interval,
399
+ "interval_method": interval_method,
400
+ "n_boot": n_boot,
401
+ "use_in_sample_residuals": use_in_sample_residuals,
402
+ "use_binned_residuals": use_binned_residuals,
403
+ "random_state": random_state,
404
+ "return_predictors": return_predictors,
405
+ "is_regression": is_regression,
406
+ }
407
+ backtest_predictions = Parallel(n_jobs=n_jobs)(
408
+ delayed(_fit_predict_forecaster)(fold=fold, **kwargs_fit_predict_forecaster)
409
+ for fold in folds
410
+ )
411
+ fold_labels = [
412
+ np.repeat(fold[0], backtest_predictions[i].shape[0])
413
+ for i, fold in enumerate(folds)
414
+ ]
415
+
416
+ backtest_predictions = pd.concat(backtest_predictions)
417
+ if isinstance(backtest_predictions, pd.Series):
418
+ backtest_predictions = backtest_predictions.to_frame()
419
+
420
+ if not is_regression:
421
+ proba_cols = [f"{cls}_proba" for cls in forecaster.classes_]
422
+ idx_max = backtest_predictions[proba_cols].to_numpy().argmax(axis=1)
423
+ backtest_predictions.insert(0, "pred", np.array(forecaster.classes_)[idx_max])
424
+
425
+ backtest_predictions.insert(0, "fold", np.concatenate(fold_labels))
426
+
427
+ train_indexes = []
428
+ for i, fold in enumerate(folds):
429
+ fit_fold = fold[-1]
430
+ if i == 0 or fit_fold:
431
+ # NOTE: When using a scaled metric, `y_train` doesn't include the
432
+ # first window_size observations used to create the predictors and/or
433
+ # rolling features.
434
+ train_iloc_start = fold[1][0] + window_size
435
+ train_iloc_end = fold[1][1]
436
+ train_indexes.append(np.arange(train_iloc_start, train_iloc_end))
437
+
438
+ train_indexes = np.unique(np.concatenate(train_indexes))
439
+ y_train = y.iloc[train_indexes]
440
+
441
+ backtest_predictions_for_metrics = backtest_predictions
442
+ if overlapping_folds:
443
+ backtest_predictions_for_metrics = backtest_predictions_for_metrics.loc[
444
+ ~backtest_predictions_for_metrics.index.duplicated(keep="last")
445
+ ]
446
+
447
+ y_true = y.loc[backtest_predictions_for_metrics.index]
448
+ y_pred = backtest_predictions_for_metrics["pred"]
449
+ metric_values = [
450
+ [m(y_true=y_true, y_pred=y_pred, y_train=y_train) for m in metrics]
451
+ ]
452
+
453
+ metric_values = pd.DataFrame(
454
+ data=metric_values, columns=[m.__name__ for m in metrics]
455
+ )
456
+
457
+ set_skforecast_warnings(suppress_warnings, action="default")
458
+
459
+ return metric_values, backtest_predictions
460
+
461
+
462
+ def backtesting_forecaster(
463
+ forecaster: object,
464
+ y: pd.Series,
465
+ cv: TimeSeriesFold,
466
+ metric: str | Callable | list[str | Callable],
467
+ exog: pd.Series | pd.DataFrame | None = None,
468
+ interval: float | list[float] | tuple[float] | str | object | None = None,
469
+ interval_method: str = "bootstrapping",
470
+ n_boot: int = 250,
471
+ use_in_sample_residuals: bool = True,
472
+ use_binned_residuals: bool = True,
473
+ random_state: int = 123,
474
+ return_predictors: bool = False,
475
+ n_jobs: int | str = "auto",
476
+ verbose: bool = False,
477
+ show_progress: bool = True,
478
+ suppress_warnings: bool = False,
479
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
480
+ """
481
+ Backtesting of forecaster model following the folds generated by the TimeSeriesFold
482
+ class and using the metric(s) provided.
483
+
484
+ If `forecaster` is already trained and `initial_train_size` is set to `None` in the
485
+ TimeSeriesFold class, no initial train will be done and all data will be used
486
+ to evaluate the model. However, the first `len(forecaster.last_window)` observations
487
+ are needed to create the initial predictors, so no predictions are calculated for
488
+ them.
489
+
490
+ A copy of the original forecaster is created so that it is not modified during
491
+ the process.
492
+
493
+ Args:
494
+ forecaster (ForecasterRecursive, ForecasterDirect, ForecasterEquivalentDate):
495
+ Forecaster model.
496
+ y (pd.Series): Training time series.
497
+ cv (TimeSeriesFold): TimeSeriesFold object with the information needed to
498
+ split the data into folds.
499
+ metric (str | Callable | list): Metric used to quantify the goodness of fit
500
+ of the model.
501
+
502
+ - If `str`: {'mean_squared_error', 'mean_absolute_error',
503
+ 'mean_absolute_percentage_error', 'mean_squared_log_error',
504
+ 'mean_absolute_scaled_error', 'root_mean_squared_scaled_error'}
505
+ - If `Callable`: Function with arguments `y_true`, `y_pred` and `y_train`
506
+ (Optional) that returns a float.
507
+ - If `list`: List containing multiple strings and/or Callables.
508
+ exog (pd.Series | pd.DataFrame, optional): Exogenous variable/s included as
509
+ predictor/s. Must have the same number of observations as `y` and should
510
+ be aligned so that y[i] is regressed on exog[i]. Defaults to None.
511
+ interval (float | list | tuple | str | object, optional): Specifies whether
512
+ probabilistic predictions should be estimated and the method to use.
513
+ The following options are supported:
514
+
515
+ - If `float`, represents the nominal (expected) coverage (between 0 and 1).
516
+ For instance, `interval=0.95` corresponds to `[2.5, 97.5]` percentiles.
517
+ - If `list` or `tuple`: Sequence of percentiles to compute, each value must
518
+ be between 0 and 100 inclusive. For example, a 95% confidence interval can
519
+ be specified as `interval = [2.5, 97.5]` or multiple percentiles (e.g. 10,
520
+ 50 and 90) as `interval = [10, 50, 90]`.
521
+ - If 'bootstrapping' (str): `n_boot` bootstrapping predictions will be
522
+ generated.
523
+ - If scipy.stats distribution object, the distribution parameters will
524
+ be estimated for each prediction.
525
+ - If None, no probabilistic predictions are estimated.
526
+ Defaults to None.
527
+ interval_method (str, optional): Technique used to estimate prediction
528
+ intervals. Available options:
529
+
530
+ - 'bootstrapping': Bootstrapping is used to generate prediction intervals.
531
+ - 'conformal': Employs the conformal prediction split method for
532
+ interval estimation.
533
+ Defaults to 'bootstrapping'.
534
+ n_boot (int, optional): Number of bootstrapping iterations to perform when
535
+ estimating prediction intervals. Defaults to 250.
536
+ use_in_sample_residuals (bool, optional): If `True`, residuals from the
537
+ training data are used as proxy of prediction error to create predictions.
538
+ If `False`, out of sample residuals (calibration) are used.
539
+ Out-of-sample residuals must be precomputed using Forecaster's
540
+ `set_out_sample_residuals()` method. Defaults to True.
541
+ use_binned_residuals (bool, optional): If `True`, residuals are selected
542
+ based on the predicted values (binned selection).
543
+ If `False`, residuals are selected randomly. Defaults to True.
544
+ random_state (int, optional): Seed for the random number generator to
545
+ ensure reproducibility. Defaults to 123.
546
+ return_predictors (bool, optional): If `True`, the predictors used to make
547
+ the predictions are also returned. Defaults to False.
548
+ n_jobs (int | str, optional): The number of jobs to run in parallel. If `-1`,
549
+ then the number of jobs is set to the number of cores. If 'auto', `n_jobs`
550
+ is set using the function `skforecast.utils.select_n_jobs_backtesting`.
551
+ Defaults to 'auto'.
552
+ verbose (bool, optional): Print number of folds and index of training and
553
+ validation sets used for backtesting. Defaults to False.
554
+ show_progress (bool, optional): Whether to show a progress bar.
555
+ Defaults to True.
556
+ suppress_warnings (bool, optional): If `True`, spotforecast warnings will be
557
+ suppressed during the backtesting process. See
558
+ `spotforecast.exceptions.warn_skforecast_categories` for more information.
559
+ Defaults to False.
560
+
561
+ Returns:
562
+ tuple (pd.DataFrame, pd.DataFrame):
563
+ - metric_values: Value(s) of the metric(s).
564
+ - backtest_predictions: Value of predictions. The DataFrame includes
565
+ the following columns:
566
+
567
+ - fold: Indicates the fold number where the prediction was made.
568
+ - pred: Predicted values for the corresponding series and time steps.
569
+
570
+ If `interval` is not `None`, additional columns are included depending
571
+ on the method:
572
+
573
+ - For `float`: Columns `lower_bound` and `upper_bound`.
574
+ - For `list` or `tuple` of 2 elements: Columns `lower_bound` and
575
+ `upper_bound`.
576
+ - For `list` or `tuple` with multiple percentiles: One column per
577
+ percentile (e.g., `p_10`, `p_50`, `p_90`).
578
+ - For `'bootstrapping'`: One column per bootstrapping iteration
579
+ (e.g., `pred_boot_0`, `pred_boot_1`, ..., `pred_boot_n`).
580
+ - For `scipy.stats` distribution objects: One column for each
581
+ estimated parameter of the distribution (e.g., `loc`, `scale`).
582
+
583
+ If `return_predictors` is `True`, one column per predictor is created.
584
+
585
+ Depending on the relation between `steps` and `fold_stride`, the output
586
+ may include repeated indexes (if `fold_stride < steps`) or gaps
587
+ (if `fold_stride > steps`). See Notes below for more details.
588
+
589
+ Notes:
590
+ Note on `fold_stride` vs. `steps`:
591
+
592
+ - If `fold_stride == steps`, test sets are placed back-to-back without overlap.
593
+ Each observation appears only once in the output DataFrame, so the
594
+ index is unique.
595
+ - If `fold_stride < steps`, test sets overlap. Multiple forecasts are
596
+ generated for the same observations and, therefore, the output
597
+ DataFrame contains repeated indexes.
598
+ - If `fold_stride > steps`, there are gaps between consecutive test sets.
599
+ Some observations in the series will not have associated predictions,
600
+ so the output DataFrame has non-contiguous indexes.
601
+
602
+ Examples:
603
+ >>> import pandas as pd
604
+ >>> from sklearn.ensemble import RandomForestRegressor
605
+ >>> from spotforecast2.forecaster.recursive import ForecasterRecursive
606
+ >>> from spotforecast2.model_selection import backtesting_forecaster, TimeSeriesFold
607
+ >>> y = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
608
+ >>> forecaster = ForecasterRecursive(
609
+ ... estimator=RandomForestRegressor(random_state=123),
610
+ ... lags=2
611
+ ... )
612
+ >>> cv = TimeSeriesFold(
613
+ ... steps=2,
614
+ ... initial_train_size=5,
615
+ ... refit=False
616
+ ... )
617
+ >>> metric_values, backtest_predictions = backtesting_forecaster(
618
+ ... forecaster=forecaster,
619
+ ... y=y,
620
+ ... cv=cv,
621
+ ... metric='mean_squared_error'
622
+ ... )
623
+ >>> metric_values
624
+ mean_squared_error
625
+ 0 0.201334
626
+ >>> backtest_predictions
627
+ fold pred
628
+ 5 0 5.18
629
+ 6 0 6.10
630
+ 7 1 7.36
631
+ 8 1 8.40
632
+ 9 2 9.31
633
+ """
634
+
635
+ forecaters_allowed = [
636
+ "ForecasterRecursive",
637
+ "ForecasterDirect",
638
+ "ForecasterEquivalentDate",
639
+ "ForecasterRecursiveClassifier",
640
+ ]
641
+
642
+ if type(forecaster).__name__ not in forecaters_allowed:
643
+ raise TypeError(
644
+ f"`forecaster` must be of type {forecaters_allowed}. For all other "
645
+ f"types of forecasters use the other functions available in the "
646
+ f"`model_selection` module."
647
+ )
648
+
649
+ check_backtesting_input(
650
+ forecaster=forecaster,
651
+ cv=cv,
652
+ y=y,
653
+ metric=metric,
654
+ interval=interval,
655
+ interval_method=interval_method,
656
+ n_boot=n_boot,
657
+ use_in_sample_residuals=use_in_sample_residuals,
658
+ use_binned_residuals=use_binned_residuals,
659
+ random_state=random_state,
660
+ return_predictors=return_predictors,
661
+ n_jobs=n_jobs,
662
+ show_progress=show_progress,
663
+ suppress_warnings=suppress_warnings,
664
+ )
665
+
666
+ metric_values, backtest_predictions = _backtesting_forecaster(
667
+ forecaster=forecaster,
668
+ y=y,
669
+ cv=cv,
670
+ metric=metric,
671
+ exog=exog,
672
+ interval=interval,
673
+ interval_method=interval_method,
674
+ n_boot=n_boot,
675
+ use_in_sample_residuals=use_in_sample_residuals,
676
+ use_binned_residuals=use_binned_residuals,
677
+ random_state=random_state,
678
+ return_predictors=return_predictors,
679
+ n_jobs=n_jobs,
680
+ verbose=verbose,
681
+ show_progress=show_progress,
682
+ suppress_warnings=suppress_warnings,
683
+ )
684
+
685
+ return metric_values, backtest_predictions