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,527 @@
1
+ """Metrics for evaluating forecasting models.
2
+
3
+ This module provides various metric functions for evaluating forecasting performance,
4
+ including custom metrics like MASE, RMSSE, and probabilistic metrics like CRPS.
5
+
6
+ Examples:
7
+ Using standard metrics::
8
+
9
+ import numpy as np
10
+ from spotforecast2.forecaster.metrics import _get_metric
11
+
12
+ y_true = np.array([1, 2, 3, 4, 5])
13
+ y_pred = np.array([1.1, 1.9, 3.2, 3.8, 5.1])
14
+
15
+ # Get a metric function
16
+ mse = _get_metric('mean_squared_error')
17
+ error = mse(y_true, y_pred)
18
+
19
+ Using scaled metrics::
20
+
21
+ from spotforecast2.forecaster.metrics import mean_absolute_scaled_error
22
+
23
+ y_train = np.array([1, 2, 3, 4, 5, 6, 7, 8])
24
+ y_true = np.array([9, 10, 11])
25
+ y_pred = np.array([8.8, 10.2, 10.9])
26
+
27
+ mase = mean_absolute_scaled_error(y_true, y_pred, y_train)
28
+ """
29
+
30
+ from __future__ import annotations
31
+ from typing import Callable
32
+ import numpy as np
33
+ import pandas as pd
34
+ import inspect
35
+ from functools import wraps
36
+ from sklearn.metrics import (
37
+ mean_squared_error,
38
+ mean_absolute_error,
39
+ mean_absolute_percentage_error,
40
+ mean_squared_log_error,
41
+ median_absolute_error,
42
+ mean_pinball_loss,
43
+ accuracy_score,
44
+ balanced_accuracy_score,
45
+ f1_score,
46
+ precision_score,
47
+ recall_score,
48
+ )
49
+
50
+
51
+ def _get_metric(metric: str) -> Callable:
52
+ """Get the corresponding scikit-learn function to calculate the metric.
53
+
54
+ Args:
55
+ metric: Metric used to quantify the goodness of fit of the model.
56
+
57
+ Returns:
58
+ scikit-learn function to calculate the desired metric.
59
+
60
+ Examples:
61
+ >>> from spotforecast2.forecaster.metrics import _get_metric
62
+ >>> mse_func = _get_metric('mean_squared_error')
63
+ >>> y_true = np.array([1, 2, 3])
64
+ >>> y_pred = np.array([1.1, 1.9, 3.2])
65
+ >>> error = mse_func(y_true, y_pred)
66
+ >>> error > 0
67
+ True
68
+ """
69
+
70
+ allowed_metrics = [
71
+ # Regression metrics
72
+ "mean_squared_error",
73
+ "mean_absolute_error",
74
+ "mean_absolute_percentage_error",
75
+ "mean_squared_log_error",
76
+ "mean_absolute_scaled_error",
77
+ "root_mean_squared_scaled_error",
78
+ "median_absolute_error",
79
+ "symmetric_mean_absolute_percentage_error",
80
+ # Classification metrics
81
+ "accuracy_score",
82
+ "balanced_accuracy_score",
83
+ "f1_score",
84
+ "precision_score",
85
+ "recall_score",
86
+ ]
87
+
88
+ if metric not in allowed_metrics:
89
+ raise ValueError(f"Allowed metrics are: {allowed_metrics}. Got {metric}.")
90
+
91
+ metrics = {
92
+ "mean_squared_error": mean_squared_error,
93
+ "mean_absolute_error": mean_absolute_error,
94
+ "mean_absolute_percentage_error": mean_absolute_percentage_error,
95
+ "mean_squared_log_error": mean_squared_log_error,
96
+ "mean_absolute_scaled_error": mean_absolute_scaled_error,
97
+ "root_mean_squared_scaled_error": root_mean_squared_scaled_error,
98
+ "median_absolute_error": median_absolute_error,
99
+ "symmetric_mean_absolute_percentage_error": symmetric_mean_absolute_percentage_error,
100
+ "accuracy_score": accuracy_score,
101
+ "balanced_accuracy_score": balanced_accuracy_score,
102
+ "f1_score": f1_score,
103
+ "precision_score": precision_score,
104
+ "recall_score": recall_score,
105
+ }
106
+
107
+ metric = add_y_train_argument(metrics[metric])
108
+
109
+ return metric
110
+
111
+
112
+ def add_y_train_argument(func: Callable) -> Callable:
113
+ """Add `y_train` argument to a function if it is not already present.
114
+
115
+ Args:
116
+ func: Function to which the argument is added.
117
+
118
+ Returns:
119
+ Function with `y_train` argument added.
120
+
121
+ Examples:
122
+ >>> def my_metric(y_true, y_pred):
123
+ ... return np.mean(np.abs(y_true - y_pred))
124
+ >>> enhanced_metric = add_y_train_argument(my_metric)
125
+ >>> # Now the function accepts y_train parameter
126
+ >>> result = enhanced_metric(np.array([1,2,3]), np.array([1,2,3]), y_train=None)
127
+ """
128
+
129
+ sig = inspect.signature(func)
130
+
131
+ if "y_train" in sig.parameters:
132
+ return func
133
+
134
+ new_params = list(sig.parameters.values()) + [
135
+ inspect.Parameter("y_train", inspect.Parameter.KEYWORD_ONLY, default=None)
136
+ ]
137
+ new_sig = sig.replace(parameters=new_params)
138
+
139
+ @wraps(func)
140
+ def wrapper(*args, y_train=None, **kwargs):
141
+ return func(*args, **kwargs)
142
+
143
+ wrapper.__signature__ = new_sig
144
+
145
+ return wrapper
146
+
147
+
148
+ def mean_absolute_scaled_error(
149
+ y_true: np.ndarray | pd.Series,
150
+ y_pred: np.ndarray | pd.Series,
151
+ y_train: list[float] | np.ndarray | pd.Series,
152
+ ) -> float:
153
+ """Mean Absolute Scaled Error (MASE).
154
+
155
+ MASE is a scale-independent error metric that measures the accuracy of
156
+ a forecast. It is the mean absolute error of the forecast divided by the
157
+ mean absolute error of a naive forecast in the training set. The naive
158
+ forecast is the one obtained by shifting the time series by one period.
159
+ If y_train is a list of numpy arrays or pandas Series, it is considered
160
+ that each element is the true value of the target variable in the training
161
+ set for each time series. In this case, the naive forecast is calculated
162
+ for each time series separately.
163
+
164
+ Args:
165
+ y_true: True values of the target variable.
166
+ y_pred: Predicted values of the target variable.
167
+ y_train: True values of the target variable in the training set. If `list`, it
168
+ is consider that each element is the true value of the target variable
169
+ in the training set for each time series.
170
+
171
+ Returns:
172
+ MASE value.
173
+
174
+ Examples:
175
+ >>> from spotforecast2.forecaster.metrics import mean_absolute_scaled_error
176
+ >>> y_train = np.array([1, 2, 3, 4, 5, 6, 7, 8])
177
+ >>> y_true = np.array([9, 10, 11])
178
+ >>> y_pred = np.array([8.8, 10.2, 10.9])
179
+ >>> mase = mean_absolute_scaled_error(y_true, y_pred, y_train)
180
+ >>> mase < 1.0 # Good forecast
181
+ True
182
+ """
183
+
184
+ # NOTE: When using this metric in validation, `y_train` doesn't include
185
+ # the first window_size observations used to create the predictors and/or
186
+ # rolling features.
187
+
188
+ if not isinstance(y_true, (pd.Series, np.ndarray)):
189
+ raise TypeError("`y_true` must be a pandas Series or numpy ndarray.")
190
+ if not isinstance(y_pred, (pd.Series, np.ndarray)):
191
+ raise TypeError("`y_pred` must be a pandas Series or numpy ndarray.")
192
+ if not isinstance(y_train, (list, pd.Series, np.ndarray)):
193
+ raise TypeError("`y_train` must be a list, pandas Series or numpy ndarray.")
194
+ if isinstance(y_train, list):
195
+ for x in y_train:
196
+ if not isinstance(x, (pd.Series, np.ndarray)):
197
+ raise TypeError(
198
+ "When `y_train` is a list, each element must be a pandas Series "
199
+ "or numpy ndarray."
200
+ )
201
+ if len(y_true) != len(y_pred):
202
+ raise ValueError("`y_true` and `y_pred` must have the same length.")
203
+ if len(y_true) == 0 or len(y_pred) == 0:
204
+ raise ValueError("`y_true` and `y_pred` must have at least one element.")
205
+
206
+ if isinstance(y_train, list):
207
+ # Flatten list of arrays for naive forecast if meaningful, but MASE usually assumes
208
+ # naive forecast on single series. If list, we might be doing something else.
209
+ # Original code does: np.concatenate([np.diff(x) for x in y_train])
210
+ # This assumes independent series and we average error over all of them.
211
+ naive_forecast = np.concatenate([np.diff(x) for x in y_train])
212
+ else:
213
+ naive_forecast = np.diff(y_train)
214
+
215
+ mase = np.mean(np.abs(y_true - y_pred)) / np.nanmean(np.abs(naive_forecast))
216
+
217
+ return mase
218
+
219
+
220
+ def root_mean_squared_scaled_error(
221
+ y_true: np.ndarray | pd.Series,
222
+ y_pred: np.ndarray | pd.Series,
223
+ y_train: list[float] | np.ndarray | pd.Series,
224
+ ) -> float:
225
+ """Root Mean Squared Scaled Error (RMSSE).
226
+
227
+ RMSSE is a scale-independent error metric that measures the accuracy of
228
+ a forecast. It is the root mean squared error of the forecast divided by
229
+ the root mean squared error of a naive forecast in the training set. The
230
+ naive forecast is the one obtained by shifting the time series by one period.
231
+ If y_train is a list of numpy arrays or pandas Series, it is considered
232
+ that each element is the true value of the target variable in the training
233
+ set for each time series. In this case, the naive forecast is calculated
234
+ for each time series separately.
235
+
236
+ Args:
237
+ y_true: True values of the target variable.
238
+ y_pred: Predicted values of the target variable.
239
+ y_train: True values of the target variable in the training set. If list, it
240
+ is consider that each element is the true value of the target variable
241
+ in the training set for each time series.
242
+
243
+ Returns:
244
+ RMSSE value.
245
+
246
+ Examples:
247
+ >>> from spotforecast2.forecaster.metrics import root_mean_squared_scaled_error
248
+ >>> y_train = np.array([1, 2, 3, 4, 5, 6, 7, 8])
249
+ >>> y_true = np.array([9, 10, 11])
250
+ >>> y_pred = np.array([8.8, 10.2, 10.9])
251
+ >>> rmsse = root_mean_squared_scaled_error(y_true, y_pred, y_train)
252
+ >>> rmsse < 1.0 # Good forecast
253
+ True
254
+ """
255
+
256
+ # NOTE: When using this metric in validation, `y_train` doesn't include
257
+ # the first window_size observations used to create the predictors and/or
258
+ # rolling features.
259
+
260
+ if not isinstance(y_true, (pd.Series, np.ndarray)):
261
+ raise TypeError("`y_true` must be a pandas Series or numpy ndarray.")
262
+ if not isinstance(y_pred, (pd.Series, np.ndarray)):
263
+ raise TypeError("`y_pred` must be a pandas Series or numpy ndarray.")
264
+ if not isinstance(y_train, (list, pd.Series, np.ndarray)):
265
+ raise TypeError("`y_train` must be a list, pandas Series or numpy ndarray.")
266
+ if isinstance(y_train, list):
267
+ for x in y_train:
268
+ if not isinstance(x, (pd.Series, np.ndarray)):
269
+ raise TypeError(
270
+ "When `y_train` is a list, each element must be a pandas Series "
271
+ "or numpy ndarray."
272
+ )
273
+ if len(y_true) != len(y_pred):
274
+ raise ValueError("`y_true` and `y_pred` must have the same length.")
275
+ if len(y_true) == 0 or len(y_pred) == 0:
276
+ raise ValueError("`y_true` and `y_pred` must have at least one element.")
277
+
278
+ if isinstance(y_train, list):
279
+ naive_forecast = np.concatenate([np.diff(x) for x in y_train])
280
+ else:
281
+ naive_forecast = np.diff(y_train)
282
+
283
+ rmsse = np.sqrt(np.mean((y_true - y_pred) ** 2)) / np.sqrt(
284
+ np.nanmean(naive_forecast**2)
285
+ )
286
+
287
+ return rmsse
288
+
289
+
290
+ def crps_from_predictions(y_true: float, y_pred: np.ndarray) -> float:
291
+ """Compute the Continuous Ranked Probability Score (CRPS) from predictions.
292
+
293
+ The CRPS compares the empirical distribution of a set of forecasted values
294
+ to a scalar observation. The smaller the CRPS, the better.
295
+
296
+ Args:
297
+ y_true: The true value of the random variable.
298
+ y_pred: The predicted values of the random variable. These are the multiple
299
+ forecasted values for a single observation.
300
+
301
+ Returns:
302
+ The CRPS score.
303
+
304
+ Examples:
305
+ >>> from spotforecast2.forecaster.metrics import crps_from_predictions
306
+ >>> y_true = 5.0
307
+ >>> y_pred = np.array([4.5, 5.1, 4.9, 5.3, 4.7])
308
+ >>> crps = crps_from_predictions(y_true, y_pred)
309
+ >>> crps >= 0
310
+ True
311
+ """
312
+ if not isinstance(y_pred, np.ndarray) or y_pred.ndim != 1:
313
+ raise TypeError("`y_pred` must be a 1D numpy array.")
314
+
315
+ if not isinstance(y_true, (float, int)):
316
+ raise TypeError("`y_true` must be a float or integer.")
317
+
318
+ y_pred = np.sort(y_pred)
319
+ # Define the grid for integration including the true value
320
+ grid = np.concatenate(([y_true], y_pred))
321
+ grid = np.sort(grid)
322
+ cdf_values = np.searchsorted(y_pred, grid, side="right") / len(y_pred)
323
+ indicator = grid >= y_true
324
+ diffs = np.diff(grid)
325
+ crps = np.sum(diffs * (cdf_values[:-1] - indicator[:-1]) ** 2)
326
+
327
+ return crps
328
+
329
+
330
+ def crps_from_quantiles(
331
+ y_true: float,
332
+ pred_quantiles: np.ndarray,
333
+ quantile_levels: np.ndarray,
334
+ ) -> float:
335
+ """Calculate the Continuous Ranked Probability Score (CRPS) from quantiles.
336
+
337
+ The empirical cdf is approximated using linear interpolation
338
+ between the predicted quantiles.
339
+
340
+ Args:
341
+ y_true: The true value of the random variable.
342
+ pred_quantiles: The predicted quantile values.
343
+ quantile_levels: The quantile levels corresponding to the predicted quantiles.
344
+
345
+ Returns:
346
+ The CRPS score.
347
+
348
+ Examples:
349
+ >>> from spotforecast2.forecaster.metrics import crps_from_quantiles
350
+ >>> y_true = 5.0
351
+ >>> pred_quantiles = np.array([4.0, 4.5, 5.0, 5.5, 6.0])
352
+ >>> quantile_levels = np.array([0.1, 0.25, 0.5, 0.75, 0.9])
353
+ >>> crps = crps_from_quantiles(y_true, pred_quantiles, quantile_levels)
354
+ >>> crps >= 0
355
+ True
356
+ """
357
+ if not isinstance(y_true, (float, int)):
358
+ raise TypeError("`y_true` must be a float or integer.")
359
+
360
+ if not isinstance(pred_quantiles, np.ndarray) or pred_quantiles.ndim != 1:
361
+ raise TypeError("`pred_quantiles` must be a 1D numpy array.")
362
+
363
+ if not isinstance(quantile_levels, np.ndarray) or quantile_levels.ndim != 1:
364
+ raise TypeError("`quantile_levels` must be a 1D numpy array.")
365
+
366
+ if len(pred_quantiles) != len(quantile_levels):
367
+ raise ValueError(
368
+ "The number of predicted quantiles and quantile levels must be equal."
369
+ )
370
+
371
+ sorted_indices = np.argsort(pred_quantiles)
372
+ pred_quantiles = pred_quantiles[sorted_indices]
373
+ quantile_levels = quantile_levels[sorted_indices]
374
+
375
+ # Define the empirical CDF function using interpolation
376
+ def empirical_cdf(x):
377
+ return np.interp(x, pred_quantiles, quantile_levels, left=0.0, right=1.0)
378
+
379
+ # Define the CRPS integrand
380
+ def crps_integrand(x):
381
+ return (empirical_cdf(x) - (x >= y_true)) ** 2
382
+
383
+ # Integration bounds: Extend slightly beyond predicted quantiles
384
+ xmin = np.min(pred_quantiles) * 0.9
385
+ xmax = np.max(pred_quantiles) * 1.1
386
+
387
+ # Create a fine grid of x values for integration
388
+ x_values = np.linspace(xmin, xmax, 1000)
389
+
390
+ # Compute the integrand values and integrate using the trapezoidal rule
391
+ integrand_values = crps_integrand(x_values)
392
+ if np.__version__ >= "2.0.0":
393
+ crps = np.trapezoid(integrand_values, x=x_values)
394
+ else:
395
+ crps = np.trapz(integrand_values, x_values)
396
+
397
+ return crps
398
+
399
+
400
+ def calculate_coverage(
401
+ y_true: np.ndarray | pd.Series,
402
+ lower_bound: np.ndarray | pd.Series,
403
+ upper_bound: np.ndarray | pd.Series,
404
+ ) -> float:
405
+ """Calculate coverage of a given interval.
406
+
407
+ Coverage is the proportion of true values that fall within the interval.
408
+
409
+ Args:
410
+ y_true: True values of the target variable.
411
+ lower_bound: Lower bound of the interval.
412
+ upper_bound: Upper bound of the interval.
413
+
414
+ Returns:
415
+ Coverage of the interval.
416
+
417
+ Examples:
418
+ >>> from spotforecast2.forecaster.metrics import calculate_coverage
419
+ >>> y_true = np.array([1, 2, 3, 4, 5])
420
+ >>> lower_bound = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
421
+ >>> upper_bound = np.array([1.5, 2.5, 3.5, 4.5, 5.5])
422
+ >>> coverage = calculate_coverage(y_true, lower_bound, upper_bound)
423
+ >>> coverage == 1.0 # All values within bounds
424
+ True
425
+ """
426
+ if not isinstance(y_true, (np.ndarray, pd.Series)) or y_true.ndim != 1:
427
+ raise TypeError("`y_true` must be a 1D numpy array or pandas Series.")
428
+
429
+ if not isinstance(lower_bound, (np.ndarray, pd.Series)) or lower_bound.ndim != 1:
430
+ raise TypeError("`lower_bound` must be a 1D numpy array or pandas Series.")
431
+
432
+ if not isinstance(upper_bound, (np.ndarray, pd.Series)) or upper_bound.ndim != 1:
433
+ raise TypeError("`upper_bound` must be a 1D numpy array or pandas Series.")
434
+
435
+ y_true = np.asarray(y_true)
436
+ lower_bound = np.asarray(lower_bound)
437
+ upper_bound = np.asarray(upper_bound)
438
+
439
+ if y_true.shape != lower_bound.shape or y_true.shape != upper_bound.shape:
440
+ raise ValueError(
441
+ "`y_true`, `lower_bound` and `upper_bound` must have the same shape."
442
+ )
443
+
444
+ coverage = np.mean(np.logical_and(y_true >= lower_bound, y_true <= upper_bound))
445
+
446
+ return coverage
447
+
448
+
449
+ def create_mean_pinball_loss(alpha: float) -> callable:
450
+ """Create pinball loss for a given quantile.
451
+
452
+ Also known as quantile loss. Internally, it uses the `mean_pinball_loss`
453
+ function from scikit-learn.
454
+
455
+ Args:
456
+ alpha: Quantile for which the Pinball loss is calculated.
457
+ Must be between 0 and 1, inclusive.
458
+
459
+ Returns:
460
+ Mean Pinball loss function for the given quantile.
461
+
462
+ Examples:
463
+ >>> from spotforecast2.forecaster.metrics import create_mean_pinball_loss
464
+ >>> pinball_loss_50 = create_mean_pinball_loss(alpha=0.5)
465
+ >>> y_true = np.array([1, 2, 3, 4, 5])
466
+ >>> y_pred = np.array([1.1, 1.9, 3.2, 3.8, 5.1])
467
+ >>> loss = pinball_loss_50(y_true, y_pred)
468
+ >>> loss >= 0
469
+ True
470
+ """
471
+ if not (0 <= alpha <= 1):
472
+ raise ValueError("alpha must be between 0 and 1, both inclusive.")
473
+
474
+ def mean_pinball_loss_q(y_true, y_pred):
475
+ return mean_pinball_loss(y_true, y_pred, alpha=alpha)
476
+
477
+ return mean_pinball_loss_q
478
+
479
+
480
+ def symmetric_mean_absolute_percentage_error(
481
+ y_true: np.ndarray | pd.Series, y_pred: np.ndarray | pd.Series
482
+ ) -> float:
483
+ """Compute the Symmetric Mean Absolute Percentage Error (SMAPE).
484
+
485
+ SMAPE is a relative error metric used to measure the accuracy
486
+ of forecasts. Unlike MAPE, it is symmetric and prevents division
487
+ by zero by averaging the absolute values of actual and predicted values.
488
+
489
+ The result is expressed as a percentage and ranges from 0%
490
+ (perfect prediction) to 200% (maximum error).
491
+
492
+ Args:
493
+ y_true: True values of the target variable.
494
+ y_pred: Predicted values of the target variable.
495
+
496
+ Returns:
497
+ SMAPE value as a percentage.
498
+
499
+ Examples:
500
+ >>> from spotforecast2.forecaster.metrics import symmetric_mean_absolute_percentage_error
501
+ >>> y_true = np.array([100, 200, 0])
502
+ >>> y_pred = np.array([110, 180, 10])
503
+ >>> result = symmetric_mean_absolute_percentage_error(y_true, y_pred)
504
+ >>> 0 <= result <= 200
505
+ True
506
+ """
507
+
508
+ if not isinstance(y_true, (pd.Series, np.ndarray)):
509
+ raise TypeError("`y_true` must be a pandas Series or numpy ndarray.")
510
+ if not isinstance(y_pred, (pd.Series, np.ndarray)):
511
+ raise TypeError("`y_pred` must be a pandas Series or numpy ndarray.")
512
+ if len(y_true) != len(y_pred):
513
+ raise ValueError("`y_true` and `y_pred` must have the same length.")
514
+ if len(y_true) == 0 or len(y_pred) == 0:
515
+ raise ValueError("`y_true` and `y_pred` must have at least one element.")
516
+
517
+ numerator = np.abs(y_true - y_pred)
518
+ denominator = (np.abs(y_true) + np.abs(y_pred)) / 2
519
+
520
+ # NOTE: Avoid division by zero
521
+ mask = denominator != 0
522
+ smape_values = np.zeros_like(denominator)
523
+ smape_values[mask] = numerator[mask] / denominator[mask]
524
+
525
+ smape = 100 * np.mean(smape_values)
526
+
527
+ return smape
@@ -0,0 +1,4 @@
1
+ from ._forecaster_recursive import ForecasterRecursive
2
+ from ._forecaster_equivalent_date import ForecasterEquivalentDate
3
+
4
+ __all__ = ["ForecasterRecursive", "ForecasterEquivalentDate"]