stmmlib 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 (78) hide show
  1. stmmlib-0.2.0/LICENSE +21 -0
  2. stmmlib-0.2.0/PKG-INFO +215 -0
  3. stmmlib-0.2.0/README.md +180 -0
  4. stmmlib-0.2.0/pyproject.toml +52 -0
  5. stmmlib-0.2.0/setup.cfg +4 -0
  6. stmmlib-0.2.0/stmmlib/__init__.py +30 -0
  7. stmmlib-0.2.0/stmmlib/_backend.py +153 -0
  8. stmmlib-0.2.0/stmmlib/_utils.py +175 -0
  9. stmmlib-0.2.0/stmmlib/beta/__init__.py +17 -0
  10. stmmlib-0.2.0/stmmlib/beta/config.py +108 -0
  11. stmmlib-0.2.0/stmmlib/beta/criteria.py +247 -0
  12. stmmlib-0.2.0/stmmlib/beta/design.py +222 -0
  13. stmmlib-0.2.0/stmmlib/beta/diagnose.py +168 -0
  14. stmmlib-0.2.0/stmmlib/beta/diagnostics.py +94 -0
  15. stmmlib-0.2.0/stmmlib/beta/fit.py +598 -0
  16. stmmlib-0.2.0/stmmlib/beta/gpu_solvers.py +169 -0
  17. stmmlib-0.2.0/stmmlib/beta/inference.py +403 -0
  18. stmmlib-0.2.0/stmmlib/beta/lrt_fixed.py +551 -0
  19. stmmlib-0.2.0/stmmlib/beta/moran_panel.py +733 -0
  20. stmmlib-0.2.0/stmmlib/beta/plot_data.py +252 -0
  21. stmmlib-0.2.0/stmmlib/beta/precision.py +98 -0
  22. stmmlib-0.2.0/stmmlib/beta/report.py +373 -0
  23. stmmlib-0.2.0/stmmlib/beta/result.py +696 -0
  24. stmmlib-0.2.0/stmmlib/beta/simulation.py +343 -0
  25. stmmlib-0.2.0/stmmlib/beta/solvers.py +2021 -0
  26. stmmlib-0.2.0/stmmlib/beta/structure.py +56 -0
  27. stmmlib-0.2.0/stmmlib/diagnostics_suite.py +663 -0
  28. stmmlib-0.2.0/stmmlib/gaussian/__init__.py +1 -0
  29. stmmlib-0.2.0/stmmlib/gaussian/blup.py +252 -0
  30. stmmlib-0.2.0/stmmlib/gaussian/covariance.py +599 -0
  31. stmmlib-0.2.0/stmmlib/gaussian/design.py +312 -0
  32. stmmlib-0.2.0/stmmlib/gaussian/diagnostics.py +308 -0
  33. stmmlib-0.2.0/stmmlib/gaussian/gls.py +285 -0
  34. stmmlib-0.2.0/stmmlib/gaussian/hessian.py +197 -0
  35. stmmlib-0.2.0/stmmlib/gaussian/lrt_fixed.py +289 -0
  36. stmmlib-0.2.0/stmmlib/gaussian/models.py +528 -0
  37. stmmlib-0.2.0/stmmlib/gaussian/optimization.py +361 -0
  38. stmmlib-0.2.0/stmmlib/gaussian/simulation.py +186 -0
  39. stmmlib-0.2.0/stmmlib/gaussian/summary.py +277 -0
  40. stmmlib-0.2.0/stmmlib/gaussian/transforms.py +419 -0
  41. stmmlib-0.2.0/stmmlib/gaussian/variance_decomp.py +191 -0
  42. stmmlib-0.2.0/stmmlib/lrt.py +74 -0
  43. stmmlib-0.2.0/stmmlib/output_format.py +148 -0
  44. stmmlib-0.2.0/stmmlib/output_format_beta.py +299 -0
  45. stmmlib-0.2.0/stmmlib/output_format_normal.py +118 -0
  46. stmmlib-0.2.0/stmmlib/registry.py +326 -0
  47. stmmlib-0.2.0/stmmlib/stmm.py +265 -0
  48. stmmlib-0.2.0/stmmlib.egg-info/PKG-INFO +215 -0
  49. stmmlib-0.2.0/stmmlib.egg-info/SOURCES.txt +76 -0
  50. stmmlib-0.2.0/stmmlib.egg-info/dependency_links.txt +1 -0
  51. stmmlib-0.2.0/stmmlib.egg-info/requires.txt +11 -0
  52. stmmlib-0.2.0/stmmlib.egg-info/top_level.txt +1 -0
  53. stmmlib-0.2.0/tests/test_beta_all.py +152 -0
  54. stmmlib-0.2.0/tests/test_beta_config.py +80 -0
  55. stmmlib-0.2.0/tests/test_beta_criteria.py +44 -0
  56. stmmlib-0.2.0/tests/test_beta_ebp.py +80 -0
  57. stmmlib-0.2.0/tests/test_beta_input_guards.py +65 -0
  58. stmmlib-0.2.0/tests/test_beta_lrt_fixed.py +539 -0
  59. stmmlib-0.2.0/tests/test_beta_penalty.py +46 -0
  60. stmmlib-0.2.0/tests/test_beta_regression.py +39 -0
  61. stmmlib-0.2.0/tests/test_beta_report_certificate.py +73 -0
  62. stmmlib-0.2.0/tests/test_bugfix_b1_b4.py +51 -0
  63. stmmlib-0.2.0/tests/test_coverage_gaps.py +50 -0
  64. stmmlib-0.2.0/tests/test_diagnostics_suite.py +57 -0
  65. stmmlib-0.2.0/tests/test_fixed_effects_diag.py +45 -0
  66. stmmlib-0.2.0/tests/test_gaussian_all.py +85 -0
  67. stmmlib-0.2.0/tests/test_gaussian_lrt.py +157 -0
  68. stmmlib-0.2.0/tests/test_gaussian_m5_eblup.py +60 -0
  69. stmmlib-0.2.0/tests/test_grad_analytic.py +82 -0
  70. stmmlib-0.2.0/tests/test_lrt.py +54 -0
  71. stmmlib-0.2.0/tests/test_predict_interval.py +46 -0
  72. stmmlib-0.2.0/tests/test_random_effects_diag.py +70 -0
  73. stmmlib-0.2.0/tests/test_registry_equation.py +34 -0
  74. stmmlib-0.2.0/tests/test_universal.py +25 -0
  75. stmmlib-0.2.0/tests/test_universal_api.py +35 -0
  76. stmmlib-0.2.0/tests/test_validate_assumptions.py +49 -0
  77. stmmlib-0.2.0/tests/test_variance_ci.py +41 -0
  78. stmmlib-0.2.0/tests/test_w_edge_cases.py +112 -0
