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.
- spotforecast2/.DS_Store +0 -0
- spotforecast2/__init__.py +2 -0
- spotforecast2/data/__init__.py +0 -0
- spotforecast2/data/data.py +130 -0
- spotforecast2/data/fetch_data.py +209 -0
- spotforecast2/exceptions.py +681 -0
- spotforecast2/forecaster/.DS_Store +0 -0
- spotforecast2/forecaster/__init__.py +7 -0
- spotforecast2/forecaster/base.py +448 -0
- spotforecast2/forecaster/metrics.py +527 -0
- spotforecast2/forecaster/recursive/__init__.py +4 -0
- spotforecast2/forecaster/recursive/_forecaster_equivalent_date.py +1075 -0
- spotforecast2/forecaster/recursive/_forecaster_recursive.py +939 -0
- spotforecast2/forecaster/recursive/_warnings.py +15 -0
- spotforecast2/forecaster/utils.py +954 -0
- spotforecast2/model_selection/__init__.py +5 -0
- spotforecast2/model_selection/bayesian_search.py +453 -0
- spotforecast2/model_selection/grid_search.py +314 -0
- spotforecast2/model_selection/random_search.py +151 -0
- spotforecast2/model_selection/split_base.py +357 -0
- spotforecast2/model_selection/split_one_step.py +245 -0
- spotforecast2/model_selection/split_ts_cv.py +634 -0
- spotforecast2/model_selection/utils_common.py +718 -0
- spotforecast2/model_selection/utils_metrics.py +103 -0
- spotforecast2/model_selection/validation.py +685 -0
- spotforecast2/preprocessing/__init__.py +30 -0
- spotforecast2/preprocessing/_binner.py +378 -0
- spotforecast2/preprocessing/_common.py +123 -0
- spotforecast2/preprocessing/_differentiator.py +123 -0
- spotforecast2/preprocessing/_rolling.py +136 -0
- spotforecast2/preprocessing/curate_data.py +254 -0
- spotforecast2/preprocessing/imputation.py +92 -0
- spotforecast2/preprocessing/outlier.py +114 -0
- spotforecast2/preprocessing/split.py +139 -0
- spotforecast2/py.typed +0 -0
- spotforecast2/utils/__init__.py +43 -0
- spotforecast2/utils/convert_to_utc.py +44 -0
- spotforecast2/utils/data_transform.py +208 -0
- spotforecast2/utils/forecaster_config.py +344 -0
- spotforecast2/utils/generate_holiday.py +70 -0
- spotforecast2/utils/validation.py +569 -0
- spotforecast2/weather/__init__.py +0 -0
- spotforecast2/weather/weather_client.py +288 -0
- spotforecast2-0.0.1.dist-info/METADATA +47 -0
- spotforecast2-0.0.1.dist-info/RECORD +46 -0
- 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
|