forecastbox 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.
- forecastbox-0.1.0/.github/workflows/publish.yml +59 -0
- forecastbox-0.1.0/.gitignore +30 -0
- forecastbox-0.1.0/CHANGELOG.md +26 -0
- forecastbox-0.1.0/LICENSE +21 -0
- forecastbox-0.1.0/PKG-INFO +66 -0
- forecastbox-0.1.0/README.md +40 -0
- forecastbox-0.1.0/benchmarks/bench_auto_arima.py +89 -0
- forecastbox-0.1.0/benchmarks/bench_combination.py +113 -0
- forecastbox-0.1.0/benchmarks/bench_nowcast.py +93 -0
- forecastbox-0.1.0/benchmarks/bench_pipeline.py +96 -0
- forecastbox-0.1.0/forecastbox/__init__.py +12 -0
- forecastbox-0.1.0/forecastbox/__version__.py +1 -0
- forecastbox-0.1.0/forecastbox/_logging.py +16 -0
- forecastbox-0.1.0/forecastbox/auto/__init__.py +30 -0
- forecastbox-0.1.0/forecastbox/auto/_adapters.py +241 -0
- forecastbox-0.1.0/forecastbox/auto/_baselines.py +225 -0
- forecastbox-0.1.0/forecastbox/auto/_stepwise.py +444 -0
- forecastbox-0.1.0/forecastbox/auto/arima.py +696 -0
- forecastbox-0.1.0/forecastbox/auto/ets.py +613 -0
- forecastbox-0.1.0/forecastbox/auto/select.py +548 -0
- forecastbox-0.1.0/forecastbox/auto/var.py +568 -0
- forecastbox-0.1.0/forecastbox/auto/zoo.py +325 -0
- forecastbox-0.1.0/forecastbox/cli/__init__.py +5 -0
- forecastbox-0.1.0/forecastbox/cli/combine_cmd.py +134 -0
- forecastbox-0.1.0/forecastbox/cli/evaluate_cmd.py +217 -0
- forecastbox-0.1.0/forecastbox/cli/forecast_cmd.py +192 -0
- forecastbox-0.1.0/forecastbox/cli/main.py +44 -0
- forecastbox-0.1.0/forecastbox/cli/monitor_cmd.py +120 -0
- forecastbox-0.1.0/forecastbox/cli/nowcast_cmd.py +141 -0
- forecastbox-0.1.0/forecastbox/combination/__init__.py +27 -0
- forecastbox-0.1.0/forecastbox/combination/base.py +188 -0
- forecastbox-0.1.0/forecastbox/combination/bma.py +268 -0
- forecastbox-0.1.0/forecastbox/combination/ols.py +212 -0
- forecastbox-0.1.0/forecastbox/combination/optimal.py +186 -0
- forecastbox-0.1.0/forecastbox/combination/simple.py +164 -0
- forecastbox-0.1.0/forecastbox/combination/stacking.py +285 -0
- forecastbox-0.1.0/forecastbox/combination/time_varying.py +183 -0
- forecastbox-0.1.0/forecastbox/combination/weighted.py +124 -0
- forecastbox-0.1.0/forecastbox/core/__init__.py +8 -0
- forecastbox-0.1.0/forecastbox/core/config.py +19 -0
- forecastbox-0.1.0/forecastbox/core/forecast.py +369 -0
- forecastbox-0.1.0/forecastbox/core/horizon.py +131 -0
- forecastbox-0.1.0/forecastbox/core/results.py +260 -0
- forecastbox-0.1.0/forecastbox/core/vintage.py +154 -0
- forecastbox-0.1.0/forecastbox/cv/__init__.py +11 -0
- forecastbox-0.1.0/forecastbox/cv/cross_validation.py +284 -0
- forecastbox-0.1.0/forecastbox/cv/rolling_blocked.py +418 -0
- forecastbox-0.1.0/forecastbox/datasets/__init__.py +5 -0
- forecastbox-0.1.0/forecastbox/datasets/data/_generate_datasets.py +498 -0
- forecastbox-0.1.0/forecastbox/datasets/data/airline.csv +145 -0
- forecastbox-0.1.0/forecastbox/datasets/data/brazil_gdp_vintages.csv +61 -0
- forecastbox-0.1.0/forecastbox/datasets/data/commodity_prices.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/electricity.csv +201 -0
- forecastbox-0.1.0/forecastbox/datasets/data/exchange_rates.csv +251 -0
- forecastbox-0.1.0/forecastbox/datasets/data/gas.csv +201 -0
- forecastbox-0.1.0/forecastbox/datasets/data/interest_rates.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/m3_monthly.csv +121 -0
- forecastbox-0.1.0/forecastbox/datasets/data/m3_quarterly.csv +81 -0
- forecastbox-0.1.0/forecastbox/datasets/data/m4_monthly.csv +151 -0
- forecastbox-0.1.0/forecastbox/datasets/data/m4_quarterly.csv +81 -0
- forecastbox-0.1.0/forecastbox/datasets/data/macro_brazil.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/macro_europe.csv +251 -0
- forecastbox-0.1.0/forecastbox/datasets/data/macro_us.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/retail.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/simulated_dfm.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/simulated_var.csv +501 -0
- forecastbox-0.1.0/forecastbox/datasets/data/sunspot.csv +301 -0
- forecastbox-0.1.0/forecastbox/datasets/data/tourism.csv +81 -0
- forecastbox-0.1.0/forecastbox/datasets/data/us_gdp_vintages.csv +81 -0
- forecastbox-0.1.0/forecastbox/datasets/load.py +164 -0
- forecastbox-0.1.0/forecastbox/evaluation/__init__.py +25 -0
- forecastbox-0.1.0/forecastbox/evaluation/_hac.py +161 -0
- forecastbox-0.1.0/forecastbox/evaluation/diebold_mariano.py +245 -0
- forecastbox-0.1.0/forecastbox/evaluation/encompassing.py +217 -0
- forecastbox-0.1.0/forecastbox/evaluation/giacomini_white.py +195 -0
- forecastbox-0.1.0/forecastbox/evaluation/mcs.py +366 -0
- forecastbox-0.1.0/forecastbox/evaluation/mincer_zarnowitz.py +226 -0
- forecastbox-0.1.0/forecastbox/experiment.py +769 -0
- forecastbox-0.1.0/forecastbox/metrics/__init__.py +27 -0
- forecastbox-0.1.0/forecastbox/metrics/advanced_metrics.py +335 -0
- forecastbox-0.1.0/forecastbox/metrics/point_metrics.py +173 -0
- forecastbox-0.1.0/forecastbox/nowcasting/__init__.py +35 -0
- forecastbox-0.1.0/forecastbox/nowcasting/bridge.py +422 -0
- forecastbox-0.1.0/forecastbox/nowcasting/dfm.py +806 -0
- forecastbox-0.1.0/forecastbox/nowcasting/midas.py +626 -0
- forecastbox-0.1.0/forecastbox/nowcasting/news.py +576 -0
- forecastbox-0.1.0/forecastbox/nowcasting/realtime.py +535 -0
- forecastbox-0.1.0/forecastbox/pipeline/__init__.py +18 -0
- forecastbox-0.1.0/forecastbox/pipeline/alerts.py +307 -0
- forecastbox-0.1.0/forecastbox/pipeline/monitor.py +426 -0
- forecastbox-0.1.0/forecastbox/pipeline/pipeline.py +677 -0
- forecastbox-0.1.0/forecastbox/pipeline/recurring.py +237 -0
- forecastbox-0.1.0/forecastbox/py.typed +0 -0
- forecastbox-0.1.0/forecastbox/reports/__init__.py +12 -0
- forecastbox-0.1.0/forecastbox/reports/builder.py +192 -0
- forecastbox-0.1.0/forecastbox/reports/sections.py +314 -0
- forecastbox-0.1.0/forecastbox/reports/template_renderer.py +106 -0
- forecastbox-0.1.0/forecastbox/reports/templates/default_html.jinja2 +245 -0
- forecastbox-0.1.0/forecastbox/reports/templates/default_latex.jinja2 +85 -0
- forecastbox-0.1.0/forecastbox/reports/templates/executive_html.jinja2 +147 -0
- forecastbox-0.1.0/forecastbox/reports/templates/technical_html.jinja2 +262 -0
- forecastbox-0.1.0/forecastbox/reports/transformers/__init__.py +15 -0
- forecastbox-0.1.0/forecastbox/reports/transformers/html.py +197 -0
- forecastbox-0.1.0/forecastbox/reports/transformers/json_transformer.py +102 -0
- forecastbox-0.1.0/forecastbox/reports/transformers/latex.py +135 -0
- forecastbox-0.1.0/forecastbox/reports/transformers/markdown.py +133 -0
- forecastbox-0.1.0/forecastbox/reports/transformers/pdf.py +95 -0
- forecastbox-0.1.0/forecastbox/scenarios/__init__.py +33 -0
- forecastbox-0.1.0/forecastbox/scenarios/_protocols.py +191 -0
- forecastbox-0.1.0/forecastbox/scenarios/builder.py +472 -0
- forecastbox-0.1.0/forecastbox/scenarios/conditional.py +487 -0
- forecastbox-0.1.0/forecastbox/scenarios/counterfactual.py +372 -0
- forecastbox-0.1.0/forecastbox/scenarios/fan_chart.py +361 -0
- forecastbox-0.1.0/forecastbox/scenarios/monte_carlo.py +410 -0
- forecastbox-0.1.0/forecastbox/scenarios/stress_test.py +572 -0
- forecastbox-0.1.0/forecastbox/utils/__init__.py +17 -0
- forecastbox-0.1.0/forecastbox/utils/types.py +13 -0
- forecastbox-0.1.0/forecastbox/utils/validation.py +112 -0
- forecastbox-0.1.0/forecastbox/viz/__init__.py +19 -0
- forecastbox-0.1.0/forecastbox/viz/_style.py +129 -0
- forecastbox-0.1.0/forecastbox/viz/eval_plots.py +239 -0
- forecastbox-0.1.0/forecastbox/viz/forecast_plots.py +195 -0
- forecastbox-0.1.0/forecastbox/viz/nowcast_plots.py +75 -0
- forecastbox-0.1.0/forecastbox/viz/pipeline_plots.py +165 -0
- forecastbox-0.1.0/forecastbox/viz/plotter.py +347 -0
- forecastbox-0.1.0/forecastbox/viz/scenario_plots.py +78 -0
- forecastbox-0.1.0/mkdocs.yml +101 -0
- forecastbox-0.1.0/pyproject.toml +73 -0
- forecastbox-0.1.0/validation/validate_vs_r_fable.py +121 -0
- forecastbox-0.1.0/validation/validate_vs_r_forecast.py +141 -0
- forecastbox-0.1.0/validation/validate_vs_r_mcs.py +134 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
contents: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout code
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.12'
|
|
22
|
+
|
|
23
|
+
- name: Install build dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install build twine
|
|
27
|
+
|
|
28
|
+
- name: Verify version matches release tag
|
|
29
|
+
run: |
|
|
30
|
+
VERSION=$(python -c "import sys; sys.path.insert(0, 'forecastbox'); from __version__ import __version__; print(__version__)")
|
|
31
|
+
TAG=${GITHUB_REF#refs/tags/v}
|
|
32
|
+
echo "Package version: $VERSION"
|
|
33
|
+
echo "Release tag: $TAG"
|
|
34
|
+
if [ "$VERSION" != "$TAG" ]; then
|
|
35
|
+
echo "Error: Version mismatch!"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
- name: Build package
|
|
40
|
+
run: python -m build
|
|
41
|
+
|
|
42
|
+
- name: Check package
|
|
43
|
+
run: twine check dist/*
|
|
44
|
+
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
47
|
+
with:
|
|
48
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
49
|
+
skip-existing: false
|
|
50
|
+
verbose: true
|
|
51
|
+
|
|
52
|
+
- name: Upload release assets
|
|
53
|
+
uses: softprops/action-gh-release@v2
|
|
54
|
+
with:
|
|
55
|
+
files: |
|
|
56
|
+
dist/*.tar.gz
|
|
57
|
+
dist/*.whl
|
|
58
|
+
env:
|
|
59
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
|
|
10
|
+
# Testing
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
htmlcov/
|
|
14
|
+
.hypothesis/
|
|
15
|
+
.mutmut-cache
|
|
16
|
+
mutants/
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
# Docs
|
|
23
|
+
site/
|
|
24
|
+
|
|
25
|
+
# Logs
|
|
26
|
+
logs/
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-03-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Core**: Forecast container, ForecastResults, ForecastHorizon, DataVintage
|
|
13
|
+
- **Auto-Forecasting**: AutoARIMA, AutoETS, Theta, AutoVAR, AutoSelect
|
|
14
|
+
- **Combination**: 7 methods (mean, median, inverse_mse, ols, bma, stacking, optimal)
|
|
15
|
+
- **Evaluation**: Diebold-Mariano, MCS, Giacomini-White, Mincer-Zarnowitz, Encompassing
|
|
16
|
+
- **Metrics**: MAE, RMSE, MAPE, MASE, CRPS, coverage
|
|
17
|
+
- **Scenarios**: Conditional forecasts, stress testing, fan charts
|
|
18
|
+
- **Nowcasting**: DFM, bridge equations, MIDAS, news decomposition
|
|
19
|
+
- **Pipeline**: ForecastPipeline, ForecastMonitor with alerts
|
|
20
|
+
- **Visualization**: Forecast plots, comparison plots, fan charts
|
|
21
|
+
- **Reports**: HTML/Markdown/JSON report generation
|
|
22
|
+
- **CLI**: 5 commands (forecast, evaluate, nowcast, monitor, combine)
|
|
23
|
+
- **Datasets**: 20 built-in datasets
|
|
24
|
+
- **ForecastExperiment**: High-level workflow API
|
|
25
|
+
- **Cross-Validation**: Expanding and sliding window
|
|
26
|
+
- **Documentation**: MkDocs with ~37 pages
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NodesEcon
|
|
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,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forecastbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Forecast containers, evaluation metrics, and cross-validation for time series
|
|
5
|
+
Author: NodesEcon
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: click>=8.0
|
|
17
|
+
Requires-Dist: matplotlib>=3.7
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: pandas>=2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# forecastbox
|
|
28
|
+
|
|
29
|
+
Forecast containers, evaluation metrics, and cross-validation for time series.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import numpy as np
|
|
41
|
+
import pandas as pd
|
|
42
|
+
from forecastbox import Forecast
|
|
43
|
+
from forecastbox.metrics import mae, rmse
|
|
44
|
+
from forecastbox.datasets import load_dataset
|
|
45
|
+
|
|
46
|
+
# Create a forecast
|
|
47
|
+
fc = Forecast(
|
|
48
|
+
point=np.array([100.5, 101.2, 102.0]),
|
|
49
|
+
index=pd.date_range('2024-01', periods=3, freq='MS'),
|
|
50
|
+
model_name='MyModel',
|
|
51
|
+
horizon=3
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Evaluate
|
|
55
|
+
actual = np.array([100.8, 100.9, 103.1])
|
|
56
|
+
print(f"MAE: {mae(actual, fc.point):.2f}")
|
|
57
|
+
print(f"RMSE: {rmse(actual, fc.point):.2f}")
|
|
58
|
+
|
|
59
|
+
# Load dataset
|
|
60
|
+
data = load_dataset('macro_brazil')
|
|
61
|
+
print(data['ipca'].head())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# forecastbox
|
|
2
|
+
|
|
3
|
+
Forecast containers, evaluation metrics, and cross-validation for time series.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
from forecastbox import Forecast
|
|
17
|
+
from forecastbox.metrics import mae, rmse
|
|
18
|
+
from forecastbox.datasets import load_dataset
|
|
19
|
+
|
|
20
|
+
# Create a forecast
|
|
21
|
+
fc = Forecast(
|
|
22
|
+
point=np.array([100.5, 101.2, 102.0]),
|
|
23
|
+
index=pd.date_range('2024-01', periods=3, freq='MS'),
|
|
24
|
+
model_name='MyModel',
|
|
25
|
+
horizon=3
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Evaluate
|
|
29
|
+
actual = np.array([100.8, 100.9, 103.1])
|
|
30
|
+
print(f"MAE: {mae(actual, fc.point):.2f}")
|
|
31
|
+
print(f"RMSE: {rmse(actual, fc.point):.2f}")
|
|
32
|
+
|
|
33
|
+
# Load dataset
|
|
34
|
+
data = load_dataset('macro_brazil')
|
|
35
|
+
print(data['ipca'].head())
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Benchmark: AutoARIMA fit time.
|
|
2
|
+
|
|
3
|
+
Target: < 2s for 300 observations.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python benchmarks/bench_auto_arima.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bench_auto_arima() -> dict[str, float]:
|
|
19
|
+
"""Benchmark AutoARIMA fit time.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
dict[str, float]
|
|
24
|
+
Benchmark results with keys: n_obs, fit_time, forecast_time, total_time.
|
|
25
|
+
"""
|
|
26
|
+
print("=" * 60)
|
|
27
|
+
print("Benchmark: AutoARIMA")
|
|
28
|
+
print("=" * 60)
|
|
29
|
+
|
|
30
|
+
# Load data
|
|
31
|
+
try:
|
|
32
|
+
from forecastbox.datasets import load_dataset
|
|
33
|
+
|
|
34
|
+
data = load_dataset("macro_brazil")
|
|
35
|
+
series = data["ipca"]
|
|
36
|
+
except ImportError:
|
|
37
|
+
rng = np.random.default_rng(42)
|
|
38
|
+
dates = pd.date_range("2000-01-01", periods=300, freq="MS")
|
|
39
|
+
series = pd.Series(
|
|
40
|
+
0.5 + rng.normal(0, 0.3, 300), index=dates, name="ipca"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
n_obs = len(series)
|
|
44
|
+
print(f"Data: {n_obs} observations")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from forecastbox.auto.arima import AutoARIMA
|
|
48
|
+
|
|
49
|
+
# Fit
|
|
50
|
+
model = AutoARIMA(seasonal=True, m=12)
|
|
51
|
+
start = time.time()
|
|
52
|
+
result = model.fit(series)
|
|
53
|
+
fit_time = time.time() - start
|
|
54
|
+
|
|
55
|
+
# Forecast
|
|
56
|
+
start = time.time()
|
|
57
|
+
fc = result.forecast(h=12)
|
|
58
|
+
forecast_time = time.time() - start
|
|
59
|
+
|
|
60
|
+
total_time = fit_time + forecast_time
|
|
61
|
+
|
|
62
|
+
print("\nResults:")
|
|
63
|
+
print(
|
|
64
|
+
f" Fit time: {fit_time:.3f}s "
|
|
65
|
+
f"{'PASS' if fit_time < 2.0 else 'FAIL'} (target: <2s)"
|
|
66
|
+
)
|
|
67
|
+
print(f" Forecast time: {forecast_time:.3f}s")
|
|
68
|
+
print(f" Total time: {total_time:.3f}s")
|
|
69
|
+
print(f" Model: {fc.model_name}")
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"n_obs": n_obs,
|
|
73
|
+
"fit_time": fit_time,
|
|
74
|
+
"forecast_time": forecast_time,
|
|
75
|
+
"total_time": total_time,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
except ImportError:
|
|
79
|
+
print("SKIP: AutoARIMA not available")
|
|
80
|
+
return {"n_obs": n_obs, "fit_time": 0, "forecast_time": 0, "total_time": 0}
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"FAIL: {e}")
|
|
83
|
+
return {"n_obs": n_obs, "fit_time": -1, "forecast_time": -1, "total_time": -1}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
results = bench_auto_arima()
|
|
88
|
+
passed = results.get("fit_time", -1) < 2.0 or results.get("fit_time", -1) == 0
|
|
89
|
+
sys.exit(0 if passed else 1)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Benchmark: Forecast combination methods.
|
|
2
|
+
|
|
3
|
+
Target: < 1s for 7 methods, 12-step ahead.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python benchmarks/bench_combination.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bench_combination() -> dict[str, float]:
|
|
18
|
+
"""Benchmark combination methods.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
dict[str, float]
|
|
23
|
+
Time per method.
|
|
24
|
+
"""
|
|
25
|
+
print("=" * 60)
|
|
26
|
+
print("Benchmark: Forecast Combination")
|
|
27
|
+
print("=" * 60)
|
|
28
|
+
|
|
29
|
+
from forecastbox.core.forecast import Forecast
|
|
30
|
+
|
|
31
|
+
# Create 5 synthetic forecasts
|
|
32
|
+
rng = np.random.default_rng(42)
|
|
33
|
+
forecasts: list[Forecast] = []
|
|
34
|
+
for i in range(5):
|
|
35
|
+
fc = Forecast(
|
|
36
|
+
point=100 + rng.normal(0, 5, 12),
|
|
37
|
+
lower_80=100 + rng.normal(0, 5, 12) - 5,
|
|
38
|
+
upper_80=100 + rng.normal(0, 5, 12) + 5,
|
|
39
|
+
model_name=f"Model_{i + 1}",
|
|
40
|
+
)
|
|
41
|
+
forecasts.append(fc)
|
|
42
|
+
|
|
43
|
+
actual = 100 + rng.normal(0, 3, 12)
|
|
44
|
+
|
|
45
|
+
results: dict[str, float] = {}
|
|
46
|
+
|
|
47
|
+
# Simple methods (always available via Forecast.combine)
|
|
48
|
+
for method in ["mean", "median"]:
|
|
49
|
+
start = time.time()
|
|
50
|
+
Forecast.combine(forecasts, method=method)
|
|
51
|
+
elapsed = time.time() - start
|
|
52
|
+
results[method] = elapsed
|
|
53
|
+
print(f" {method}: {elapsed:.4f}s")
|
|
54
|
+
|
|
55
|
+
# Advanced methods via combination module
|
|
56
|
+
combiner_map = {
|
|
57
|
+
"inverse_mse": "forecastbox.combination.weighted",
|
|
58
|
+
"ols": "forecastbox.combination.ols",
|
|
59
|
+
"bma": "forecastbox.combination.bma",
|
|
60
|
+
"stacking": "forecastbox.combination.stacking",
|
|
61
|
+
"optimal": "forecastbox.combination.optimal",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Extract point arrays for training
|
|
65
|
+
fc_arrays = [fc.point for fc in forecasts]
|
|
66
|
+
|
|
67
|
+
for method, module_path in combiner_map.items():
|
|
68
|
+
try:
|
|
69
|
+
import importlib
|
|
70
|
+
|
|
71
|
+
mod = importlib.import_module(module_path)
|
|
72
|
+
# Get the combiner class (first class that ends with Combiner)
|
|
73
|
+
combiner_cls = None
|
|
74
|
+
for attr_name in dir(mod):
|
|
75
|
+
attr = getattr(mod, attr_name)
|
|
76
|
+
if (
|
|
77
|
+
isinstance(attr, type)
|
|
78
|
+
and attr_name.endswith("Combiner")
|
|
79
|
+
and attr_name != "BaseCombiner"
|
|
80
|
+
):
|
|
81
|
+
combiner_cls = attr
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
if combiner_cls is None:
|
|
85
|
+
print(f" {method}: SKIP (no combiner class found)")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
start = time.time()
|
|
89
|
+
combiner = combiner_cls()
|
|
90
|
+
combiner.fit(fc_arrays, actual)
|
|
91
|
+
combiner.combine(forecasts)
|
|
92
|
+
elapsed = time.time() - start
|
|
93
|
+
results[method] = elapsed
|
|
94
|
+
print(f" {method}: {elapsed:.4f}s")
|
|
95
|
+
except ImportError:
|
|
96
|
+
print(f" {method}: SKIP (module not available)")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f" {method}: FAIL ({e})")
|
|
99
|
+
results[method] = -1
|
|
100
|
+
|
|
101
|
+
total = sum(v for v in results.values() if v > 0)
|
|
102
|
+
print(
|
|
103
|
+
f"\nTotal time: {total:.4f}s "
|
|
104
|
+
f"{'PASS' if total < 1.0 else 'FAIL'} (target: <1s)"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return results
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
results = bench_combination()
|
|
112
|
+
total = sum(v for v in results.values() if v > 0)
|
|
113
|
+
sys.exit(0 if total < 1.0 else 1)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Benchmark: DFM nowcast.
|
|
2
|
+
|
|
3
|
+
Target: < 5s for 10 series.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python benchmarks/bench_nowcast.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bench_nowcast() -> dict[str, float]:
|
|
19
|
+
"""Benchmark DFM nowcast.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
dict[str, float]
|
|
24
|
+
Benchmark results.
|
|
25
|
+
"""
|
|
26
|
+
print("=" * 60)
|
|
27
|
+
print("Benchmark: DFM Nowcast")
|
|
28
|
+
print("=" * 60)
|
|
29
|
+
|
|
30
|
+
# Load simulated DFM data
|
|
31
|
+
try:
|
|
32
|
+
from forecastbox.datasets import load_dataset
|
|
33
|
+
|
|
34
|
+
data = load_dataset("simulated_dfm")
|
|
35
|
+
df = pd.DataFrame(data)
|
|
36
|
+
except (ImportError, KeyError):
|
|
37
|
+
rng = np.random.default_rng(42)
|
|
38
|
+
dates = pd.date_range("2000-01-01", periods=300, freq="MS")
|
|
39
|
+
df = pd.DataFrame(
|
|
40
|
+
{f"series_{i + 1}": rng.normal(0, 1, 300) for i in range(10)},
|
|
41
|
+
index=dates,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
n_series = len(df.columns)
|
|
45
|
+
n_obs = len(df)
|
|
46
|
+
print(f"Data: {n_series} series, {n_obs} observations")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from forecastbox.nowcasting.dfm import DFMNowcaster
|
|
50
|
+
|
|
51
|
+
# Build frequency map (all monthly for synthetic data)
|
|
52
|
+
freq_map = {col: "M" for col in df.columns}
|
|
53
|
+
|
|
54
|
+
# Fit
|
|
55
|
+
start = time.time()
|
|
56
|
+
nowcaster = DFMNowcaster(n_factors=2, frequency_map=freq_map)
|
|
57
|
+
nowcaster.fit(df)
|
|
58
|
+
fit_time = time.time() - start
|
|
59
|
+
|
|
60
|
+
# Nowcast
|
|
61
|
+
start = time.time()
|
|
62
|
+
nowcaster.nowcast(target=df.columns[0])
|
|
63
|
+
nowcast_time = time.time() - start
|
|
64
|
+
|
|
65
|
+
total_time = fit_time + nowcast_time
|
|
66
|
+
|
|
67
|
+
print("\nResults:")
|
|
68
|
+
print(f" Fit time: {fit_time:.3f}s")
|
|
69
|
+
print(f" Nowcast time: {nowcast_time:.3f}s")
|
|
70
|
+
print(
|
|
71
|
+
f" Total time: {total_time:.3f}s "
|
|
72
|
+
f"{'PASS' if total_time < 5.0 else 'FAIL'} (target: <5s)"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"n_series": float(n_series),
|
|
77
|
+
"n_obs": float(n_obs),
|
|
78
|
+
"fit_time": fit_time,
|
|
79
|
+
"nowcast_time": nowcast_time,
|
|
80
|
+
"total_time": total_time,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
except ImportError:
|
|
84
|
+
print("SKIP: DFM module not available")
|
|
85
|
+
return {"n_series": float(n_series), "n_obs": float(n_obs), "total_time": 0}
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"FAIL: {e}")
|
|
88
|
+
return {"n_series": float(n_series), "n_obs": float(n_obs), "total_time": -1}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
results = bench_nowcast()
|
|
93
|
+
sys.exit(0 if results.get("total_time", -1) < 5.0 else 1)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Benchmark: Full pipeline.
|
|
2
|
+
|
|
3
|
+
Target: < 60s for 3 models, 300 observations.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python benchmarks/bench_pipeline.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bench_pipeline() -> dict[str, float]:
|
|
19
|
+
"""Benchmark full forecast pipeline.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
dict[str, float]
|
|
24
|
+
Benchmark results.
|
|
25
|
+
"""
|
|
26
|
+
print("=" * 60)
|
|
27
|
+
print("Benchmark: Full Pipeline")
|
|
28
|
+
print("=" * 60)
|
|
29
|
+
|
|
30
|
+
# Load data
|
|
31
|
+
try:
|
|
32
|
+
from forecastbox.datasets import load_dataset
|
|
33
|
+
|
|
34
|
+
data = load_dataset("macro_brazil")
|
|
35
|
+
df = pd.DataFrame(data)
|
|
36
|
+
except ImportError:
|
|
37
|
+
rng = np.random.default_rng(42)
|
|
38
|
+
dates = pd.date_range("2000-01-01", periods=300, freq="MS")
|
|
39
|
+
df = pd.DataFrame(
|
|
40
|
+
{
|
|
41
|
+
"ipca": 0.5 + rng.normal(0, 0.3, 300),
|
|
42
|
+
"selic": 10 + np.cumsum(rng.normal(0, 0.5, 300)),
|
|
43
|
+
"cambio": 3 + np.cumsum(rng.normal(0, 0.1, 300)),
|
|
44
|
+
},
|
|
45
|
+
index=dates,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
n_obs = len(df)
|
|
49
|
+
print(f"Data: {n_obs} observations, {len(df.columns)} variables")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from forecastbox.experiment import ForecastExperiment
|
|
53
|
+
|
|
54
|
+
models = ["auto_arima", "auto_ets", "theta"]
|
|
55
|
+
print(f"Models: {models}")
|
|
56
|
+
|
|
57
|
+
start = time.time()
|
|
58
|
+
exp = ForecastExperiment(
|
|
59
|
+
data=df,
|
|
60
|
+
target="ipca",
|
|
61
|
+
models=models,
|
|
62
|
+
combination="mean",
|
|
63
|
+
horizon=12,
|
|
64
|
+
cv_type="expanding",
|
|
65
|
+
)
|
|
66
|
+
results = exp.run()
|
|
67
|
+
total_time = time.time() - start
|
|
68
|
+
|
|
69
|
+
n_forecasts = len(results.forecasts)
|
|
70
|
+
|
|
71
|
+
print("\nResults:")
|
|
72
|
+
print(f" Models fitted: {n_forecasts}/{len(models)}")
|
|
73
|
+
print(f" Combination: {'Yes' if results.combination else 'No'}")
|
|
74
|
+
print(
|
|
75
|
+
f" Total time: {total_time:.2f}s "
|
|
76
|
+
f"{'PASS' if total_time < 60.0 else 'FAIL'} (target: <60s)"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"n_obs": float(n_obs),
|
|
81
|
+
"n_models": float(n_forecasts),
|
|
82
|
+
"total_time": total_time,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
except ImportError:
|
|
86
|
+
print("SKIP: Required modules not available")
|
|
87
|
+
return {"n_obs": float(n_obs), "n_models": 0, "total_time": 0}
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"FAIL: {e}")
|
|
90
|
+
return {"n_obs": float(n_obs), "n_models": 0, "total_time": -1}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
results = bench_pipeline()
|
|
95
|
+
t = results.get("total_time", -1)
|
|
96
|
+
sys.exit(0 if t < 60.0 or t == 0 else 1)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""forecastbox - Forecast containers, evaluation metrics, and cross-validation for time series."""
|
|
2
|
+
|
|
3
|
+
from forecastbox.__version__ import __version__
|
|
4
|
+
from forecastbox.core.forecast import Forecast
|
|
5
|
+
from forecastbox.experiment import ExperimentResults, ForecastExperiment
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"__version__",
|
|
9
|
+
"Forecast",
|
|
10
|
+
"ForecastExperiment",
|
|
11
|
+
"ExperimentResults",
|
|
12
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Logging configuration for forecastbox."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_logger(name: str) -> logging.Logger:
|
|
9
|
+
"""Get a logger with the forecastbox namespace."""
|
|
10
|
+
logger = logging.getLogger(f"forecastbox.{name}")
|
|
11
|
+
if not logger.handlers:
|
|
12
|
+
handler = logging.StreamHandler()
|
|
13
|
+
handler.setFormatter(logging.Formatter(_LOG_FORMAT))
|
|
14
|
+
logger.addHandler(handler)
|
|
15
|
+
logger.setLevel(logging.WARNING)
|
|
16
|
+
return logger
|