chronobox 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 (152) hide show
  1. chronobox-0.1.0/.gitignore +30 -0
  2. chronobox-0.1.0/.pre-commit-config.yaml +27 -0
  3. chronobox-0.1.0/CHANGELOG.md +38 -0
  4. chronobox-0.1.0/LICENSE +21 -0
  5. chronobox-0.1.0/PKG-INFO +74 -0
  6. chronobox-0.1.0/README.md +35 -0
  7. chronobox-0.1.0/chronobox/__init__.py +42 -0
  8. chronobox-0.1.0/chronobox/__version__.py +1 -0
  9. chronobox-0.1.0/chronobox/_logging.py +16 -0
  10. chronobox-0.1.0/chronobox/analysis/__init__.py +25 -0
  11. chronobox-0.1.0/chronobox/analysis/counterfactual.py +260 -0
  12. chronobox-0.1.0/chronobox/analysis/fevd.py +232 -0
  13. chronobox-0.1.0/chronobox/analysis/granger.py +184 -0
  14. chronobox-0.1.0/chronobox/analysis/hd.py +381 -0
  15. chronobox-0.1.0/chronobox/analysis/irf.py +390 -0
  16. chronobox-0.1.0/chronobox/analysis/spillover.py +456 -0
  17. chronobox-0.1.0/chronobox/cli/__init__.py +5 -0
  18. chronobox-0.1.0/chronobox/cli/main.py +667 -0
  19. chronobox-0.1.0/chronobox/core/__init__.py +7 -0
  20. chronobox-0.1.0/chronobox/core/config.py +19 -0
  21. chronobox-0.1.0/chronobox/core/dates.py +107 -0
  22. chronobox-0.1.0/chronobox/core/lag_polynomial.py +134 -0
  23. chronobox-0.1.0/chronobox/core/results.py +352 -0
  24. chronobox-0.1.0/chronobox/core/transforms.py +244 -0
  25. chronobox-0.1.0/chronobox/core/tsdata.py +298 -0
  26. chronobox-0.1.0/chronobox/datasets/__init__.py +270 -0
  27. chronobox-0.1.0/chronobox/datasets/data/brazil/ipca_monthly.csv +361 -0
  28. chronobox-0.1.0/chronobox/datasets/data/classic/airline.csv +145 -0
  29. chronobox-0.1.0/chronobox/datasets/data/classic/co2.csv +469 -0
  30. chronobox-0.1.0/chronobox/datasets/data/classic/lynx.csv +115 -0
  31. chronobox-0.1.0/chronobox/datasets/data/classic/nile.csv +101 -0
  32. chronobox-0.1.0/chronobox/datasets/data/classic/sunspot.csv +310 -0
  33. chronobox-0.1.0/chronobox/datasets/data/classic/uspop.csv +23 -0
  34. chronobox-0.1.0/chronobox/datasets/data/macro/canada.csv +85 -0
  35. chronobox-0.1.0/chronobox/datasets/data/macro/us_gdp.csv +313 -0
  36. chronobox-0.1.0/chronobox/datasets/data/macro/us_macro_quarterly.csv +245 -0
  37. chronobox-0.1.0/chronobox/datasets/load.py +11 -0
  38. chronobox-0.1.0/chronobox/datasets/scripts/__init__.py +1 -0
  39. chronobox-0.1.0/chronobox/datasets/scripts/download_bcb.py +233 -0
  40. chronobox-0.1.0/chronobox/datasets/simulated.py +218 -0
  41. chronobox-0.1.0/chronobox/decomposition/__init__.py +6 -0
  42. chronobox-0.1.0/chronobox/decomposition/classical.py +157 -0
  43. chronobox-0.1.0/chronobox/decomposition/stl.py +387 -0
  44. chronobox-0.1.0/chronobox/decomposition/x13_wrapper.py +241 -0
  45. chronobox-0.1.0/chronobox/experiment/__init__.py +15 -0
  46. chronobox-0.1.0/chronobox/experiment/experiment.py +987 -0
  47. chronobox-0.1.0/chronobox/filters/__init__.py +26 -0
  48. chronobox-0.1.0/chronobox/filters/bk.py +166 -0
  49. chronobox-0.1.0/chronobox/filters/bn.py +278 -0
  50. chronobox-0.1.0/chronobox/filters/cf.py +130 -0
  51. chronobox-0.1.0/chronobox/filters/hamilton.py +208 -0
  52. chronobox-0.1.0/chronobox/filters/hp.py +128 -0
  53. chronobox-0.1.0/chronobox/models/__init__.py +42 -0
  54. chronobox-0.1.0/chronobox/models/ardl.py +506 -0
  55. chronobox-0.1.0/chronobox/models/arfima.py +694 -0
  56. chronobox-0.1.0/chronobox/models/arima.py +854 -0
  57. chronobox-0.1.0/chronobox/models/bvar.py +885 -0
  58. chronobox-0.1.0/chronobox/models/ecm.py +313 -0
  59. chronobox-0.1.0/chronobox/models/ets.py +1024 -0
  60. chronobox-0.1.0/chronobox/models/favar.py +462 -0
  61. chronobox-0.1.0/chronobox/models/gvar.py +464 -0
  62. chronobox-0.1.0/chronobox/models/holtwinters.py +466 -0
  63. chronobox-0.1.0/chronobox/models/svar.py +563 -0
  64. chronobox-0.1.0/chronobox/models/theta.py +224 -0
  65. chronobox-0.1.0/chronobox/models/tvpvar.py +392 -0
  66. chronobox-0.1.0/chronobox/models/var.py +820 -0
  67. chronobox-0.1.0/chronobox/models/vecm.py +891 -0
  68. chronobox-0.1.0/chronobox/py.typed +0 -0
  69. chronobox-0.1.0/chronobox/reports/__init__.py +21 -0
  70. chronobox-0.1.0/chronobox/reports/css_manager.py +439 -0
  71. chronobox-0.1.0/chronobox/reports/exporters/__init__.py +13 -0
  72. chronobox-0.1.0/chronobox/reports/exporters/html_exporter.py +277 -0
  73. chronobox-0.1.0/chronobox/reports/exporters/latex_exporter.py +255 -0
  74. chronobox-0.1.0/chronobox/reports/exporters/markdown_exporter.py +130 -0
  75. chronobox-0.1.0/chronobox/reports/report_manager.py +485 -0
  76. chronobox-0.1.0/chronobox/reports/template_manager.py +307 -0
  77. chronobox-0.1.0/chronobox/reports/templates/arima_report.html +125 -0
  78. chronobox-0.1.0/chronobox/reports/templates/base.html +123 -0
  79. chronobox-0.1.0/chronobox/reports/templates/generic_report.html +4 -0
  80. chronobox-0.1.0/chronobox/reports/templates/spillover_report.html +18 -0
  81. chronobox-0.1.0/chronobox/reports/templates/svar_report.html +15 -0
  82. chronobox-0.1.0/chronobox/reports/templates/tests_report.html +36 -0
  83. chronobox-0.1.0/chronobox/reports/templates/var_report.html +41 -0
  84. chronobox-0.1.0/chronobox/reports/transformers/__init__.py +34 -0
  85. chronobox-0.1.0/chronobox/reports/transformers/ardl_transformer.py +102 -0
  86. chronobox-0.1.0/chronobox/reports/transformers/arima_transformer.py +336 -0
  87. chronobox-0.1.0/chronobox/reports/transformers/base_transformer.py +194 -0
  88. chronobox-0.1.0/chronobox/reports/transformers/bvar_transformer.py +73 -0
  89. chronobox-0.1.0/chronobox/reports/transformers/ets_transformer.py +85 -0
  90. chronobox-0.1.0/chronobox/reports/transformers/spillover_transformer.py +134 -0
  91. chronobox-0.1.0/chronobox/reports/transformers/svar_transformer.py +86 -0
  92. chronobox-0.1.0/chronobox/reports/transformers/tests_transformer.py +139 -0
  93. chronobox-0.1.0/chronobox/reports/transformers/var_transformer.py +261 -0
  94. chronobox-0.1.0/chronobox/selection/__init__.py +6 -0
  95. chronobox-0.1.0/chronobox/selection/auto_arima.py +344 -0
  96. chronobox-0.1.0/chronobox/selection/auto_ets.py +269 -0
  97. chronobox-0.1.0/chronobox/selection/lag_selection.py +291 -0
  98. chronobox-0.1.0/chronobox/tests_stat/__init__.py +90 -0
  99. chronobox-0.1.0/chronobox/tests_stat/base.py +116 -0
  100. chronobox-0.1.0/chronobox/tests_stat/cointegration/__init__.py +22 -0
  101. chronobox-0.1.0/chronobox/tests_stat/cointegration/bounds_test.py +236 -0
  102. chronobox-0.1.0/chronobox/tests_stat/cointegration/engle_granger.py +180 -0
  103. chronobox-0.1.0/chronobox/tests_stat/cointegration/gregory_hansen.py +217 -0
  104. chronobox-0.1.0/chronobox/tests_stat/cointegration/phillips_ouliaris.py +161 -0
  105. chronobox-0.1.0/chronobox/tests_stat/critical_values/__init__.py +31 -0
  106. chronobox-0.1.0/chronobox/tests_stat/critical_values/mackinnon.py +214 -0
  107. chronobox-0.1.0/chronobox/tests_stat/critical_values/osterwald_lenum.py +151 -0
  108. chronobox-0.1.0/chronobox/tests_stat/critical_values/pss_bounds.py +205 -0
  109. chronobox-0.1.0/chronobox/tests_stat/specification/__init__.py +34 -0
  110. chronobox-0.1.0/chronobox/tests_stat/specification/arch_lm.py +89 -0
  111. chronobox-0.1.0/chronobox/tests_stat/specification/bds.py +148 -0
  112. chronobox-0.1.0/chronobox/tests_stat/specification/breusch_godfrey.py +109 -0
  113. chronobox-0.1.0/chronobox/tests_stat/specification/durbin_watson.py +78 -0
  114. chronobox-0.1.0/chronobox/tests_stat/specification/jarque_bera.py +89 -0
  115. chronobox-0.1.0/chronobox/tests_stat/specification/ljung_box.py +104 -0
  116. chronobox-0.1.0/chronobox/tests_stat/specification/reset.py +107 -0
  117. chronobox-0.1.0/chronobox/tests_stat/specification/white.py +103 -0
  118. chronobox-0.1.0/chronobox/tests_stat/structural_breaks/__init__.py +24 -0
  119. chronobox-0.1.0/chronobox/tests_stat/structural_breaks/bai_perron.py +278 -0
  120. chronobox-0.1.0/chronobox/tests_stat/structural_breaks/chow.py +120 -0
  121. chronobox-0.1.0/chronobox/tests_stat/structural_breaks/cusum.py +219 -0
  122. chronobox-0.1.0/chronobox/tests_stat/structural_breaks/qlr.py +146 -0
  123. chronobox-0.1.0/chronobox/tests_stat/unit_root/__init__.py +31 -0
  124. chronobox-0.1.0/chronobox/tests_stat/unit_root/adf.py +275 -0
  125. chronobox-0.1.0/chronobox/tests_stat/unit_root/ers.py +251 -0
  126. chronobox-0.1.0/chronobox/tests_stat/unit_root/hegy.py +230 -0
  127. chronobox-0.1.0/chronobox/tests_stat/unit_root/kpss.py +175 -0
  128. chronobox-0.1.0/chronobox/tests_stat/unit_root/lee_strazicich.py +278 -0
  129. chronobox-0.1.0/chronobox/tests_stat/unit_root/pp.py +205 -0
  130. chronobox-0.1.0/chronobox/tests_stat/unit_root/zivot_andrews.py +225 -0
  131. chronobox-0.1.0/chronobox/utils/__init__.py +21 -0
  132. chronobox-0.1.0/chronobox/utils/array_ops.py +78 -0
  133. chronobox-0.1.0/chronobox/utils/logging.py +82 -0
  134. chronobox-0.1.0/chronobox/utils/numba_core.py +257 -0
  135. chronobox-0.1.0/chronobox/utils/validation.py +101 -0
  136. chronobox-0.1.0/chronobox/visualization/__init__.py +62 -0
  137. chronobox-0.1.0/chronobox/visualization/coef_plot.py +237 -0
  138. chronobox-0.1.0/chronobox/visualization/decomposition_plot.py +212 -0
  139. chronobox-0.1.0/chronobox/visualization/diagnostics_plot.py +373 -0
  140. chronobox-0.1.0/chronobox/visualization/export.py +279 -0
  141. chronobox-0.1.0/chronobox/visualization/fevd_plot.py +257 -0
  142. chronobox-0.1.0/chronobox/visualization/forecast_plot.py +258 -0
  143. chronobox-0.1.0/chronobox/visualization/hd_plot.py +224 -0
  144. chronobox-0.1.0/chronobox/visualization/irf_plot.py +295 -0
  145. chronobox-0.1.0/chronobox/visualization/spillover_plot.py +427 -0
  146. chronobox-0.1.0/chronobox/visualization/test_plot.py +462 -0
  147. chronobox-0.1.0/chronobox/visualization/themes.py +340 -0
  148. chronobox-0.1.0/chronobox/visualization/ts_plot.py +246 -0
  149. chronobox-0.1.0/mkdocs.yml +141 -0
  150. chronobox-0.1.0/pyproject.toml +145 -0
  151. chronobox-0.1.0/pyrightconfig.json +22 -0
  152. chronobox-0.1.0/ruff.toml +44 -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,27 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.5.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-json
