randomstatsmodels 0.1.0__tar.gz
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.
- randomstatsmodels-0.1.0/LICENSE +21 -0
- randomstatsmodels-0.1.0/PKG-INFO +89 -0
- randomstatsmodels-0.1.0/README.md +45 -0
- randomstatsmodels-0.1.0/pyproject.toml +41 -0
- randomstatsmodels-0.1.0/randomstatsmodels/__init__.py +39 -0
- randomstatsmodels-0.1.0/randomstatsmodels/benchmarking/__init__.py +1 -0
- randomstatsmodels-0.1.0/randomstatsmodels/benchmarking/benchmarking.py +304 -0
- randomstatsmodels-0.1.0/randomstatsmodels/metrics/__init__.py +1 -0
- randomstatsmodels-0.1.0/randomstatsmodels/metrics/metrics.py +27 -0
- randomstatsmodels-0.1.0/randomstatsmodels/models/__init__.py +15 -0
- randomstatsmodels-0.1.0/randomstatsmodels/models/model_utils.py +47 -0
- randomstatsmodels-0.1.0/randomstatsmodels/models/models.py +3263 -0
- randomstatsmodels-0.1.0/randomstatsmodels.egg-info/PKG-INFO +89 -0
- randomstatsmodels-0.1.0/randomstatsmodels.egg-info/SOURCES.txt +17 -0
- randomstatsmodels-0.1.0/randomstatsmodels.egg-info/dependency_links.txt +1 -0
- randomstatsmodels-0.1.0/randomstatsmodels.egg-info/requires.txt +1 -0
- randomstatsmodels-0.1.0/randomstatsmodels.egg-info/top_level.txt +1 -0
- randomstatsmodels-0.1.0/setup.cfg +13 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jacob
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to do so, subject to the following
|
|
10
|
+
conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: randomstatsmodels
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tools for benchmarking, metrics, and models.
|
|
5
|
+
Author: Jacob Wright
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Jacob
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to do so, subject to the following
|
|
15
|
+
conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in
|
|
18
|
+
all copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Keywords: statistics,machine learning,metrics,models
|
|
28
|
+
Classifier: Development Status :: 3 - Alpha
|
|
29
|
+
Classifier: Intended Audience :: Science/Research
|
|
30
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
31
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Requires-Python: >=3.9
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Requires-Dist: numpy>=1.24
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# randomstatsmodels
|
|
46
|
+
|
|
47
|
+
A tiny, modern Python package skeleton for experimenting with forecasting and statistics utilities.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
```bash
|
|
51
|
+
# from the project root
|
|
52
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
53
|
+
pip install -e ".[dev]" # install in editable mode with dev extras
|
|
54
|
+
randomstatsmodels --version
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
```python
|
|
59
|
+
from randomstatsmodels.metrics import mae, rmse
|
|
60
|
+
|
|
61
|
+
y_true = [1, 2, 3]
|
|
62
|
+
y_pred = [1.1, 1.9, 3.2]
|
|
63
|
+
print(mae(y_true, y_pred))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Testing
|
|
67
|
+
```bash
|
|
68
|
+
pytest -q
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Using your models
|
|
72
|
+
Put your custom models in `randomstatsmodels/user_models.py` (we copied your uploaded file there).
|
|
73
|
+
|
|
74
|
+
### Python API
|
|
75
|
+
```python
|
|
76
|
+
from randomstatsmodels.api import predict
|
|
77
|
+
# Model ref can be 'randomstatsmodels.user_models:MyModel' or an object/callable
|
|
78
|
+
yhat = predict("randomstatsmodels.user_models:MyModel", X_dataframe)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### CLI
|
|
82
|
+
```bash
|
|
83
|
+
randomstatsmodels predict \ --model randomstatsmodels.user_models:MyModel \ --input data.csv \ --output preds.csv
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The adapter accepts:
|
|
87
|
+
- Objects with `.predict(X, **kwargs)`
|
|
88
|
+
- Callables like `def f(X): ...`
|
|
89
|
+
- Classes that can be instantiated without args and have `.predict(X)`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# randomstatsmodels
|
|
2
|
+
|
|
3
|
+
A tiny, modern Python package skeleton for experimenting with forecasting and statistics utilities.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
```bash
|
|
7
|
+
# from the project root
|
|
8
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
9
|
+
pip install -e ".[dev]" # install in editable mode with dev extras
|
|
10
|
+
randomstatsmodels --version
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
```python
|
|
15
|
+
from randomstatsmodels.metrics import mae, rmse
|
|
16
|
+
|
|
17
|
+
y_true = [1, 2, 3]
|
|
18
|
+
y_pred = [1.1, 1.9, 3.2]
|
|
19
|
+
print(mae(y_true, y_pred))
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Testing
|
|
23
|
+
```bash
|
|
24
|
+
pytest -q
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Using your models
|
|
28
|
+
Put your custom models in `randomstatsmodels/user_models.py` (we copied your uploaded file there).
|
|
29
|
+
|
|
30
|
+
### Python API
|
|
31
|
+
```python
|
|
32
|
+
from randomstatsmodels.api import predict
|
|
33
|
+
# Model ref can be 'randomstatsmodels.user_models:MyModel' or an object/callable
|
|
34
|
+
yhat = predict("randomstatsmodels.user_models:MyModel", X_dataframe)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### CLI
|
|
38
|
+
```bash
|
|
39
|
+
randomstatsmodels predict \ --model randomstatsmodels.user_models:MyModel \ --input data.csv \ --output preds.csv
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The adapter accepts:
|
|
43
|
+
- Objects with `.predict(X, **kwargs)`
|
|
44
|
+
- Callables like `def f(X): ...`
|
|
45
|
+
- Classes that can be instantiated without args and have `.predict(X)`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "randomstatsmodels"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tools for benchmarking, metrics, and models."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Jacob Wright" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.24"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
keywords = ["statistics", "machine learning", "metrics", "models"]
|
|
20
|
+
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Operating System :: OS Independent"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["."]
|
|
37
|
+
include = ["randomstatsmodels*"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
# if you ever add CSVs or py.typed, include them here
|
|
41
|
+
randomstatsmodels = []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# randomstatsmodels/__init__.py
|
|
2
|
+
# This makes the folder a Python package.
|
|
3
|
+
|
|
4
|
+
from .metrics.metrics import mae, mape, smape, rmse
|
|
5
|
+
from .models.models import (
|
|
6
|
+
AutoHybridForecaster,
|
|
7
|
+
AutoKNN,
|
|
8
|
+
AutoMELD,
|
|
9
|
+
AutoNEO,
|
|
10
|
+
AutoPALF,
|
|
11
|
+
AutoThetaAR,
|
|
12
|
+
AutoPolymath,
|
|
13
|
+
AutoSeasonalAR,
|
|
14
|
+
AutoFourier,
|
|
15
|
+
AutoRollingMedian,
|
|
16
|
+
AutoTrimmedMean,
|
|
17
|
+
AutoWindow,
|
|
18
|
+
AutoRankInsertion,
|
|
19
|
+
)
|
|
20
|
+
from .benchmarking.benchmarking import benchmark_model, benchmark_models
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"mae",
|
|
26
|
+
"mape",
|
|
27
|
+
"smape",
|
|
28
|
+
"rmse",
|
|
29
|
+
"AutoHybridForecaster",
|
|
30
|
+
"AutoKNN",
|
|
31
|
+
"AutoMELD",
|
|
32
|
+
"AutoNEO",
|
|
33
|
+
"AutoPALF",
|
|
34
|
+
"AutoThetaAR",
|
|
35
|
+
"AutoPolymath",
|
|
36
|
+
"AutoSeasonalAR",
|
|
37
|
+
"benchmark_models",
|
|
38
|
+
"benchmark_model",
|
|
39
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .benchmarking import benchmark_model, benchmark_models
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import math
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ..metrics import mae, rmse, mape, smape
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import numpy as np
|
|
8
|
+
from ..metrics import mae, rmse, mape, smape
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def benchmark_model(model_class, data, iterations=1, h=7):
|
|
12
|
+
"""
|
|
13
|
+
Benchmark the training + prediction speed of a time series model,
|
|
14
|
+
and compute MAE, RMSE, MAPE, sMAPE on the last h points.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
model_class : class
|
|
19
|
+
The model class to initialize (e.g., AutoNEO).
|
|
20
|
+
data : array-like
|
|
21
|
+
The time series data.
|
|
22
|
+
iterations : int, default=5
|
|
23
|
+
Number of times to run the benchmark.
|
|
24
|
+
h : int, default=20
|
|
25
|
+
Forecast horizon.
|
|
26
|
+
**fit_kwargs : dict
|
|
27
|
+
Additional arguments passed to model.fit().
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
results : dict
|
|
32
|
+
{
|
|
33
|
+
"avg_total_time_s": float,
|
|
34
|
+
"avg_fit_time_s": float,
|
|
35
|
+
"avg_predict_time_s": float,
|
|
36
|
+
"avg_mae": float,
|
|
37
|
+
"avg_rmse": float,
|
|
38
|
+
"avg_mape": float,
|
|
39
|
+
"avg_smape": float,
|
|
40
|
+
"per_iteration": [
|
|
41
|
+
{
|
|
42
|
+
"fit_time_s": float,
|
|
43
|
+
"predict_time_s": float,
|
|
44
|
+
"total_time_s": float,
|
|
45
|
+
"mae": float,
|
|
46
|
+
"rmse": float,
|
|
47
|
+
"mape": float,
|
|
48
|
+
"smape": float
|
|
49
|
+
},
|
|
50
|
+
...
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
data = np.asarray(data)
|
|
55
|
+
assert len(data) > h, "Data length must be greater than forecast horizon h."
|
|
56
|
+
|
|
57
|
+
per_iter = []
|
|
58
|
+
for i in range(iterations):
|
|
59
|
+
|
|
60
|
+
model = model_class() # fresh model each run
|
|
61
|
+
|
|
62
|
+
model_name = model_class.__name__
|
|
63
|
+
|
|
64
|
+
t0 = time.time()
|
|
65
|
+
model.fit(data[:-h])
|
|
66
|
+
fit_time = time.time() - t0
|
|
67
|
+
|
|
68
|
+
t1 = time.time()
|
|
69
|
+
y_pred = model.predict(h)
|
|
70
|
+
|
|
71
|
+
predict_time = time.time() - t1
|
|
72
|
+
|
|
73
|
+
total_time = fit_time + predict_time
|
|
74
|
+
|
|
75
|
+
y_true = data[-h:]
|
|
76
|
+
# Ensure shapes compatible
|
|
77
|
+
y_pred = np.asarray(y_pred).reshape(-1)[:h]
|
|
78
|
+
|
|
79
|
+
if model_name == "AutoETS":
|
|
80
|
+
y_pred = y_pred[0]["mean"]
|
|
81
|
+
|
|
82
|
+
iter_metrics = {
|
|
83
|
+
"fit_time_s": fit_time,
|
|
84
|
+
"predict_time_s": predict_time,
|
|
85
|
+
"total_time_s": total_time,
|
|
86
|
+
"mae": float(mae(y_true, y_pred)),
|
|
87
|
+
"rmse": float(rmse(y_true, y_pred)),
|
|
88
|
+
"mape": float(mape(y_true, y_pred)),
|
|
89
|
+
"smape": float(smape(y_true, y_pred)),
|
|
90
|
+
}
|
|
91
|
+
per_iter.append(iter_metrics)
|
|
92
|
+
|
|
93
|
+
# Averages
|
|
94
|
+
avg_total = float(np.mean([x["total_time_s"] for x in per_iter]))
|
|
95
|
+
avg_fit = float(np.mean([x["fit_time_s"] for x in per_iter]))
|
|
96
|
+
avg_predict = float(np.mean([x["predict_time_s"] for x in per_iter]))
|
|
97
|
+
avg_mae_ = float(np.mean([x["mae"] for x in per_iter]))
|
|
98
|
+
avg_rmse_ = float(np.mean([x["rmse"] for x in per_iter]))
|
|
99
|
+
avg_mape_ = float(np.mean([x["mape"] for x in per_iter]))
|
|
100
|
+
avg_smape_ = float(np.mean([x["smape"] for x in per_iter]))
|
|
101
|
+
|
|
102
|
+
print(
|
|
103
|
+
f"\nAverages over {iterations} runs --> "
|
|
104
|
+
f"fit: {avg_fit:.4f}s | predict: {avg_predict:.4f}s | total: {avg_total:.4f}s | "
|
|
105
|
+
f"MAE: {avg_mae_:.4f} | RMSE: {avg_rmse_:.4f} | MAPE: {avg_mape_:.4f} | sMAPE: {avg_smape_:.4f}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"avg_total_time_s": round(avg_total, 2),
|
|
110
|
+
"avg_fit_time_s": round(avg_fit, 2),
|
|
111
|
+
"avg_predict_time_s": round(avg_predict, 2),
|
|
112
|
+
"avg_mae": round(avg_mae_, 3),
|
|
113
|
+
"avg_rmse": round(avg_rmse_, 3),
|
|
114
|
+
"avg_mape": round(avg_mape_, 3),
|
|
115
|
+
"avg_smape": round(avg_smape_, 3),
|
|
116
|
+
"per_iteration": per_iter,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
import time
|
|
121
|
+
import numpy as np
|
|
122
|
+
from ..metrics import mae, rmse, mape, smape
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _coerce_forecast(yp, h, model_name):
|
|
126
|
+
"""
|
|
127
|
+
Coerce various model outputs to a 1D np.ndarray of length h.
|
|
128
|
+
Handles special cases like AutoETS structure.
|
|
129
|
+
"""
|
|
130
|
+
# Special-case: your AutoETS wrapper shape
|
|
131
|
+
if model_name == "AutoETS":
|
|
132
|
+
# expected like [{'mean': np.array([...])}, ...] or similar
|
|
133
|
+
try:
|
|
134
|
+
yp = yp["mean"]
|
|
135
|
+
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
if yp.shape[0] != h:
|
|
140
|
+
raise ValueError(f"{model_name}.predict({h}) returned length {yp.shape[0]} (expected {h}).")
|
|
141
|
+
return yp
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _metrics_dict(y_true, y_pred):
|
|
145
|
+
return {
|
|
146
|
+
"mae": float(mae(y_true, y_pred)),
|
|
147
|
+
"rmse": float(rmse(y_true, y_pred)),
|
|
148
|
+
"mape": float(mape(y_true, y_pred)),
|
|
149
|
+
"smape": float(smape(y_true, y_pred)),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _avg_block(per_iter):
|
|
154
|
+
return {
|
|
155
|
+
"avg_total_time_s": (
|
|
156
|
+
round(float(np.mean([x["total_time_s"] for x in per_iter])), 2)
|
|
157
|
+
if per_iter and "total_time_s" in per_iter[0]
|
|
158
|
+
else None
|
|
159
|
+
),
|
|
160
|
+
"avg_fit_time_s": (
|
|
161
|
+
round(float(np.mean([x["fit_time_s"] for x in per_iter])), 2)
|
|
162
|
+
if per_iter and "fit_time_s" in per_iter[0]
|
|
163
|
+
else None
|
|
164
|
+
),
|
|
165
|
+
"avg_predict_time_s": (
|
|
166
|
+
round(float(np.mean([x["predict_time_s"] for x in per_iter])), 2)
|
|
167
|
+
if per_iter and "predict_time_s" in per_iter[0]
|
|
168
|
+
else None
|
|
169
|
+
),
|
|
170
|
+
"avg_mae": round(float(np.mean([x["mae"] for x in per_iter])), 3) if per_iter else None,
|
|
171
|
+
"avg_rmse": round(float(np.mean([x["rmse"] for x in per_iter])), 3) if per_iter else None,
|
|
172
|
+
"avg_mape": round(float(np.mean([x["mape"] for x in per_iter])), 3) if per_iter else None,
|
|
173
|
+
"avg_smape": round(float(np.mean([x["smape"] for x in per_iter])), 3) if per_iter else None,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _safe_fmt(x, fmt=".3f"):
|
|
178
|
+
if x is None:
|
|
179
|
+
return "—"
|
|
180
|
+
try:
|
|
181
|
+
if isinstance(x, float) and (math.isnan(x) or math.isinf(x)):
|
|
182
|
+
return "—"
|
|
183
|
+
return format(x, fmt)
|
|
184
|
+
except Exception:
|
|
185
|
+
return "—"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def benchmark_models(
|
|
189
|
+
model_classes,
|
|
190
|
+
data,
|
|
191
|
+
iterations=1,
|
|
192
|
+
h=7,
|
|
193
|
+
ensembles=("mean", "median"),
|
|
194
|
+
exclude_from_ensemble=None,
|
|
195
|
+
):
|
|
196
|
+
"""
|
|
197
|
+
(docstring unchanged)
|
|
198
|
+
"""
|
|
199
|
+
data = np.asarray(data)
|
|
200
|
+
assert len(data) > h, "Data length must be greater than forecast horizon h."
|
|
201
|
+
y_true = data[-h:]
|
|
202
|
+
model_classes = list(model_classes)
|
|
203
|
+
|
|
204
|
+
if exclude_from_ensemble is None:
|
|
205
|
+
exclude_from_ensemble = []
|
|
206
|
+
exclude_names = {(cls.__name__ if not isinstance(cls, str) else cls) for cls in exclude_from_ensemble}
|
|
207
|
+
|
|
208
|
+
results = {
|
|
209
|
+
"meta": {"iterations": iterations, "h": h, "n_models": len(model_classes)},
|
|
210
|
+
"models": {},
|
|
211
|
+
"ensembles": {},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Prepare per-model storage
|
|
215
|
+
per_model_iters = {cls.__name__: [] for cls in model_classes}
|
|
216
|
+
failed_models = set()
|
|
217
|
+
|
|
218
|
+
# Per-iteration: collect predictions for ensembles
|
|
219
|
+
ens_iters_preds = [] # list per iteration: 2D array [n_models_used x h]
|
|
220
|
+
|
|
221
|
+
for i in range(iterations):
|
|
222
|
+
iter_preds = []
|
|
223
|
+
for cls in model_classes:
|
|
224
|
+
model_name = cls.__name__
|
|
225
|
+
try:
|
|
226
|
+
# fresh model each run
|
|
227
|
+
t0 = time.time()
|
|
228
|
+
model = cls()
|
|
229
|
+
model.fit(data[:-h])
|
|
230
|
+
fit_time = time.time() - t0
|
|
231
|
+
|
|
232
|
+
t1 = time.time()
|
|
233
|
+
y_pred = model.predict(h)
|
|
234
|
+
predict_time = time.time() - t1
|
|
235
|
+
|
|
236
|
+
y_pred = _coerce_forecast(y_pred, h, model_name)
|
|
237
|
+
if np.isnan(y_pred).any():
|
|
238
|
+
print(f"Skipping model {model_name}: NaN in predictions")
|
|
239
|
+
failed_models.add(model_name)
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
total_time = fit_time + predict_time
|
|
243
|
+
|
|
244
|
+
# metrics
|
|
245
|
+
m = _metrics_dict(y_true, y_pred)
|
|
246
|
+
|
|
247
|
+
per_model_iters[model_name].append(
|
|
248
|
+
{
|
|
249
|
+
"fit_time_s": float(fit_time),
|
|
250
|
+
"predict_time_s": float(predict_time),
|
|
251
|
+
"total_time_s": float(total_time),
|
|
252
|
+
**m,
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# include in ensemble only if not excluded
|
|
257
|
+
if model_name not in exclude_names:
|
|
258
|
+
iter_preds.append(y_pred)
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
print(f"Skipping model {model_name}: {e}")
|
|
262
|
+
failed_models.add(model_name)
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# Store stacked predictions for ensembles this iteration
|
|
266
|
+
if iter_preds:
|
|
267
|
+
ens_iters_preds.append(np.vstack(iter_preds)) # shape: (n_used_models, h)
|
|
268
|
+
|
|
269
|
+
# Aggregate per-model (only include models with at least one valid iteration)
|
|
270
|
+
for model_name, per_iter in per_model_iters.items():
|
|
271
|
+
if not per_iter:
|
|
272
|
+
# Do not include empty models to avoid None in summary formatting
|
|
273
|
+
continue
|
|
274
|
+
results["models"][model_name] = {**_avg_block(per_iter), "per_iteration": per_iter}
|
|
275
|
+
|
|
276
|
+
# Compute ensembles (metrics only; no timing)
|
|
277
|
+
valid_ens = set([e.lower() for e in ensembles]) if ensembles else set()
|
|
278
|
+
for ens_type in ("mean", "median"):
|
|
279
|
+
if ens_type in valid_ens and ens_iters_preds:
|
|
280
|
+
per_iter_metrics = []
|
|
281
|
+
for stacked in ens_iters_preds:
|
|
282
|
+
if stacked.size == 0:
|
|
283
|
+
continue
|
|
284
|
+
if ens_type == "mean":
|
|
285
|
+
y_ens = np.nanmean(stacked, axis=0)
|
|
286
|
+
else: # median
|
|
287
|
+
y_ens = np.nanmedian(stacked, axis=0)
|
|
288
|
+
per_iter_metrics.append(_metrics_dict(y_true, y_ens))
|
|
289
|
+
|
|
290
|
+
if per_iter_metrics:
|
|
291
|
+
results["ensembles"][ens_type] = {
|
|
292
|
+
"avg_mae": round(float(np.mean([x["mae"] for x in per_iter_metrics])), 3),
|
|
293
|
+
"avg_rmse": round(float(np.mean([x["rmse"] for x in per_iter_metrics])), 3),
|
|
294
|
+
"avg_mape": round(float(np.mean([x["mape"] for x in per_iter_metrics])), 3),
|
|
295
|
+
"avg_smape": round(float(np.mean([x["smape"] for x in per_iter_metrics])), 3),
|
|
296
|
+
"per_iteration": per_iter_metrics,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Optional: brief console summary (use safe formatting)
|
|
300
|
+
print(f"\nBenchmark over {iterations} runs (h={h})")
|
|
301
|
+
if failed_models:
|
|
302
|
+
print(f"(Some models were skipped due to errors/NaNs: {sorted(failed_models)})")
|
|
303
|
+
|
|
304
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .metrics import mae, rmse, mape, smape
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def mae(y_true, y_pred):
|
|
5
|
+
y_true = np.asarray(y_true, float)
|
|
6
|
+
y_pred = np.asarray(y_pred, float)
|
|
7
|
+
return np.mean(np.abs(y_true - y_pred))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def rmse(y_true, y_pred):
|
|
11
|
+
y_true = np.asarray(y_true, float)
|
|
12
|
+
y_pred = np.asarray(y_pred, float)
|
|
13
|
+
return np.sqrt(np.mean((y_true - y_pred) ** 2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def mape(y_true, y_pred, epsilon=1e-8):
|
|
17
|
+
y_true = np.asarray(y_true, float)
|
|
18
|
+
y_pred = np.asarray(y_pred, float)
|
|
19
|
+
denom = np.maximum(np.abs(y_true), epsilon)
|
|
20
|
+
return np.mean(np.abs((y_true - y_pred) / denom)) * 100.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def smape(y_true, y_pred, epsilon=1e-8):
|
|
24
|
+
y_true = np.asarray(y_true, float)
|
|
25
|
+
y_pred = np.asarray(y_pred, float)
|
|
26
|
+
denom = np.maximum((np.abs(y_true) + np.abs(y_pred)) / 2.0, epsilon)
|
|
27
|
+
return np.mean(np.abs(y_true - y_pred) / denom) * 100.0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _weighted_quantile(values, weights, q):
|
|
5
|
+
values = np.asarray(values, float)
|
|
6
|
+
weights = np.asarray(weights, float)
|
|
7
|
+
srt = np.argsort(values)
|
|
8
|
+
v, w = values[srt], weights[srt]
|
|
9
|
+
cw = np.cumsum(w) / np.sum(w)
|
|
10
|
+
idx = np.searchsorted(cw, q, side="left")
|
|
11
|
+
idx = np.clip(idx, 0, len(v) - 1)
|
|
12
|
+
return float(v[idx])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _golden_section_minimize(f, a, b, tol=1e-6, max_iter=200):
|
|
16
|
+
phi = (1 + 5**0.5) / 2
|
|
17
|
+
invphi = 1 / phi
|
|
18
|
+
c = b - invphi * (b - a)
|
|
19
|
+
d = a + invphi * (b - a)
|
|
20
|
+
fc = f(c)
|
|
21
|
+
fd = f(d)
|
|
22
|
+
for _ in range(max_iter):
|
|
23
|
+
if abs(b - a) < tol:
|
|
24
|
+
break
|
|
25
|
+
if fc < fd:
|
|
26
|
+
b, d, fd = d, c, fc
|
|
27
|
+
c = b - invphi * (b - a)
|
|
28
|
+
fc = f(c)
|
|
29
|
+
else:
|
|
30
|
+
a, c, fc = c, d, fd
|
|
31
|
+
d = a + invphi * (b - a)
|
|
32
|
+
fd = f(d)
|
|
33
|
+
return (a + b) / 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _penalty_value(r, kind="l2", delta=1.0, tau=0.5):
|
|
37
|
+
if kind == "l2":
|
|
38
|
+
return 0.5 * r * r
|
|
39
|
+
elif kind == "l1":
|
|
40
|
+
return np.abs(r)
|
|
41
|
+
elif kind == "huber":
|
|
42
|
+
a = np.abs(r)
|
|
43
|
+
return np.where(a <= delta, 0.5 * r * r, delta * (a - 0.5 * delta))
|
|
44
|
+
elif kind == "pinball":
|
|
45
|
+
return np.where(r >= 0, tau * r, (tau - 1.0) * r)
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError("Unknown penalty")
|