stmmlib-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ghardapaty Ghiffary
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.
stmmlib-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: stmmlib
3
+ Version: 0.2.0
4
+ Summary: Family-aware spatio-temporal mixed models (Gaussian + Beta) with optional GPU acceleration
5
+ Author: Ghardapaty Ghiffary
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ghiffahry/STMMLib
8
+ Project-URL: Repository, https://github.com/ghiffahry/STMMLib
9
+ Project-URL: Issues, https://github.com/ghiffahry/STMMLib/issues
10
+ Keywords: spatio-temporal,mixed models,small area estimation,Beta regression,generalized linear mixed models,areal data
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.24
26
+ Requires-Dist: scipy>=1.11
27
+ Requires-Dist: pandas>=2.0
28
+ Provides-Extra: gpu
29
+ Requires-Dist: cupy-cuda12x>=13.0; extra == "gpu"
30
+ Requires-Dist: torch>=2.0; extra == "gpu"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0; extra == "dev"
33
+ Requires-Dist: pytest-cov; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # stmmlib
37
+
38
+ Spatio-temporal mixed models for areal panel data, with Gaussian and Beta
39
+ families behind one API.
40
+
41
+ ## Overview
42
+
43
+ `stmmlib` fits area-level panel models that combine spatial random effects
44
+ (IID, SAR, nested, SEBLUP Rao-Yu), temporal random effects (IID, AR(1)), and
45
+ optional area-time interactions. Two likelihood families are available, so the
46
+ same model structure can be fitted to continuous responses or to proportions.
47
+
48
+ Developed as a reference implementation for a study of Indonesian district
49
+ poverty data. It is a general tool for area-level panel data, does not
50
+ depend on that dataset, and is published as a software reference.
51
+
52
+ ## Features
53
+
54
+ - 14 model structures (M1 to M14) plus a Beta regression baseline
55
+ - Gaussian family: ML and REML by marginal likelihood
56
+ - Beta family: Laplace-approximated ML with convergence certificates (gradient
57
+ norm, Hessian positive definiteness, iteration count)
58
+ - Prediction: EBLUP for Gaussian, EBP from posterior draws for Beta, both
59
+ in-sample and for same-area holdout
60
+ - Configurable column names, so data does not need renaming
61
+ - Fixed-effect inference in both families: Wald tests and restricted-refit
62
+ likelihood-ratio tests (`inference="lrt"`, `lrt_terms=[...]`)
63
+ - Optional GPU backend through CuPy or PyTorch
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install stmmlib
69
+ ```
70
+
71
+ For the optional GPU backend:
72
+
73
+ ```bash
74
+ pip install "stmmlib[gpu]"
75
+ ```
76
+
77
+ Install from source instead:
78
+
79
+ ```bash
80
+ git clone https://github.com/ghiffahry/STMMLib.git
81
+ cd STMMLib
82
+ pip install -e .
83
+ ```
84
+
85
+ Run the test suite:
86
+
87
+ ```bash
88
+ pip install -e ".[dev]"
89
+ pytest
90
+ ```
91
+
92
+ ## Quick start
93
+
94
+ Runs end to end on synthetic data, nothing external needed:
95
+
96
+ ```python
97
+ import numpy as np
98
+ import pandas as pd
99
+ from stmmlib.stmm import stmm
100
+
101
+ m, T = 12, 6
102
+ rng = np.random.default_rng(7)
103
+ area = np.repeat(np.arange(m), T)
104
+ time = np.tile(np.arange(T), m)
105
+
106
+ df = pd.DataFrame({
107
+ "y": np.clip(rng.beta(6, 4, m * T), 1e-4, 1 - 1e-4), # in (0, 1), as required
108
+ "area": area,
109
+ "time": time,
110
+ "x1": rng.normal(size=m * T),
111
+ "x2": rng.normal(size=m * T),
112
+ })
113
+
114
+ fit = stmm(data=df, model="m1_iid_iid", family="beta",
115
+ method="Laplace", gpu="no", n_starts=1)
116
+ print(fit.summary()) # reads the certificate, refuses to call an invalid fit inference-grade
117
+ print(fit.certificate["valid"]) # True/False: may the SEs be quoted?
118
+ ```
119
+
120
+ Gaussian family, same call surface:
121
+
122
+ ```python
123
+ df_gauss = df.assign(y=df["x1"] + rng.normal(0, 1, len(df)))
124
+ W = np.ones((m, m)); np.fill_diagonal(W, 0); W /= W.sum(1, keepdims=True)
125
+
126
+ fit = stmm(data=df_gauss, model="m3_sar_iid", family="gaussian",
127
+ W=W, method="REML", area_col="area", time_col="time", gpu="no")
128
+ fit["beta_hat"]; fit["se_beta"]; fit["blup"]["v_hat"]
129
+ ```
130
+
131
+ Fixed-effect inference in both families, Wald or likelihood-ratio test,
132
+ and the requirement that the LRT compares ML likelihoods, are documented
133
+ in `docs/API.md`.
134
+
135
+ ## Models
136
+
137
+ | ID | Area effect | Time effect | Interaction |
138
+ |---|---|---|---|
139
+ | `m1_iid_iid` | IID | IID | none |
140
+ | `m2_iid_ar1` | IID | AR(1) | none |
141
+ | `m3_sar_iid` | SAR | IID | none |
142
+ | `m4_sar_ar1` | SAR | AR(1) | none |
143
+ | `m5_seblup_rao_yu_it` | SEBLUP Rao-Yu | AR(1) | none |
144
+ | `m6_nested_iid_iid` | nested IID | IID | none |
145
+ | `m7_nested_ar1` | nested SAR | AR(1) | none |
146
+ | `m8_iid_iid_int` | IID | IID | IID |
147
+ | `m9_sar_iid_int` | SAR | IID | spatial |
148
+ | `m10_iid_ar1_int` | IID | AR(1) | temporal |
149
+ | `m11_sar_ar1_int` | SAR | AR(1) | spatio-temporal |
150
+ | `m12_seblup_rao_yu_it_int` | SEBLUP Rao-Yu | AR(1) | spatio-temporal |
151
+ | `m13_nested_iid_iid_int` | nested IID | IID | temporal |
152
+ | `m14_nested_ar1_int` | nested SAR | AR(1) | IID |
153
+ | `beta_regression` | none | none | none |
154
+
155
+ Models ending in `_int` add an interaction term between area and time effects.
156
+ Models with SAR or SEBLUP area effects require `W`.
157
+
158
+ ## Estimation and prediction
159
+
160
+ - Gaussian: `method="ML"` or `method="REML"`. The fit is returned as a dict
161
+ with `beta_hat`, `se_beta`, `log_lik`, `AIC`, `BIC`, and `blup`
162
+ (`v_hat`, `u_hat`, `se_v`).
163
+ - Beta: `method="Laplace"`. The fit is an object with `summary()`,
164
+ `certificate`, `beta_original`, `se_original`, `ebp()`, and `ebp_newdata()`.
165
+ - Fixed effects in the Beta family are standardised internally. Use
166
+ `beta_original` and `se_original` for reporting, not the standardised values.
167
+ - `ebp_newdata()` predicts for held-out periods of areas seen during fitting.
168
+ Areas never seen during fitting are rejected.
169
+ - Accuracy presets: `fast`, `balanced`, `medium`, `full`.
170
+
171
+ ## Requirements
172
+
173
+ - Python 3.10 or newer
174
+ - NumPy 1.24+, SciPy 1.11+, pandas 2.0+
175
+ - CuPy and PyTorch only for the `gpu` extra
176
+
177
+ ## Documentation
178
+
179
+ `docs/API.md` holds a compact API reference covering fitting, diagnostics, LRT,
180
+ and prediction intervals.
181
+
182
+ ## Limitations
183
+
184
+ - EBP here means `E[mu | y]`, computed from posterior draws. It is not the
185
+ Molina-Rao EBP, and its mean squared error is not implemented.
186
+ - Spatial block cross-validation supports fixed effects only.
187
+ - The Gaussian path is CPU only. Run it with `gpu="no"`.
188
+ - Gaussian fits return a dict, Beta fits return an object. The two return types
189
+ are not unified yet.
190
+ - Nested models with interactions are dense and slow. Raise `max_iter` for them.
191
+ - `simulate_beta` supports the flat models M1-M5 and the interaction models
192
+ M8-M12. Nested models M6/M7/M13/M14 raise `ValueError`.
193
+ - `beta_criteria` (cAIC, conditional R2) returns NaN for nested models; their
194
+ province/district/cell precision builder is not implemented.
195
+ - When the automatic prior refit is used, the reported `logLik`, `AIC`, and
196
+ `BIC` are the penalized objective. The fit's `optim` record
197
+ (`fit._opt["optim"]`) flags this with `penalty_auto` and reports the
198
+ unpenalized value at the same estimate in `penalty_refit_msg`.
199
+
200
+ ## Citation
201
+
202
+ ```bibtex
203
+ @software{ghiffahry_stmmlib,
204
+ author = {Ghiffary, Ghardapaty},
205
+ title = {STMMLib: spatio-temporal mixed models for areal panel data},
206
+ year = {2026},
207
+ version = {0.2.0},
208
+ url = {https://github.com/ghiffahry/STMMLib},
209
+ license = {MIT}
210
+ }
211
+ ```
212
+
213
+ ## License
214
+
215
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,180 @@
1
+ # stmmlib
2
+
3
+ Spatio-temporal mixed models for areal panel data, with Gaussian and Beta
4
+ families behind one API.
5
+
6
+ ## Overview
7
+
8
+ `stmmlib` fits area-level panel models that combine spatial random effects
9
+ (IID, SAR, nested, SEBLUP Rao-Yu), temporal random effects (IID, AR(1)), and
10
+ optional area-time interactions. Two likelihood families are available, so the
11
+ same model structure can be fitted to continuous responses or to proportions.
12
+
13
+ Developed as a reference implementation for a study of Indonesian district
14
+ poverty data. It is a general tool for area-level panel data, does not
15
+ depend on that dataset, and is published as a software reference.
16
+
17
+ ## Features
18
+
19
+ - 14 model structures (M1 to M14) plus a Beta regression baseline
20
+ - Gaussian family: ML and REML by marginal likelihood
21
+ - Beta family: Laplace-approximated ML with convergence certificates (gradient
22
+ norm, Hessian positive definiteness, iteration count)
23
+ - Prediction: EBLUP for Gaussian, EBP from posterior draws for Beta, both
24
+ in-sample and for same-area holdout
25
+ - Configurable column names, so data does not need renaming
26
+ - Fixed-effect inference in both families: Wald tests and restricted-refit
27
+ likelihood-ratio tests (`inference="lrt"`, `lrt_terms=[...]`)
28
+ - Optional GPU backend through CuPy or PyTorch
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install stmmlib
34
+ ```
35
+
36
+ For the optional GPU backend:
37
+
38
+ ```bash
39
+ pip install "stmmlib[gpu]"
40
+ ```
41
+
42
+ Install from source instead:
43
+
44
+ ```bash
45
+ git clone https://github.com/ghiffahry/STMMLib.git
46
+ cd STMMLib
47
+ pip install -e .
48
+ ```
49
+
50
+ Run the test suite:
51
+
52
+ ```bash
53
+ pip install -e ".[dev]"
54
+ pytest
55
+ ```
56
+
57
+ ## Quick start
58
+
59
+ Runs end to end on synthetic data, nothing external needed:
60
+
61
+ ```python
62
+ import numpy as np
63
+ import pandas as pd
64
+ from stmmlib.stmm import stmm
65
+
66
+ m, T = 12, 6
67
+ rng = np.random.default_rng(7)
68
+ area = np.repeat(np.arange(m), T)
69
+ time = np.tile(np.arange(T), m)
70
+
71
+ df = pd.DataFrame({
72
+ "y": np.clip(rng.beta(6, 4, m * T), 1e-4, 1 - 1e-4), # in (0, 1), as required
73
+ "area": area,
74
+ "time": time,
75
+ "x1": rng.normal(size=m * T),
76
+ "x2": rng.normal(size=m * T),
77
+ })
78
+
79
+ fit = stmm(data=df, model="m1_iid_iid", family="beta",
80
+ method="Laplace", gpu="no", n_starts=1)
81
+ print(fit.summary()) # reads the certificate, refuses to call an invalid fit inference-grade
82
+ print(fit.certificate["valid"]) # True/False: may the SEs be quoted?
83
+ ```
84
+
85
+ Gaussian family, same call surface:
86
+
87
+ ```python
88
+ df_gauss = df.assign(y=df["x1"] + rng.normal(0, 1, len(df)))
89
+ W = np.ones((m, m)); np.fill_diagonal(W, 0); W /= W.sum(1, keepdims=True)
90
+
91
+ fit = stmm(data=df_gauss, model="m3_sar_iid", family="gaussian",
92
+ W=W, method="REML", area_col="area", time_col="time", gpu="no")
93
+ fit["beta_hat"]; fit["se_beta"]; fit["blup"]["v_hat"]
94
+ ```
95
+
96
+ Fixed-effect inference in both families, Wald or likelihood-ratio test,
97
+ and the requirement that the LRT compares ML likelihoods, are documented
98
+ in `docs/API.md`.
99
+
100
+ ## Models
101
+
102
+ | ID | Area effect | Time effect | Interaction |
103
+ |---|---|---|---|
104
+ | `m1_iid_iid` | IID | IID | none |
105
+ | `m2_iid_ar1` | IID | AR(1) | none |
106
+ | `m3_sar_iid` | SAR | IID | none |
107
+ | `m4_sar_ar1` | SAR | AR(1) | none |
108
+ | `m5_seblup_rao_yu_it` | SEBLUP Rao-Yu | AR(1) | none |
109
+ | `m6_nested_iid_iid` | nested IID | IID | none |
110
+ | `m7_nested_ar1` | nested SAR | AR(1) | none |
111
+ | `m8_iid_iid_int` | IID | IID | IID |
112
+ | `m9_sar_iid_int` | SAR | IID | spatial |
113
+ | `m10_iid_ar1_int` | IID | AR(1) | temporal |
114
+ | `m11_sar_ar1_int` | SAR | AR(1) | spatio-temporal |
115
+ | `m12_seblup_rao_yu_it_int` | SEBLUP Rao-Yu | AR(1) | spatio-temporal |
116
+ | `m13_nested_iid_iid_int` | nested IID | IID | temporal |
117
+ | `m14_nested_ar1_int` | nested SAR | AR(1) | IID |
118
+ | `beta_regression` | none | none | none |
119
+
120
+ Models ending in `_int` add an interaction term between area and time effects.
121
+ Models with SAR or SEBLUP area effects require `W`.
122
+
123
+ ## Estimation and prediction
124
+
125
+ - Gaussian: `method="ML"` or `method="REML"`. The fit is returned as a dict
126
+ with `beta_hat`, `se_beta`, `log_lik`, `AIC`, `BIC`, and `blup`
127
+ (`v_hat`, `u_hat`, `se_v`).
128
+ - Beta: `method="Laplace"`. The fit is an object with `summary()`,
129
+ `certificate`, `beta_original`, `se_original`, `ebp()`, and `ebp_newdata()`.
130
+ - Fixed effects in the Beta family are standardised internally. Use
131
+ `beta_original` and `se_original` for reporting, not the standardised values.
132
+ - `ebp_newdata()` predicts for held-out periods of areas seen during fitting.
133
+ Areas never seen during fitting are rejected.
134
+ - Accuracy presets: `fast`, `balanced`, `medium`, `full`.
135
+
136
+ ## Requirements
137
+
138
+ - Python 3.10 or newer
139
+ - NumPy 1.24+, SciPy 1.11+, pandas 2.0+
140
+ - CuPy and PyTorch only for the `gpu` extra
141
+
142
+ ## Documentation
143
+
144
+ `docs/API.md` holds a compact API reference covering fitting, diagnostics, LRT,
145
+ and prediction intervals.
146
+
147
+ ## Limitations
148
+
149
+ - EBP here means `E[mu | y]`, computed from posterior draws. It is not the
150
+ Molina-Rao EBP, and its mean squared error is not implemented.
151
+ - Spatial block cross-validation supports fixed effects only.
152
+ - The Gaussian path is CPU only. Run it with `gpu="no"`.
153
+ - Gaussian fits return a dict, Beta fits return an object. The two return types
154
+ are not unified yet.
155
+ - Nested models with interactions are dense and slow. Raise `max_iter` for them.
156
+ - `simulate_beta` supports the flat models M1-M5 and the interaction models
157
+ M8-M12. Nested models M6/M7/M13/M14 raise `ValueError`.
158
+ - `beta_criteria` (cAIC, conditional R2) returns NaN for nested models; their
159
+ province/district/cell precision builder is not implemented.
160
+ - When the automatic prior refit is used, the reported `logLik`, `AIC`, and
161
+ `BIC` are the penalized objective. The fit's `optim` record
162
+ (`fit._opt["optim"]`) flags this with `penalty_auto` and reports the
163
+ unpenalized value at the same estimate in `penalty_refit_msg`.
164
+
165
+ ## Citation
166
+
167
+ ```bibtex
168
+ @software{ghiffahry_stmmlib,
169
+ author = {Ghiffary, Ghardapaty},
170
+ title = {STMMLib: spatio-temporal mixed models for areal panel data},
171
+ year = {2026},
172
+ version = {0.2.0},
173
+ url = {https://github.com/ghiffahry/STMMLib},
174
+ license = {MIT}
175
+ }
176
+ ```
177
+
178
+ ## License
179
+
180
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stmmlib"
7
+ version = "0.2.0"
8
+ description = "Family-aware spatio-temporal mixed models (Gaussian + Beta) with optional GPU acceleration"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Ghardapaty Ghiffary"},
14
+ ]
15
+ keywords = [
16
+ "spatio-temporal",
17
+ "mixed models",
18
+ "small area estimation",
19
+ "Beta regression",
20
+ "generalized linear mixed models",
21
+ "areal data",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Science/Research",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Operating System :: OS Independent",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ "Topic :: Scientific/Engineering",
35
+ ]
36
+ dependencies = [
37
+ "numpy>=1.24",
38
+ "scipy>=1.11",
39
+ "pandas>=2.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/ghiffahry/STMMLib"
44
+ Repository = "https://github.com/ghiffahry/STMMLib"
45
+ Issues = "https://github.com/ghiffahry/STMMLib/issues"
46
+
47
+ [project.optional-dependencies]
48
+ gpu = ["cupy-cuda12x>=13.0", "torch>=2.0"]
49
+ dev = ["pytest>=7.0", "pytest-cov"]
50
+
51
+ [tool.setuptools.packages.find]
52
+ include = ["stmmlib*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ """stmmlib — Family-aware spatio-temporal mixed models.
2
+
3
+ Supports 14 model structures (M1-M14) for Gaussian and Beta families,
4
+ with optional CUDA/GPU acceleration via CuPy.
5
+ """
6
+
7
+ from stmmlib.registry import (
8
+ stmm_available_models,
9
+ stmm_model_label,
10
+ stmm_parse_model,
11
+ )
12
+ from stmmlib.stmm import stmm, stmm_normal, stmm_beta
13
+ from stmmlib.output_format import format_output
14
+ from stmmlib._backend import Backend
15
+
16
+ try: # single source of truth for the version: the installed package metadata
17
+ from importlib.metadata import version as _pkg_version
18
+ __version__ = _pkg_version("stmmlib")
19
+ except Exception: # source checkout without an install
20
+ __version__ = "0.2.0"
21
+ __all__ = [
22
+ "stmm",
23
+ "stmm_normal",
24
+ "stmm_beta",
25
+ "format_output",
26
+ "stmm_available_models",
27
+ "stmm_model_label",
28
+ "stmm_parse_model",
29
+ "Backend",
30
+ ]
@@ -0,0 +1,153 @@
1
+ """GPU/CPU backend abstraction with auto-detection.
2
+
3
+ Usage:
4
+ backend = Backend(gpu="auto") # auto-detect GPU
5
+ backend = Backend(gpu=True) # force GPU (error if unavailable)
6
+ backend = Backend(gpu=False) # force CPU
7
+
8
+ All array operations go through backend.xp (numpy or cupy).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import warnings
14
+ from typing import Literal
15
+
16
+
17
+ class Backend:
18
+ """Array backend that routes computation to CPU or GPU.
19
+
20
+ Attributes:
21
+ xp: Array namespace (numpy or cupy).
22
+ sparse: Sparse matrix namespace (scipy.sparse or cupyx.scipy.sparse).
23
+ device: "cpu" or "gpu".
24
+ """
25
+
26
+ def __init__(self, gpu: bool | Literal["auto"] = "auto") -> None:
27
+ self.xp = None
28
+ self.sparse = None
29
+ self.device: Literal["cpu", "gpu"] = "cpu"
30
+
31
+ use_gpu = self._resolve_gpu(gpu)
32
+
33
+ if use_gpu:
34
+ try:
35
+ import cupy as cp # type: ignore[import-untyped]
36
+ import cupyx.scipy.sparse as csp # type: ignore[import-untyped]
37
+
38
+ cp.zeros(1, dtype=cp.float64) # test allocation
39
+ self.xp = cp # type: ignore[assignment]
40
+ self.sparse = csp # type: ignore[assignment]
41
+ self.device = "gpu"
42
+ return
43
+ except Exception as e:
44
+ if gpu is True:
45
+ raise RuntimeError(f"GPU requested but unavailable: {e}") from e
46
+ warnings.warn(
47
+ f"GPU unavailable ({e}), falling back to CPU.",
48
+ stacklevel=2,
49
+ )
50
+
51
+ import numpy as np
52
+ import scipy.sparse as sp
53
+
54
+ self.xp = np # type: ignore[assignment]
55
+ self.sparse = sp # type: ignore[assignment]
56
+ self.device = "cpu"
57
+
58
+ @staticmethod
59
+ def _resolve_gpu(gpu: bool | Literal["auto"]) -> bool:
60
+ if isinstance(gpu, str):
61
+ g = gpu.strip().lower()
62
+ if g == "auto":
63
+ try:
64
+ import cupy as cp # type: ignore[import-untyped]
65
+
66
+ cp.zeros(1, dtype=cp.float64)
67
+ return True
68
+ except Exception:
69
+ return False
70
+ if g in ("no", "false", "0", "off", "cpu"):
71
+ return False
72
+ if g in ("yes", "true", "1", "on", "gpu"):
73
+ return True
74
+ if gpu == "auto":
75
+ try:
76
+ import cupy as cp # type: ignore[import-untyped]
77
+
78
+ cp.zeros(1, dtype=cp.float64)
79
+ return True
80
+ except Exception:
81
+ return False
82
+ return bool(gpu)
83
+
84
+ def to_cpu(self, arr):
85
+ """Move array to CPU. No-op if already on CPU."""
86
+ if self.device == "gpu":
87
+ return self.xp.asnumpy(arr)
88
+ return arr
89
+
90
+ def to_gpu(self, arr):
91
+ """Move array to GPU. No-op if already on CPU."""
92
+ if self.device == "gpu":
93
+ return self.xp.asarray(arr)
94
+ return arr
95
+
96
+ def is_sparse(self, m) -> bool:
97
+ """Check if matrix is a sparse matrix."""
98
+ if self.device == "gpu":
99
+ return self.sparse.issparse(m)
100
+ import scipy.sparse as sp
101
+
102
+ return sp.issparse(m)
103
+
104
+ def to_sparse(self, m):
105
+ """Convert dense array to sparse CSR."""
106
+ if self.is_sparse(m):
107
+ return m
108
+ if self.device == "gpu":
109
+ return self.sparse.csr_matrix(m)
110
+ import scipy.sparse as sp
111
+
112
+ return sp.csr_matrix(m)
113
+
114
+ def sparse_kron(self, A, B, format="csr"):
115
+ """Kronecker product of two (sparse) matrices."""
116
+ return self.sparse.kron(A, B, format=format)
117
+
118
+ def sparse_block_diag(self, mats):
119
+ """Block-diagonal from list of matrices."""
120
+ if self.device == "gpu":
121
+ return self.sparse.block_diag(mats, format="csr")
122
+ import scipy.sparse as sp
123
+
124
+ return sp.block_diag(mats, format="csr")
125
+
126
+ def sparse_eye(self, n, dtype=None):
127
+ """Identity matrix (sparse)."""
128
+ if dtype is None:
129
+ dtype = self.xp.float64
130
+ return self.sparse.eye(n, dtype=dtype, format="csr")
131
+
132
+ def solve(self, A, b):
133
+ """Solve linear system A @ x = b."""
134
+ return self.xp.linalg.solve(A, b)
135
+
136
+ def cholesky(self, A):
137
+ """Cholesky decomposition. Returns lower-triangular L where A = L @ L.T."""
138
+ return self.xp.linalg.cholesky(A)
139
+
140
+ def slogdet(self, A):
141
+ """Log-determinant via sign and log-abs-determinant."""
142
+ return self.xp.linalg.slogdet(A)
143
+
144
+ def eigh(self, A):
145
+ """Eigenvalues and eigenvectors of symmetric matrix."""
146
+ return self.xp.linalg.eigh(A)
147
+
148
+ def pinv(self, A):
149
+ """Moore-Penrose pseudo-inverse."""
150
+ return self.xp.linalg.pinv(A)
151
+
152
+ def __repr__(self) -> str:
153
+ return f"Backend(device={self.device!r})"