moeforecast 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.
Files changed (35) hide show
  1. moeforecast-0.1.0/.github/workflows/publish-pypi.yml +77 -0
  2. moeforecast-0.1.0/.github/workflows/publish-testpypi.yml +71 -0
  3. moeforecast-0.1.0/.github/workflows/tests.yml +25 -0
  4. moeforecast-0.1.0/.gitignore +14 -0
  5. moeforecast-0.1.0/LICENSE +21 -0
  6. moeforecast-0.1.0/PKG-INFO +262 -0
  7. moeforecast-0.1.0/README.md +221 -0
  8. moeforecast-0.1.0/docs/design.md +39 -0
  9. moeforecast-0.1.0/docs/release-process.md +50 -0
  10. moeforecast-0.1.0/examples/quickstart.ipynb +530 -0
  11. moeforecast-0.1.0/pyproject.toml +59 -0
  12. moeforecast-0.1.0/src/moeforecast/__init__.py +57 -0
  13. moeforecast-0.1.0/src/moeforecast/_plotting.py +35 -0
  14. moeforecast-0.1.0/src/moeforecast/analysis.py +267 -0
  15. moeforecast-0.1.0/src/moeforecast/classification.py +227 -0
  16. moeforecast-0.1.0/src/moeforecast/clustering.py +454 -0
  17. moeforecast-0.1.0/src/moeforecast/data.py +174 -0
  18. moeforecast-0.1.0/src/moeforecast/datasets.py +27 -0
  19. moeforecast-0.1.0/src/moeforecast/experts.py +294 -0
  20. moeforecast-0.1.0/src/moeforecast/features.py +157 -0
  21. moeforecast-0.1.0/src/moeforecast/gating.py +132 -0
  22. moeforecast-0.1.0/src/moeforecast/metrics.py +52 -0
  23. moeforecast-0.1.0/src/moeforecast/model_selection.py +39 -0
  24. moeforecast-0.1.0/src/moeforecast/persistence.py +32 -0
  25. moeforecast-0.1.0/src/moeforecast/pipeline.py +576 -0
  26. moeforecast-0.1.0/src/moeforecast/tuning.py +121 -0
  27. moeforecast-0.1.0/tests/conftest.py +9 -0
  28. moeforecast-0.1.0/tests/test_analysis.py +66 -0
  29. moeforecast-0.1.0/tests/test_clustering.py +69 -0
  30. moeforecast-0.1.0/tests/test_data.py +75 -0
  31. moeforecast-0.1.0/tests/test_features.py +31 -0
  32. moeforecast-0.1.0/tests/test_gating.py +42 -0
  33. moeforecast-0.1.0/tests/test_metrics.py +34 -0
  34. moeforecast-0.1.0/tests/test_pipeline.py +150 -0
  35. moeforecast-0.1.0/tests/test_tuning.py +75 -0
