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.
- stmmlib-0.2.0/LICENSE +21 -0
- stmmlib-0.2.0/PKG-INFO +215 -0
- stmmlib-0.2.0/README.md +180 -0
- stmmlib-0.2.0/pyproject.toml +52 -0
- stmmlib-0.2.0/setup.cfg +4 -0
- stmmlib-0.2.0/stmmlib/__init__.py +30 -0
- stmmlib-0.2.0/stmmlib/_backend.py +153 -0
- stmmlib-0.2.0/stmmlib/_utils.py +175 -0
- stmmlib-0.2.0/stmmlib/beta/__init__.py +17 -0
- stmmlib-0.2.0/stmmlib/beta/config.py +108 -0
- stmmlib-0.2.0/stmmlib/beta/criteria.py +247 -0
- stmmlib-0.2.0/stmmlib/beta/design.py +222 -0
- stmmlib-0.2.0/stmmlib/beta/diagnose.py +168 -0
- stmmlib-0.2.0/stmmlib/beta/diagnostics.py +94 -0
- stmmlib-0.2.0/stmmlib/beta/fit.py +598 -0
- stmmlib-0.2.0/stmmlib/beta/gpu_solvers.py +169 -0
- stmmlib-0.2.0/stmmlib/beta/inference.py +403 -0
- stmmlib-0.2.0/stmmlib/beta/lrt_fixed.py +551 -0
- stmmlib-0.2.0/stmmlib/beta/moran_panel.py +733 -0
- stmmlib-0.2.0/stmmlib/beta/plot_data.py +252 -0
- stmmlib-0.2.0/stmmlib/beta/precision.py +98 -0
- stmmlib-0.2.0/stmmlib/beta/report.py +373 -0
- stmmlib-0.2.0/stmmlib/beta/result.py +696 -0
- stmmlib-0.2.0/stmmlib/beta/simulation.py +343 -0
- stmmlib-0.2.0/stmmlib/beta/solvers.py +2021 -0
- stmmlib-0.2.0/stmmlib/beta/structure.py +56 -0
- stmmlib-0.2.0/stmmlib/diagnostics_suite.py +663 -0
- stmmlib-0.2.0/stmmlib/gaussian/__init__.py +1 -0
- stmmlib-0.2.0/stmmlib/gaussian/blup.py +252 -0
- stmmlib-0.2.0/stmmlib/gaussian/covariance.py +599 -0
- stmmlib-0.2.0/stmmlib/gaussian/design.py +312 -0
- stmmlib-0.2.0/stmmlib/gaussian/diagnostics.py +308 -0
- stmmlib-0.2.0/stmmlib/gaussian/gls.py +285 -0
- stmmlib-0.2.0/stmmlib/gaussian/hessian.py +197 -0
- stmmlib-0.2.0/stmmlib/gaussian/lrt_fixed.py +289 -0
- stmmlib-0.2.0/stmmlib/gaussian/models.py +528 -0
- stmmlib-0.2.0/stmmlib/gaussian/optimization.py +361 -0
- stmmlib-0.2.0/stmmlib/gaussian/simulation.py +186 -0
- stmmlib-0.2.0/stmmlib/gaussian/summary.py +277 -0
- stmmlib-0.2.0/stmmlib/gaussian/transforms.py +419 -0
- stmmlib-0.2.0/stmmlib/gaussian/variance_decomp.py +191 -0
- stmmlib-0.2.0/stmmlib/lrt.py +74 -0
- stmmlib-0.2.0/stmmlib/output_format.py +148 -0
- stmmlib-0.2.0/stmmlib/output_format_beta.py +299 -0
- stmmlib-0.2.0/stmmlib/output_format_normal.py +118 -0
- stmmlib-0.2.0/stmmlib/registry.py +326 -0
- stmmlib-0.2.0/stmmlib/stmm.py +265 -0
- stmmlib-0.2.0/stmmlib.egg-info/PKG-INFO +215 -0
- stmmlib-0.2.0/stmmlib.egg-info/SOURCES.txt +76 -0
- stmmlib-0.2.0/stmmlib.egg-info/dependency_links.txt +1 -0
- stmmlib-0.2.0/stmmlib.egg-info/requires.txt +11 -0
- stmmlib-0.2.0/stmmlib.egg-info/top_level.txt +1 -0
- stmmlib-0.2.0/tests/test_beta_all.py +152 -0
- stmmlib-0.2.0/tests/test_beta_config.py +80 -0
- stmmlib-0.2.0/tests/test_beta_criteria.py +44 -0
- stmmlib-0.2.0/tests/test_beta_ebp.py +80 -0
- stmmlib-0.2.0/tests/test_beta_input_guards.py +65 -0
- stmmlib-0.2.0/tests/test_beta_lrt_fixed.py +539 -0
- stmmlib-0.2.0/tests/test_beta_penalty.py +46 -0
- stmmlib-0.2.0/tests/test_beta_regression.py +39 -0
- stmmlib-0.2.0/tests/test_beta_report_certificate.py +73 -0
- stmmlib-0.2.0/tests/test_bugfix_b1_b4.py +51 -0
- stmmlib-0.2.0/tests/test_coverage_gaps.py +50 -0
- stmmlib-0.2.0/tests/test_diagnostics_suite.py +57 -0
- stmmlib-0.2.0/tests/test_fixed_effects_diag.py +45 -0
- stmmlib-0.2.0/tests/test_gaussian_all.py +85 -0
- stmmlib-0.2.0/tests/test_gaussian_lrt.py +157 -0
- stmmlib-0.2.0/tests/test_gaussian_m5_eblup.py +60 -0
- stmmlib-0.2.0/tests/test_grad_analytic.py +82 -0
- stmmlib-0.2.0/tests/test_lrt.py +54 -0
- stmmlib-0.2.0/tests/test_predict_interval.py +46 -0
- stmmlib-0.2.0/tests/test_random_effects_diag.py +70 -0
- stmmlib-0.2.0/tests/test_registry_equation.py +34 -0
- stmmlib-0.2.0/tests/test_universal.py +25 -0
- stmmlib-0.2.0/tests/test_universal_api.py +35 -0
- stmmlib-0.2.0/tests/test_validate_assumptions.py +49 -0
- stmmlib-0.2.0/tests/test_variance_ci.py +41 -0
- 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).
|
stmmlib-0.2.0/README.md
ADDED
|
@@ -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*"]
|
stmmlib-0.2.0/setup.cfg
ADDED
|
@@ -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})"
|