eb-adapters 0.1.0__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.
- eb_adapters/__init__.py +34 -0
- eb_adapters/base.py +151 -0
- eb_adapters/catboost.py +218 -0
- eb_adapters/lightgbm.py +213 -0
- eb_adapters/prophet.py +171 -0
- eb_adapters/statsmodels.py +366 -0
- eb_adapters-0.1.0.dist-info/METADATA +182 -0
- eb_adapters-0.1.0.dist-info/RECORD +11 -0
- eb_adapters-0.1.0.dist-info/WHEEL +5 -0
- eb_adapters-0.1.0.dist-info/licenses/LICENSE +28 -0
- eb_adapters-0.1.0.dist-info/top_level.txt +1 -0
eb_adapters/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
eb_adapters.
|
|
5
|
+
|
|
6
|
+
Adapter classes for integrating external forecasting and regression engines
|
|
7
|
+
(Prophet, statsmodels, CatBoost, LightGBM, etc.) into the ElectricBarometer
|
|
8
|
+
ecosystem using a consistent scikit-learn-like interface.
|
|
9
|
+
|
|
10
|
+
All adapters exposed by this package implement:
|
|
11
|
+
|
|
12
|
+
- `fit(X, y, sample_weight=None)` returning `self`
|
|
13
|
+
- `predict(X)` returning a one-dimensional numpy array
|
|
14
|
+
|
|
15
|
+
This allows ElectricBarometer evaluation, selection, and cloning utilities to
|
|
16
|
+
treat native scikit-learn estimators and wrapped external models uniformly.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .base import BaseAdapter, _clone_model, clone_model
|
|
20
|
+
from .prophet import ProphetAdapter
|
|
21
|
+
from .statsmodels import SarimaxAdapter, ArimaAdapter
|
|
22
|
+
from .catboost import CatBoostAdapter
|
|
23
|
+
from .lightgbm import LightGBMRegressorAdapter
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BaseAdapter",
|
|
27
|
+
"_clone_model",
|
|
28
|
+
"clone_model",
|
|
29
|
+
"ProphetAdapter",
|
|
30
|
+
"SarimaxAdapter",
|
|
31
|
+
"ArimaAdapter",
|
|
32
|
+
"CatBoostAdapter",
|
|
33
|
+
"LightGBMRegressorAdapter",
|
|
34
|
+
]
|
eb_adapters/base.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Base adapter interfaces and cloning utilities.
|
|
5
|
+
|
|
6
|
+
This module defines the minimal adapter contract used throughout the
|
|
7
|
+
ElectricBarometer ecosystem, along with a lightweight cloning helper for
|
|
8
|
+
estimator-like objects.
|
|
9
|
+
|
|
10
|
+
Adapters are intended to wrap non-scikit-learn forecasting or regression
|
|
11
|
+
libraries (for example, statsmodels, Prophet, or custom models) and expose
|
|
12
|
+
a scikit-learn-like interface so they can be used interchangeably inside
|
|
13
|
+
ElectricBarometer evaluation and selection workflows.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _clone_model(model: Any) -> Any:
|
|
22
|
+
"""
|
|
23
|
+
Lightweight cloning utility for estimator-like or adapter-like objects.
|
|
24
|
+
|
|
25
|
+
This function attempts to reconstruct a fresh instance of a model using a
|
|
26
|
+
best-effort strategy that favors compatibility with scikit-learn-style APIs
|
|
27
|
+
while remaining usable for custom adapters.
|
|
28
|
+
|
|
29
|
+
Cloning strategy
|
|
30
|
+
----------------
|
|
31
|
+
The following steps are attempted in order:
|
|
32
|
+
|
|
33
|
+
1. If scikit-learn is available, call `sklearn.base.clone(model)`.
|
|
34
|
+
2. Otherwise, if the object implements `get_params()`, re-instantiate via::
|
|
35
|
+
|
|
36
|
+
model.__class__(**model.get_params())
|
|
37
|
+
|
|
38
|
+
3. As a final fallback, instantiate the class with no arguments::
|
|
39
|
+
|
|
40
|
+
model.__class__()
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
For custom adapters, the most reliable approach is to make the adapter
|
|
45
|
+
configuration-only at initialization time and implement `get_params()`
|
|
46
|
+
so that the instance can be reconstructed deterministically.
|
|
47
|
+
|
|
48
|
+
If a model cannot be meaningfully cloned using parameters alone, callers
|
|
49
|
+
may choose to bypass this helper and explicitly construct fresh adapter
|
|
50
|
+
instances before passing them into ElectricBarometer workflows.
|
|
51
|
+
"""
|
|
52
|
+
# Try sklearn.clone if available
|
|
53
|
+
try: # pragma: no cover - optional dependency path
|
|
54
|
+
from sklearn.base import clone as sk_clone # type: ignore
|
|
55
|
+
|
|
56
|
+
return sk_clone(model)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# Fallback: re-create via class + get_params
|
|
61
|
+
if hasattr(model, "get_params"):
|
|
62
|
+
try:
|
|
63
|
+
params = model.get_params() # type: ignore[assignment]
|
|
64
|
+
return model.__class__(**params)
|
|
65
|
+
except Exception:
|
|
66
|
+
# If get_params exists but reconstruction fails, fall through
|
|
67
|
+
# to the final fallback below.
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# Last resort: call class with no args
|
|
71
|
+
return model.__class__()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Optional public alias for convenience / backwards compatibility
|
|
75
|
+
clone_model = _clone_model
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BaseAdapter:
|
|
79
|
+
"""
|
|
80
|
+
Minimal base class defining the adapter contract for ElectricBarometer.
|
|
81
|
+
|
|
82
|
+
This class documents the expected interface for wrapping non-scikit-learn
|
|
83
|
+
forecasting or regression engines so they can be evaluated and selected
|
|
84
|
+
alongside native scikit-learn estimators.
|
|
85
|
+
|
|
86
|
+
Subclasses are expected to present a scikit-learn-like API:
|
|
87
|
+
|
|
88
|
+
- `fit(X, y, sample_weight=None)` returning `self`
|
|
89
|
+
- `predict(X)` returning a one-dimensional numpy array
|
|
90
|
+
|
|
91
|
+
The ElectricBarometer engine does not distinguish between native
|
|
92
|
+
scikit-learn estimators and adapters; it simply calls `fit` and `predict`.
|
|
93
|
+
This base class serves as a clear, documented contract for adapter authors.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def fit(
|
|
97
|
+
self,
|
|
98
|
+
X: np.ndarray,
|
|
99
|
+
y: np.ndarray,
|
|
100
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
101
|
+
) -> "BaseAdapter":
|
|
102
|
+
"""
|
|
103
|
+
Fit the underlying forecasting or regression model.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
X : numpy.ndarray
|
|
108
|
+
Feature matrix. For pure time-series models, this may be ignored
|
|
109
|
+
or used only for alignment.
|
|
110
|
+
y : numpy.ndarray
|
|
111
|
+
One-dimensional target vector.
|
|
112
|
+
sample_weight : numpy.ndarray | None
|
|
113
|
+
Optional per-sample weights. Adapters may ignore this argument if
|
|
114
|
+
weighting is not supported by the underlying model.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
BaseAdapter
|
|
119
|
+
The fitted adapter instance (self).
|
|
120
|
+
|
|
121
|
+
Raises
|
|
122
|
+
------
|
|
123
|
+
NotImplementedError
|
|
124
|
+
If the subclass does not override this method.
|
|
125
|
+
"""
|
|
126
|
+
raise NotImplementedError(
|
|
127
|
+
"BaseAdapter subclasses must implement fit(X, y, sample_weight=None)."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
131
|
+
"""
|
|
132
|
+
Generate predictions from the fitted model.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
X : numpy.ndarray
|
|
137
|
+
Feature matrix used to generate predictions.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
numpy.ndarray
|
|
142
|
+
One-dimensional array of predictions.
|
|
143
|
+
|
|
144
|
+
Raises
|
|
145
|
+
------
|
|
146
|
+
NotImplementedError
|
|
147
|
+
If the subclass does not override this method.
|
|
148
|
+
"""
|
|
149
|
+
raise NotImplementedError(
|
|
150
|
+
"BaseAdapter subclasses must implement predict(X)."
|
|
151
|
+
)
|
eb_adapters/catboost.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
CatBoost adapter.
|
|
5
|
+
|
|
6
|
+
This module provides `CatBoostAdapter`, a thin wrapper around
|
|
7
|
+
`catboost.CatBoostRegressor` with a scikit-learn-like interface (`fit`, `predict`).
|
|
8
|
+
|
|
9
|
+
The adapter is designed for use within the ElectricBarometer ecosystem and aims to be:
|
|
10
|
+
|
|
11
|
+
- Lightweight: minimal behavior beyond input normalization and parameter storage.
|
|
12
|
+
- Cloneable: constructor parameters are preserved in `self.params` so cloning utilities
|
|
13
|
+
can reconstruct the instance consistently.
|
|
14
|
+
- Optional-dependency safe: importing this module does not require CatBoost, but
|
|
15
|
+
instantiating `CatBoostAdapter` does.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from .base import BaseAdapter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Optional CatBoost dependency guard -----------------------------------------
|
|
27
|
+
try: # pragma: no cover - optional dependency
|
|
28
|
+
from catboost import CatBoostRegressor # type: ignore
|
|
29
|
+
|
|
30
|
+
HAS_CATBOOST = True
|
|
31
|
+
except Exception: # pragma: no cover - optional dependency
|
|
32
|
+
CatBoostRegressor = None
|
|
33
|
+
HAS_CATBOOST = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CatBoostAdapter(BaseAdapter):
|
|
37
|
+
"""
|
|
38
|
+
Adapter for `catboost.CatBoostRegressor`.
|
|
39
|
+
|
|
40
|
+
This adapter exposes a scikit-learn-like API and stores initialization parameters
|
|
41
|
+
so the instance can be reconstructed by cloning utilities (for example, an internal
|
|
42
|
+
`clone_model()` helper or `sklearn.base.clone`).
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
**params
|
|
47
|
+
Keyword arguments forwarded to `catboost.CatBoostRegressor`.
|
|
48
|
+
|
|
49
|
+
Notes
|
|
50
|
+
-----
|
|
51
|
+
- `X` and `y` are treated as standard tabular regression inputs.
|
|
52
|
+
- If provided, `sample_weight` is passed through to CatBoost training.
|
|
53
|
+
- Training verbosity is disabled by default (`verbose=False`) unless the caller
|
|
54
|
+
supplies `verbose` explicitly.
|
|
55
|
+
- All initialization parameters are stored in `self.params`.
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
>>> model = CatBoostAdapter(
|
|
60
|
+
... depth=4,
|
|
61
|
+
... learning_rate=0.1,
|
|
62
|
+
... iterations=200,
|
|
63
|
+
... loss_function="RMSE",
|
|
64
|
+
... )
|
|
65
|
+
>>> # X, y are numpy arrays (or array-like)
|
|
66
|
+
>>> # model.fit(X, y).predict(X)
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, **params: Any) -> None:
|
|
71
|
+
if not HAS_CATBOOST:
|
|
72
|
+
raise ImportError(
|
|
73
|
+
"CatBoostAdapter requires the optional 'catboost' package. "
|
|
74
|
+
"Install it via `pip install catboost`."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Store params for clone() compatibility
|
|
78
|
+
self.params: Dict[str, Any] = dict(params)
|
|
79
|
+
|
|
80
|
+
# Default: no spammy training logs
|
|
81
|
+
if "verbose" not in self.params:
|
|
82
|
+
self.params["verbose"] = False
|
|
83
|
+
|
|
84
|
+
# Instantiate the underlying CatBoost model
|
|
85
|
+
self.model: Optional[CatBoostRegressor] = CatBoostRegressor(**self.params)
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Fit
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
def fit(
|
|
91
|
+
self,
|
|
92
|
+
X: np.ndarray,
|
|
93
|
+
y: np.ndarray,
|
|
94
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
95
|
+
) -> "CatBoostAdapter":
|
|
96
|
+
"""
|
|
97
|
+
Fit the underlying `catboost.CatBoostRegressor`.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
X : numpy.ndarray
|
|
102
|
+
Feature matrix of shape (n_samples, n_features).
|
|
103
|
+
y : numpy.ndarray
|
|
104
|
+
Target vector of shape (n_samples,).
|
|
105
|
+
sample_weight : numpy.ndarray | None
|
|
106
|
+
Optional per-sample weights of shape (n_samples,). If provided, this is
|
|
107
|
+
forwarded to CatBoost training.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
CatBoostAdapter
|
|
112
|
+
The fitted adapter (self), allowing method chaining.
|
|
113
|
+
|
|
114
|
+
Raises
|
|
115
|
+
------
|
|
116
|
+
RuntimeError
|
|
117
|
+
If CatBoost is not available or the internal model is not initialized.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
if not HAS_CATBOOST or self.model is None:
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"CatBoostAdapter cannot train: CatBoost is not available or "
|
|
123
|
+
"the internal model was not initialized correctly."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
X_arr = np.asarray(X)
|
|
127
|
+
y_arr = np.asarray(y, dtype=float)
|
|
128
|
+
|
|
129
|
+
if sample_weight is not None:
|
|
130
|
+
sw_arr = np.asarray(sample_weight, dtype=float)
|
|
131
|
+
self.model.fit(X_arr, y_arr, sample_weight=sw_arr)
|
|
132
|
+
else:
|
|
133
|
+
self.model.fit(X_arr, y_arr)
|
|
134
|
+
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
# Predict
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
141
|
+
"""
|
|
142
|
+
Predict using the fitted CatBoost model.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
X : numpy.ndarray
|
|
147
|
+
Feature matrix of shape (n_samples, n_features).
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
numpy.ndarray
|
|
152
|
+
Predicted values of shape (n_samples,).
|
|
153
|
+
|
|
154
|
+
Raises
|
|
155
|
+
------
|
|
156
|
+
RuntimeError
|
|
157
|
+
If the adapter has not been fit yet.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
if self.model is None:
|
|
161
|
+
raise RuntimeError("CatBoostAdapter has not been fit yet. Call `fit(...)` first.")
|
|
162
|
+
|
|
163
|
+
X_arr = np.asarray(X)
|
|
164
|
+
preds = self.model.predict(X_arr)
|
|
165
|
+
return np.asarray(preds, dtype=float).ravel()
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Param API for clone_model() compatibility
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Return initialization parameters for cloning utilities.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
deep : bool
|
|
177
|
+
Included for scikit-learn compatibility. This adapter does not expose
|
|
178
|
+
nested estimators, so the value does not change the output.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
dict[str, Any]
|
|
183
|
+
A shallow copy of the stored initialization parameters.
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
_ = deep # intentionally unused; kept for API compatibility
|
|
187
|
+
return dict(self.params)
|
|
188
|
+
|
|
189
|
+
def set_params(self, **params: Any) -> "CatBoostAdapter":
|
|
190
|
+
"""
|
|
191
|
+
Update parameters and rebuild the underlying CatBoost model.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
**params
|
|
196
|
+
Keyword parameters to merge into the stored initialization parameters.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
CatBoostAdapter
|
|
201
|
+
The updated adapter instance (self).
|
|
202
|
+
|
|
203
|
+
Notes
|
|
204
|
+
-----
|
|
205
|
+
This method updates `self.params` and then re-instantiates
|
|
206
|
+
`catboost.CatBoostRegressor` using the merged parameter set.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
self.params.update(params)
|
|
210
|
+
if HAS_CATBOOST:
|
|
211
|
+
self.model = CatBoostRegressor(**self.params)
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
# ------------------------------------------------------------------
|
|
215
|
+
# Representation
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
def __repr__(self) -> str:
|
|
218
|
+
return f"CatBoostAdapter(params={self.params})"
|
eb_adapters/lightgbm.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
LightGBM adapter.
|
|
5
|
+
|
|
6
|
+
This module provides `LightGBMRegressorAdapter`, a thin wrapper around
|
|
7
|
+
`lightgbm.LGBMRegressor` with a scikit-learn-like interface (`fit`, `predict`).
|
|
8
|
+
|
|
9
|
+
The adapter is designed for use within the ElectricBarometer ecosystem and aims to be:
|
|
10
|
+
|
|
11
|
+
- Lightweight: minimal behavior beyond input normalization and parameter storage.
|
|
12
|
+
- Cloneable: constructor parameters are preserved so cloning utilities can
|
|
13
|
+
reconstruct the instance consistently.
|
|
14
|
+
- Optional-dependency safe: importing this module does not require LightGBM, but
|
|
15
|
+
instantiating `LightGBMRegressorAdapter` does.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from .base import BaseAdapter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Optional LightGBM dependency guard ------------------------------------------
|
|
27
|
+
try: # pragma: no cover - optional dependency
|
|
28
|
+
from lightgbm import LGBMRegressor # type: ignore
|
|
29
|
+
|
|
30
|
+
HAS_LIGHTGBM = True
|
|
31
|
+
except Exception: # pragma: no cover - optional dependency
|
|
32
|
+
LGBMRegressor = None
|
|
33
|
+
HAS_LIGHTGBM = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LightGBMRegressorAdapter(BaseAdapter):
|
|
37
|
+
"""
|
|
38
|
+
Adapter for `lightgbm.LGBMRegressor`.
|
|
39
|
+
|
|
40
|
+
This adapter exposes a scikit-learn-like API and stores initialization parameters
|
|
41
|
+
so the instance can be reconstructed by cloning utilities (for example, an internal
|
|
42
|
+
`clone_model()` helper or `sklearn.base.clone`).
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
**lgbm_params
|
|
47
|
+
Keyword arguments forwarded to `lightgbm.LGBMRegressor`.
|
|
48
|
+
|
|
49
|
+
Notes
|
|
50
|
+
-----
|
|
51
|
+
- `X` and `y` are treated as standard tabular regression inputs.
|
|
52
|
+
- If provided, `sample_weight` is passed through to LightGBM training.
|
|
53
|
+
- All initialization parameters are stored in `self.lgbm_params`.
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
>>> model = LightGBMRegressorAdapter(
|
|
58
|
+
... n_estimators=200,
|
|
59
|
+
... learning_rate=0.05,
|
|
60
|
+
... max_depth=-1,
|
|
61
|
+
... )
|
|
62
|
+
>>> # X, y are numpy arrays (or array-like)
|
|
63
|
+
>>> # model.fit(X, y).predict(X)
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, **lgbm_params: Any) -> None:
|
|
68
|
+
if not HAS_LIGHTGBM:
|
|
69
|
+
raise ImportError(
|
|
70
|
+
"LightGBMRegressorAdapter requires the optional 'lightgbm' package. "
|
|
71
|
+
"Install it via `pip install lightgbm`."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Store init params so the adapter is cloneable.
|
|
75
|
+
self.lgbm_params: Dict[str, Any] = dict(lgbm_params)
|
|
76
|
+
|
|
77
|
+
# Underlying LightGBM model instance
|
|
78
|
+
self.model: Optional[LGBMRegressor] = LGBMRegressor(**self.lgbm_params)
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Fit
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
def fit(
|
|
84
|
+
self,
|
|
85
|
+
X: np.ndarray,
|
|
86
|
+
y: np.ndarray,
|
|
87
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
88
|
+
) -> "LightGBMRegressorAdapter":
|
|
89
|
+
"""
|
|
90
|
+
Fit the underlying `lightgbm.LGBMRegressor`.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
X : numpy.ndarray
|
|
95
|
+
Feature matrix of shape (n_samples, n_features).
|
|
96
|
+
y : numpy.ndarray
|
|
97
|
+
Target vector of shape (n_samples,).
|
|
98
|
+
sample_weight : numpy.ndarray | None
|
|
99
|
+
Optional per-sample weights of shape (n_samples,). If provided, this is
|
|
100
|
+
forwarded to LightGBM training.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
LightGBMRegressorAdapter
|
|
105
|
+
The fitted adapter (self), allowing method chaining.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
RuntimeError
|
|
110
|
+
If LightGBM is not available or the internal model is not initialized.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
if not HAS_LIGHTGBM or self.model is None:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"LightGBMRegressorAdapter cannot train: LightGBM is not available "
|
|
116
|
+
"or the internal model was not initialized."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
X_arr = np.asarray(X)
|
|
120
|
+
y_arr = np.asarray(y, dtype=float)
|
|
121
|
+
|
|
122
|
+
if sample_weight is not None:
|
|
123
|
+
sw_arr = np.asarray(sample_weight, dtype=float)
|
|
124
|
+
self.model.fit(X_arr, y_arr, sample_weight=sw_arr)
|
|
125
|
+
else:
|
|
126
|
+
self.model.fit(X_arr, y_arr)
|
|
127
|
+
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Predict
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
134
|
+
"""
|
|
135
|
+
Predict using the fitted LightGBM model.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
X : numpy.ndarray
|
|
140
|
+
Feature matrix of shape (n_samples, n_features).
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
numpy.ndarray
|
|
145
|
+
Predicted values of shape (n_samples,).
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
RuntimeError
|
|
150
|
+
If the adapter has not been fit yet.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
if self.model is None:
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
"LightGBMRegressorAdapter has not been fit yet. Call `fit(...)` first."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
X_arr = np.asarray(X)
|
|
159
|
+
preds = self.model.predict(X_arr)
|
|
160
|
+
return np.asarray(preds, dtype=float).ravel()
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
# Param API for clone_model() compatibility
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
166
|
+
"""
|
|
167
|
+
Return initialization parameters for cloning utilities.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
deep : bool
|
|
172
|
+
Included for scikit-learn compatibility. This adapter does not expose
|
|
173
|
+
nested estimators, so the value does not change the output.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
dict[str, Any]
|
|
178
|
+
A shallow copy of the stored initialization parameters.
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
_ = deep # intentionally unused; kept for API compatibility
|
|
182
|
+
return dict(self.lgbm_params)
|
|
183
|
+
|
|
184
|
+
def set_params(self, **params: Any) -> "LightGBMRegressorAdapter":
|
|
185
|
+
"""
|
|
186
|
+
Update parameters and rebuild the underlying LightGBM model.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
**params
|
|
191
|
+
Keyword parameters to merge into the stored initialization parameters.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
LightGBMRegressorAdapter
|
|
196
|
+
The updated adapter instance (self).
|
|
197
|
+
|
|
198
|
+
Notes
|
|
199
|
+
-----
|
|
200
|
+
This method updates `self.lgbm_params` and then re-instantiates
|
|
201
|
+
`lightgbm.LGBMRegressor` using the merged parameter set.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
self.lgbm_params.update(params)
|
|
205
|
+
if HAS_LIGHTGBM:
|
|
206
|
+
self.model = LGBMRegressor(**self.lgbm_params)
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# Representation
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
return f"LightGBMRegressorAdapter(params={self.lgbm_params})"
|
eb_adapters/prophet.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Prophet adapter.
|
|
5
|
+
|
|
6
|
+
This module provides `ProphetAdapter`, a thin wrapper around `prophet.Prophet`
|
|
7
|
+
with a scikit-learn-like interface (`fit`, `predict`).
|
|
8
|
+
|
|
9
|
+
Unlike tabular regressors, Prophet expects a pandas DataFrame with a timestamp
|
|
10
|
+
column named `ds` and a target column named `y`. This adapter converts common
|
|
11
|
+
array-like inputs into that canonical format for use within the
|
|
12
|
+
ElectricBarometer ecosystem.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from .base import BaseAdapter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProphetAdapter(BaseAdapter):
|
|
23
|
+
"""
|
|
24
|
+
Adapter for `prophet.Prophet`.
|
|
25
|
+
|
|
26
|
+
This adapter enables Prophet models to be used inside ElectricBarometer or
|
|
27
|
+
other CWSL-based evaluation workflows by exposing a scikit-learn-like API.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
model : Any | None
|
|
32
|
+
Optional pre-configured `prophet.Prophet` instance. If None, a default
|
|
33
|
+
Prophet model is constructed. If the `prophet` package is not installed,
|
|
34
|
+
constructing a default model will raise `ImportError`.
|
|
35
|
+
|
|
36
|
+
Notes
|
|
37
|
+
-----
|
|
38
|
+
Input conventions:
|
|
39
|
+
|
|
40
|
+
- `X` encodes the time index as either:
|
|
41
|
+
- shape (n_samples,) of datetime-like values, or
|
|
42
|
+
- shape (n_samples, n_features) where the first column is datetime-like
|
|
43
|
+
- `y` is a one-dimensional array-like of numeric targets.
|
|
44
|
+
|
|
45
|
+
At fit time, the adapter constructs a DataFrame with columns:
|
|
46
|
+
|
|
47
|
+
- `ds`: timestamps parsed from `X`
|
|
48
|
+
- `y`: targets from `y`
|
|
49
|
+
|
|
50
|
+
and calls `Prophet.fit(df)`.
|
|
51
|
+
|
|
52
|
+
At predict time, the adapter constructs a DataFrame with column `ds` and
|
|
53
|
+
returns the `yhat` predictions as a one-dimensional numpy array.
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
>>> from prophet import Prophet
|
|
58
|
+
>>> base = Prophet()
|
|
59
|
+
>>> model = ProphetAdapter(model=base)
|
|
60
|
+
>>> # X contains datetimes, y contains numeric targets
|
|
61
|
+
>>> # model.fit(X, y).predict(X)
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, model: Optional[Any] = None) -> None:
|
|
66
|
+
if model is None:
|
|
67
|
+
try:
|
|
68
|
+
from prophet import Prophet as _Prophet # type: ignore
|
|
69
|
+
except Exception as e: # pragma: no cover - import failure path
|
|
70
|
+
raise ImportError(
|
|
71
|
+
"ProphetAdapter requires the optional 'prophet' package. "
|
|
72
|
+
"Install it via `pip install prophet`."
|
|
73
|
+
) from e
|
|
74
|
+
|
|
75
|
+
model = _Prophet()
|
|
76
|
+
|
|
77
|
+
self.model = model
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# Fit
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
def fit(
|
|
83
|
+
self,
|
|
84
|
+
X: np.ndarray,
|
|
85
|
+
y: np.ndarray,
|
|
86
|
+
sample_weight: Optional[np.ndarray] = None, # ignored
|
|
87
|
+
) -> "ProphetAdapter":
|
|
88
|
+
"""
|
|
89
|
+
Fit the underlying Prophet model.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
X : numpy.ndarray
|
|
94
|
+
Time index values. Accepted forms are:
|
|
95
|
+
- shape (n_samples,) of datetime-like values, or
|
|
96
|
+
- shape (n_samples, n_features) where the first column is datetime-like
|
|
97
|
+
y : numpy.ndarray
|
|
98
|
+
Target vector of shape (n_samples,).
|
|
99
|
+
sample_weight : numpy.ndarray | None
|
|
100
|
+
Accepted for API compatibility but ignored by this adapter.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
ProphetAdapter
|
|
105
|
+
The fitted adapter (self), allowing method chaining.
|
|
106
|
+
|
|
107
|
+
Notes
|
|
108
|
+
-----
|
|
109
|
+
This method imports pandas locally to avoid making pandas a hard
|
|
110
|
+
dependency at module import time.
|
|
111
|
+
"""
|
|
112
|
+
_ = sample_weight # intentionally unused; kept for API compatibility
|
|
113
|
+
|
|
114
|
+
# Local import to avoid making pandas a hard dependency for the module
|
|
115
|
+
import pandas as pd
|
|
116
|
+
|
|
117
|
+
X_arr = np.asarray(X)
|
|
118
|
+
|
|
119
|
+
# Use the first column if 2D
|
|
120
|
+
if X_arr.ndim > 1:
|
|
121
|
+
X_arr = X_arr[:, 0]
|
|
122
|
+
|
|
123
|
+
ds = pd.to_datetime(X_arr)
|
|
124
|
+
y_arr = np.asarray(y, dtype=float)
|
|
125
|
+
|
|
126
|
+
df = pd.DataFrame({"ds": ds, "y": y_arr})
|
|
127
|
+
self.model.fit(df)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Predict
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
134
|
+
"""
|
|
135
|
+
Predict using the fitted Prophet model.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
X : numpy.ndarray
|
|
140
|
+
Time index values in the same format accepted by `fit`.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
numpy.ndarray
|
|
145
|
+
Predicted values of shape (n_samples,), taken from Prophet's `yhat`
|
|
146
|
+
output column.
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
RuntimeError
|
|
151
|
+
If the Prophet forecast output does not contain the `yhat` column.
|
|
152
|
+
|
|
153
|
+
Notes
|
|
154
|
+
-----
|
|
155
|
+
This method imports pandas locally to avoid making pandas a hard
|
|
156
|
+
dependency at module import time.
|
|
157
|
+
"""
|
|
158
|
+
import pandas as pd
|
|
159
|
+
|
|
160
|
+
X_arr = np.asarray(X)
|
|
161
|
+
if X_arr.ndim > 1:
|
|
162
|
+
X_arr = X_arr[:, 0]
|
|
163
|
+
|
|
164
|
+
ds = pd.to_datetime(X_arr)
|
|
165
|
+
df_future = pd.DataFrame({"ds": ds})
|
|
166
|
+
forecast = self.model.predict(df_future)
|
|
167
|
+
|
|
168
|
+
if "yhat" not in forecast.columns:
|
|
169
|
+
raise RuntimeError("ProphetAdapter: expected 'yhat' column in forecast output.")
|
|
170
|
+
|
|
171
|
+
return np.asarray(forecast["yhat"], dtype=float)
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Statsmodels adapters.
|
|
5
|
+
|
|
6
|
+
This module provides thin wrappers around univariate statsmodels time-series models so
|
|
7
|
+
they can be used inside the ElectricBarometer ecosystem via a scikit-learn-like API.
|
|
8
|
+
|
|
9
|
+
Adapters in this module intentionally treat `X` as an index placeholder:
|
|
10
|
+
|
|
11
|
+
- `fit(X, y)` fits the underlying time-series model to `y` only.
|
|
12
|
+
- `predict(X)` forecasts `len(X)` steps ahead from the end of the training sample.
|
|
13
|
+
|
|
14
|
+
This design supports evaluation workflows that expect the `predict(X)` signature while
|
|
15
|
+
remaining faithful to how classic univariate ARIMA-family models operate.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from .base import BaseAdapter
|
|
23
|
+
|
|
24
|
+
# Optional statsmodels support ------------------------------------------------
|
|
25
|
+
try: # pragma: no cover - import guard
|
|
26
|
+
import statsmodels.api as _sm # type: ignore[import]
|
|
27
|
+
|
|
28
|
+
HAS_STATSMODELS = True
|
|
29
|
+
except Exception: # pragma: no cover - import guard
|
|
30
|
+
_sm = None
|
|
31
|
+
HAS_STATSMODELS = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SarimaxAdapter(BaseAdapter):
|
|
35
|
+
"""
|
|
36
|
+
Adapter for `statsmodels` SARIMAX.
|
|
37
|
+
|
|
38
|
+
This wrapper fits a univariate SARIMAX model on `y` and produces forecasts for
|
|
39
|
+
`len(X)` steps ahead when `predict(X)` is called.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
order : tuple[int, int, int], default (1, 0, 0)
|
|
44
|
+
ARIMA (p, d, q) order.
|
|
45
|
+
seasonal_order : tuple[int, int, int, int], default (0, 0, 0, 0)
|
|
46
|
+
Seasonal (P, D, Q, s) order.
|
|
47
|
+
trend : str | None, default None
|
|
48
|
+
Trend specification forwarded to SARIMAX.
|
|
49
|
+
enforce_stationarity : bool, default True
|
|
50
|
+
Whether to enforce stationarity in the SARIMAX model.
|
|
51
|
+
enforce_invertibility : bool, default True
|
|
52
|
+
Whether to enforce invertibility in the SARIMAX model.
|
|
53
|
+
|
|
54
|
+
Notes
|
|
55
|
+
-----
|
|
56
|
+
- `X` is ignored during fitting. It is only used at prediction time to
|
|
57
|
+
determine the forecast horizon (`n_steps = len(X)`).
|
|
58
|
+
- This adapter stores initialization parameters via `get_params()` so cloning
|
|
59
|
+
utilities can reconstruct the adapter.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
order: tuple[int, int, int] = (1, 0, 0),
|
|
65
|
+
seasonal_order: tuple[int, int, int, int] = (0, 0, 0, 0),
|
|
66
|
+
trend: Optional[str] = None,
|
|
67
|
+
enforce_stationarity: bool = True,
|
|
68
|
+
enforce_invertibility: bool = True,
|
|
69
|
+
) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.order = order
|
|
72
|
+
self.seasonal_order = seasonal_order
|
|
73
|
+
self.trend = trend
|
|
74
|
+
self.enforce_stationarity = enforce_stationarity
|
|
75
|
+
self.enforce_invertibility = enforce_invertibility
|
|
76
|
+
|
|
77
|
+
self._result = None
|
|
78
|
+
|
|
79
|
+
# Failure is intentionally delayed until fit(); this flag supports
|
|
80
|
+
# feature detection and optional-dependency behavior.
|
|
81
|
+
if not HAS_STATSMODELS:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def fit(
|
|
85
|
+
self,
|
|
86
|
+
X: np.ndarray,
|
|
87
|
+
y: np.ndarray,
|
|
88
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
89
|
+
) -> "SarimaxAdapter":
|
|
90
|
+
"""
|
|
91
|
+
Fit a univariate SARIMAX model on `y`.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
X : numpy.ndarray
|
|
96
|
+
Ignored. Present for API compatibility.
|
|
97
|
+
y : numpy.ndarray
|
|
98
|
+
Target series of shape (n_samples,).
|
|
99
|
+
sample_weight : numpy.ndarray | None
|
|
100
|
+
Accepted for API compatibility but ignored by this adapter.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
SarimaxAdapter
|
|
105
|
+
The fitted adapter (self), allowing method chaining.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
ImportError
|
|
110
|
+
If `statsmodels` is not installed.
|
|
111
|
+
"""
|
|
112
|
+
_ = X # intentionally unused; kept for API compatibility
|
|
113
|
+
_ = sample_weight # intentionally unused; kept for API compatibility
|
|
114
|
+
|
|
115
|
+
if not HAS_STATSMODELS:
|
|
116
|
+
raise ImportError(
|
|
117
|
+
"SarimaxAdapter requires the optional 'statsmodels' package. "
|
|
118
|
+
"Install it via `pip install statsmodels`."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
y_arr = np.asarray(y, dtype=float)
|
|
122
|
+
|
|
123
|
+
model = _sm.tsa.statespace.SARIMAX(
|
|
124
|
+
y_arr,
|
|
125
|
+
order=self.order,
|
|
126
|
+
seasonal_order=self.seasonal_order,
|
|
127
|
+
trend=self.trend,
|
|
128
|
+
enforce_stationarity=self.enforce_stationarity,
|
|
129
|
+
enforce_invertibility=self.enforce_invertibility,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Keep fitting lightweight for typical adapter usage and tests.
|
|
133
|
+
self._result = model.fit(disp=False, maxiter=50)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
137
|
+
"""
|
|
138
|
+
Forecast `len(X)` steps ahead from the end of the training sample.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
X : numpy.ndarray
|
|
143
|
+
Array-like placeholder used to determine the forecast horizon.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
numpy.ndarray
|
|
148
|
+
Forecast values of shape (len(X),).
|
|
149
|
+
|
|
150
|
+
Raises
|
|
151
|
+
------
|
|
152
|
+
RuntimeError
|
|
153
|
+
If the adapter has not been fit yet.
|
|
154
|
+
"""
|
|
155
|
+
if self._result is None:
|
|
156
|
+
raise RuntimeError("SarimaxAdapter has not been fit yet. Call `fit(X, y)` first.")
|
|
157
|
+
|
|
158
|
+
n_steps = len(X)
|
|
159
|
+
if n_steps <= 0:
|
|
160
|
+
return np.array([], dtype=float)
|
|
161
|
+
|
|
162
|
+
forecast = self._result.forecast(steps=n_steps)
|
|
163
|
+
return np.asarray(forecast, dtype=float)
|
|
164
|
+
|
|
165
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
166
|
+
"""
|
|
167
|
+
Return initialization parameters for cloning utilities.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
deep : bool
|
|
172
|
+
Included for scikit-learn compatibility.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
dict[str, Any]
|
|
177
|
+
Initialization parameters that can be passed back to `__init__`.
|
|
178
|
+
"""
|
|
179
|
+
_ = deep # intentionally unused; kept for API compatibility
|
|
180
|
+
return {
|
|
181
|
+
"order": self.order,
|
|
182
|
+
"seasonal_order": self.seasonal_order,
|
|
183
|
+
"trend": self.trend,
|
|
184
|
+
"enforce_stationarity": self.enforce_stationarity,
|
|
185
|
+
"enforce_invertibility": self.enforce_invertibility,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def set_params(self, **params: Any) -> "SarimaxAdapter":
|
|
189
|
+
"""
|
|
190
|
+
Update adapter parameters.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
**params
|
|
195
|
+
Parameters to set as attributes on the adapter instance.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
SarimaxAdapter
|
|
200
|
+
The updated adapter instance (self).
|
|
201
|
+
"""
|
|
202
|
+
for k, v in params.items():
|
|
203
|
+
setattr(self, k, v)
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
return (
|
|
208
|
+
f"SarimaxAdapter(order={self.order}, "
|
|
209
|
+
f"seasonal_order={self.seasonal_order}, trend={self.trend!r})"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ArimaAdapter(BaseAdapter):
|
|
214
|
+
"""
|
|
215
|
+
Adapter for `statsmodels` ARIMA.
|
|
216
|
+
|
|
217
|
+
This wrapper fits a univariate ARIMA model on `y` and produces forecasts for
|
|
218
|
+
`len(X)` steps ahead when `predict(X)` is called.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
order : tuple[int, int, int], default (1, 0, 0)
|
|
223
|
+
ARIMA (p, d, q) order.
|
|
224
|
+
trend : str | None, default None
|
|
225
|
+
Trend specification forwarded to `statsmodels.tsa.ARIMA`.
|
|
226
|
+
|
|
227
|
+
Notes
|
|
228
|
+
-----
|
|
229
|
+
- `X` is ignored during fitting. It is only used at prediction time to
|
|
230
|
+
determine the forecast horizon (`n_steps = len(X)`).
|
|
231
|
+
- This adapter stores initialization parameters via `get_params()` so cloning
|
|
232
|
+
utilities can reconstruct the adapter.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def __init__(
|
|
236
|
+
self,
|
|
237
|
+
order: tuple[int, int, int] = (1, 0, 0),
|
|
238
|
+
trend: Optional[str] = None,
|
|
239
|
+
) -> None:
|
|
240
|
+
super().__init__()
|
|
241
|
+
self.order = order
|
|
242
|
+
self.trend = trend
|
|
243
|
+
self._result = None
|
|
244
|
+
|
|
245
|
+
# Failure is intentionally delayed until fit(); this flag supports
|
|
246
|
+
# feature detection and optional-dependency behavior.
|
|
247
|
+
if not HAS_STATSMODELS:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def fit(
|
|
251
|
+
self,
|
|
252
|
+
X: np.ndarray,
|
|
253
|
+
y: np.ndarray,
|
|
254
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
255
|
+
) -> "ArimaAdapter":
|
|
256
|
+
"""
|
|
257
|
+
Fit a univariate ARIMA model on `y`.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
X : numpy.ndarray
|
|
262
|
+
Ignored. Present for API compatibility.
|
|
263
|
+
y : numpy.ndarray
|
|
264
|
+
Target series of shape (n_samples,).
|
|
265
|
+
sample_weight : numpy.ndarray | None
|
|
266
|
+
Accepted for API compatibility but ignored by this adapter.
|
|
267
|
+
|
|
268
|
+
Returns
|
|
269
|
+
-------
|
|
270
|
+
ArimaAdapter
|
|
271
|
+
The fitted adapter (self), allowing method chaining.
|
|
272
|
+
|
|
273
|
+
Raises
|
|
274
|
+
------
|
|
275
|
+
ImportError
|
|
276
|
+
If `statsmodels` is not installed.
|
|
277
|
+
"""
|
|
278
|
+
_ = X # intentionally unused; kept for API compatibility
|
|
279
|
+
_ = sample_weight # intentionally unused; kept for API compatibility
|
|
280
|
+
|
|
281
|
+
if not HAS_STATSMODELS:
|
|
282
|
+
raise ImportError(
|
|
283
|
+
"ArimaAdapter requires the optional 'statsmodels' package. "
|
|
284
|
+
"Install it via `pip install statsmodels`."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
y_arr = np.asarray(y, dtype=float)
|
|
288
|
+
|
|
289
|
+
model = _sm.tsa.ARIMA(
|
|
290
|
+
y_arr,
|
|
291
|
+
order=self.order,
|
|
292
|
+
trend=self.trend,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
self._result = model.fit()
|
|
296
|
+
return self
|
|
297
|
+
|
|
298
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
299
|
+
"""
|
|
300
|
+
Forecast `len(X)` steps ahead from the end of the training sample.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
X : numpy.ndarray
|
|
305
|
+
Array-like placeholder used to determine the forecast horizon.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
numpy.ndarray
|
|
310
|
+
Forecast values of shape (len(X),).
|
|
311
|
+
|
|
312
|
+
Raises
|
|
313
|
+
------
|
|
314
|
+
RuntimeError
|
|
315
|
+
If the adapter has not been fit yet.
|
|
316
|
+
"""
|
|
317
|
+
if self._result is None:
|
|
318
|
+
raise RuntimeError("ArimaAdapter has not been fit yet. Call `fit(X, y)` first.")
|
|
319
|
+
|
|
320
|
+
n_steps = len(X)
|
|
321
|
+
if n_steps <= 0:
|
|
322
|
+
return np.array([], dtype=float)
|
|
323
|
+
|
|
324
|
+
forecast = self._result.forecast(steps=n_steps)
|
|
325
|
+
return np.asarray(forecast, dtype=float)
|
|
326
|
+
|
|
327
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Return initialization parameters for cloning utilities.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
deep : bool
|
|
334
|
+
Included for scikit-learn compatibility.
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
dict[str, Any]
|
|
339
|
+
Initialization parameters that can be passed back to `__init__`.
|
|
340
|
+
"""
|
|
341
|
+
_ = deep # intentionally unused; kept for API compatibility
|
|
342
|
+
return {
|
|
343
|
+
"order": self.order,
|
|
344
|
+
"trend": self.trend,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
def set_params(self, **params: Any) -> "ArimaAdapter":
|
|
348
|
+
"""
|
|
349
|
+
Update adapter parameters.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
**params
|
|
354
|
+
Parameters to set as attributes on the adapter instance.
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
ArimaAdapter
|
|
359
|
+
The updated adapter instance (self).
|
|
360
|
+
"""
|
|
361
|
+
for k, v in params.items():
|
|
362
|
+
setattr(self, k, v)
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
def __repr__(self) -> str:
|
|
366
|
+
return f"ArimaAdapter(order={self.order}, trend={self.trend!r})"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eb-adapters
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Adapter layer for third-party forecasting libraries in Electric Barometer
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: pandas
|
|
11
|
+
Provides-Extra: prophet
|
|
12
|
+
Requires-Dist: prophet>=1.1; extra == "prophet"
|
|
13
|
+
Provides-Extra: statsmodels
|
|
14
|
+
Requires-Dist: statsmodels>=0.14; extra == "statsmodels"
|
|
15
|
+
Provides-Extra: catboost
|
|
16
|
+
Requires-Dist: catboost>=1.2; extra == "catboost"
|
|
17
|
+
Provides-Extra: lightgbm
|
|
18
|
+
Requires-Dist: lightgbm>=4.0; extra == "lightgbm"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Electric Barometer Adapters (`eb-adapters`)
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+

|
|
30
|
+
[](https://economistician.github.io/eb-docs/)
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
This repository contains the **model adapter layer** of the *Electric Barometer*
|
|
34
|
+
ecosystem.
|
|
35
|
+
|
|
36
|
+
`eb-adapters` provides standardized interfaces that adapt common forecasting and
|
|
37
|
+
machine-learning libraries to the Electric Barometer evaluation and readiness
|
|
38
|
+
frameworks, enabling consistent metric computation and model comparison across
|
|
39
|
+
heterogeneous modeling approaches.
|
|
40
|
+
|
|
41
|
+
Conceptual definitions and evaluation philosophy are maintained in the companion
|
|
42
|
+
research repository:
|
|
43
|
+
**`eb-papers`**.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Naming convention
|
|
48
|
+
|
|
49
|
+
Electric Barometer packages follow standard Python packaging conventions:
|
|
50
|
+
|
|
51
|
+
- **Distribution names** (used with `pip install`) use hyphens
|
|
52
|
+
e.g. `pip install eb-adapters`
|
|
53
|
+
- **Python import paths** use underscores
|
|
54
|
+
e.g. `import eb_adapters`
|
|
55
|
+
|
|
56
|
+
This distinction is intentional and consistent across the Electric Barometer
|
|
57
|
+
ecosystem.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Role Within Electric Barometer
|
|
62
|
+
|
|
63
|
+
Within the Electric Barometer ecosystem:
|
|
64
|
+
|
|
65
|
+
- **`eb-papers`** defines *concepts, frameworks, and meaning*
|
|
66
|
+
- **`eb-metrics`** implements *individual metrics*
|
|
67
|
+
- **`eb-evaluation`** orchestrates *evaluation workflows*
|
|
68
|
+
- **`eb-adapters`** standardizes *model interfaces*
|
|
69
|
+
|
|
70
|
+
This repository focuses on bridging external modeling libraries into a common,
|
|
71
|
+
evaluation-ready form.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## What This Library Provides
|
|
76
|
+
|
|
77
|
+
- **A common adapter base class** defining a unified interface for forecast models
|
|
78
|
+
- **Library-specific adapters** for popular forecasting and ML frameworks
|
|
79
|
+
- **Consistent prediction and evaluation hooks** compatible with EB metrics
|
|
80
|
+
- **Extensible patterns** for adding new model families without changing
|
|
81
|
+
downstream evaluation logic
|
|
82
|
+
|
|
83
|
+
Current adapters include support for:
|
|
84
|
+
- CatBoost
|
|
85
|
+
- LightGBM
|
|
86
|
+
- Prophet
|
|
87
|
+
- Statsmodels-based models
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Scope
|
|
92
|
+
|
|
93
|
+
This repository focuses on **model adaptation**, not model training algorithms or
|
|
94
|
+
metric definitions.
|
|
95
|
+
|
|
96
|
+
**In scope:**
|
|
97
|
+
- Wrapping external model APIs behind a consistent adapter interface
|
|
98
|
+
- Standardizing prediction outputs for evaluation
|
|
99
|
+
- Providing compatibility with EB evaluation pipelines
|
|
100
|
+
|
|
101
|
+
**Out of scope:**
|
|
102
|
+
- Metric definitions and loss formulations (see `eb-metrics`)
|
|
103
|
+
- Evaluation orchestration logic (see `eb-evaluation`)
|
|
104
|
+
- Model training methodology or hyperparameter optimization
|
|
105
|
+
- Conceptual framework definitions (see `eb-papers`)
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Installation
|
|
110
|
+
|
|
111
|
+
Install from PyPI:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install eb-adapters
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For development or local use:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install -e .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Package Structure
|
|
126
|
+
|
|
127
|
+
The repository follows a clean, modern Python package layout:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
eb-adapters/
|
|
131
|
+
├── src/eb_adapters/
|
|
132
|
+
│ ├── base.py # Abstract adapter base class
|
|
133
|
+
│ ├── catboost.py # CatBoost model adapter
|
|
134
|
+
│ ├── lightgbm.py # LightGBM model adapter
|
|
135
|
+
│ ├── prophet.py # Prophet model adapter
|
|
136
|
+
│ └── statsmodels.py # Statsmodels-based adapters
|
|
137
|
+
│
|
|
138
|
+
├── tests/
|
|
139
|
+
│ └── adapters/ # Unit tests for adapter implementations
|
|
140
|
+
│
|
|
141
|
+
├── pyproject.toml # Build and dependency configuration
|
|
142
|
+
├── README.md # Project documentation
|
|
143
|
+
└── LICENSE # BSD-3-Clause license
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Relationship to Other EB Repositories
|
|
149
|
+
|
|
150
|
+
- **`eb-papers`**
|
|
151
|
+
Source of truth for conceptual definitions and evaluation philosophy.
|
|
152
|
+
|
|
153
|
+
- **`eb-metrics`**
|
|
154
|
+
Provides the metric implementations used during evaluation.
|
|
155
|
+
|
|
156
|
+
- **`eb-evaluation`**
|
|
157
|
+
Orchestrates evaluation workflows using adapted models.
|
|
158
|
+
|
|
159
|
+
- **`eb-adapters`**
|
|
160
|
+
Ensures heterogeneous models can be evaluated consistently.
|
|
161
|
+
|
|
162
|
+
When discrepancies arise, conceptual intent in `eb-papers` should be treated as
|
|
163
|
+
authoritative.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Development and Testing
|
|
168
|
+
|
|
169
|
+
Tests are located under the `tests/` directory and mirror adapter coverage.
|
|
170
|
+
|
|
171
|
+
To run the test suite:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pytest
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Status
|
|
180
|
+
|
|
181
|
+
This package is under active development.
|
|
182
|
+
Public APIs may evolve prior to the first stable release.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
eb_adapters/__init__.py,sha256=aFGCWDxo03BMHND98rf-ZGV1h2LPAgVn9OS2T9hP7PM,1036
|
|
2
|
+
eb_adapters/base.py,sha256=-ZZ8-rPMZhhTNJ-aqA4TnmKciRxGCUaLsjupcXNJHZM,5020
|
|
3
|
+
eb_adapters/catboost.py,sha256=R7U7cUFD4jJuZRHGAitmZ9scPCbg1k6edCjTZPLDFk4,7149
|
|
4
|
+
eb_adapters/lightgbm.py,sha256=kHH7ea49PiarEmkE5kKjCcZtrQAKB0PX5lk4p5ITHhU,6997
|
|
5
|
+
eb_adapters/prophet.py,sha256=6FBoBG-zFhg9XDxGTiy5OjcJpvLeD4K5iddZ6sG_JD0,5459
|
|
6
|
+
eb_adapters/statsmodels.py,sha256=IhaZbY04bX7gyerM_fQEjR4HTAn-697vhDHpBT8ss88,11365
|
|
7
|
+
eb_adapters-0.1.0.dist-info/licenses/LICENSE,sha256=qFjBKWIfPVLU4ZK4DgROjINVdZVLuNznTfyVaEgCa9w,1526
|
|
8
|
+
eb_adapters-0.1.0.dist-info/METADATA,sha256=MhIHfGjhT-Piv7SE5mG2mGPUbqCRmW1ouKOH3FKHZ1E,5275
|
|
9
|
+
eb_adapters-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
eb_adapters-0.1.0.dist-info/top_level.txt,sha256=uVNItEBTrYVjvjMvKEpz91-Pzhc5oNT8vZB8wPOMP9w,12
|
|
11
|
+
eb_adapters-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Kyle Corrie
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eb_adapters
|