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.
Files changed (112) hide show
  1. kalmanbox-0.1.0/.gitignore +30 -0
  2. kalmanbox-0.1.0/.pre-commit-config.yaml +33 -0
  3. kalmanbox-0.1.0/CHANGELOG.md +39 -0
  4. kalmanbox-0.1.0/LICENSE +21 -0
  5. kalmanbox-0.1.0/PKG-INFO +63 -0
  6. kalmanbox-0.1.0/README.md +25 -0
  7. kalmanbox-0.1.0/kalmanbox/__init__.py +33 -0
  8. kalmanbox-0.1.0/kalmanbox/__version__.py +3 -0
  9. kalmanbox-0.1.0/kalmanbox/_logging.py +73 -0
  10. kalmanbox-0.1.0/kalmanbox/cli/__init__.py +5 -0
  11. kalmanbox-0.1.0/kalmanbox/cli/main.py +294 -0
  12. kalmanbox-0.1.0/kalmanbox/core/__init__.py +7 -0
  13. kalmanbox-0.1.0/kalmanbox/core/config.py +19 -0
  14. kalmanbox-0.1.0/kalmanbox/core/model.py +226 -0
  15. kalmanbox-0.1.0/kalmanbox/core/representation.py +147 -0
  16. kalmanbox-0.1.0/kalmanbox/core/results.py +267 -0
  17. kalmanbox-0.1.0/kalmanbox/datasets/__init__.py +5 -0
  18. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/cambio.csv +1 -0
  19. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/desemprego.csv +1 -0
  20. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/gdp_quarterly.csv +113 -0
  21. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/industrial.csv +1 -0
  22. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/ipca.csv +1 -0
  23. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/ipca_monthly.csv +289 -0
  24. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/m1.csv +1 -0
  25. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/pib.csv +1 -0
  26. kalmanbox-0.1.0/kalmanbox/datasets/data/brazil/selic.csv +1 -0
  27. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/airline.csv +145 -0
  28. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/lynx.csv +115 -0
  29. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/nile.csv +101 -0
  30. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/sunspots.csv +253 -0
  31. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/uk_drivers.csv +193 -0
  32. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/uk_gas.csv +109 -0
  33. kalmanbox-0.1.0/kalmanbox/datasets/data/classic/ukdrivers.csv +193 -0
  34. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/co2.csv +791 -0
  35. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/exchange_rates.csv +6521 -0
  36. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/global_temp.csv +1729 -0
  37. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/uk_gas.csv +109 -0
  38. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/us_gdp.csv +309 -0
  39. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/us_inflation.csv +925 -0
  40. kalmanbox-0.1.0/kalmanbox/datasets/data/macro/us_unemployment.csv +913 -0
  41. kalmanbox-0.1.0/kalmanbox/datasets/load.py +178 -0
  42. kalmanbox-0.1.0/kalmanbox/diagnostics/__init__.py +41 -0
  43. kalmanbox-0.1.0/kalmanbox/diagnostics/convergence.py +273 -0
  44. kalmanbox-0.1.0/kalmanbox/diagnostics/missing.py +204 -0
  45. kalmanbox-0.1.0/kalmanbox/diagnostics/residuals.py +191 -0
  46. kalmanbox-0.1.0/kalmanbox/diagnostics/tests.py +413 -0
  47. kalmanbox-0.1.0/kalmanbox/estimation/__init__.py +23 -0
  48. kalmanbox-0.1.0/kalmanbox/estimation/bayesian.py +567 -0
  49. kalmanbox-0.1.0/kalmanbox/estimation/diffuse.py +258 -0
  50. kalmanbox-0.1.0/kalmanbox/estimation/em.py +252 -0
  51. kalmanbox-0.1.0/kalmanbox/estimation/mle.py +177 -0
  52. kalmanbox-0.1.0/kalmanbox/experiment/__init__.py +10 -0
  53. kalmanbox-0.1.0/kalmanbox/experiment/comparison.py +220 -0
  54. kalmanbox-0.1.0/kalmanbox/experiment/experiment.py +343 -0
  55. kalmanbox-0.1.0/kalmanbox/filters/__init__.py +24 -0
  56. kalmanbox-0.1.0/kalmanbox/filters/ekf.py +300 -0
  57. kalmanbox-0.1.0/kalmanbox/filters/enkf.py +279 -0
  58. kalmanbox-0.1.0/kalmanbox/filters/information.py +277 -0
  59. kalmanbox-0.1.0/kalmanbox/filters/kalman.py +263 -0
  60. kalmanbox-0.1.0/kalmanbox/filters/square_root.py +266 -0
  61. kalmanbox-0.1.0/kalmanbox/filters/ukf.py +340 -0
  62. kalmanbox-0.1.0/kalmanbox/models/__init__.py +25 -0
  63. kalmanbox-0.1.0/kalmanbox/models/arima_ssm.py +157 -0
  64. kalmanbox-0.1.0/kalmanbox/models/bsm.py +154 -0
  65. kalmanbox-0.1.0/kalmanbox/models/custom.py +120 -0
  66. kalmanbox-0.1.0/kalmanbox/models/cycle.py +117 -0
  67. kalmanbox-0.1.0/kalmanbox/models/dynamic_factor.py +267 -0
  68. kalmanbox-0.1.0/kalmanbox/models/local_level.py +73 -0
  69. kalmanbox-0.1.0/kalmanbox/models/local_linear_trend.py +60 -0
  70. kalmanbox-0.1.0/kalmanbox/models/regression_ssm.py +147 -0
  71. kalmanbox-0.1.0/kalmanbox/models/tvp.py +309 -0
  72. kalmanbox-0.1.0/kalmanbox/models/ucm.py +467 -0
  73. kalmanbox-0.1.0/kalmanbox/py.typed +0 -0
  74. kalmanbox-0.1.0/kalmanbox/reports/__init__.py +15 -0
  75. kalmanbox-0.1.0/kalmanbox/reports/css_manager.py +253 -0
  76. kalmanbox-0.1.0/kalmanbox/reports/exporters/__init__.py +7 -0
  77. kalmanbox-0.1.0/kalmanbox/reports/exporters/html.py +88 -0
  78. kalmanbox-0.1.0/kalmanbox/reports/exporters/latex.py +215 -0
  79. kalmanbox-0.1.0/kalmanbox/reports/exporters/markdown.py +156 -0
  80. kalmanbox-0.1.0/kalmanbox/reports/report_manager.py +258 -0
  81. kalmanbox-0.1.0/kalmanbox/reports/template_manager.py +143 -0
  82. kalmanbox-0.1.0/kalmanbox/reports/templates/base.html +44 -0
  83. kalmanbox-0.1.0/kalmanbox/reports/templates/ssm_report.html +201 -0
  84. kalmanbox-0.1.0/kalmanbox/reports/transformers/__init__.py +8 -0
  85. kalmanbox-0.1.0/kalmanbox/reports/transformers/dfm.py +103 -0
  86. kalmanbox-0.1.0/kalmanbox/reports/transformers/ssm.py +193 -0
  87. kalmanbox-0.1.0/kalmanbox/reports/transformers/tvp.py +151 -0
  88. kalmanbox-0.1.0/kalmanbox/reports/transformers/ucm.py +102 -0
  89. kalmanbox-0.1.0/kalmanbox/simulation/__init__.py +12 -0
  90. kalmanbox-0.1.0/kalmanbox/simulation/bootstrap.py +241 -0
  91. kalmanbox-0.1.0/kalmanbox/simulation/simulate.py +174 -0
  92. kalmanbox-0.1.0/kalmanbox/smoothers/__init__.py +18 -0
  93. kalmanbox-0.1.0/kalmanbox/smoothers/disturbance.py +201 -0
  94. kalmanbox-0.1.0/kalmanbox/smoothers/fixed_interval.py +136 -0
  95. kalmanbox-0.1.0/kalmanbox/smoothers/fixed_lag.py +138 -0
  96. kalmanbox-0.1.0/kalmanbox/smoothers/rts.py +96 -0
  97. kalmanbox-0.1.0/kalmanbox/utils/__init__.py +37 -0
  98. kalmanbox-0.1.0/kalmanbox/utils/matrix_ops.py +45 -0
  99. kalmanbox-0.1.0/kalmanbox/utils/numba_core.py +284 -0
  100. kalmanbox-0.1.0/kalmanbox/utils/transforms.py +26 -0
  101. kalmanbox-0.1.0/kalmanbox/visualization/__init__.py +67 -0
  102. kalmanbox-0.1.0/kalmanbox/visualization/components.py +276 -0
  103. kalmanbox-0.1.0/kalmanbox/visualization/diagnostics_plot.py +221 -0
  104. kalmanbox-0.1.0/kalmanbox/visualization/export.py +217 -0
  105. kalmanbox-0.1.0/kalmanbox/visualization/factor_plot.py +333 -0
  106. kalmanbox-0.1.0/kalmanbox/visualization/filter_plot.py +223 -0
  107. kalmanbox-0.1.0/kalmanbox/visualization/forecast_plot.py +263 -0
  108. kalmanbox-0.1.0/kalmanbox/visualization/state_plot.py +241 -0
  109. kalmanbox-0.1.0/kalmanbox/visualization/themes.py +269 -0
  110. kalmanbox-0.1.0/mkdocs.yml +94 -0
  111. kalmanbox-0.1.0/pyproject.toml +102 -0
  112. 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
@@ -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,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,3 @@
1
+ """Version information for kalmanbox."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,5 @@
1
+ """KalmanBox command-line interface."""
2
+
3
+ from kalmanbox.cli.main import main
4
+
5
+ __all__ = ["main"]
@@ -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()