rustima 0.1.0__tar.gz → 0.2.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 (148) hide show
  1. {rustima-0.1.0 → rustima-0.2.0}/Cargo.lock +1 -1
  2. {rustima-0.1.0 → rustima-0.2.0}/Cargo.toml +4 -2
  3. {rustima-0.1.0 → rustima-0.2.0}/PKG-INFO +2 -1
  4. {rustima-0.1.0 → rustima-0.2.0}/benches/bench_fit.rs +2 -2
  5. {rustima-0.1.0 → rustima-0.2.0}/benches/bench_kalman.rs +5 -5
  6. rustima-0.2.0/lbfgsb_c/License.txt +60 -0
  7. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/lbfgsb.h +6 -0
  8. {rustima-0.1.0 → rustima-0.2.0}/pyproject.toml +10 -2
  9. {rustima-0.1.0 → rustima-0.2.0}/python/rustima/__init__.py +7 -2
  10. {rustima-0.1.0 → rustima-0.2.0}/python/rustima/model.py +225 -0
  11. {rustima-0.1.0 → rustima-0.2.0}/python_tests/conftest.py +6 -2
  12. {rustima-0.1.0 → rustima-0.2.0}/python_tests/generate_fixtures.py +33 -7
  13. {rustima-0.1.0 → rustima-0.2.0}/python_tests/generate_matrix_fixtures.py +5 -1
  14. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_exog.py +19 -16
  15. rustima-0.2.0/python_tests/test_extend.py +286 -0
  16. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_fit.py +11 -4
  17. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_inference.py +10 -10
  18. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_input_validation.py +6 -6
  19. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_multi_order_accuracy.py +1 -1
  20. rustima-0.2.0/python_tests/test_rolling.py +229 -0
  21. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_safety_guards.py +10 -3
  22. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_simple_diff.py +1 -1
  23. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_trend.py +10 -10
  24. rustima-0.2.0/python_tests/test_variance_parity.py +137 -0
  25. {rustima-0.1.0 → rustima-0.2.0}/src/batch.rs +5 -2
  26. {rustima-0.1.0 → rustima-0.2.0}/src/forecast.rs +207 -14
  27. {rustima-0.1.0 → rustima-0.2.0}/src/kalman.rs +70 -2
  28. {rustima-0.1.0 → rustima-0.2.0}/src/lib.rs +70 -0
  29. {rustima-0.1.0 → rustima-0.2.0}/src/optimizer.rs +26 -10
  30. {rustima-0.1.0 → rustima-0.2.0}/src/params.rs +5 -4
  31. {rustima-0.1.0 → rustima-0.2.0}/src/score.rs +70 -11
  32. {rustima-0.1.0 → rustima-0.2.0}/src/start_params.rs +4 -4
  33. {rustima-0.1.0 → rustima-0.2.0}/src/types.rs +6 -1
  34. {rustima-0.1.0 → rustima-0.2.0}/tests/fixtures/matrix_tier_a.json +60 -30
  35. {rustima-0.1.0 → rustima-0.2.0}/tests/fixtures/matrix_tier_b.json +138 -69
  36. rustima-0.2.0/tests/fixtures/statsmodels_forecast_reference.json +1081 -0
  37. {rustima-0.1.0 → rustima-0.2.0}/tests/fixtures/statsmodels_reference.json +10 -5
  38. rustima-0.1.0/.github/workflows/ci.yml +0 -64
  39. rustima-0.1.0/.github/workflows/nightly.yml +0 -83
  40. rustima-0.1.0/.github/workflows/release.yml +0 -242
  41. rustima-0.1.0/BENCHMARK_SPEC.md +0 -219
  42. rustima-0.1.0/benchmark_comprehensive.py +0 -152
  43. rustima-0.1.0/benchmark_higher_order.py +0 -178
  44. rustima-0.1.0/compare_all_orders.py +0 -240
  45. rustima-0.1.0/debug_loglike.py +0 -672
  46. rustima-0.1.0/debug_mismatch.py +0 -268
  47. rustima-0.1.0/docs/param_compare_2019.md +0 -105
  48. rustima-0.1.0/docs/ver5.2/spec_numerical_hessian.md +0 -257
  49. rustima-0.1.0/docs/ver5.2/spec_opg.md +0 -299
  50. rustima-0.1.0/docs/ver5.2/spec_simple_differencing.md +0 -551
  51. rustima-0.1.0/fit_summary_report.md +0 -335
  52. rustima-0.1.0/python_tests/bench_batch_forecast.py +0 -223
  53. rustima-0.1.0/python_tests/bench_comparison.py +0 -201
  54. rustima-0.1.0/python_tests/bench_full_comparison.py +0 -377
  55. rustima-0.1.0/python_tests/bench_grid_5x5.py +0 -433
  56. rustima-0.1.0/python_tests/bench_matrix_6x6_s7_s12_s24.py +0 -206
  57. rustima-0.1.0/python_tests/bench_matrix_parent.py +0 -190
  58. rustima-0.1.0/python_tests/bench_matrix_worker.py +0 -119
  59. rustima-0.1.0/python_tests/bench_memory_speed.py +0 -342
  60. rustima-0.1.0/python_tests/bench_pmdarima_compare.py +0 -165
  61. rustima-0.1.0/python_tests/bench_power_2019_2023.py +0 -220
  62. rustima-0.1.0/python_tests/bench_readme.py +0 -338
  63. rustima-0.1.0/python_tests/bench_report_arima.py +0 -278
  64. rustima-0.1.0/python_tests/bench_report_arimax.py +0 -270
  65. rustima-0.1.0/python_tests/bench_report_sarima.py +0 -317
  66. rustima-0.1.0/python_tests/bench_report_sarimax.py +0 -317
  67. rustima-0.1.0/python_tests/bench_s24_highorder.py +0 -259
  68. rustima-0.1.0/python_tests/bench_sarima_plot.py +0 -156
  69. rustima-0.1.0/python_tests/bench_sarima_retry_pmdarima_1y.py +0 -127
  70. rustima-0.1.0/python_tests/bench_sarima_scaling.py +0 -251
  71. rustima-0.1.0/python_tests/bench_sarima_worker.py +0 -136
  72. rustima-0.1.0/python_tests/bench_v5_report.py +0 -604
  73. rustima-0.1.0/python_tests/benchmark_vs_statsmodels.py +0 -433
  74. rustima-0.1.0/python_tests/build_ppt_pmdarima.py +0 -234
  75. rustima-0.1.0/python_tests/compare_profile_methods_2019.py +0 -136
  76. rustima-0.1.0/python_tests/compare_profile_methods_R.R +0 -54
  77. rustima-0.1.0/python_tests/compare_with_r.R +0 -73
  78. rustima-0.1.0/python_tests/diagnose_profile_tr_plateau.py +0 -93
  79. rustima-0.1.0/python_tests/gen_tex_report.py +0 -499
  80. rustima-0.1.0/python_tests/phase_c_power_auto.py +0 -139
  81. rustima-0.1.0/python_tests/phase_c_robustness.py +0 -150
  82. rustima-0.1.0/python_tests/report_convergence_failures.py +0 -301
  83. rustima-0.1.0/python_tests/run_fit_summary.py +0 -331
  84. rustima-0.1.0/python_tests/show_matrix_coefs.py +0 -78
  85. rustima-0.1.0/python_tests/verify_ll_at_same_params.py +0 -91
  86. rustima-0.1.0/python_tests/verify_sarima_vs_sarimax.py +0 -86
  87. rustima-0.1.0/result_v1/00_SUMMARY.md +0 -105
  88. rustima-0.1.0/result_v1/01_ARIMA_comparison.md +0 -51
  89. rustima-0.1.0/result_v1/02_SARIMA_s12_comparison.md +0 -192
  90. rustima-0.1.0/result_v1/02_SARIMA_s24_comparison.md +0 -192
  91. rustima-0.1.0/result_v1/02_SARIMA_s7_comparison.md +0 -192
  92. rustima-0.1.0/result_v1/03_ARIMAX_comparison.md +0 -50
  93. rustima-0.1.0/result_v1/04_SARIMAX_s12_comparison.md +0 -192
  94. rustima-0.1.0/result_v1/04_SARIMAX_s24_comparison.md +0 -192
  95. rustima-0.1.0/result_v1/04_SARIMAX_s7_comparison.md +0 -192
  96. rustima-0.1.0/result_v1/05_auto_arima_benchmark.md +0 -129
  97. rustima-0.1.0/result_v1/bench_all_models.py +0 -397
  98. rustima-0.1.0/result_v1/bench_auto_arima.py +0 -435
  99. rustima-0.1.0/test.ipynb +0 -2108
  100. rustima-0.1.0/tests/fixtures/statsmodels_forecast_reference.json +0 -1077
  101. {rustima-0.1.0 → rustima-0.2.0}/.env.example +0 -0
  102. {rustima-0.1.0 → rustima-0.2.0}/.gitignore +0 -0
  103. {rustima-0.1.0 → rustima-0.2.0}/LICENSE +0 -0
  104. {rustima-0.1.0 → rustima-0.2.0}/README.md +0 -0
  105. {rustima-0.1.0 → rustima-0.2.0}/build.rs +0 -0
  106. {rustima-0.1.0 → rustima-0.2.0}/docs/PERF_DIAGNOSIS.md +0 -0
  107. {rustima-0.1.0 → rustima-0.2.0}/docs/SUPPORTED_COMBINATIONS.md +0 -0
  108. {rustima-0.1.0 → rustima-0.2.0}/docs/api_reference.md +0 -0
  109. {rustima-0.1.0 → rustima-0.2.0}/docs/error_codes.md +0 -0
  110. {rustima-0.1.0 → rustima-0.2.0}/docs/migration_guide.md +0 -0
  111. {rustima-0.1.0 → rustima-0.2.0}/docs/profiled_kalman_gls_plan.md +0 -0
  112. {rustima-0.1.0 → rustima-0.2.0}/docs/statsmodels_compat.md +0 -0
  113. {rustima-0.1.0 → rustima-0.2.0}/docs/ver5_convergence_improvement.md +0 -0
  114. {rustima-0.1.0 → rustima-0.2.0}/examples/bench_comprehensive.py +0 -0
  115. {rustima-0.1.0 → rustima-0.2.0}/examples/bench_vs_statsmodels.py +0 -0
  116. {rustima-0.1.0 → rustima-0.2.0}/examples/high_order_bench.rs +0 -0
  117. {rustima-0.1.0 → rustima-0.2.0}/examples/sarimax_exog_demo.py +0 -0
  118. {rustima-0.1.0 → rustima-0.2.0}/examples/sarimax_hourly_s24.py +0 -0
  119. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/lbfgsb.c +0 -0
  120. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/linesearch.c +0 -0
  121. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/linpack.c +0 -0
  122. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/miniCBLAS.c +0 -0
  123. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/print.c +0 -0
  124. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/subalgorithms.c +0 -0
  125. {rustima-0.1.0 → rustima-0.2.0}/lbfgsb_c/timer.c +0 -0
  126. {rustima-0.1.0 → rustima-0.2.0}/python/rustima/auto.py +0 -0
  127. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_auto.py +0 -0
  128. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_batch.py +0 -0
  129. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_forecast.py +0 -0
  130. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_high_order_accuracy.py +0 -0
  131. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_matrix_tier_a.py +0 -0
  132. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_polars.py +0 -0
  133. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_prediction_quality.py +0 -0
  134. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_profile_trust_region.py +0 -0
  135. {rustima-0.1.0 → rustima-0.2.0}/python_tests/test_smoke.py +0 -0
  136. {rustima-0.1.0 → rustima-0.2.0}/src/css.rs +0 -0
  137. {rustima-0.1.0 → rustima-0.2.0}/src/error.rs +0 -0
  138. {rustima-0.1.0 → rustima-0.2.0}/src/inference.rs +0 -0
  139. {rustima-0.1.0 → rustima-0.2.0}/src/initialization.rs +0 -0
  140. {rustima-0.1.0 → rustima-0.2.0}/src/lbfgsb_ffi.rs +0 -0
  141. {rustima-0.1.0 → rustima-0.2.0}/src/lbfgsb_wrapper.rs +0 -0
  142. {rustima-0.1.0 → rustima-0.2.0}/src/pipeline.rs +0 -0
  143. {rustima-0.1.0 → rustima-0.2.0}/src/polynomial.rs +0 -0
  144. {rustima-0.1.0 → rustima-0.2.0}/src/state_space.rs +0 -0
  145. {rustima-0.1.0 → rustima-0.2.0}/src/test_helpers.rs +0 -0
  146. {rustima-0.1.0 → rustima-0.2.0}/tests/fixtures/.gitkeep +0 -0
  147. {rustima-0.1.0 → rustima-0.2.0}/tests/fixtures/statsmodels_fit_reference.json +0 -0
  148. {rustima-0.1.0 → rustima-0.2.0}/uv.lock +0 -0