@@ -0,0 +1,77 @@
1
+ name: PyPI release
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ paths:
7
+ - "pyproject.toml"
8
+ - "src/moeforecast/**"
9
+ - "README.md"
10
+ - "LICENSE"
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ test:
17
+ name: Test release candidate
18
+ runs-on: ubuntu-latest
19
+ outputs:
20
+ stable: ${{ steps.version.outputs.stable }}
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.12"
26
+ - name: Install package and development dependencies
27
+ run: python -m pip install -e ".[dev]"
28
+ - name: Lint and test
29
+ run: |
30
+ ruff check .
31
+ ruff format --check .
32
+ pytest -p no:warnings
33
+ - name: Check whether this is a stable release
34
+ id: version
35
+ run: |
36
+ python - <<'PY'
37
+ import os
38
+ from pathlib import Path
39
+ from packaging.version import Version
40
+ import re
41
+
42
+ source = Path("src/moeforecast/__init__.py").read_text()
43
+ match = re.search(r'__version__ = "([^"]+)"', source)
44
+ if not match:
45
+ raise SystemExit("Could not read moeforecast.__version__")
46
+ version = Version(match.group(1))
47
+ stable = not version.is_prerelease and not version.is_devrelease
48
+ with open(os.environ["GITHUB_OUTPUT"], "a") as output:
49
+ output.write(f"stable={str(stable).lower()}\n")
50
+ if not stable:
51
+ print(f"{version} is a development/pre-release version; skipping PyPI publication.")
52
+ else:
53
+ print(f"Stable version {version} is eligible for PyPI publication.")
54
+ PY
55
+
56
+ publish:
57
+ name: Publish stable package to PyPI
58
+ needs: test
59
+ if: needs.test.outputs.stable == 'true'
60
+ runs-on: ubuntu-latest
61
+ environment:
62
+ name: pypi
63
+ url: https://pypi.org/p/moeforecast
64
+ permissions:
65
+ contents: read
66
+ id-token: write
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - uses: actions/setup-python@v5
70
+ with:
71
+ python-version: "3.12"
72
+ - name: Build distributions
73
+ run: |
74
+ python -m pip install --upgrade build
75
+ python -m build
76
+ - name: Publish to PyPI
77
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,71 @@
1
+ name: TestPyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [release]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ concurrency:
11
+ group: testpypi-release-branch
12
+ cancel-in-progress: false
13
+
14
+ jobs:
15
+ test:
16
+ name: Test release branch
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Install package and development dependencies
24
+ run: python -m pip install -e ".[dev]"
25
+ - name: Lint and test
26
+ run: |
27
+ ruff check .
28
+ ruff format --check .
29
+ pytest -p no:warnings
30
+
31
+ publish:
32
+ name: Publish a development build to TestPyPI
33
+ needs: test
34
+ runs-on: ubuntu-latest
35
+ environment:
36
+ name: testpypi
37
+ url: https://test.pypi.org/p/moeforecast
38
+ permissions:
39
+ contents: read
40
+ id-token: write
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: actions/setup-python@v5
44
+ with:
45
+ python-version: "3.12"
46
+ - name: Set a unique TestPyPI version
47
+ env:
48
+ RUN_NUMBER: ${{ github.run_number }}
49
+ run: |
50
+ python - <<'PY'
51
+ import os
52
+ import re
53
+ from pathlib import Path
54
+
55
+ path = Path("src/moeforecast/__init__.py")
56
+ source = path.read_text()
57
+ match = re.search(r'__version__ = "(\d+\.\d+\.\d+)(?:\.dev\d+)?"', source)
58
+ if not match:
59
+ raise SystemExit("Expected __version__ in X.Y.Z or X.Y.Z.devN format")
60
+ version = f"{match.group(1)}.dev{os.environ['RUN_NUMBER']}"
61
+ path.write_text(re.sub(r'__version__ = "[^"]+"', f'__version__ = "{version}"', source, count=1))
62
+ print(f"Building TestPyPI version {version}")
63
+ PY
64
+ - name: Build distributions
65
+ run: |
66
+ python -m pip install --upgrade build
67
+ python -m build
68
+ - name: Publish to TestPyPI
69
+ uses: pypa/gh-action-pypi-publish@release/v1
70
+ with:
71
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,25 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [release, master, main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: pip install -e ".[dev]"
22
+ - name: Lint
23
+ run: ruff check . && ruff format --check .
24
+ - name: Test
25
+ run: pytest -p no:warnings
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .ipynb_checkpoints/
13
+ .DS_Store
14
+ *.joblib
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastián Torres
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 whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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,262 @@
1
+ Metadata-Version: 2.5
2
+ Name: moeforecast
3
+ Version: 0.1.0
4
+ Summary: Mixture-of-Experts time series forecasting with regime clustering and adaptive gating, with a scikit-learn style API.
5
+ Project-URL: Homepage, https://github.com/astorress/moeforecast
6
+ Project-URL: Issues, https://github.com/astorress/moeforecast/issues
7
+ Author-email: Sebastián Torres <sebastiantorres8800@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: clustering,forecasting,load forecasting,mixture of experts,scikit-learn,time series
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: joblib>=1.3
20
+ Requires-Dist: matplotlib>=3.7
21
+ Requires-Dist: numpy>=1.24
22
+ Requires-Dist: pandas>=2.0
23
+ Requires-Dist: scikit-learn>=1.3
24
+ Requires-Dist: statsmodels>=0.14
25
+ Provides-Extra: all
26
+ Requires-Dist: holidays>=0.40; extra == 'all'
27
+ Requires-Dist: lightgbm>=4.0; extra == 'all'
28
+ Provides-Extra: dev
29
+ Requires-Dist: holidays>=0.40; extra == 'dev'
30
+ Requires-Dist: lightgbm>=4.0; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
32
+ Requires-Dist: pytest>=7.4; extra == 'dev'
33
+ Requires-Dist: ruff>=0.5; extra == 'dev'
34
+ Provides-Extra: dtw
35
+ Requires-Dist: tslearn>=0.6.3; extra == 'dtw'
36
+ Provides-Extra: holidays
37
+ Requires-Dist: holidays>=0.40; extra == 'holidays'
38
+ Provides-Extra: lightgbm
39
+ Requires-Dist: lightgbm>=4.0; extra == 'lightgbm'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # moeforecast
43
+
44
+ **Mixture-of-Experts time series forecasting with a scikit-learn style API.**
45
+
46
+ `moeforecast` builds a forecaster that learns the *regimes* of a series and trains one specialised model (an *expert*) per regime. A regime is, for example, a type of day (working day, weekend, holiday) or a type of week. A gating network then combines the experts at prediction time. The method comes from hourly electricity demand forecasting, but it applies to any regularly sampled series with recurring seasonal profiles.
47
+
48
+ > Status: pre-alpha (0.1.0.dev0). The API may change.
49
+
50
+ ```python
51
+ import pandas as pd
52
+ from moeforecast import prepare_series, temporal_train_test_split
53
+
54
+ df = pd.read_csv("load.csv") # your data
55
+ y = prepare_series(df, target="load", time_col="timestamp") # regular, sorted series
56
+
57
+ train, test = temporal_train_test_split(y, test_size="2025-01-01")
58
+ # Continue with the analysis and cluster-selection workflow below.
59
+ ```
60
+
61
+ ## What the library does (and does not do)
62
+
63
+ `moeforecast` gives you the tools to **build** the model and to choose its configuration, like scikit-learn does. Benchmarking against other models, statistical comparison tests and robustness studies are left to the user.
64
+
65
+ | Step | Tool | What it gives you |
66
+ |---|---|---|
67
+ | 1. Prepare the data | `prepare_series`, `fill_gaps` | Sorted series on a regular grid; warns about gaps and never imputes silently |
68
+ | 2. Analyse the series | `SeriesAnalyzer` | Suggested seasonal periods and lags (ACF/PACF); ACF/PACF, seasonal-profile and MSTL plots |
69
+ | 3. Build features | `FeatureBuilder` | Causal calendar, Fourier, holiday (any country), lag and rolling features. The same code serves training, prediction and forecasting |
70
+ | 4. Rank features | `feature_relevance` | Mutual information or permutation importance |
71
+ | 5. Find regimes | `ProfileClusterer` | Daily, weekly or monthly profile diagnostics, distance choice, silhouette plots and explicit selection of k |
72
+ | 6. Predict regimes | `RegimeClassifier`, `compare_classifiers` | Regime probabilities for the gating; time-ordered comparison of classifiers |
73
+ | 7. Train experts | `ExpertSelector` | Best candidate model per regime (forward-chaining CV); ten default regressors, user additions/removals and MAE/RMSE/MAPE selection |
74
+ | 8. Combine experts | `HardGating`, `SoftGating`, `AdaptiveGating` | Expert weights per time step |
75
+ | 9. All together | `MoEForecaster` | `fit`, `predict`, `predict_with_metadata`, `expert_weights`, `forecast` |
76
+ | 10. Persist | `save`, `load` | Serialization with version check |
77
+ | Metrics | `moeforecast.metrics` | MAE, RMSE, MAPE, MASE |
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install "moeforecast[all]" # with LightGBM and public holidays
83
+ pip install "moeforecast[dtw]" # optional DTW profile clustering
84
+ pip install moeforecast # core only: numpy, pandas, scikit-learn, statsmodels, matplotlib
85
+ ```
86
+
87
+ Not on PyPI yet. Install from source:
88
+
89
+ ```bash
90
+ git clone https://github.com/astorress/moeforecast.git
91
+ cd moeforecast
92
+ pip install -e ".[dev]"
93
+ pytest
94
+ ```
95
+
96
+ ## Step by step
97
+
98
+ ```python
99
+ from moeforecast import (
100
+ SeriesAnalyzer,
101
+ FeatureBuilder,
102
+ feature_relevance,
103
+ ProfileClusterer,
104
+ compare_classifiers,
105
+ ExpertSelector,
106
+ AdaptiveGating,
107
+ MoEForecaster,
108
+ )
109
+
110
+ # 1. Which periods and lags matter?
111
+ analysis = SeriesAnalyzer().fit(train)
112
+ print(analysis.summary()) # e.g. periods [24, 168], lags [1, 2, 23, 24, 25, 167, 168, 169]
113
+ analysis.plot_acf_pacf()
114
+
115
+ # 2. Features and their relevance
116
+ features = FeatureBuilder(lags=analysis.suggested_lags_, fourier_periods=(24, 168), holidays_country="CO")
117
+ X = features.fit(train).transform(train)
118
+ feature_relevance(X, train, method="permutation").head(15)
119
+
120
+ # 3. Inspect cluster-validation metrics before choosing the cluster count.
121
+ days = ProfileClusterer("D", name="day", k_range=range(2, 11))
122
+ days.evaluate(train)
123
+ days.plot_scores()
124
+ days.plot_silhouette() # inspect all candidate k values before deciding
125
+ days.set_params(n_clusters=3).fit(train) # choose k after reviewing the scores
126
+ days.plot_profiles()
127
+
128
+ weeks = ProfileClusterer("W", name="week", k_range=range(2, 11))
129
+ weeks.evaluate(train)
130
+ weeks.plot_scores()
131
+ weeks.set_params(n_clusters=2).fit(train)
132
+
133
+ # 4. Can the regimes be predicted from the features?
134
+ compare_classifiers(X, days.labels_for(train.index))
135
+
136
+ # 5. The full model with the chosen configuration
137
+ model = MoEForecaster(
138
+ features=features,
139
+ clusterers=[days, weeks],
140
+ selector=ExpertSelector(metric="mae", search="random", n_iter=20),
141
+ gating=AdaptiveGating(threshold=0.75),
142
+ ).fit(train)
143
+
144
+ model.selector_.results_ # every candidate's CV score per expert
145
+ report = model.inspect()
146
+ report["expert_hyperparameter_searches"] # every tried parameter set and its CV score
147
+ pred, meta = model.predict_with_metadata(y)
148
+ meta["mode"].value_counts() # hard vs. soft gating decisions
149
+
150
+ # Tune experts and classifiers by forward-chaining CV ("grid" is also supported).
151
+ # Override any named default with your own sklearn parameter distributions.
152
+ selector = ExpertSelector(
153
+ search="random",
154
+ n_iter=30,
155
+ search_spaces={
156
+ "random_forest": {
157
+ "n_estimators": [100, 250, 500],
158
+ "max_depth": [None, 8, 16],
159
+ "min_samples_leaf": [1, 3, 5],
160
+ }
161
+ },
162
+ )
163
+ model = MoEForecaster(
164
+ features=features,
165
+ clusterers=[days, weeks],
166
+ selector=selector,
167
+ classifier_search="grid",
168
+ ).fit(train)
169
+
170
+ # Optimize gating on data not used by fit; review scores before using the setting.
171
+ gating_scores = model.tune_gating(validation, search="grid", metric="mae")
172
+ gating_scores.head()
173
+
174
+ # Export an image of the actual fitted architecture.
175
+ model.plot_model("outputs/moe-architecture.png")
176
+ ```
177
+
178
+ ## Inspect every fitted component
179
+
180
+ The fitted estimators are not hidden behind the forecaster. `inspect()` returns
181
+ the actual scikit-learn objects together with copied diagnostic tables:
182
+
183
+ ```python
184
+ report = model.inspect()
185
+
186
+ # Cluster assignments, candidate-k scores, source profiles and cluster means
187
+ report["clusters"]["day"]["summary"]
188
+ report["clusters"]["day"]["scores"]
189
+ report["clusters"]["day"]["assignments"]
190
+ report["clusters"]["day"]["mean_profiles"]
191
+
192
+ # Fitted classifier and its input features / class counts
193
+ classifier = report["classifiers"]["day"]
194
+ print(classifier["estimator"])
195
+ classifier["features"], classifier["class_counts"]
196
+
197
+ # Every candidate's time-series CV results and each final fitted expert
198
+ report["expert_selection"]
199
+ for expert_name, estimator in report["experts"].items():
200
+ print(expert_name, estimator)
201
+
202
+ # Candidate model templates, fitted feature builder, and gating estimator
203
+ report["candidate_experts"]
204
+ report["configuration"]["features"].get_params()
205
+ report["gating"].get_params()
206
+ ```
207
+
208
+ `inspect()` also exposes the full `ProfileClusterer` objects and fitted k-means
209
+ estimators. `RegimeClassifier.estimator_`, `ExpertSelector.experts_`,
210
+ `ExpertSelector.candidate_estimators_`, `ProfileClusterer.labels_`,
211
+ `ProfileClusterer.scores_`, and `ProfileClusterer.mean_profiles_` remain
212
+ available directly for users who prefer to inspect individual components.
213
+
214
+ See the [quickstart notebook](examples/quickstart.ipynb) for a complete,
215
+ synthetic-data walkthrough of the public modeling API, including diagnostics,
216
+ regime classifiers, all gating strategies, model inspection, forecasting and
217
+ persistence.
218
+
219
+ HPO uses forward-chaining folds. ``ExpertSelector(search="grid"|"random")``
220
+ searches each candidate's parameter space independently inside each regime,
221
+ then compares the tuned candidates and keeps the best estimator for that expert.
222
+ Defaults are available for built-in model names through
223
+ ``default_search_spaces()``; pass ``search_spaces`` to replace or extend them.
224
+ ``MoEForecaster(classifier_search=...)`` tunes its regime classifiers.
225
+ ``tune_gating`` requires a later validation series that was not used for fitting;
226
+ it searches adaptive-gating confidence thresholds, minimum expert weights and
227
+ probability temperature. All fitted HPO objects and per-setting scores remain
228
+ inspectable through ``inspect()`` and the component attributes.
229
+ ``plot_model(path)`` saves a diagram showing the fitted clusters, classifiers,
230
+ experts and gating component.
231
+
232
+ ## Design
233
+
234
+ These principles prevent the errors that are easy to make when a Mixture of Experts is built by hand:
235
+
236
+ - **No look-ahead.** Every feature at time *t* uses only values before *t*. `fit` only sees the training series. The classifier comparison and the expert selection use forward-chaining (time-ordered) cross-validation.
237
+ - **One feature function.** `FeatureBuilder.transform` is used for training, prediction and recursive forecasting, so there is no train/serve skew. Fourier terms are anchored to a fixed origin, not to where the data starts.
238
+ - **Typed expert identifiers.** An expert is a tuple of regime labels, e.g. `(0, 1)`, used everywhere. `"day0-week1"` is only a display name.
239
+ - **Deterministic choices.** Cluster labels are ordered by level and ties are broken by candidate order. Selections come from the stated metric.
240
+
241
+ [docs/design.md](docs/design.md) explains each decision.
242
+
243
+ ## Release process
244
+
245
+ Development builds are published to TestPyPI from the `release` branch. Stable
246
+ versions are published to PyPI only after merging to `master` or `main`. See
247
+ [the release guide](docs/release-process.md) for the full workflow and the
248
+ one-time Trusted Publisher setup.
249
+
250
+ ## Roadmap
251
+
252
+ - Deep-learning experts (LSTM/GRU) as an optional extra (`moeforecast[dl]`).
253
+ - DTW and k-Shape clustering (optional `tslearn` extra).
254
+ - HTML report of the analysis and the fitted model.
255
+
256
+ ## Citation
257
+
258
+ If you use `moeforecast` in research, please cite the accompanying article (reference to be added).
259
+
260
+ ## License
261
+
262
+ [MIT](LICENSE)