kalmanbox 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.
- kalmanbox-0.1.0/.gitignore +30 -0
- kalmanbox-0.1.0/.pre-commit-config.yaml +33 -0
- kalmanbox-0.1.0/CHANGELOG.md +39 -0
- kalmanbox-0.1.0/LICENSE +21 -0
- kalmanbox-0.1.0/PKG-INFO +63 -0
- kalmanbox-0.1.0/README.md +25 -0
- kalmanbox-0.1.0/kalmanbox/__init__.py +33 -0
- kalmanbox-0.1.0/kalmanbox/__version__.py +3 -0
- kalmanbox-0.1.0/kalmanbox/_logging.py +73 -0
- kalmanbox-0.1.0/kalmanbox/cli/__init__.py +5 -0
- kalmanbox-0.1.0/kalmanbox/cli/main.py +294 -0
- kalmanbox-0.1.0/kalmanbox/core/__init__.py +7 -0
- kalmanbox-0.1.0/kalmanbox/core/config.py +19 -0
- kalmanbox-0.1.0/kalmanbox/core/model.py +226 -0
- kalmanbox-0.1.0/kalmanbox/core/representation.py +147 -0
- kalmanbox-0.1.0/kalmanbox/core/results.py +267 -0
- kalmanbox-0.1.0/kalmanbox/datasets/__init__.py +5 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/cambio.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/desemprego.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/gdp_quarterly.csv +113 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/industrial.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/ipca.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/ipca_monthly.csv +289 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/m1.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/pib.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/selic.csv +1 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/airline.csv +145 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/lynx.csv +115 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/nile.csv +101 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/sunspots.csv +253 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/uk_drivers.csv +193 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/uk_gas.csv +109 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/classic/ukdrivers.csv +193 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/co2.csv +791 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/exchange_rates.csv +6521 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/global_temp.csv +1729 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/uk_gas.csv +109 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/us_gdp.csv +309 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/us_inflation.csv +925 -0
- kalmanbox-0.1.0/kalmanbox/datasets/data/macro/us_unemployment.csv +913 -0
- kalmanbox-0.1.0/kalmanbox/datasets/load.py +178 -0
- kalmanbox-0.1.0/kalmanbox/diagnostics/__init__.py +41 -0
- kalmanbox-0.1.0/kalmanbox/diagnostics/convergence.py +273 -0
- kalmanbox-0.1.0/kalmanbox/diagnostics/missing.py +204 -0
- kalmanbox-0.1.0/kalmanbox/diagnostics/residuals.py +191 -0
- kalmanbox-0.1.0/kalmanbox/diagnostics/tests.py +413 -0
- kalmanbox-0.1.0/kalmanbox/estimation/__init__.py +23 -0
- kalmanbox-0.1.0/kalmanbox/estimation/bayesian.py +567 -0
- kalmanbox-0.1.0/kalmanbox/estimation/diffuse.py +258 -0
- kalmanbox-0.1.0/kalmanbox/estimation/em.py +252 -0
- kalmanbox-0.1.0/kalmanbox/estimation/mle.py +177 -0
- kalmanbox-0.1.0/kalmanbox/experiment/__init__.py +10 -0
- kalmanbox-0.1.0/kalmanbox/experiment/comparison.py +220 -0
- kalmanbox-0.1.0/kalmanbox/experiment/experiment.py +343 -0
- kalmanbox-0.1.0/kalmanbox/filters/__init__.py +24 -0
- kalmanbox-0.1.0/kalmanbox/filters/ekf.py +300 -0
- kalmanbox-0.1.0/kalmanbox/filters/enkf.py +279 -0
- kalmanbox-0.1.0/kalmanbox/filters/information.py +277 -0
- kalmanbox-0.1.0/kalmanbox/filters/kalman.py +263 -0
- kalmanbox-0.1.0/kalmanbox/filters/square_root.py +266 -0
- kalmanbox-0.1.0/kalmanbox/filters/ukf.py +340 -0
- kalmanbox-0.1.0/kalmanbox/models/__init__.py +25 -0
- kalmanbox-0.1.0/kalmanbox/models/arima_ssm.py +157 -0
- kalmanbox-0.1.0/kalmanbox/models/bsm.py +154 -0
- kalmanbox-0.1.0/kalmanbox/models/custom.py +120 -0
- kalmanbox-0.1.0/kalmanbox/models/cycle.py +117 -0
- kalmanbox-0.1.0/kalmanbox/models/dynamic_factor.py +267 -0
- kalmanbox-0.1.0/kalmanbox/models/local_level.py +73 -0
- kalmanbox-0.1.0/kalmanbox/models/local_linear_trend.py +60 -0
- kalmanbox-0.1.0/kalmanbox/models/regression_ssm.py +147 -0
- kalmanbox-0.1.0/kalmanbox/models/tvp.py +309 -0
- kalmanbox-0.1.0/kalmanbox/models/ucm.py +467 -0
- kalmanbox-0.1.0/kalmanbox/py.typed +0 -0
- kalmanbox-0.1.0/kalmanbox/reports/__init__.py +15 -0
- kalmanbox-0.1.0/kalmanbox/reports/css_manager.py +253 -0
- kalmanbox-0.1.0/kalmanbox/reports/exporters/__init__.py +7 -0
- kalmanbox-0.1.0/kalmanbox/reports/exporters/html.py +88 -0
- kalmanbox-0.1.0/kalmanbox/reports/exporters/latex.py +215 -0
- kalmanbox-0.1.0/kalmanbox/reports/exporters/markdown.py +156 -0
- kalmanbox-0.1.0/kalmanbox/reports/report_manager.py +258 -0
- kalmanbox-0.1.0/kalmanbox/reports/template_manager.py +143 -0
- kalmanbox-0.1.0/kalmanbox/reports/templates/base.html +44 -0
- kalmanbox-0.1.0/kalmanbox/reports/templates/ssm_report.html +201 -0
- kalmanbox-0.1.0/kalmanbox/reports/transformers/__init__.py +8 -0
- kalmanbox-0.1.0/kalmanbox/reports/transformers/dfm.py +103 -0
- kalmanbox-0.1.0/kalmanbox/reports/transformers/ssm.py +193 -0
- kalmanbox-0.1.0/kalmanbox/reports/transformers/tvp.py +151 -0
- kalmanbox-0.1.0/kalmanbox/reports/transformers/ucm.py +102 -0
- kalmanbox-0.1.0/kalmanbox/simulation/__init__.py +12 -0
- kalmanbox-0.1.0/kalmanbox/simulation/bootstrap.py +241 -0
- kalmanbox-0.1.0/kalmanbox/simulation/simulate.py +174 -0
- kalmanbox-0.1.0/kalmanbox/smoothers/__init__.py +18 -0
- kalmanbox-0.1.0/kalmanbox/smoothers/disturbance.py +201 -0
- kalmanbox-0.1.0/kalmanbox/smoothers/fixed_interval.py +136 -0
- kalmanbox-0.1.0/kalmanbox/smoothers/fixed_lag.py +138 -0
- kalmanbox-0.1.0/kalmanbox/smoothers/rts.py +96 -0
- kalmanbox-0.1.0/kalmanbox/utils/__init__.py +37 -0
- kalmanbox-0.1.0/kalmanbox/utils/matrix_ops.py +45 -0
- kalmanbox-0.1.0/kalmanbox/utils/numba_core.py +284 -0
- kalmanbox-0.1.0/kalmanbox/utils/transforms.py +26 -0
- kalmanbox-0.1.0/kalmanbox/visualization/__init__.py +67 -0
- kalmanbox-0.1.0/kalmanbox/visualization/components.py +276 -0
- kalmanbox-0.1.0/kalmanbox/visualization/diagnostics_plot.py +221 -0
- kalmanbox-0.1.0/kalmanbox/visualization/export.py +217 -0
- kalmanbox-0.1.0/kalmanbox/visualization/factor_plot.py +333 -0
- kalmanbox-0.1.0/kalmanbox/visualization/filter_plot.py +223 -0
- kalmanbox-0.1.0/kalmanbox/visualization/forecast_plot.py +263 -0
- kalmanbox-0.1.0/kalmanbox/visualization/state_plot.py +241 -0
- kalmanbox-0.1.0/kalmanbox/visualization/themes.py +269 -0
- kalmanbox-0.1.0/mkdocs.yml +94 -0
- kalmanbox-0.1.0/pyproject.toml +102 -0
- kalmanbox-0.1.0/scripts/download_brazil_data.py +114 -0
|
@@ -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,33 @@
|
|
|
1
|
+
# Pre-commit hooks for kalmanbox
|
|
2
|
+
# Install: pre-commit install
|
|
3
|
+
# Run all: pre-commit run --all-files
|
|
4
|
+
|
|
5
|
+
repos:
|
|
6
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
7
|
+
rev: v4.6.0
|
|
8
|
+
hooks:
|
|
9
|
+
- id: trailing-whitespace
|
|
10
|
+
- id: end-of-file-fixer
|
|
11
|
+
- id: check-yaml
|
|
12
|
+
- id: check-toml
|
|
13
|
+
- id: check-added-large-files
|
|
14
|
+
args: ['--maxkb=500']
|
|
15
|
+
- id: check-merge-conflict
|
|
16
|
+
- id: debug-statements
|
|
17
|
+
|
|
18
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
19
|
+
rev: v0.4.8
|
|
20
|
+
hooks:
|
|
21
|
+
- id: ruff
|
|
22
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
23
|
+
- id: ruff-format
|
|
24
|
+
|
|
25
|
+
- repo: https://github.com/RobertCraiworthy/pyright-python
|
|
26
|
+
rev: v1.1.367
|
|
27
|
+
hooks:
|
|
28
|
+
- id: pyright
|
|
29
|
+
additional_dependencies:
|
|
30
|
+
- numpy
|
|
31
|
+
- scipy
|
|
32
|
+
- pandas
|
|
33
|
+
- pandas-stubs
|
|
@@ -0,0 +1,39 @@
|
|
|
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.1.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**: StateSpaceRepresentation with system matrices (T, Z, R, H, Q)
|
|
13
|
+
- **Kalman Filter**: Numerically stable implementation with Joseph-form updates
|
|
14
|
+
- **RTS Smoother**: Rauch-Tung-Striebel backward smoother
|
|
15
|
+
- **Models**:
|
|
16
|
+
- LocalLevel (random walk + noise)
|
|
17
|
+
- LocalLinearTrend (trend + level)
|
|
18
|
+
- BSM (Basic Structural Model with seasonality)
|
|
19
|
+
- UCM (Unobserved Components Model)
|
|
20
|
+
- DynamicFactor (dynamic factor model)
|
|
21
|
+
- TVPRegression (time-varying parameter regression)
|
|
22
|
+
- ARIMASSM (ARIMA in state-space form)
|
|
23
|
+
- **Estimation**: Maximum Likelihood via scipy.optimize
|
|
24
|
+
- **Forecasting**: Point forecasts with confidence intervals
|
|
25
|
+
- **Diagnostics**: Ljung-Box test, Jarque-Bera test, residual analysis
|
|
26
|
+
- **Missing data**: Automatic handling of NaN observations
|
|
27
|
+
- **Reports**: HTML and LaTeX report generation
|
|
28
|
+
- **Datasets**: 19 built-in datasets (classic, macro, Brazilian)
|
|
29
|
+
- **CLI**: Command-line interface (estimate, info, forecast)
|
|
30
|
+
- **Experiment**: KalmanExperiment for model comparison and validation
|
|
31
|
+
- **Numba**: Optional JIT acceleration for core loops (5x+ speedup)
|
|
32
|
+
- **Documentation**: Full MkDocs site with tutorials, API reference, theory
|
|
33
|
+
|
|
34
|
+
### Dependencies
|
|
35
|
+
|
|
36
|
+
- Python >= 3.11
|
|
37
|
+
- NumPy >= 1.24
|
|
38
|
+
- SciPy >= 1.10
|
|
39
|
+
- pandas >= 2.0
|
kalmanbox-0.1.0/LICENSE
ADDED
|
@@ -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.
|
kalmanbox-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kalmanbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: State-space models and Kalman filtering for time series analysis
|
|
5
|
+
Author: NodesEcon
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: numpy>=1.24
|
|
16
|
+
Requires-Dist: pandas>=2.0
|
|
17
|
+
Requires-Dist: scipy>=1.10
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: bandit>=1.7; extra == 'dev'
|
|
20
|
+
Requires-Dist: hypothesis>=6.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: interrogate>=1.5; extra == 'dev'
|
|
22
|
+
Requires-Dist: mutmut>=2.4; extra == 'dev'
|
|
23
|
+
Requires-Dist: pre-commit>=3.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: radon>=6.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
30
|
+
Requires-Dist: structlog>=23.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
33
|
+
Requires-Dist: mkdocs>=1.5; extra == 'docs'
|
|
34
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
35
|
+
Provides-Extra: numba
|
|
36
|
+
Requires-Dist: numba>=0.58; extra == 'numba'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# kalmanbox
|
|
40
|
+
|
|
41
|
+
State-space models and Kalman filtering for time series analysis.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install -e ".[dev]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from kalmanbox import LocalLevel
|
|
53
|
+
from kalmanbox.datasets import load_dataset
|
|
54
|
+
|
|
55
|
+
nile = load_dataset('nile')
|
|
56
|
+
model = LocalLevel(nile['volume'])
|
|
57
|
+
results = model.fit()
|
|
58
|
+
print(results.summary())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# kalmanbox
|
|
2
|
+
|
|
3
|
+
State-space models and Kalman filtering for time series analysis.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from kalmanbox import LocalLevel
|
|
15
|
+
from kalmanbox.datasets import load_dataset
|
|
16
|
+
|
|
17
|
+
nile = load_dataset('nile')
|
|
18
|
+
model = LocalLevel(nile['volume'])
|
|
19
|
+
results = model.fit()
|
|
20
|
+
print(results.summary())
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""kalmanbox - State-space models and Kalman filtering for time series analysis."""
|
|
2
|
+
|
|
3
|
+
from kalmanbox.__version__ import __version__
|
|
4
|
+
from kalmanbox.core.model import StateSpaceModel
|
|
5
|
+
from kalmanbox.core.representation import StateSpaceRepresentation
|
|
6
|
+
from kalmanbox.core.results import StateSpaceResults
|
|
7
|
+
from kalmanbox.models.arima_ssm import ARIMA_SSM
|
|
8
|
+
from kalmanbox.models.bsm import BasicStructuralModel
|
|
9
|
+
from kalmanbox.models.custom import CustomStateSpace
|
|
10
|
+
from kalmanbox.models.cycle import CycleModel
|
|
11
|
+
from kalmanbox.models.dynamic_factor import DynamicFactorModel
|
|
12
|
+
from kalmanbox.models.local_level import LocalLevel
|
|
13
|
+
from kalmanbox.models.local_linear_trend import LocalLinearTrend
|
|
14
|
+
from kalmanbox.models.regression_ssm import RegressionSSM
|
|
15
|
+
from kalmanbox.models.tvp import TimeVaryingParameters
|
|
16
|
+
from kalmanbox.models.ucm import UnobservedComponents
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"__version__",
|
|
20
|
+
"ARIMA_SSM",
|
|
21
|
+
"BasicStructuralModel",
|
|
22
|
+
"CustomStateSpace",
|
|
23
|
+
"CycleModel",
|
|
24
|
+
"DynamicFactorModel",
|
|
25
|
+
"LocalLevel",
|
|
26
|
+
"LocalLinearTrend",
|
|
27
|
+
"RegressionSSM",
|
|
28
|
+
"StateSpaceModel",
|
|
29
|
+
"StateSpaceRepresentation",
|
|
30
|
+
"StateSpaceResults",
|
|
31
|
+
"TimeVaryingParameters",
|
|
32
|
+
"UnobservedComponents",
|
|
33
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Logging configuration for kalmanbox.
|
|
2
|
+
|
|
3
|
+
Uses structlog if available, falls back to stdlib logging.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
_LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
|
|
12
|
+
|
|
13
|
+
_has_structlog = False
|
|
14
|
+
try:
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
_has_structlog = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import structlog
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_logger(name: str) -> logging.Logger:
|
|
26
|
+
"""Get a logger with the kalmanbox namespace.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
name : str
|
|
31
|
+
Logger name (will be prefixed with 'kalmanbox.').
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
logging.Logger
|
|
36
|
+
Configured logger instance.
|
|
37
|
+
"""
|
|
38
|
+
logger = logging.getLogger(f"kalmanbox.{name}")
|
|
39
|
+
if not logger.handlers:
|
|
40
|
+
handler = logging.StreamHandler()
|
|
41
|
+
handler.setFormatter(logging.Formatter(_LOG_FORMAT))
|
|
42
|
+
logger.addHandler(handler)
|
|
43
|
+
logger.setLevel(logging.WARNING)
|
|
44
|
+
return logger
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def configure_logging(level: str = "WARNING", use_structlog: bool = True) -> None:
|
|
48
|
+
"""Configure kalmanbox logging.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
level : str
|
|
53
|
+
Log level: DEBUG, INFO, WARNING, ERROR.
|
|
54
|
+
use_structlog : bool
|
|
55
|
+
Use structlog if available (default True).
|
|
56
|
+
"""
|
|
57
|
+
numeric_level = getattr(logging, level.upper(), logging.WARNING)
|
|
58
|
+
root = logging.getLogger("kalmanbox")
|
|
59
|
+
root.setLevel(numeric_level)
|
|
60
|
+
|
|
61
|
+
if use_structlog and _has_structlog:
|
|
62
|
+
structlog.configure(
|
|
63
|
+
processors=[
|
|
64
|
+
structlog.stdlib.filter_by_level,
|
|
65
|
+
structlog.stdlib.add_log_level,
|
|
66
|
+
structlog.stdlib.add_logger_name,
|
|
67
|
+
structlog.dev.ConsoleRenderer(),
|
|
68
|
+
],
|
|
69
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
70
|
+
context_class=dict,
|
|
71
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
72
|
+
cache_logger_on_first_use=True,
|
|
73
|
+
)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""KalmanBox CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from kalmanbox.__version__ import __version__
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Model registry: maps CLI names to (module_path, class_name)
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
_MODEL_REGISTRY: dict[str, tuple[str, str]] = {
|
|
20
|
+
"local_level": ("kalmanbox.models.local_level", "LocalLevel"),
|
|
21
|
+
"local_linear_trend": ("kalmanbox.models.local_linear_trend", "LocalLinearTrend"),
|
|
22
|
+
"bsm": ("kalmanbox.models.bsm", "BasicStructuralModel"),
|
|
23
|
+
"ucm": ("kalmanbox.models.ucm", "UnobservedComponents"),
|
|
24
|
+
"arima": ("kalmanbox.models.arima_ssm", "ARIMA_SSM"),
|
|
25
|
+
"tvp": ("kalmanbox.models.tvp", "TimeVaryingParameters"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_model_class(name: str) -> type: # type: ignore[type-arg]
|
|
30
|
+
"""Dynamically import and return a model class by CLI name."""
|
|
31
|
+
if name not in _MODEL_REGISTRY:
|
|
32
|
+
available = ", ".join(sorted(_MODEL_REGISTRY.keys()))
|
|
33
|
+
print(f"Error: unknown model '{name}'. Available: {available}", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
module_path, class_name = _MODEL_REGISTRY[name]
|
|
37
|
+
import importlib
|
|
38
|
+
|
|
39
|
+
module = importlib.import_module(module_path)
|
|
40
|
+
return getattr(module, class_name) # type: ignore[no-any-return]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_data(path: str) -> pd.DataFrame:
|
|
44
|
+
"""Load CSV data file."""
|
|
45
|
+
data_path = Path(path)
|
|
46
|
+
if not data_path.exists():
|
|
47
|
+
print(f"Error: data file not found: {path}", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
return pd.read_csv(data_path) # type: ignore[no-any-return]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_model_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
|
53
|
+
"""Build keyword arguments for model constructor from CLI args."""
|
|
54
|
+
kwargs: dict[str, Any] = {}
|
|
55
|
+
if hasattr(args, "seasonal_period") and args.seasonal_period is not None:
|
|
56
|
+
kwargs["seasonal_period"] = args.seasonal_period
|
|
57
|
+
if hasattr(args, "order") and args.order is not None:
|
|
58
|
+
parts = [int(x) for x in args.order.split(",")]
|
|
59
|
+
if len(parts) == 3:
|
|
60
|
+
kwargs["order"] = tuple(parts)
|
|
61
|
+
else:
|
|
62
|
+
print("Error: --order must be p,d,q (e.g. 1,1,1)", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
if hasattr(args, "exog") and args.exog is not None:
|
|
65
|
+
exog_path = Path(args.exog)
|
|
66
|
+
if not exog_path.exists():
|
|
67
|
+
print(f"Error: exog file not found: {args.exog}", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
exog_df = pd.read_csv(exog_path)
|
|
70
|
+
kwargs["exog"] = exog_df.to_numpy(dtype=np.float64)
|
|
71
|
+
return kwargs
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _NumpyEncoder(json.JSONEncoder):
|
|
75
|
+
"""JSON encoder that handles numpy types."""
|
|
76
|
+
|
|
77
|
+
def default(self, o: Any) -> Any:
|
|
78
|
+
"""Encode numpy types to JSON-serializable Python types."""
|
|
79
|
+
if isinstance(o, np.ndarray):
|
|
80
|
+
return o.tolist()
|
|
81
|
+
if isinstance(o, np.integer):
|
|
82
|
+
return int(o)
|
|
83
|
+
if isinstance(o, np.floating):
|
|
84
|
+
return float(o)
|
|
85
|
+
return super().default(o)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Commands
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
def estimate_command(args: argparse.Namespace) -> None:
|
|
92
|
+
"""Execute the 'estimate' command."""
|
|
93
|
+
data = _load_data(args.data)
|
|
94
|
+
|
|
95
|
+
# Determine the target column (first numeric column)
|
|
96
|
+
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
|
97
|
+
if len(numeric_cols) == 0:
|
|
98
|
+
print("Error: no numeric columns found in data", file=sys.stderr)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
y = data[numeric_cols[0]].to_numpy(dtype=np.float64)
|
|
102
|
+
model_class = _get_model_class(args.model)
|
|
103
|
+
kwargs = _build_model_kwargs(args)
|
|
104
|
+
|
|
105
|
+
model = model_class(y, **kwargs)
|
|
106
|
+
results = model.fit()
|
|
107
|
+
|
|
108
|
+
# Build output dict
|
|
109
|
+
output: dict[str, Any] = {
|
|
110
|
+
"model": args.model,
|
|
111
|
+
"data": args.data,
|
|
112
|
+
"n_obs": len(y),
|
|
113
|
+
"loglike": float(results.loglike),
|
|
114
|
+
"aic": float(results.aic),
|
|
115
|
+
"bic": float(results.bic),
|
|
116
|
+
"params": {
|
|
117
|
+
name: float(val) for name, val in zip(results.param_names, results.params, strict=True)
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
output_path = Path(args.output)
|
|
122
|
+
with open(output_path, "w") as f:
|
|
123
|
+
json.dump(output, f, indent=2, cls=_NumpyEncoder)
|
|
124
|
+
|
|
125
|
+
print(f"Results saved to {output_path}")
|
|
126
|
+
print(f" Model: {args.model}")
|
|
127
|
+
print(f" LogLike: {output['loglike']:.4f}")
|
|
128
|
+
print(f" AIC: {output['aic']:.4f}")
|
|
129
|
+
print(f" BIC: {output['bic']:.4f}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def info_command(args: argparse.Namespace) -> None:
|
|
133
|
+
"""Execute the 'info' command."""
|
|
134
|
+
model_class = _get_model_class(args.model)
|
|
135
|
+
|
|
136
|
+
# Create a dummy model with minimal data to inspect structure
|
|
137
|
+
dummy_y = np.ones(10, dtype=np.float64)
|
|
138
|
+
kwargs = _build_model_kwargs(args)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
model = model_class(dummy_y, **kwargs)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"Error creating model: {e}", file=sys.stderr)
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
# Build representation from start params to inspect structure
|
|
147
|
+
rep = model._build_ssm(model.start_params) # noqa: SLF001
|
|
148
|
+
print(f"Model: {args.model}")
|
|
149
|
+
print(f" k_states: {rep.k_states}")
|
|
150
|
+
print(f" k_obs: {rep.k_endog}")
|
|
151
|
+
print(f" State dimensions: T={rep.T.shape}, Z={rep.Z.shape}")
|
|
152
|
+
print(f" Parameters: {model.param_names}")
|
|
153
|
+
print(f" n_params: {len(model.param_names)}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def forecast_command(args: argparse.Namespace) -> None:
|
|
157
|
+
"""Execute the 'forecast' command."""
|
|
158
|
+
data = _load_data(args.data)
|
|
159
|
+
|
|
160
|
+
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
|
161
|
+
if len(numeric_cols) == 0:
|
|
162
|
+
print("Error: no numeric columns found in data", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
y = data[numeric_cols[0]].to_numpy(dtype=np.float64)
|
|
166
|
+
model_class = _get_model_class(args.model)
|
|
167
|
+
kwargs = _build_model_kwargs(args)
|
|
168
|
+
|
|
169
|
+
model = model_class(y, **kwargs)
|
|
170
|
+
results = model.fit()
|
|
171
|
+
|
|
172
|
+
fc = results.forecast(steps=args.steps)
|
|
173
|
+
|
|
174
|
+
# The forecast dict uses 'lower'/'upper' keys; arrays may be 2D (steps, k_endog)
|
|
175
|
+
lower_key = "lower_95" if "lower_95" in fc else "lower"
|
|
176
|
+
upper_key = "upper_95" if "upper_95" in fc else "upper"
|
|
177
|
+
|
|
178
|
+
mean_arr = np.asarray(fc["mean"]).flatten()
|
|
179
|
+
lower_arr = np.asarray(fc[lower_key]).flatten()
|
|
180
|
+
upper_arr = np.asarray(fc[upper_key]).flatten()
|
|
181
|
+
|
|
182
|
+
# Build forecast DataFrame
|
|
183
|
+
fc_df = pd.DataFrame(
|
|
184
|
+
{
|
|
185
|
+
"step": list(range(1, args.steps + 1)),
|
|
186
|
+
"mean": mean_arr,
|
|
187
|
+
"lower_95": lower_arr,
|
|
188
|
+
"upper_95": upper_arr,
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
output_path = Path(args.output)
|
|
193
|
+
fc_df.to_csv(output_path, index=False)
|
|
194
|
+
|
|
195
|
+
print(f"Forecast saved to {output_path}")
|
|
196
|
+
print(f" Model: {args.model}")
|
|
197
|
+
print(f" Steps: {args.steps}")
|
|
198
|
+
print(f" Forecast mean (first 5): {mean_arr[:5].tolist()}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Shared argument helpers
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
def _add_model_args(parser: argparse.ArgumentParser) -> None:
|
|
205
|
+
"""Add common model arguments to a subparser."""
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"--model",
|
|
208
|
+
required=True,
|
|
209
|
+
choices=list(_MODEL_REGISTRY.keys()),
|
|
210
|
+
help="Model type to use",
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--seasonal-period",
|
|
214
|
+
type=int,
|
|
215
|
+
default=None,
|
|
216
|
+
help="Seasonal period (required for bsm, ucm with seasonal)",
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--order",
|
|
220
|
+
type=str,
|
|
221
|
+
default=None,
|
|
222
|
+
help="ARIMA order as p,d,q (e.g. 1,1,1)",
|
|
223
|
+
)
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"--exog",
|
|
226
|
+
type=str,
|
|
227
|
+
default=None,
|
|
228
|
+
help="Path to CSV with exogenous variables (for tvp)",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# Main parser
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
236
|
+
"""Build the CLI argument parser."""
|
|
237
|
+
parser = argparse.ArgumentParser(
|
|
238
|
+
prog="kalmanbox",
|
|
239
|
+
description="KalmanBox: State-space models and Kalman filtering for time series",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--version",
|
|
243
|
+
action="version",
|
|
244
|
+
version=f"kalmanbox {__version__}",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
248
|
+
|
|
249
|
+
# --- estimate ---
|
|
250
|
+
est = subparsers.add_parser(
|
|
251
|
+
"estimate",
|
|
252
|
+
help="Estimate a state-space model from data",
|
|
253
|
+
)
|
|
254
|
+
_add_model_args(est)
|
|
255
|
+
est.add_argument("--data", required=True, help="Path to CSV data file")
|
|
256
|
+
est.add_argument("--output", default="results.json", help="Output JSON file path")
|
|
257
|
+
est.set_defaults(func=estimate_command)
|
|
258
|
+
|
|
259
|
+
# --- info ---
|
|
260
|
+
info = subparsers.add_parser(
|
|
261
|
+
"info",
|
|
262
|
+
help="Show model information (dimensions, parameters)",
|
|
263
|
+
)
|
|
264
|
+
_add_model_args(info)
|
|
265
|
+
info.set_defaults(func=info_command)
|
|
266
|
+
|
|
267
|
+
# --- forecast ---
|
|
268
|
+
fc = subparsers.add_parser(
|
|
269
|
+
"forecast",
|
|
270
|
+
help="Fit model and produce forecasts",
|
|
271
|
+
)
|
|
272
|
+
_add_model_args(fc)
|
|
273
|
+
fc.add_argument("--data", required=True, help="Path to CSV data file")
|
|
274
|
+
fc.add_argument("--steps", type=int, required=True, help="Number of forecast steps")
|
|
275
|
+
fc.add_argument("--output", default="forecast.csv", help="Output CSV file path")
|
|
276
|
+
fc.set_defaults(func=forecast_command)
|
|
277
|
+
|
|
278
|
+
return parser
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def main(argv: list[str] | None = None) -> None:
|
|
282
|
+
"""CLI entry point."""
|
|
283
|
+
parser = build_parser()
|
|
284
|
+
args = parser.parse_args(argv)
|
|
285
|
+
|
|
286
|
+
if args.command is None:
|
|
287
|
+
parser.print_help()
|
|
288
|
+
sys.exit(0)
|
|
289
|
+
|
|
290
|
+
args.func(args)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
main()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Core state-space model components."""
|
|
2
|
+
|
|
3
|
+
from kalmanbox.core.model import StateSpaceModel
|
|
4
|
+
from kalmanbox.core.representation import StateSpaceRepresentation
|
|
5
|
+
from kalmanbox.core.results import StateSpaceResults
|
|
6
|
+
|
|
7
|
+
__all__ = ["StateSpaceModel", "StateSpaceRepresentation", "StateSpaceResults"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Global configuration for kalmanbox."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class KalmanBoxConfig:
|
|
8
|
+
"""Global configuration."""
|
|
9
|
+
|
|
10
|
+
default_optimizer: str = "L-BFGS-B"
|
|
11
|
+
max_iterations: int = 500
|
|
12
|
+
tolerance: float = 1e-8
|
|
13
|
+
diffuse_initial_variance: float = 1e7
|
|
14
|
+
symmetry_enforcement: bool = True
|
|
15
|
+
cholesky_fallback_eps: float = 1e-8
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Singleton global config
|
|
19
|
+
config = KalmanBoxConfig()
|