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.
Files changed (131) hide show
  1. forecastbox-0.1.0/.github/workflows/publish.yml +59 -0
  2. forecastbox-0.1.0/.gitignore +30 -0
  3. forecastbox-0.1.0/CHANGELOG.md +26 -0
  4. forecastbox-0.1.0/LICENSE +21 -0
  5. forecastbox-0.1.0/PKG-INFO +66 -0
  6. forecastbox-0.1.0/README.md +40 -0
  7. forecastbox-0.1.0/benchmarks/bench_auto_arima.py +89 -0
  8. forecastbox-0.1.0/benchmarks/bench_combination.py +113 -0
  9. forecastbox-0.1.0/benchmarks/bench_nowcast.py +93 -0
  10. forecastbox-0.1.0/benchmarks/bench_pipeline.py +96 -0
  11. forecastbox-0.1.0/forecastbox/__init__.py +12 -0
  12. forecastbox-0.1.0/forecastbox/__version__.py +1 -0
  13. forecastbox-0.1.0/forecastbox/_logging.py +16 -0
  14. forecastbox-0.1.0/forecastbox/auto/__init__.py +30 -0
  15. forecastbox-0.1.0/forecastbox/auto/_adapters.py +241 -0
  16. forecastbox-0.1.0/forecastbox/auto/_baselines.py +225 -0
  17. forecastbox-0.1.0/forecastbox/auto/_stepwise.py +444 -0
  18. forecastbox-0.1.0/forecastbox/auto/arima.py +696 -0
  19. forecastbox-0.1.0/forecastbox/auto/ets.py +613 -0
  20. forecastbox-0.1.0/forecastbox/auto/select.py +548 -0
  21. forecastbox-0.1.0/forecastbox/auto/var.py +568 -0
  22. forecastbox-0.1.0/forecastbox/auto/zoo.py +325 -0
  23. forecastbox-0.1.0/forecastbox/cli/__init__.py +5 -0
  24. forecastbox-0.1.0/forecastbox/cli/combine_cmd.py +134 -0
  25. forecastbox-0.1.0/forecastbox/cli/evaluate_cmd.py +217 -0
  26. forecastbox-0.1.0/forecastbox/cli/forecast_cmd.py +192 -0
  27. forecastbox-0.1.0/forecastbox/cli/main.py +44 -0
  28. forecastbox-0.1.0/forecastbox/cli/monitor_cmd.py +120 -0
  29. forecastbox-0.1.0/forecastbox/cli/nowcast_cmd.py +141 -0
  30. forecastbox-0.1.0/forecastbox/combination/__init__.py +27 -0
  31. forecastbox-0.1.0/forecastbox/combination/base.py +188 -0
  32. forecastbox-0.1.0/forecastbox/combination/bma.py +268 -0
  33. forecastbox-0.1.0/forecastbox/combination/ols.py +212 -0
  34. forecastbox-0.1.0/forecastbox/combination/optimal.py +186 -0
  35. forecastbox-0.1.0/forecastbox/combination/simple.py +164 -0
  36. forecastbox-0.1.0/forecastbox/combination/stacking.py +285 -0
  37. forecastbox-0.1.0/forecastbox/combination/time_varying.py +183 -0
  38. forecastbox-0.1.0/forecastbox/combination/weighted.py +124 -0
  39. forecastbox-0.1.0/forecastbox/core/__init__.py +8 -0
  40. forecastbox-0.1.0/forecastbox/core/config.py +19 -0
  41. forecastbox-0.1.0/forecastbox/core/forecast.py +369 -0
  42. forecastbox-0.1.0/forecastbox/core/horizon.py +131 -0
  43. forecastbox-0.1.0/forecastbox/core/results.py +260 -0
  44. forecastbox-0.1.0/forecastbox/core/vintage.py +154 -0
  45. forecastbox-0.1.0/forecastbox/cv/__init__.py +11 -0
  46. forecastbox-0.1.0/forecastbox/cv/cross_validation.py +284 -0
  47. forecastbox-0.1.0/forecastbox/cv/rolling_blocked.py +418 -0
  48. forecastbox-0.1.0/forecastbox/datasets/__init__.py +5 -0
  49. forecastbox-0.1.0/forecastbox/datasets/data/_generate_datasets.py +498 -0
  50. forecastbox-0.1.0/forecastbox/datasets/data/airline.csv +145 -0
  51. forecastbox-0.1.0/forecastbox/datasets/data/brazil_gdp_vintages.csv +61 -0
  52. forecastbox-0.1.0/forecastbox/datasets/data/commodity_prices.csv +301 -0
  53. forecastbox-0.1.0/forecastbox/datasets/data/electricity.csv +201 -0
  54. forecastbox-0.1.0/forecastbox/datasets/data/exchange_rates.csv +251 -0
  55. forecastbox-0.1.0/forecastbox/datasets/data/gas.csv +201 -0
  56. forecastbox-0.1.0/forecastbox/datasets/data/interest_rates.csv +301 -0
  57. forecastbox-0.1.0/forecastbox/datasets/data/m3_monthly.csv +121 -0
  58. forecastbox-0.1.0/forecastbox/datasets/data/m3_quarterly.csv +81 -0
  59. forecastbox-0.1.0/forecastbox/datasets/data/m4_monthly.csv +151 -0
  60. forecastbox-0.1.0/forecastbox/datasets/data/m4_quarterly.csv +81 -0
  61. forecastbox-0.1.0/forecastbox/datasets/data/macro_brazil.csv +301 -0
  62. forecastbox-0.1.0/forecastbox/datasets/data/macro_europe.csv +251 -0
  63. forecastbox-0.1.0/forecastbox/datasets/data/macro_us.csv +301 -0
  64. forecastbox-0.1.0/forecastbox/datasets/data/retail.csv +301 -0
  65. forecastbox-0.1.0/forecastbox/datasets/data/simulated_dfm.csv +301 -0
  66. forecastbox-0.1.0/forecastbox/datasets/data/simulated_var.csv +501 -0
  67. forecastbox-0.1.0/forecastbox/datasets/data/sunspot.csv +301 -0
  68. forecastbox-0.1.0/forecastbox/datasets/data/tourism.csv +81 -0
  69. forecastbox-0.1.0/forecastbox/datasets/data/us_gdp_vintages.csv +81 -0
  70. forecastbox-0.1.0/forecastbox/datasets/load.py +164 -0
  71. forecastbox-0.1.0/forecastbox/evaluation/__init__.py +25 -0
  72. forecastbox-0.1.0/forecastbox/evaluation/_hac.py +161 -0
  73. forecastbox-0.1.0/forecastbox/evaluation/diebold_mariano.py +245 -0
  74. forecastbox-0.1.0/forecastbox/evaluation/encompassing.py +217 -0
  75. forecastbox-0.1.0/forecastbox/evaluation/giacomini_white.py +195 -0
  76. forecastbox-0.1.0/forecastbox/evaluation/mcs.py +366 -0
  77. forecastbox-0.1.0/forecastbox/evaluation/mincer_zarnowitz.py +226 -0
  78. forecastbox-0.1.0/forecastbox/experiment.py +769 -0
  79. forecastbox-0.1.0/forecastbox/metrics/__init__.py +27 -0
  80. forecastbox-0.1.0/forecastbox/metrics/advanced_metrics.py +335 -0
  81. forecastbox-0.1.0/forecastbox/metrics/point_metrics.py +173 -0
  82. forecastbox-0.1.0/forecastbox/nowcasting/__init__.py +35 -0
  83. forecastbox-0.1.0/forecastbox/nowcasting/bridge.py +422 -0
  84. forecastbox-0.1.0/forecastbox/nowcasting/dfm.py +806 -0
  85. forecastbox-0.1.0/forecastbox/nowcasting/midas.py +626 -0
  86. forecastbox-0.1.0/forecastbox/nowcasting/news.py +576 -0
  87. forecastbox-0.1.0/forecastbox/nowcasting/realtime.py +535 -0
  88. forecastbox-0.1.0/forecastbox/pipeline/__init__.py +18 -0
  89. forecastbox-0.1.0/forecastbox/pipeline/alerts.py +307 -0
  90. forecastbox-0.1.0/forecastbox/pipeline/monitor.py +426 -0
  91. forecastbox-0.1.0/forecastbox/pipeline/pipeline.py +677 -0
  92. forecastbox-0.1.0/forecastbox/pipeline/recurring.py +237 -0
  93. forecastbox-0.1.0/forecastbox/py.typed +0 -0
  94. forecastbox-0.1.0/forecastbox/reports/__init__.py +12 -0
  95. forecastbox-0.1.0/forecastbox/reports/builder.py +192 -0
  96. forecastbox-0.1.0/forecastbox/reports/sections.py +314 -0
  97. forecastbox-0.1.0/forecastbox/reports/template_renderer.py +106 -0
  98. forecastbox-0.1.0/forecastbox/reports/templates/default_html.jinja2 +245 -0
  99. forecastbox-0.1.0/forecastbox/reports/templates/default_latex.jinja2 +85 -0
  100. forecastbox-0.1.0/forecastbox/reports/templates/executive_html.jinja2 +147 -0
  101. forecastbox-0.1.0/forecastbox/reports/templates/technical_html.jinja2 +262 -0
  102. forecastbox-0.1.0/forecastbox/reports/transformers/__init__.py +15 -0
  103. forecastbox-0.1.0/forecastbox/reports/transformers/html.py +197 -0
  104. forecastbox-0.1.0/forecastbox/reports/transformers/json_transformer.py +102 -0
  105. forecastbox-0.1.0/forecastbox/reports/transformers/latex.py +135 -0
  106. forecastbox-0.1.0/forecastbox/reports/transformers/markdown.py +133 -0
  107. forecastbox-0.1.0/forecastbox/reports/transformers/pdf.py +95 -0
  108. forecastbox-0.1.0/forecastbox/scenarios/__init__.py +33 -0
  109. forecastbox-0.1.0/forecastbox/scenarios/_protocols.py +191 -0
  110. forecastbox-0.1.0/forecastbox/scenarios/builder.py +472 -0
  111. forecastbox-0.1.0/forecastbox/scenarios/conditional.py +487 -0
  112. forecastbox-0.1.0/forecastbox/scenarios/counterfactual.py +372 -0
  113. forecastbox-0.1.0/forecastbox/scenarios/fan_chart.py +361 -0
  114. forecastbox-0.1.0/forecastbox/scenarios/monte_carlo.py +410 -0
  115. forecastbox-0.1.0/forecastbox/scenarios/stress_test.py +572 -0
  116. forecastbox-0.1.0/forecastbox/utils/__init__.py +17 -0
  117. forecastbox-0.1.0/forecastbox/utils/types.py +13 -0
  118. forecastbox-0.1.0/forecastbox/utils/validation.py +112 -0
  119. forecastbox-0.1.0/forecastbox/viz/__init__.py +19 -0
  120. forecastbox-0.1.0/forecastbox/viz/_style.py +129 -0
  121. forecastbox-0.1.0/forecastbox/viz/eval_plots.py +239 -0
  122. forecastbox-0.1.0/forecastbox/viz/forecast_plots.py +195 -0
  123. forecastbox-0.1.0/forecastbox/viz/nowcast_plots.py +75 -0
  124. forecastbox-0.1.0/forecastbox/viz/pipeline_plots.py +165 -0
  125. forecastbox-0.1.0/forecastbox/viz/plotter.py +347 -0
  126. forecastbox-0.1.0/forecastbox/viz/scenario_plots.py +78 -0
  127. forecastbox-0.1.0/mkdocs.yml +101 -0
  128. forecastbox-0.1.0/pyproject.toml +73 -0
  129. forecastbox-0.1.0/validation/validate_vs_r_fable.py +121 -0
  130. forecastbox-0.1.0/validation/validate_vs_r_forecast.py +141 -0
  131. 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