9
+ - id: check-toml
10
+ - id: check-added-large-files
11
+ args: ['--maxkb=500']
12
+ - id: check-merge-conflict
13
+ - id: detect-private-key
14
+
15
+ - repo: https://github.com/astral-sh/ruff-pre-commit
16
+ rev: v0.4.0
17
+ hooks:
18
+ - id: ruff
19
+ args: [--fix, --exit-non-zero-on-fix]
20
+ - id: ruff-format
21
+
22
+ - repo: https://github.com/pre-commit/mirrors-mypy
23
+ rev: v1.9.0
24
+ hooks:
25
+ - id: mypy
26
+ additional_dependencies: [numpy, pandas-stubs]
27
+ args: [--ignore-missing-imports]
@@ -0,0 +1,38 @@
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
+ - ARIMA/SARIMA model with MLE via Kalman filter (kalmanbox)
12
+ - Auto-ARIMA with stepwise algorithm and information criteria
13
+ - VAR model with lag selection, Granger causality, IRF, FEVD
14
+ - SVAR model with Cholesky, short-run, and long-run identification
15
+ - VECM model with Johansen cointegration test
16
+ - ARDL model with bounds testing and ECM
17
+ - Unit root tests: ADF, Phillips-Perron, KPSS, ERS/DF-GLS
18
+ - Cointegration tests: Johansen trace and max eigenvalue
19
+ - Diagnostic tests: Ljung-Box, Breusch-Godfrey, ARCH-LM
20
+ - Filters: Hodrick-Prescott, Baxter-King, Christiano-Fitzgerald, Hamilton
21
+ - Time series decomposition (STL, classical)
22
+ - Visualization module with diagnostic plots
23
+ - Report generation (HTML, professional themes)
24
+ - ChronoExperiment pattern for systematic model comparison
25
+ - CLI with 5 commands: estimate, test, forecast, decompose, filter
26
+ - ~30 built-in datasets (classic, macro, finance, simulated)
27
+ - Download scripts for Brazilian macro data (BCB SGS, IBGE SIDRA)
28
+ - Numba optimization for critical loops (@optional_jit)
29
+ - MkDocs documentation with ~40 pages
30
+ - CI/CD with GitHub Actions
31
+ - 10 quality assurance phases
32
+
33
+ ### Dependencies
34
+ - kalmanbox (Kalman filter and MLE)
35
+ - NumPy >= 1.24
36
+ - SciPy >= 1.10
37
+ - pandas >= 2.0
38
+ - matplotlib >= 3.7
@@ -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,74 @@
1
+ Metadata-Version: 2.1
2
+ Name: chronobox
3
+ Version: 0.1.0
4
+ Summary: Time series analysis library with ARIMA, state-space models, and automatic model selection
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: kalmanbox
17
+ Requires-Dist: matplotlib>=3.7
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: pandas>=2.0
20
+ Requires-Dist: scipy>=1.10
21
+ Provides-Extra: dev
22
+ Requires-Dist: bandit>=1.7; extra == 'dev'
23
+ Requires-Dist: hypothesis>=6.0; extra == 'dev'
24
+ Requires-Dist: interrogate>=1.5; extra == 'dev'
25
+ Requires-Dist: mkdocs-material>=9.0; extra == 'dev'
26
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'dev'
27
+ Requires-Dist: mutmut>=2.4; extra == 'dev'
28
+ Requires-Dist: pre-commit>=3.0; extra == 'dev'
29
+ Requires-Dist: pyright>=1.1; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Requires-Dist: radon>=6.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4; extra == 'dev'
34
+ Requires-Dist: safety>=2.3; extra == 'dev'
35
+ Requires-Dist: structlog>=23.0; extra == 'dev'
36
+ Provides-Extra: numba
37
+ Requires-Dist: numba>=0.57; extra == 'numba'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # chronobox
41
+
42
+ Time series analysis library with ARIMA, state-space models, and automatic model selection.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install -e ".[dev]"
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ from chronobox import ARIMA
54
+ from chronobox.datasets import load_dataset
55
+
56
+ airline = load_dataset('airline')
57
+ model = ARIMA(order=(0,1,1), seasonal_order=(0,1,1,12))
58
+ results = model.fit(airline['passengers'])
59
+ print(results.summary())
60
+ forecast = results.forecast(steps=12)
61
+ ```
62
+
63
+ ## Auto-ARIMA
64
+
65
+ ```python
66
+ from chronobox import auto_arima
67
+
68
+ best = auto_arima(airline['passengers'], seasonal=True, m=12)
69
+ print(best.summary())
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,35 @@
1
+ # chronobox
2
+
3
+ Time series analysis library with ARIMA, state-space models, and automatic model selection.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e ".[dev]"
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from chronobox import ARIMA
15
+ from chronobox.datasets import load_dataset
16
+
17
+ airline = load_dataset('airline')
18
+ model = ARIMA(order=(0,1,1), seasonal_order=(0,1,1,12))
19
+ results = model.fit(airline['passengers'])
20
+ print(results.summary())
21
+ forecast = results.forecast(steps=12)
22
+ ```
23
+
24
+ ## Auto-ARIMA
25
+
26
+ ```python
27
+ from chronobox import auto_arima
28
+
29
+ best = auto_arima(airline['passengers'], seasonal=True, m=12)
30
+ print(best.summary())
31
+ ```
32
+
33
+ ## License
34
+
35
+ MIT
@@ -0,0 +1,42 @@
1
+ """chronobox - Time series analysis library with ARIMA and automatic model selection."""
2
+
3
+ from chronobox.__version__ import __version__
4
+ from chronobox.analysis.counterfactual import Counterfactual
5
+ from chronobox.analysis.hd import HistoricalDecomposition
6
+ from chronobox.decomposition import STL, ClassicalDecomposition
7
+ from chronobox.models.arfima import ARFIMA
8
+ from chronobox.models.arima import ARIMA
9
+ from chronobox.models.bvar import BayesianVAR
10
+ from chronobox.models.ets import ETS
11
+ from chronobox.models.favar import FAVAR
12
+ from chronobox.models.gvar import GVAR
13
+ from chronobox.models.holtwinters import HoltWinters
14
+ from chronobox.models.svar import SVAR
15
+ from chronobox.models.theta import ThetaMethod
16
+ from chronobox.models.tvpvar import TVPVAR
17
+ from chronobox.models.var import VAR
18
+ from chronobox.models.vecm import VECM
19
+ from chronobox.selection.auto_arima import auto_arima
20
+ from chronobox.selection.auto_ets import auto_ets
21
+
22
+ __all__ = [
23
+ "ARFIMA",
24
+ "ARIMA",
25
+ "ETS",
26
+ "FAVAR",
27
+ "GVAR",
28
+ "STL",
29
+ "SVAR",
30
+ "TVPVAR",
31
+ "VAR",
32
+ "VECM",
33
+ "BayesianVAR",
34
+ "ClassicalDecomposition",
35
+ "Counterfactual",
36
+ "HistoricalDecomposition",
37
+ "HoltWinters",
38
+ "ThetaMethod",
39
+ "__version__",
40
+ "auto_arima",
41
+ "auto_ets",
42
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,16 @@
1
+ """Logging configuration for chronobox."""
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 chronobox namespace."""
10
+ logger = logging.getLogger(f"chronobox.{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
@@ -0,0 +1,25 @@
1
+ """Time series analysis tools: IRF, FEVD, Granger causality, HD, Counterfactual, Spillover."""
2
+
3
+ from chronobox.analysis.counterfactual import Counterfactual, CounterfactualResult
4
+ from chronobox.analysis.fevd import FEVD
5
+ from chronobox.analysis.granger import granger_causality
6
+ from chronobox.analysis.hd import HistoricalDecomposition, HistoricalDecompositionResult
7
+ from chronobox.analysis.irf import IRF
8
+ from chronobox.analysis.spillover import (
9
+ RollingSpilloverResult,
10
+ SpilloverIndex,
11
+ SpilloverResult,
12
+ )
13
+
14
+ __all__ = [
15
+ "FEVD",
16
+ "IRF",
17
+ "Counterfactual",
18
+ "CounterfactualResult",
19
+ "HistoricalDecomposition",
20
+ "HistoricalDecompositionResult",
21
+ "RollingSpilloverResult",
22
+ "SpilloverIndex",
23
+ "SpilloverResult",
24
+ "granger_causality",
25
+ ]
@@ -0,0 +1,260 @@
1
+ """
2
+ Counterfactual analysis based on historical decomposition.
3
+
4
+ Answers: "What would have happened if shock k had not occurred?"
5
+
6
+ References
7
+ ----------
8
+ - Kilian, L. & Lutkepohl, H. (2017). Structural Vector Autoregressive
9
+ Analysis. Cambridge University Press. Chapter 4.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Any
16
+
17
+ import numpy as np
18
+ from numpy.typing import NDArray
19
+
20
+
21
+ @dataclass
22
+ class CounterfactualResult:
23
+ """Results from counterfactual analysis.
24
+
25
+ Attributes
26
+ ----------
27
+ counterfactual : ndarray of shape (T, K_vars)
28
+ Counterfactual time series.
29
+ observed : ndarray of shape (T, K_vars)
30
+ Observed time series.
31
+ removed_contribution : ndarray of shape (T, K_vars)
32
+ The contribution that was removed.
33
+ shock_index : int | list[int]
34
+ Index(es) of the shock(s) removed.
35
+ scale : float
36
+ Scale factor applied (0 = full removal, 0.5 = half removal).
37
+ """
38
+
39
+ counterfactual: NDArray[np.floating[Any]]
40
+ observed: NDArray[np.floating[Any]]
41
+ removed_contribution: NDArray[np.floating[Any]]
42
+ shock_index: int | list[int]
43
+ scale: float
44
+
45
+
46
+ class Counterfactual:
47
+ """Counterfactual analysis based on historical decomposition.
48
+
49
+ Parameters
50
+ ----------
51
+ hd : HistoricalDecomposition or HistoricalDecompositionResult
52
+ Historical decomposition results.
53
+ """
54
+
55
+ def __init__(self, hd: Any) -> None:
56
+ # Accept either HistoricalDecomposition or HistoricalDecompositionResult
57
+ if hasattr(hd, "result"):
58
+ self._hd_result = hd.result
59
+ elif hasattr(hd, "decomposition") and hasattr(hd, "base"):
60
+ self._hd_result = hd
61
+ else:
62
+ msg = "hd must be a HistoricalDecomposition or HistoricalDecompositionResult"
63
+ raise TypeError(msg)
64
+
65
+ @property
66
+ def decomposition(self) -> NDArray[np.floating[Any]]:
67
+ """Decomposition array (T, K_shocks, K_vars)."""
68
+ return self._hd_result.decomposition
69
+
70
+ @property
71
+ def base(self) -> NDArray[np.floating[Any]]:
72
+ """Base forecast (T, K_vars)."""
73
+ return self._hd_result.base
74
+
75
+ @property
76
+ def observed(self) -> NDArray[np.floating[Any]]:
77
+ """Observed values (T, K_vars)."""
78
+ return self._hd_result.observed
79
+
80
+ def without_shock(
81
+ self, shock_index: int | list[int]
82
+ ) -> NDArray[np.floating[Any]]:
83
+ """Compute counterfactual without specified shock(s).
84
+
85
+ Y_cf = Y - HD_k (for single shock)
86
+ Y_cf = Y - sum(HD_k for k in shock_indices) (for multiple shocks)
87
+
88
+ Parameters
89
+ ----------
90
+ shock_index : int or list[int]
91
+ Index(es) of the structural shock(s) to remove.
92
+
93
+ Returns
94
+ -------
95
+ ndarray of shape (T, K_vars)
96
+ """
97
+ if isinstance(shock_index, int):
98
+ shock_index = [shock_index]
99
+
100
+ removed = np.zeros_like(self.observed)
101
+ for k in shock_index:
102
+ removed += self.decomposition[:, k, :]
103
+
104
+ return self.observed - removed
105
+
106
+ def with_modified_shock(
107
+ self, shock_index: int, scale: float = 0.5
108
+ ) -> NDArray[np.floating[Any]]:
109
+ """Compute counterfactual with modified (scaled) shock.
110
+
111
+ Y_cf = Y - HD_k * (1 - scale)
112
+
113
+ Parameters
114
+ ----------
115
+ shock_index : int
116
+ Index of the structural shock to modify.
117
+ scale : float
118
+ Scale factor. 0 = remove entirely, 1 = keep as-is, 0.5 = halve.
119
+
120
+ Returns
121
+ -------
122
+ ndarray of shape (T, K_vars)
123
+ """
124
+ contribution = self.decomposition[:, shock_index, :]
125
+ return self.observed - contribution * (1.0 - scale)
126
+
127
+ def without_all_shocks(self) -> NDArray[np.floating[Any]]:
128
+ """Remove all shocks, leaving only the base forecast.
129
+
130
+ Returns
131
+ -------
132
+ ndarray of shape (T, K_vars)
133
+ Should equal self.base.
134
+ """
135
+ n_shocks = self.decomposition.shape[1]
136
+ return self.without_shock(list(range(n_shocks)))
137
+
138
+ def compute(
139
+ self,
140
+ shock_index: int | list[int],
141
+ scale: float = 0.0,
142
+ ) -> CounterfactualResult:
143
+ """Compute counterfactual with full result object.
144
+
145
+ Parameters
146
+ ----------
147
+ shock_index : int or list[int]
148
+ Shock(s) to modify.
149
+ scale : float
150
+ Scale factor (0 = remove, 1 = keep).
151
+
152
+ Returns
153
+ -------
154
+ CounterfactualResult
155
+ """
156
+ if isinstance(shock_index, int):
157
+ cf = self.with_modified_shock(shock_index, scale)
158
+ removed = self.decomposition[:, shock_index, :] * (1.0 - scale)
159
+ else:
160
+ removed = np.zeros_like(self.observed)
161
+ for k in shock_index:
162
+ removed += self.decomposition[:, k, :] * (1.0 - scale)
163
+ cf = self.observed - removed
164
+
165
+ return CounterfactualResult(
166
+ counterfactual=cf,
167
+ observed=self.observed,
168
+ removed_contribution=removed,
169
+ shock_index=shock_index,
170
+ scale=scale,
171
+ )
172
+
173
+ def plot(
174
+ self,
175
+ variable: int,
176
+ shock_index: int | list[int],
177
+ scale: float = 0.0,
178
+ figsize: tuple[int, int] = (12, 6),
179
+ title: str | None = None,
180
+ variable_name: str | None = None,
181
+ shock_name: str | None = None,
182
+ ) -> Any:
183
+ """Plot counterfactual vs observed.
184
+
185
+ Parameters
186
+ ----------
187
+ variable : int
188
+ Variable index to plot.
189
+ shock_index : int or list[int]
190
+ Shock(s) to remove.
191
+ scale : float
192
+ Scale factor (0 = remove, 1 = keep).
193
+ figsize : tuple
194
+ Figure size.
195
+ title : str or None
196
+ Plot title.
197
+ variable_name : str or None
198
+ Name of the variable for the plot label.
199
+ shock_name : str or None
200
+ Name of the shock for the plot label.
201
+
202
+ Returns
203
+ -------
204
+ matplotlib Figure
205
+ """
206
+ import matplotlib.pyplot as plt
207
+
208
+ result = self.compute(shock_index, scale)
209
+
210
+ fig, ax = plt.subplots(figsize=figsize)
211
+
212
+ n_t = result.observed.shape[0]
213
+ t_axis = np.arange(n_t)
214
+
215
+ ax.plot(
216
+ t_axis,
217
+ result.observed[:, variable],
218
+ "b-",
219
+ linewidth=1.5,
220
+ label="Observed",
221
+ )
222
+ ax.plot(
223
+ t_axis,
224
+ result.counterfactual[:, variable],
225
+ "r--",
226
+ linewidth=1.5,
227
+ label="Counterfactual",
228
+ )
229
+
230
+ # Shade the difference
231
+ ax.fill_between(
232
+ t_axis,
233
+ result.observed[:, variable],
234
+ result.counterfactual[:, variable],
235
+ alpha=0.2,
236
+ color="gray",
237
+ label="Shock contribution",
238
+ )
239
+
240
+ if variable_name is None:
241
+ variable_name = f"Variable {variable}"
242
+ if shock_name is None:
243
+ shock_name = f"Shock {shock_index}"
244
+
245
+ if title is None:
246
+ if scale == 0:
247
+ title = (
248
+ f"Counterfactual: {variable_name} without {shock_name}"
249
+ )
250
+ else:
251
+ title = f"Counterfactual: {variable_name} with {shock_name} scaled by {scale:.1f}"
252
+
253
+ ax.set_title(title)
254
+ ax.set_xlabel("Time")
255
+ ax.set_ylabel("Value")
256
+ ax.legend()
257
+ ax.grid(True, alpha=0.3)
258
+
259
+ plt.tight_layout()
260
+ return fig