@@ -880,7 +880,7 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
880
880
 
881
881
  [[package]]
882
882
  name = "rustima"
883
- version = "0.1.0"
883
+ version = "0.2.0"
884
884
  dependencies = [
885
885
  "anyhow",
886
886
  "approx",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rustima"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  rust-version = "1.83"
6
6
  description = "High-performance SARIMAX engine with PyO3 bindings"
@@ -27,7 +27,9 @@ statrs = "0.18"
27
27
  rayon = "1.10"
28
28
 
29
29
  # Python bindings
30
- pyo3 = { version = "0.28", features = ["auto-initialize"] }
30
+ # auto-initialize 제거(2026-07-11): 크레이트 Python::with_gil 사용처 0 임베딩 불필요.
31
+ # 켜두면 manylinux(정적 python) 휠 빌드가 실패해 PyPI 릴리스를 막는다.
32
+ pyo3 = { version = "0.28" }
31
33
  numpy = "0.28"
32
34
 
33
35
  # Error handling
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rustima
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Intended Audience :: Developers
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
16
17
  Classifier: Programming Language :: Rust
17
18
  Classifier: Topic :: Scientific/Engineering
18
19
  Classifier: Topic :: Scientific/Engineering :: Mathematics
@@ -1,6 +1,6 @@
1
1
  use criterion::{criterion_group, criterion_main, Criterion};
2
- use sarimax_rs::optimizer;
3
- use sarimax_rs::types::{SarimaxConfig, SarimaxOrder, Trend};
2
+ use rustima::optimizer;
3
+ use rustima::types::{SarimaxConfig, SarimaxOrder, Trend};
4
4
 
5
5
  /// Deterministic LCG data generator (fixed seed → reproducible data).
6
6
  fn generate_ar1_data(n: usize, phi: f64, seed: u64) -> Vec<f64> {
@@ -1,9 +1,9 @@
1
1
  use criterion::{criterion_group, criterion_main, Criterion};
2
- use sarimax_rs::initialization::KalmanInit;
3
- use sarimax_rs::kalman::kalman_loglike;
4
- use sarimax_rs::params::SarimaxParams;
5
- use sarimax_rs::state_space::StateSpace;
6
- use sarimax_rs::types::{SarimaxConfig, SarimaxOrder, Trend};
2
+ use rustima::initialization::KalmanInit;
3
+ use rustima::kalman::kalman_loglike;
4
+ use rustima::params::SarimaxParams;
5
+ use rustima::state_space::StateSpace;
6
+ use rustima::types::{SarimaxConfig, SarimaxOrder, Trend};
7
7
 
8
8
  /// Deterministic LCG data generator (fixed seed → reproducible data).
9
9
  fn generate_ar1_data(n: usize, phi: f64, seed: u64) -> Vec<f64> {
@@ -0,0 +1,60 @@
1
+ L-BFGS-B License
2
+ ================
3
+
4
+ This directory contains a C translation of L-BFGS-B (version 3.0):
5
+
6
+ - Original FORTRAN L-BFGS-B: Ciyou Zhu, Richard Byrd, Jorge Nocedal,
7
+ Jose Luis Morales (Northwestern University).
8
+ - C translation (L-BFGS-B-C): Stephen Becker, 2015
9
+ (stephen.becker@colorado.edu), https://github.com/stephenbeckr/L-BFGS-B-C
10
+
11
+ L-BFGS-B is released under the "New BSD License" (aka "Modified BSD License"
12
+ or "3-clause license").
13
+
14
+ New BSD License
15
+ ---------------
16
+
17
+ Redistribution and use in source and binary forms, with or without
18
+ modification, are permitted provided that the following conditions are met:
19
+
20
+ 1. Redistributions of source code must retain the above copyright notice,
21
+ this list of conditions and the following disclaimer.
22
+
23
+ 2. Redistributions in binary form must reproduce the above copyright notice,
24
+ this list of conditions and the following disclaimer in the documentation
25
+ and/or other materials provided with the distribution.
26
+
27
+ 3. Neither the name of the copyright holder nor the names of its contributors
28
+ may be used to endorse or promote products derived from this software
29
+ without specific prior written permission.
30
+
31
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
32
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
33
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
35
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
36
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
37
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
38
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
39
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
41
+ POSSIBILITY OF SUCH DAMAGE.
42
+
43
+ Citation request (from the original authors)
44
+ --------------------------------------------
45
+
46
+ The authors of L-BFGS-B expect that all publications describing work using
47
+ this software, or all commercial products using it, quote at least one of
48
+ the references below:
49
+
50
+ R. H. Byrd, P. Lu and J. Nocedal. A Limited Memory Algorithm for Bound
51
+ Constrained Optimization (1995), SIAM Journal on Scientific and
52
+ Statistical Computing, 16, 5, pp. 1190-1208.
53
+
54
+ C. Zhu, R. H. Byrd and J. Nocedal. Algorithm 778: L-BFGS-B, FORTRAN
55
+ routines for large scale bound constrained optimization (1997), ACM
56
+ Transactions on Mathematical Software, 23, 4, pp. 550-560.
57
+
58
+ J.L. Morales and J. Nocedal. Remark on "Algorithm 778: L-BFGS-B, FORTRAN
59
+ routines for large scale bound constrained optimization" (2011), ACM
60
+ Transactions on Mathematical Software, 38, 1.
@@ -5,6 +5,12 @@
5
5
  #ifndef lbfgsb_h
6
6
  #define lbfgsb_h
7
7
 
8
+ /* MSVC's C mode has no C11 _Thread_local keyword — map to its equivalent
9
+ * so Windows wheel builds compile. gcc/clang paths are unaffected. */
10
+ #if defined(_MSC_VER) && !defined(__clang__)
11
+ #define _Thread_local __declspec(thread)
12
+ #endif
13
+
8
14
 
9
15
 
10
16
  /* Force 64-bit integers on all platforms (including Windows LLP64
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rustima"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "High-performance SARIMAX (Seasonal ARIMA with eXogenous regressors) engine in Rust with statsmodels-compatible Python API"
10
10
  readme = "README.md"
@@ -38,6 +38,7 @@ classifiers = [
38
38
  "Programming Language :: Python :: 3.11",
39
39
  "Programming Language :: Python :: 3.12",
40
40
  "Programming Language :: Python :: 3.13",
41
+ "Programming Language :: Python :: 3.14",
41
42
  "Programming Language :: Rust",
42
43
  "Topic :: Scientific/Engineering",
43
44
  "Topic :: Scientific/Engineering :: Mathematics",
@@ -72,7 +73,14 @@ notebook = [
72
73
  features = ["pyo3/extension-module"]
73
74
  module-name = "rustima"
74
75
  python-source = "python"
75
- include = ["LICENSE", "README.md"]
76
+ include = ["LICENSE", "README.md", "lbfgsb_c/License.txt"]
77
+ # sdist 위생: 벤치마크·논문 자산은 배포에서 제외 (개발용은 저장소에 유지)
78
+ exclude = [
79
+ "benchmarks/**",
80
+ "docs/ver5.2/**",
81
+ "*.ipynb",
82
+ "*.csv",
83
+ ]
76
84
 
77
85
  [tool.pytest.ini_options]
78
86
  testpaths = ["python_tests"]
@@ -30,16 +30,21 @@ if "RAYON_NUM_THREADS" not in _os.environ:
30
30
  from .rustima import * # noqa: F401,F403
31
31
 
32
32
  # High-level Python API
33
- from .model import SARIMAXModel, SARIMAXResult, ForecastResult, PredictionResult
33
+ from .model import (
34
+ SARIMAXModel, SARIMAXResult, ForecastResult, PredictionResult,
35
+ RollingForecastResult,
36
+ )
34
37
  from .auto import auto_arima, AutoARIMAResult
35
38
 
36
39
  __all__ = [
37
40
  # Rust low-level functions
38
41
  "version",
39
- "sarimax_fit", "sarimax_forecast", "sarimax_loglike", "sarimax_residuals",
42
+ "sarimax_fit", "sarimax_forecast", "sarimax_rolling_forecast",
43
+ "sarimax_loglike", "sarimax_residuals",
40
44
  "sarimax_batch_fit", "sarimax_batch_forecast", "sarimax_batch_loglike",
41
45
  "sarimax_grid_search", "sarimax_inference", "sarimax_diagnostics",
42
46
  # High-level Python API
43
47
  "SARIMAXModel", "SARIMAXResult", "ForecastResult", "PredictionResult",
48
+ "RollingForecastResult",
44
49
  "auto_arima", "AutoARIMAResult",
45
50
  ]
@@ -636,6 +636,67 @@ class SARIMAXModel:
636
636
  self._fit_result = SARIMAXResult(self, result_dict)
637
637
  return self._fit_result
638
638
 
639
+ def filter(self, params):
640
+ """Construct a result at the given parameters WITHOUT fitting.
641
+
642
+ Runs a single Kalman-filter pass to evaluate the log-likelihood at
643
+ ``params`` and returns a :class:`SARIMAXResult` bound to this model.
644
+ No optimization is performed (statsmodels ``.filter(params)``
645
+ semantics). This is the building block for walk-forward rolling
646
+ (:meth:`SARIMAXResult.extend`) and for reconstructing a result from
647
+ serialized parameters without re-estimation.
648
+
649
+ Parameters
650
+ ----------
651
+ params : array_like
652
+ Full parameter vector in statsmodels layout
653
+ ``[trend | exog | ar | ma | sar | sma | sigma2]``.
654
+
655
+ Returns
656
+ -------
657
+ SARIMAXResult
658
+ ``result.method == "filter"``, ``converged == True``.
659
+ """
660
+ params = np.ascontiguousarray(np.asarray(params, dtype=np.float64).ravel())
661
+ expected_names = _generate_param_names(
662
+ self.order, self.seasonal_order, self.n_exog, trend=self.trend
663
+ )
664
+ if len(params) != len(expected_names):
665
+ raise ValueError(
666
+ f"params length {len(params)} != expected {len(expected_names)} "
667
+ f"for this specification: {expected_names}"
668
+ )
669
+
670
+ llf = float(
671
+ rustima.sarimax_loglike(
672
+ self.endog,
673
+ self.order,
674
+ self.seasonal_order,
675
+ params,
676
+ **self._model_kwargs(),
677
+ )
678
+ )
679
+
680
+ k = len(params)
681
+ _p, d, _q = self.order
682
+ _P, D, _Q, s = self.seasonal_order
683
+ n_eff = self.nobs - ((d + s * D) if self.simple_differencing else 0)
684
+ sigma2_idx = expected_names.index("sigma2")
685
+
686
+ result_dict = {
687
+ "params": params,
688
+ "loglike": llf,
689
+ "scale": float(params[sigma2_idx]),
690
+ "aic": -2.0 * llf + 2.0 * k,
691
+ "bic": -2.0 * llf + k * np.log(n_eff),
692
+ "n_obs": n_eff,
693
+ "converged": True,
694
+ "method": "filter",
695
+ "n_iter": 0,
696
+ "n_params": k,
697
+ }
698
+ return SARIMAXResult(self, result_dict)
699
+
639
700
 
640
701
  class SARIMAXResult:
641
702
  """Fit result wrapper (statsmodels ResultsWrapper compatible).
@@ -948,6 +1009,134 @@ class SARIMAXResult:
948
1009
  """Alias for forecast() (statsmodels compatibility)."""
949
1010
  return self.forecast(steps=steps, alpha=alpha, exog=exog)
950
1011
 
1012
+ def rolling_forecast(self, start, step=1, horizon=1, alpha=0.05):
1013
+ """Single-pass rolling-origin h-step forecasts (fixed parameters).
1014
+
1015
+ One Kalman-filter pass over the full sample captures the predicted
1016
+ state at every origin ``start, start+step, ...``; each origin's
1017
+ h-step forecast is propagated from its snapshot. Total cost
1018
+ O(T + N·horizon) — versus O(N·T) for an :meth:`extend` chain — while
1019
+ producing numerically identical forecasts (Markov property).
1020
+
1021
+ Origins run while ``origin <= nobs - 1``; models with exog are
1022
+ capped at ``nobs - horizon`` (in-sample exog must cover each
1023
+ forecast window). ``simple_differencing=True`` is not supported yet.
1024
+
1025
+ Parameters
1026
+ ----------
1027
+ start : int
1028
+ First forecast origin (number of observations consumed).
1029
+ step : int
1030
+ Origin spacing (e.g. 24 for daily rolling on hourly data).
1031
+ horizon : int
1032
+ Forecast steps per origin.
1033
+ alpha : float
1034
+ CI significance level.
1035
+
1036
+ Returns
1037
+ -------
1038
+ RollingForecastResult
1039
+ ``origins`` (N,), ``predicted_mean``/``variance``/``ci_lower``/
1040
+ ``ci_upper`` (N, horizon).
1041
+ """
1042
+ result = rustima.sarimax_rolling_forecast(
1043
+ self.model.endog,
1044
+ self.model.order,
1045
+ self.model.seasonal_order,
1046
+ self.params,
1047
+ start=start,
1048
+ step=step,
1049
+ horizon=horizon,
1050
+ alpha=alpha,
1051
+ **self._rs_kwargs(),
1052
+ )
1053
+ return RollingForecastResult(result, alpha=alpha)
1054
+
1055
+ def extend(self, endog, exog=None):
1056
+ """Extend the sample with new observations, keeping parameters fixed.
1057
+
1058
+ Returns a new :class:`SARIMAXResult` whose sample is the original
1059
+ history plus ``endog``, filtered at the SAME parameters — no
1060
+ re-estimation. Subsequent :meth:`forecast` calls start after the new
1061
+ observations, enabling walk-forward rolling::
1062
+
1063
+ res = SARIMAXModel(train, order, seasonal_order).fit()
1064
+ for block in blocks:
1065
+ fc = res.get_forecast(steps=len(block)).predicted_mean
1066
+ res = res.extend(block)
1067
+
1068
+ Implementation note
1069
+ -------------------
1070
+ rustima refilters the FULL extended history with the fixed parameters
1071
+ (statsmodels ``append(refit=False)`` semantics). Because the Kalman
1072
+ filter is Markovian, post-extension forecasts are numerically
1073
+ equivalent to statsmodels' state-carry-over ``extend``. Unlike
1074
+ statsmodels ``extend``, ``llf``/``aic``/``bic`` here cover the full
1075
+ extended sample rather than only the new observations.
1076
+
1077
+ Parameters
1078
+ ----------
1079
+ endog : array_like
1080
+ New observations that come AFTER the current sample.
1081
+ exog : array_like, optional
1082
+ Exogenous values for the new observations, shape
1083
+ ``(len(endog), n_exog)``. Required iff the model has exog.
1084
+
1085
+ Returns
1086
+ -------
1087
+ SARIMAXResult
1088
+ """
1089
+ new = np.asarray(endog, dtype=np.float64).ravel()
1090
+ if new.size == 0:
1091
+ raise ValueError("endog is empty: extend() requires at least one new observation")
1092
+ if not np.isfinite(new).all():
1093
+ raise ValueError("endog contains NaN or Inf values")
1094
+
1095
+ m = self.model
1096
+ if m.exog is not None:
1097
+ if exog is None:
1098
+ raise ValueError(
1099
+ "model was built with exog; extend() requires exog for the new observations"
1100
+ )
1101
+ ex = np.asarray(exog, dtype=np.float64)
1102
+ if ex.ndim == 1:
1103
+ ex = ex.reshape(-1, 1)
1104
+ if ex.shape != (new.size, m.n_exog):
1105
+ raise ValueError(
1106
+ f"exog shape {ex.shape} != expected ({new.size}, {m.n_exog})"
1107
+ )
1108
+ full_exog = np.vstack([m.exog, ex])
1109
+ else:
1110
+ if exog is not None:
1111
+ raise ValueError(
1112
+ "model was built without exog; unexpected exog passed to extend()"
1113
+ )
1114
+ full_exog = None
1115
+
1116
+ new_model = SARIMAXModel(
1117
+ np.concatenate([m.endog, new]),
1118
+ order=m.order,
1119
+ seasonal_order=m.seasonal_order,
1120
+ exog=full_exog,
1121
+ trend=m.trend,
1122
+ enforce_stationarity=m.enforce_stationarity,
1123
+ enforce_invertibility=m.enforce_invertibility,
1124
+ simple_differencing=m.simple_differencing,
1125
+ )
1126
+ return new_model.filter(self.params)
1127
+
1128
+ def append(self, endog, exog=None, refit=False, **fit_kwargs):
1129
+ """Append new observations (statsmodels-compatible convenience).
1130
+
1131
+ ``refit=False`` (default) is an alias for :meth:`extend` — parameters
1132
+ stay fixed. ``refit=True`` re-estimates parameters on the extended
1133
+ sample via :meth:`SARIMAXModel.fit`.
1134
+ """
1135
+ extended = self.extend(endog, exog=exog)
1136
+ if refit:
1137
+ return extended.model.fit(**fit_kwargs)
1138
+ return extended
1139
+
951
1140
  @property
952
1141
  def resid(self):
953
1142
  """Standardized residuals."""
@@ -1249,6 +1438,42 @@ class ForecastResult:
1249
1438
  })
1250
1439
 
1251
1440
 
1441
+ class RollingForecastResult:
1442
+ """Rolling-origin forecast result (one row per origin).
1443
+
1444
+ Attributes
1445
+ ----------
1446
+ origins : np.ndarray, shape (N,)
1447
+ Forecast origins (observations consumed before each forecast).
1448
+ predicted_mean : np.ndarray, shape (N, horizon)
1449
+ variance : np.ndarray, shape (N, horizon)
1450
+ ci_lower : np.ndarray, shape (N, horizon)
1451
+ ci_upper : np.ndarray, shape (N, horizon)
1452
+ """
1453
+
1454
+ def __init__(self, result_dict, alpha=0.05):
1455
+ self.origins = np.asarray(result_dict["origins"], dtype=np.int64)
1456
+ self.predicted_mean = np.asarray(result_dict["mean"], dtype=np.float64)
1457
+ self.variance = np.asarray(result_dict["variance"], dtype=np.float64)
1458
+ self.ci_lower = np.asarray(result_dict["ci_lower"], dtype=np.float64)
1459
+ self.ci_upper = np.asarray(result_dict["ci_upper"], dtype=np.float64)
1460
+ self._alpha = alpha
1461
+
1462
+ def to_dataframe(self):
1463
+ """Long-format Polars DataFrame: origin, step, mean, variance, ci."""
1464
+ import polars as pl
1465
+
1466
+ n_origins, horizon = self.predicted_mean.shape
1467
+ return pl.DataFrame({
1468
+ "origin": np.repeat(self.origins, horizon),
1469
+ "step": np.tile(np.arange(1, horizon + 1), n_origins),
1470
+ "mean": self.predicted_mean.ravel(),
1471
+ "variance": self.variance.ravel(),
1472
+ "ci_lower": self.ci_lower.ravel(),
1473
+ "ci_upper": self.ci_upper.ravel(),
1474
+ })
1475
+
1476
+
1252
1477
  class PredictionResult:
1253
1478
  """In-sample (and optionally out-of-sample) prediction result.
1254
1479
 
@@ -58,10 +58,14 @@ def converged_models(models):
58
58
 
59
59
 
60
60
  def expected_k_params(order, seasonal, n_exog=0):
61
- """Expected number of estimated params (concentrated scale)."""
61
+ """Expected number of estimated params.
62
+
63
+ Non-concentrated layout (current default): [exog|ar|ma|sar|sma|sigma2],
64
+ so sigma2 adds +1.
65
+ """
62
66
  p, _d, q = order
63
67
  P, _D, Q, _s = seasonal
64
- return p + q + P + Q + n_exog
68
+ return p + q + P + Q + n_exog + 1
65
69
 
66
70
 
67
71
  # ---------------------------------------------------------------------------
@@ -83,8 +83,14 @@ def fit_and_extract(y, order, seasonal_order=(0, 0, 0, 0)):
83
83
  # Also compute loglike using model.loglike() to double-check
84
84
  loglike_check = model.loglike(res.params)
85
85
 
86
+ # The fit above concentrates the scale out (concentrate_scale=True), so
87
+ # res.params omits sigma2. The current rustima engine / SARIMAXModel use
88
+ # the full non-concentrated layout [trend|exog|ar|ma|sar|sma|sigma2], so we
89
+ # append sigma2 (= res.scale, always the LAST parameter for trend='n').
90
+ params = res.params.tolist() + [float(res.scale)]
91
+
86
92
  return {
87
- "params": res.params.tolist(),
93
+ "params": params,
88
94
  "loglike": float(loglike),
89
95
  "loglike_check": float(loglike_check),
90
96
  "scale": float(res.scale),
@@ -200,17 +206,37 @@ def main():
200
206
  concentrate_scale=True,
201
207
  )
202
208
  res = model.fit(disp=False)
203
- fcast = res.get_forecast(steps=10)
209
+
210
+ # The fit concentrates the scale out for speed, so res.params omits
211
+ # sigma2. rustima (and statsmodels' default) use the full
212
+ # NON-concentrated layout [ar|ma|sar|sma|sigma2]. To make the fixture
213
+ # convention unambiguous, refilter a non-concentrated model at the
214
+ # full parameter vector and harvest the user-facing outputs from it —
215
+ # no post-hoc scale adjustments.
216
+ sigma2 = float(res.scale)
217
+ full_params = np.asarray(res.params.tolist() + [sigma2])
218
+ model_nc = sm.tsa.SARIMAX(
219
+ data,
220
+ order=order,
221
+ seasonal_order=seasonal_order,
222
+ trend="n",
223
+ enforce_stationarity=False,
224
+ enforce_invertibility=False,
225
+ concentrate_scale=False,
226
+ )
227
+ res_nc = model_nc.filter(full_params)
228
+ fcast = res_nc.get_forecast(steps=10)
204
229
  ci = fcast.conf_int(alpha=0.05)
230
+ resid = res_nc.filter_results.standardized_forecasts_error[0]
205
231
 
206
- # Standardized residuals
207
- resid = res.filter_results.standardized_forecasts_error[0]
232
+ ci_lo = ci[:, 0] if isinstance(ci, np.ndarray) else ci.iloc[:, 0].to_numpy()
233
+ ci_hi = ci[:, 1] if isinstance(ci, np.ndarray) else ci.iloc[:, 1].to_numpy()
208
234
 
209
235
  forecast_fixtures[name] = {
210
- "params": res.params.tolist(),
236
+ "params": full_params.tolist(),
211
237
  "forecast_mean": fcast.predicted_mean.tolist(),
212
- "forecast_ci_lower": ci[:, 0].tolist() if isinstance(ci, np.ndarray) else ci.iloc[:, 0].tolist(),
213
- "forecast_ci_upper": ci[:, 1].tolist() if isinstance(ci, np.ndarray) else ci.iloc[:, 1].tolist(),
238
+ "forecast_ci_lower": ci_lo.tolist(),
239
+ "forecast_ci_upper": ci_hi.tolist(),
214
240
  "standardized_residuals": resid.tolist(),
215
241
  }
216
242
 
@@ -244,8 +244,12 @@ def fit_oracle(y, order, seasonal, exog=None, future_exog=None):
244
244
  else:
245
245
  forecast_var = list(forecast_var)
246
246
 
247
+ # Fit concentrates the scale out (concentrate_scale=True), so
248
+ # res.params omits sigma2. The current rustima engine uses the full
249
+ # non-concentrated layout [exog|ar|ma|sar|sma|sigma2], so append
250
+ # sigma2 (= res.scale) as the trailing parameter.
247
251
  return {
248
- "params": res.params.tolist(),
252
+ "params": res.params.tolist() + [float(res.scale)],
249
253
  "loglike": float(res.llf),
250
254
  "aic": float(res.aic),
251
255
  "bic": float(res.bic),
@@ -51,8 +51,8 @@ class TestExogLoglike:
51
51
  def test_loglike_with_single_exog(self):
52
52
  """loglike with 1 exog variable returns finite value."""
53
53
  y, exog = _generate_arx_data()
54
- # params: [exog_beta(1), ar(1)]
55
- params = np.array([2.0, 0.5], dtype=np.float64)
54
+ # params: [exog_beta(1), ar(1), sigma2]
55
+ params = np.array([2.0, 0.5, 0.25], dtype=np.float64)
56
56
  ll = rustima.sarimax_loglike(
57
57
  y, (1, 0, 0), (0, 0, 0, 0), params, exog=exog
58
58
  )
@@ -61,8 +61,8 @@ class TestExogLoglike:
61
61
  def test_loglike_with_multi_exog(self):
62
62
  """loglike with multiple exog variables returns finite value."""
63
63
  y, X, _ = _generate_multi_exog_data(n_exog=3)
64
- # params: [exog(3), ar(1)]
65
- params = np.array([1.5, -0.8, 0.3, 0.5], dtype=np.float64)
64
+ # params: [exog(3), ar(1), sigma2]
65
+ params = np.array([1.5, -0.8, 0.3, 0.5, 0.25], dtype=np.float64)
66
66
  ll = rustima.sarimax_loglike(
67
67
  y, (1, 0, 0), (0, 0, 0, 0), params, exog=X
68
68
  )
@@ -71,7 +71,7 @@ class TestExogLoglike:
71
71
  def test_loglike_without_exog_unchanged(self):
72
72
  """loglike without exog is unchanged (backward compatible)."""
73
73
  y, _ = _generate_arx_data()
74
- params = np.array([0.5], dtype=np.float64)
74
+ params = np.array([0.5, 1.0], dtype=np.float64)
75
75
  ll = rustima.sarimax_loglike(
76
76
  y, (1, 0, 0), (0, 0, 0, 0), params
77
77
  )
@@ -80,13 +80,15 @@ class TestExogLoglike:
80
80
  def test_loglike_exog_improves_fit(self):
81
81
  """With true exog, loglike should be better than without."""
82
82
  y, exog = _generate_arx_data(beta=2.0)
83
- # With exog (true model)
84
- params_with = np.array([2.0, 0.5], dtype=np.float64)
83
+ # With exog; sigma2 ~ innovation variance of the SARIMAX
84
+ # regression-with-AR(1)-errors form (~1.25 for this ARX process)
85
+ params_with = np.array([2.0, 0.5, 1.25], dtype=np.float64)
85
86
  ll_with = rustima.sarimax_loglike(
86
87
  y, (1, 0, 0), (0, 0, 0, 0), params_with, exog=exog
87
88
  )
88
- # Without exog (misspecified)
89
- params_without = np.array([0.5], dtype=np.float64)
89
+ # Without exog (misspecified); sigma2 ~ residual variance of the
90
+ # misspecified model (var(beta*x + eps) = 4 + 0.25)
91
+ params_without = np.array([0.5, 4.25], dtype=np.float64)
90
92
  ll_without = rustima.sarimax_loglike(
91
93
  y, (1, 0, 0), (0, 0, 0, 0), params_without
92
94
  )
@@ -107,8 +109,8 @@ class TestExogFit:
107
109
  y, (1, 0, 0), (0, 0, 0, 0), exog=exog
108
110
  )
109
111
  assert result["converged"], "fit with exog should converge"
110
- # params: [exog(1), ar(1)]
111
- assert len(result["params"]) == 2
112
+ # params: [exog(1), ar(1), sigma2]
113
+ assert len(result["params"]) == 3
112
114
  assert np.isfinite(result["loglike"])
113
115
 
114
116
  def test_fit_recovers_exog_coeff(self):
@@ -132,8 +134,8 @@ class TestExogFit:
132
134
  enforce_stationarity=False, enforce_invertibility=False,
133
135
  )
134
136
  assert result["converged"]
135
- # params: [exog(3), ar(1)] = 4
136
- assert len(result["params"]) == 4
137
+ # params: [exog(3), ar(1), sigma2] = 5
138
+ assert len(result["params"]) == 5
137
139
  assert np.isfinite(result["loglike"])
138
140
 
139
141
  def test_fit_arma_with_exog(self):
@@ -144,8 +146,8 @@ class TestExogFit:
144
146
  enforce_stationarity=False, enforce_invertibility=False,
145
147
  )
146
148
  assert result["converged"]
147
- # params: [exog(1), ar(1), ma(1)] = 3
148
- assert len(result["params"]) == 3
149
+ # params: [exog(1), ar(1), ma(1), sigma2] = 4
150
+ assert len(result["params"]) == 4
149
151
 
150
152
 
151
153
  # ---------------------------------------------------------------------------
@@ -246,7 +248,7 @@ class TestExogBatch:
246
248
  for i, r in enumerate(results):
247
249
  assert "error" not in r, f"series {i} failed: {r.get('error')}"
248
250
  assert r["converged"], f"series {i} did not converge"
249
- assert len(r["params"]) == 2 # [exog(1), ar(1)]
251
+ assert len(r["params"]) == 3 # [exog(1), ar(1), sigma2]
250
252
 
251
253
 
252
254
  # ---------------------------------------------------------------------------
@@ -281,6 +283,7 @@ class TestExogStatsmodelsComparison:
281
283
  y, (1, 0, 0), (0, 0, 0, 0),
282
284
  np.array(sm_params, dtype=np.float64),
283
285
  exog=exog,
286
+ concentrate_scale=True,
284
287
  )
285
288
 
286
289
  # Should be close (not exact due to initialization differences;