pylgm 0.6.1__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 (258) hide show
  1. pylgm-0.6.1/.gitignore +9 -0
  2. pylgm-0.6.1/LICENSE +21 -0
  3. pylgm-0.6.1/PKG-INFO +209 -0
  4. pylgm-0.6.1/README.md +172 -0
  5. pylgm-0.6.1/docs/comparison.md +214 -0
  6. pylgm-0.6.1/docs/development.md +30 -0
  7. pylgm-0.6.1/docs/effects.md +505 -0
  8. pylgm-0.6.1/docs/empirical-bayes.md +139 -0
  9. pylgm-0.6.1/docs/examples-columbus.md +119 -0
  10. pylgm-0.6.1/docs/examples-count-regression.md +89 -0
  11. pylgm-0.6.1/docs/examples-disease-mapping.md +81 -0
  12. pylgm-0.6.1/docs/examples-dynamic-network.md +146 -0
  13. pylgm-0.6.1/docs/examples.md +66 -0
  14. pylgm-0.6.1/docs/how-it-works.md +202 -0
  15. pylgm-0.6.1/docs/img/columbus_coefficients.png +0 -0
  16. pylgm-0.6.1/docs/img/columbus_spatial_field.png +0 -0
  17. pylgm-0.6.1/docs/img/crabs_fit.png +0 -0
  18. pylgm-0.6.1/docs/img/crabs_prediction.png +0 -0
  19. pylgm-0.6.1/docs/img/scotland_fit.png +0 -0
  20. pylgm-0.6.1/docs/img/scotland_shrinkage.png +0 -0
  21. pylgm-0.6.1/docs/img/state_cell_recovery.png +0 -0
  22. pylgm-0.6.1/docs/img/state_network_drift.png +0 -0
  23. pylgm-0.6.1/docs/index.md +75 -0
  24. pylgm-0.6.1/docs/inla.md +212 -0
  25. pylgm-0.6.1/docs/internals.md +89 -0
  26. pylgm-0.6.1/docs/javascripts/mathjax.js +17 -0
  27. pylgm-0.6.1/docs/likelihoods.md +277 -0
  28. pylgm-0.6.1/docs/model-comparison.md +148 -0
  29. pylgm-0.6.1/docs/prediction.md +142 -0
  30. pylgm-0.6.1/docs/roadmap.md +117 -0
  31. pylgm-0.6.1/docs/spark.md +57 -0
  32. pylgm-0.6.1/docs/spatial-effects.md +535 -0
  33. pylgm-0.6.1/docs/theory.md +398 -0
  34. pylgm-0.6.1/examples/columbus_spatial_econometrics/README.md +58 -0
  35. pylgm-0.6.1/examples/columbus_spatial_econometrics/data.csv +50 -0
  36. pylgm-0.6.1/examples/columbus_spatial_econometrics/graph.json +336 -0
  37. pylgm-0.6.1/examples/columbus_spatial_econometrics/run.py +171 -0
  38. pylgm-0.6.1/examples/count_glm/README.md +31 -0
  39. pylgm-0.6.1/examples/count_glm/config.yaml +11 -0
  40. pylgm-0.6.1/examples/count_glm/data.csv +9 -0
  41. pylgm-0.6.1/examples/count_glm/run.py +22 -0
  42. pylgm-0.6.1/examples/count_regression/README.md +19 -0
  43. pylgm-0.6.1/examples/count_regression/data.csv +174 -0
  44. pylgm-0.6.1/examples/count_regression/run.py +155 -0
  45. pylgm-0.6.1/examples/directed_network_sar/README.md +61 -0
  46. pylgm-0.6.1/examples/directed_network_sar/run.py +81 -0
  47. pylgm-0.6.1/examples/disease_mapping/README.md +36 -0
  48. pylgm-0.6.1/examples/disease_mapping/config.yaml +23 -0
  49. pylgm-0.6.1/examples/disease_mapping/data.csv +57 -0
  50. pylgm-0.6.1/examples/disease_mapping/graph.json +1 -0
  51. pylgm-0.6.1/examples/disease_mapping/run.py +134 -0
  52. pylgm-0.6.1/examples/empirical_bayes/README.md +42 -0
  53. pylgm-0.6.1/examples/empirical_bayes/data.csv +81 -0
  54. pylgm-0.6.1/examples/empirical_bayes/run.py +33 -0
  55. pylgm-0.6.1/examples/general_lgm/README.md +41 -0
  56. pylgm-0.6.1/examples/general_lgm/config.yaml +18 -0
  57. pylgm-0.6.1/examples/general_lgm/data.csv +7 -0
  58. pylgm-0.6.1/examples/general_lgm/run.py +40 -0
  59. pylgm-0.6.1/examples/general_lgm/run_spark.py +32 -0
  60. pylgm-0.6.1/examples/hybrid_nowcast/README.md +55 -0
  61. pylgm-0.6.1/examples/hybrid_nowcast/run.py +88 -0
  62. pylgm-0.6.1/examples/inla/README.md +59 -0
  63. pylgm-0.6.1/examples/inla/data.csv +145 -0
  64. pylgm-0.6.1/examples/inla/run.py +43 -0
  65. pylgm-0.6.1/examples/inla_criteria/README.md +64 -0
  66. pylgm-0.6.1/examples/inla_criteria/data.csv +145 -0
  67. pylgm-0.6.1/examples/inla_criteria/run.py +40 -0
  68. pylgm-0.6.1/examples/inla_full_laplace/README.md +78 -0
  69. pylgm-0.6.1/examples/inla_full_laplace/data.csv +133 -0
  70. pylgm-0.6.1/examples/inla_full_laplace/run.py +69 -0
  71. pylgm-0.6.1/examples/inla_sla/README.md +68 -0
  72. pylgm-0.6.1/examples/inla_sla/data.csv +133 -0
  73. pylgm-0.6.1/examples/inla_sla/run.py +52 -0
  74. pylgm-0.6.1/examples/map_ii/README.md +53 -0
  75. pylgm-0.6.1/examples/map_ii/data.csv +81 -0
  76. pylgm-0.6.1/examples/map_ii/run.py +37 -0
  77. pylgm-0.6.1/examples/method_comparison/README.md +47 -0
  78. pylgm-0.6.1/examples/method_comparison/run.py +198 -0
  79. pylgm-0.6.1/examples/midas_nowcast/README.md +36 -0
  80. pylgm-0.6.1/examples/midas_nowcast/run.py +53 -0
  81. pylgm-0.6.1/examples/nic_backtest/README.md +25 -0
  82. pylgm-0.6.1/examples/nic_backtest/config.yaml +38 -0
  83. pylgm-0.6.1/examples/predictive_selection/README.md +20 -0
  84. pylgm-0.6.1/examples/predictive_selection/config.yaml +36 -0
  85. pylgm-0.6.1/examples/predictive_selection/data.csv +31 -0
  86. pylgm-0.6.1/examples/state_income_dynamic_network/README.md +80 -0
  87. pylgm-0.6.1/examples/state_income_dynamic_network/adjacency.json +312 -0
  88. pylgm-0.6.1/examples/state_income_dynamic_network/income.csv +49 -0
  89. pylgm-0.6.1/examples/state_income_dynamic_network/run.py +238 -0
  90. pylgm-0.6.1/examples/survival_duration/README.md +96 -0
  91. pylgm-0.6.1/examples/survival_duration/run.py +89 -0
  92. pylgm-0.6.1/examples/synthetic_panel/README.md +23 -0
  93. pylgm-0.6.1/examples/synthetic_panel/config.yaml +13 -0
  94. pylgm-0.6.1/examples/synthetic_panel/data.csv +9 -0
  95. pylgm-0.6.1/examples/weighted_network/README.md +19 -0
  96. pylgm-0.6.1/examples/weighted_network/run.py +73 -0
  97. pylgm-0.6.1/mkdocs.yml +83 -0
  98. pylgm-0.6.1/overrides/main.html +19 -0
  99. pylgm-0.6.1/pyproject.toml +78 -0
  100. pylgm-0.6.1/src/pylgm/__init__.py +77 -0
  101. pylgm-0.6.1/src/pylgm/artifacts/__init__.py +6 -0
  102. pylgm-0.6.1/src/pylgm/artifacts/experiment.py +206 -0
  103. pylgm-0.6.1/src/pylgm/artifacts/run.py +252 -0
  104. pylgm-0.6.1/src/pylgm/cli.py +49 -0
  105. pylgm-0.6.1/src/pylgm/compiler.py +1234 -0
  106. pylgm-0.6.1/src/pylgm/config/__init__.py +15 -0
  107. pylgm-0.6.1/src/pylgm/config/experiment.py +271 -0
  108. pylgm-0.6.1/src/pylgm/config/load.py +57 -0
  109. pylgm-0.6.1/src/pylgm/config/model.py +336 -0
  110. pylgm-0.6.1/src/pylgm/config/schema.py +79 -0
  111. pylgm-0.6.1/src/pylgm/data/__init__.py +3 -0
  112. pylgm-0.6.1/src/pylgm/data/fingerprint.py +236 -0
  113. pylgm-0.6.1/src/pylgm/data/panel.py +126 -0
  114. pylgm-0.6.1/src/pylgm/data/scalars.py +69 -0
  115. pylgm-0.6.1/src/pylgm/data/spark.py +114 -0
  116. pylgm-0.6.1/src/pylgm/effects/__init__.py +69 -0
  117. pylgm-0.6.1/src/pylgm/effects/ar1.py +93 -0
  118. pylgm-0.6.1/src/pylgm/effects/besag.py +107 -0
  119. pylgm-0.6.1/src/pylgm/effects/bym2.py +105 -0
  120. pylgm-0.6.1/src/pylgm/effects/directed_graph.py +117 -0
  121. pylgm-0.6.1/src/pylgm/effects/fixed.py +19 -0
  122. pylgm-0.6.1/src/pylgm/effects/graph.py +235 -0
  123. pylgm-0.6.1/src/pylgm/effects/iid.py +22 -0
  124. pylgm-0.6.1/src/pylgm/effects/midas.py +96 -0
  125. pylgm-0.6.1/src/pylgm/effects/proper_car.py +98 -0
  126. pylgm-0.6.1/src/pylgm/effects/random_walk.py +54 -0
  127. pylgm-0.6.1/src/pylgm/effects/sar.py +152 -0
  128. pylgm-0.6.1/src/pylgm/effects/scaling.py +62 -0
  129. pylgm-0.6.1/src/pylgm/effects/sdpd_forecast.py +120 -0
  130. pylgm-0.6.1/src/pylgm/effects/seasonal.py +85 -0
  131. pylgm-0.6.1/src/pylgm/effects/spacetime.py +142 -0
  132. pylgm-0.6.1/src/pylgm/effects/spec.py +506 -0
  133. pylgm-0.6.1/src/pylgm/evaluation/__init__.py +21 -0
  134. pylgm-0.6.1/src/pylgm/evaluation/availability.py +30 -0
  135. pylgm-0.6.1/src/pylgm/evaluation/folds.py +297 -0
  136. pylgm-0.6.1/src/pylgm/evaluation/metrics.py +304 -0
  137. pylgm-0.6.1/src/pylgm/evaluation/persistence.py +89 -0
  138. pylgm-0.6.1/src/pylgm/evaluation/selection.py +174 -0
  139. pylgm-0.6.1/src/pylgm/exceptions.py +105 -0
  140. pylgm-0.6.1/src/pylgm/experiment.py +479 -0
  141. pylgm-0.6.1/src/pylgm/inference/__init__.py +26 -0
  142. pylgm-0.6.1/src/pylgm/inference/gaussian.py +272 -0
  143. pylgm-0.6.1/src/pylgm/inference/laplace.py +198 -0
  144. pylgm-0.6.1/src/pylgm/inference/prediction.py +410 -0
  145. pylgm-0.6.1/src/pylgm/inference/result.py +1104 -0
  146. pylgm-0.6.1/src/pylgm/inference/sparse.py +527 -0
  147. pylgm-0.6.1/src/pylgm/ir/__init__.py +27 -0
  148. pylgm-0.6.1/src/pylgm/ir/family.py +574 -0
  149. pylgm-0.6.1/src/pylgm/ir/model.py +311 -0
  150. pylgm-0.6.1/src/pylgm/likelihoods.py +652 -0
  151. pylgm-0.6.1/src/pylgm/links.py +38 -0
  152. pylgm-0.6.1/src/pylgm/model.py +532 -0
  153. pylgm-0.6.1/src/pylgm/optimization/__init__.py +16 -0
  154. pylgm-0.6.1/src/pylgm/optimization/empirical_bayes.py +322 -0
  155. pylgm-0.6.1/src/pylgm/optimization/inla.py +556 -0
  156. pylgm-0.6.1/src/pylgm/optimization/result.py +92 -0
  157. pylgm-0.6.1/src/pylgm/optimization/transforms.py +108 -0
  158. pylgm-0.6.1/src/pylgm/parameters.py +108 -0
  159. pylgm-0.6.1/src/pylgm/pipeline.py +28 -0
  160. pylgm-0.6.1/src/pylgm/priors.py +223 -0
  161. pylgm-0.6.1/tests/artifacts/test_experiment_artifacts.py +317 -0
  162. pylgm-0.6.1/tests/config/__init__.py +1 -0
  163. pylgm-0.6.1/tests/config/test_experiment.py +395 -0
  164. pylgm-0.6.1/tests/config/test_load.py +137 -0
  165. pylgm-0.6.1/tests/config/test_model.py +701 -0
  166. pylgm-0.6.1/tests/conftest.py +25 -0
  167. pylgm-0.6.1/tests/data/test_panel.py +101 -0
  168. pylgm-0.6.1/tests/data/test_spark.py +260 -0
  169. pylgm-0.6.1/tests/effects/test_ar1.py +69 -0
  170. pylgm-0.6.1/tests/effects/test_besag.py +132 -0
  171. pylgm-0.6.1/tests/effects/test_bym2.py +142 -0
  172. pylgm-0.6.1/tests/effects/test_directed_graph.py +60 -0
  173. pylgm-0.6.1/tests/effects/test_dynamic_spatial_panel.py +84 -0
  174. pylgm-0.6.1/tests/effects/test_graph.py +118 -0
  175. pylgm-0.6.1/tests/effects/test_proper_car.py +85 -0
  176. pylgm-0.6.1/tests/effects/test_proper_car_eigsh.py +38 -0
  177. pylgm-0.6.1/tests/effects/test_sar.py +54 -0
  178. pylgm-0.6.1/tests/effects/test_sdpd_forecast.py +52 -0
  179. pylgm-0.6.1/tests/effects/test_sorbye_scaling.py +24 -0
  180. pylgm-0.6.1/tests/effects/test_spec.py +336 -0
  181. pylgm-0.6.1/tests/effects/test_structured.py +71 -0
  182. pylgm-0.6.1/tests/evaluation/test_folds.py +442 -0
  183. pylgm-0.6.1/tests/evaluation/test_metrics.py +318 -0
  184. pylgm-0.6.1/tests/evaluation/test_persistence.py +143 -0
  185. pylgm-0.6.1/tests/evaluation/test_selection.py +189 -0
  186. pylgm-0.6.1/tests/inference/conftest.py +151 -0
  187. pylgm-0.6.1/tests/inference/result_surface_baseline.json +4318 -0
  188. pylgm-0.6.1/tests/inference/test_gaussian.py +472 -0
  189. pylgm-0.6.1/tests/inference/test_laplace.py +364 -0
  190. pylgm-0.6.1/tests/inference/test_prediction.py +106 -0
  191. pylgm-0.6.1/tests/inference/test_predictive_variance_convention.py +93 -0
  192. pylgm-0.6.1/tests/inference/test_quadratic_form_diagonal.py +24 -0
  193. pylgm-0.6.1/tests/inference/test_result.py +554 -0
  194. pylgm-0.6.1/tests/inference/test_result_surface.py +798 -0
  195. pylgm-0.6.1/tests/inference/test_sdpd_sparse.py +56 -0
  196. pylgm-0.6.1/tests/inference/test_sparse.py +599 -0
  197. pylgm-0.6.1/tests/inference/test_sparse_large_model.py +78 -0
  198. pylgm-0.6.1/tests/inference/test_sparse_result.py +142 -0
  199. pylgm-0.6.1/tests/inference/test_tabulated_marginals.py +33 -0
  200. pylgm-0.6.1/tests/integration/test_disease_mapping_config.py +20 -0
  201. pylgm-0.6.1/tests/integration/test_hybrid_nowcast.py +91 -0
  202. pylgm-0.6.1/tests/integration/test_nic_shaped_backtest.py +81 -0
  203. pylgm-0.6.1/tests/integration/test_predictive_selection.py +23 -0
  204. pylgm-0.6.1/tests/integration/test_synthetic_example.py +15 -0
  205. pylgm-0.6.1/tests/ir/test_compiled_family.py +54 -0
  206. pylgm-0.6.1/tests/ir/test_fixed.py +94 -0
  207. pylgm-0.6.1/tests/ir/test_gaussian_family.py +432 -0
  208. pylgm-0.6.1/tests/ir/test_model_validation.py +288 -0
  209. pylgm-0.6.1/tests/ir/test_parametric_block.py +140 -0
  210. pylgm-0.6.1/tests/optimization/test_empirical_bayes.py +965 -0
  211. pylgm-0.6.1/tests/optimization/test_full_laplace.py +131 -0
  212. pylgm-0.6.1/tests/optimization/test_inla.py +243 -0
  213. pylgm-0.6.1/tests/optimization/test_inla_criteria.py +100 -0
  214. pylgm-0.6.1/tests/optimization/test_simplified_laplace.py +86 -0
  215. pylgm-0.6.1/tests/optimization/test_skew_normal.py +33 -0
  216. pylgm-0.6.1/tests/optimization/test_transforms.py +67 -0
  217. pylgm-0.6.1/tests/test_api_ergonomics.py +92 -0
  218. pylgm-0.6.1/tests/test_ar1_fit.py +120 -0
  219. pylgm-0.6.1/tests/test_ar1_group.py +150 -0
  220. pylgm-0.6.1/tests/test_besag_fit.py +155 -0
  221. pylgm-0.6.1/tests/test_bound_diagnostics.py +125 -0
  222. pylgm-0.6.1/tests/test_bym2_fit.py +98 -0
  223. pylgm-0.6.1/tests/test_cli.py +110 -0
  224. pylgm-0.6.1/tests/test_compiler.py +691 -0
  225. pylgm-0.6.1/tests/test_experiment.py +366 -0
  226. pylgm-0.6.1/tests/test_extraconstr.py +125 -0
  227. pylgm-0.6.1/tests/test_identity_transform.py +28 -0
  228. pylgm-0.6.1/tests/test_links.py +22 -0
  229. pylgm-0.6.1/tests/test_midas_builder.py +120 -0
  230. pylgm-0.6.1/tests/test_midas_effect.py +124 -0
  231. pylgm-0.6.1/tests/test_midas_parametric_effect.py +62 -0
  232. pylgm-0.6.1/tests/test_midas_parametric_kernels.py +51 -0
  233. pylgm-0.6.1/tests/test_midas_parametric_predict.py +46 -0
  234. pylgm-0.6.1/tests/test_midas_parametric_spec.py +40 -0
  235. pylgm-0.6.1/tests/test_model.py +762 -0
  236. pylgm-0.6.1/tests/test_model_spark.py +78 -0
  237. pylgm-0.6.1/tests/test_modeling_vocabulary.py +491 -0
  238. pylgm-0.6.1/tests/test_package.py +259 -0
  239. pylgm-0.6.1/tests/test_parameters.py +24 -0
  240. pylgm-0.6.1/tests/test_parametric_design_block.py +36 -0
  241. pylgm-0.6.1/tests/test_pc_bym2_phi.py +87 -0
  242. pylgm-0.6.1/tests/test_pipeline.py +382 -0
  243. pylgm-0.6.1/tests/test_predict.py +219 -0
  244. pylgm-0.6.1/tests/test_proper_car_fit.py +96 -0
  245. pylgm-0.6.1/tests/test_proper_car_rho_estimation.py +103 -0
  246. pylgm-0.6.1/tests/test_public_exports.py +28 -0
  247. pylgm-0.6.1/tests/test_sar_fit.py +48 -0
  248. pylgm-0.6.1/tests/test_scaling.py +31 -0
  249. pylgm-0.6.1/tests/test_sdpd_fit.py +66 -0
  250. pylgm-0.6.1/tests/test_sdpd_forecast_integration.py +37 -0
  251. pylgm-0.6.1/tests/test_seasonal.py +144 -0
  252. pylgm-0.6.1/tests/test_spacetime_builder.py +126 -0
  253. pylgm-0.6.1/tests/test_spacetime_compile.py +71 -0
  254. pylgm-0.6.1/tests/test_spacetime_effect.py +78 -0
  255. pylgm-0.6.1/tests/test_spacetime_predict.py +50 -0
  256. pylgm-0.6.1/tests/test_spacetime_spec.py +53 -0
  257. pylgm-0.6.1/tests/test_weighted_graph.py +79 -0
  258. pylgm-0.6.1/tests/test_weighted_spatial_effects.py +45 -0
pylgm-0.6.1/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .superpowers/
4
+ .worktrees/
5
+ site/
6
+ dist/
7
+ validation/
8
+ .base/
9
+ .base-ast/
pylgm-0.6.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrea Panozzo
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.
pylgm-0.6.1/PKG-INFO ADDED
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.5
2
+ Name: pylgm
3
+ Version: 0.6.1
4
+ Summary: General-purpose latent Gaussian models for Python
5
+ Project-URL: Homepage, https://github.com/Ardea00/pylgm
6
+ Project-URL: Repository, https://github.com/Ardea00/pylgm
7
+ Project-URL: Issues, https://github.com/Ardea00/pylgm/issues
8
+ Author: Andrea Panozzo
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bayesian,inla,laplace approximation,latent gaussian models,statistics
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: formulaic>=1.1
21
+ Requires-Dist: numpy>=2.0
22
+ Requires-Dist: pandas>=2.2
23
+ Requires-Dist: pyarrow>=17
24
+ Requires-Dist: pydantic>=2.8
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: scipy>=1.14
27
+ Requires-Dist: typer>=0.15
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-xdist>=3.6; extra == 'dev'
30
+ Requires-Dist: pytest>=8.3; extra == 'dev'
31
+ Requires-Dist: ruff>=0.9; extra == 'dev'
32
+ Provides-Extra: docs
33
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
34
+ Provides-Extra: spark
35
+ Requires-Dist: pyspark>=3.5; extra == 'spark'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <h1 align="center">pyLGM</h1>
39
+
40
+ <p align="center">
41
+ <strong>General-purpose latent Gaussian models for Python — the model class behind INLA.</strong><br>
42
+ Deterministic Bayesian inference for structured data. No MCMC, no new dependencies.
43
+ </p>
44
+
45
+ <p align="center">
46
+ <a href="https://pypi.org/project/pylgm/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pylgm?color=3b82f6"></a>
47
+ <a href="https://pypi.org/project/pylgm/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/pylgm"></a>
48
+ <a href="https://github.com/Ardea00/pylgm/actions/workflows/test.yml"><img alt="CI" src="https://github.com/Ardea00/pylgm/actions/workflows/test.yml/badge.svg?branch=main"></a>
49
+ <a href="https://ardea00.github.io/pylgm/"><img alt="Docs" src="https://img.shields.io/badge/docs-ardea00.github.io%2Fpylgm-blue"></a>
50
+ <a href="LICENSE"><img alt="License" src="https://img.shields.io/pypi/l/pylgm?color=green"></a>
51
+ <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/badge/lint-ruff-261230"></a>
52
+ </p>
53
+
54
+ <p align="center">
55
+ <a href="https://ardea00.github.io/pylgm/">Documentation</a> ·
56
+ <a href="https://ardea00.github.io/pylgm/how-it-works/">How it works</a> ·
57
+ <a href="https://ardea00.github.io/pylgm/comparison/">Comparison</a> ·
58
+ <a href="https://ardea00.github.io/pylgm/examples/">Examples</a> ·
59
+ <a href="https://ardea00.github.io/pylgm/theory/">Theory</a>
60
+ </p>
61
+
62
+ ---
63
+
64
+ ## What it does
65
+
66
+ Declare structure — groups, space, time, frequency, networks — and pyLGM
67
+ returns a **posterior for each component**, not just a prediction. Fits run in
68
+ seconds through Laplace approximations and INLA-style integration, and are
69
+ deterministic: same data, same answer, no convergence diagnostics.
70
+
71
+ <p align="center">
72
+ <img src="docs/img/scotland_shrinkage.png" alt="Besag smoothing shrinks noisy rates toward local means" width="620">
73
+ </p>
74
+
75
+ <p align="center"><em>Partial pooling in one picture: noisy small-area rates (left, near zero) are
76
+ pulled toward their neighbours, while well-observed areas keep their signal.
77
+ Marker size is the expected count. From
78
+ <a href="https://ardea00.github.io/pylgm/examples-disease-mapping/">the disease-mapping example</a>.</em></p>
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ pip install pylgm
84
+ pip install "pylgm[spark]" # optional: Spark data boundary
85
+ ```
86
+
87
+ Requires Python ≥ 3.11.
88
+
89
+ ## 30-second example
90
+
91
+ A Poisson model with a per-region random intercept, fit with the Laplace engine:
92
+
93
+ ```python
94
+ import pandas as pd
95
+ from pylgm import Fixed, IID, LGM, Poisson
96
+
97
+ frame = pd.DataFrame({
98
+ "region": ["north", "north", "north", "south", "south", "south"],
99
+ "time": [1, 2, 3, 1, 2, 3],
100
+ "x": [0.0, 0.5, 1.0, 0.0, 0.5, 1.0],
101
+ "count": [3, 5, 8, 2, 3, 5],
102
+ })
103
+
104
+ model = LGM(
105
+ response="count",
106
+ likelihood=Poisson(), # canonical log link
107
+ predictor=Fixed("1 + x") # fixed effects
108
+ + IID("region", index="region", precision=2.0), # random intercept per region
109
+ panel=("region",),
110
+ time="time",
111
+ )
112
+
113
+ result = model.fit(frame, engine="laplace")
114
+ print("fitted_mean:", result.fitted_mean.round(3).tolist())
115
+ # fitted_mean: [3.254, 4.986, 8.15, 2.201, 3.373, 5.513]
116
+ ```
117
+
118
+ The same model can be declared in YAML and loaded with `pylgm.config.load_model`.
119
+
120
+ ## Reproducing a published result
121
+
122
+ [`examples/columbus_spatial_econometrics`](examples/columbus_spatial_econometrics)
123
+ reproduces the reference result of spatial econometrics — Anselin (1988),
124
+ Table 12.1, 49 Columbus OH neighbourhoods:
125
+
126
+ | model | const | INC | HOVAL | ρ / λ |
127
+ |---|---|---|---|---|
128
+ | published OLS | 68.619 | −1.5973 | −0.2739 | — |
129
+ | **OLS, recomputed** | **68.619** | **−1.5973** | **−0.2739** | — |
130
+ | published ML spatial error | 60.279 | −0.9573 | −0.3046 | 0.5468 |
131
+ | **pyLGM `SAR`** | **59.543** | **−0.9057** | **−0.3058** | **0.5946** |
132
+
133
+ OLS matching the published numbers exactly verifies the data and spec; the
134
+ `SAR` fit then lands next to the published ML spatial-error estimates, and
135
+ recovers the finding that ignoring spatial correlation **overstates the income
136
+ effect by 1.76×**.
137
+
138
+ ## A network that changes every year
139
+
140
+ [`examples/state_income_dynamic_network`](examples/state_income_dynamic_network)
141
+ fits 48 US states over 1997–2007 with **one network per year**, then knocks out
142
+ 20% of the panel and restores it:
143
+
144
+ | method | RMSE ↓ (log income) |
145
+ |---|---|
146
+ | **`DynamicSpatialPanel`** | **0.0246** |
147
+ | state mean | 0.1214 |
148
+ | year mean | 0.1484 |
149
+
150
+ ~5× closer than the obvious baselines. The same example reports where it loses
151
+ — a last-value forecast beats it on level forecasts, because the fitted γ ≈ 1
152
+ says log income is near a random walk.
153
+
154
+ ## Why not just use XGBoost?
155
+
156
+ Often you should — and the [comparison page](https://ardea00.github.io/pylgm/comparison/)
157
+ says so with measured numbers rather than adjectives. Both problems below are
158
+ simulated, so the *true* surface is known and scored directly:
159
+
160
+ | Problem | pyLGM | XGBoost | GLM |
161
+ |---|---|---|---|
162
+ | 200 areas, 3 counts each, smooth spatial signal (RMSE ↓) | **0.135** | 0.371 | 0.429 |
163
+ | 4000 rows, nonlinear covariate interactions (RMSE ↓) | 2.839 | **0.293** | — |
164
+
165
+ pyLGM wins where the per-unit sample is thin but the units are related, and
166
+ returns 95% intervals that cover the truth 99% of the time. Gradient boosting
167
+ wins where the signal is interactions among covariates and you have the rows to
168
+ learn them. Reproduce both with
169
+ [`examples/method_comparison`](examples/method_comparison).
170
+
171
+ ## What's in the box
172
+
173
+ | Area | What you get | Docs |
174
+ |---|---|---|
175
+ | **Likelihoods** | Gaussian (exact), Poisson, Bernoulli, Binomial, negative-binomial, Gamma, Beta, Weibull/exponential survival | [likelihoods](https://ardea00.github.io/pylgm/likelihoods/) |
176
+ | **Effects** | `Fixed`, `IID`, `RW1`/`RW2`, `AR1` (optionally group-wise), `Seasonal`, `MIDAS`, `MIDASParametric`, `SpaceTime` | [effects](https://ardea00.github.io/pylgm/effects/) |
177
+ | **Spatial** | `Besag` (ICAR), `ProperCAR`, `BYM2`, weighted graphs | [spatial](https://ardea00.github.io/pylgm/spatial-effects/) |
178
+ | **Networks** | directed `SAR`, dynamic `DynamicSpatialPanel` (SDPD) with forward forecasting | [spatial](https://ardea00.github.io/pylgm/spatial-effects/) |
179
+ | **Scale** | sparse solver past the dense guard, with the full uncertainty surface | [internals](https://ardea00.github.io/pylgm/internals/) |
180
+ | **Hyperparameters** | Empirical Bayes (type-II ML), MAP-II with PC priors, bounds | [empirical bayes](https://ardea00.github.io/pylgm/empirical-bayes/) |
181
+ | **Integration** | INLA grid quadrature, simplified/full-Laplace marginals, DIC/WAIC/CPO/PIT | [INLA](https://ardea00.github.io/pylgm/inla/) |
182
+ | **Prediction** | fit-row, out-of-sample `predict`, and forecasting via `NaN` rows | [prediction](https://ardea00.github.io/pylgm/prediction/) |
183
+ | **Constraints** | arbitrary linear constraints `A x = e` (R-INLA `extraconstr`) | [effects](https://ardea00.github.io/pylgm/effects/#linear-constraints-extraconstr) |
184
+ | **Data boundary** | Pandas, or Spark / Databricks | [spark](https://ardea00.github.io/pylgm/spark/) |
185
+
186
+ ## Documentation
187
+
188
+ Full docs: **https://ardea00.github.io/pylgm/**
189
+
190
+ - [How it works](https://ardea00.github.io/pylgm/how-it-works/) — one `fit()` call end to end, then prediction and forecasting
191
+ - [Comparison](https://ardea00.github.io/pylgm/comparison/) — against regression, gradient boosting and MCMC, including where pyLGM loses
192
+ - [Theory](https://ardea00.github.io/pylgm/theory/) — the model class and every structured effect, with references
193
+ - [Examples](https://ardea00.github.io/pylgm/examples/) — 21 runnable scripts
194
+
195
+ ## Roadmap
196
+
197
+ What's shipped and what's next is on the
198
+ [roadmap](https://ardea00.github.io/pylgm/roadmap/); scope and compatibility
199
+ policy on the [internals](https://ardea00.github.io/pylgm/internals/) page.
200
+
201
+ ## Contributing
202
+
203
+ Issues and pull requests welcome — see
204
+ [docs/development.md](docs/development.md) for the test setup. `main` is
205
+ protected: PRs need the full CI matrix green before merge.
206
+
207
+ ## License
208
+
209
+ [MIT](LICENSE)
pylgm-0.6.1/README.md ADDED
@@ -0,0 +1,172 @@
1
+ <h1 align="center">pyLGM</h1>
2
+
3
+ <p align="center">
4
+ <strong>General-purpose latent Gaussian models for Python — the model class behind INLA.</strong><br>
5
+ Deterministic Bayesian inference for structured data. No MCMC, no new dependencies.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://pypi.org/project/pylgm/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pylgm?color=3b82f6"></a>
10
+ <a href="https://pypi.org/project/pylgm/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/pylgm"></a>
11
+ <a href="https://github.com/Ardea00/pylgm/actions/workflows/test.yml"><img alt="CI" src="https://github.com/Ardea00/pylgm/actions/workflows/test.yml/badge.svg?branch=main"></a>
12
+ <a href="https://ardea00.github.io/pylgm/"><img alt="Docs" src="https://img.shields.io/badge/docs-ardea00.github.io%2Fpylgm-blue"></a>
13
+ <a href="LICENSE"><img alt="License" src="https://img.shields.io/pypi/l/pylgm?color=green"></a>
14
+ <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/badge/lint-ruff-261230"></a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="https://ardea00.github.io/pylgm/">Documentation</a> ·
19
+ <a href="https://ardea00.github.io/pylgm/how-it-works/">How it works</a> ·
20
+ <a href="https://ardea00.github.io/pylgm/comparison/">Comparison</a> ·
21
+ <a href="https://ardea00.github.io/pylgm/examples/">Examples</a> ·
22
+ <a href="https://ardea00.github.io/pylgm/theory/">Theory</a>
23
+ </p>
24
+
25
+ ---
26
+
27
+ ## What it does
28
+
29
+ Declare structure — groups, space, time, frequency, networks — and pyLGM
30
+ returns a **posterior for each component**, not just a prediction. Fits run in
31
+ seconds through Laplace approximations and INLA-style integration, and are
32
+ deterministic: same data, same answer, no convergence diagnostics.
33
+
34
+ <p align="center">
35
+ <img src="docs/img/scotland_shrinkage.png" alt="Besag smoothing shrinks noisy rates toward local means" width="620">
36
+ </p>
37
+
38
+ <p align="center"><em>Partial pooling in one picture: noisy small-area rates (left, near zero) are
39
+ pulled toward their neighbours, while well-observed areas keep their signal.
40
+ Marker size is the expected count. From
41
+ <a href="https://ardea00.github.io/pylgm/examples-disease-mapping/">the disease-mapping example</a>.</em></p>
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install pylgm
47
+ pip install "pylgm[spark]" # optional: Spark data boundary
48
+ ```
49
+
50
+ Requires Python ≥ 3.11.
51
+
52
+ ## 30-second example
53
+
54
+ A Poisson model with a per-region random intercept, fit with the Laplace engine:
55
+
56
+ ```python
57
+ import pandas as pd
58
+ from pylgm import Fixed, IID, LGM, Poisson
59
+
60
+ frame = pd.DataFrame({
61
+ "region": ["north", "north", "north", "south", "south", "south"],
62
+ "time": [1, 2, 3, 1, 2, 3],
63
+ "x": [0.0, 0.5, 1.0, 0.0, 0.5, 1.0],
64
+ "count": [3, 5, 8, 2, 3, 5],
65
+ })
66
+
67
+ model = LGM(
68
+ response="count",
69
+ likelihood=Poisson(), # canonical log link
70
+ predictor=Fixed("1 + x") # fixed effects
71
+ + IID("region", index="region", precision=2.0), # random intercept per region
72
+ panel=("region",),
73
+ time="time",
74
+ )
75
+
76
+ result = model.fit(frame, engine="laplace")
77
+ print("fitted_mean:", result.fitted_mean.round(3).tolist())
78
+ # fitted_mean: [3.254, 4.986, 8.15, 2.201, 3.373, 5.513]
79
+ ```
80
+
81
+ The same model can be declared in YAML and loaded with `pylgm.config.load_model`.
82
+
83
+ ## Reproducing a published result
84
+
85
+ [`examples/columbus_spatial_econometrics`](examples/columbus_spatial_econometrics)
86
+ reproduces the reference result of spatial econometrics — Anselin (1988),
87
+ Table 12.1, 49 Columbus OH neighbourhoods:
88
+
89
+ | model | const | INC | HOVAL | ρ / λ |
90
+ |---|---|---|---|---|
91
+ | published OLS | 68.619 | −1.5973 | −0.2739 | — |
92
+ | **OLS, recomputed** | **68.619** | **−1.5973** | **−0.2739** | — |
93
+ | published ML spatial error | 60.279 | −0.9573 | −0.3046 | 0.5468 |
94
+ | **pyLGM `SAR`** | **59.543** | **−0.9057** | **−0.3058** | **0.5946** |
95
+
96
+ OLS matching the published numbers exactly verifies the data and spec; the
97
+ `SAR` fit then lands next to the published ML spatial-error estimates, and
98
+ recovers the finding that ignoring spatial correlation **overstates the income
99
+ effect by 1.76×**.
100
+
101
+ ## A network that changes every year
102
+
103
+ [`examples/state_income_dynamic_network`](examples/state_income_dynamic_network)
104
+ fits 48 US states over 1997–2007 with **one network per year**, then knocks out
105
+ 20% of the panel and restores it:
106
+
107
+ | method | RMSE ↓ (log income) |
108
+ |---|---|
109
+ | **`DynamicSpatialPanel`** | **0.0246** |
110
+ | state mean | 0.1214 |
111
+ | year mean | 0.1484 |
112
+
113
+ ~5× closer than the obvious baselines. The same example reports where it loses
114
+ — a last-value forecast beats it on level forecasts, because the fitted γ ≈ 1
115
+ says log income is near a random walk.
116
+
117
+ ## Why not just use XGBoost?
118
+
119
+ Often you should — and the [comparison page](https://ardea00.github.io/pylgm/comparison/)
120
+ says so with measured numbers rather than adjectives. Both problems below are
121
+ simulated, so the *true* surface is known and scored directly:
122
+
123
+ | Problem | pyLGM | XGBoost | GLM |
124
+ |---|---|---|---|
125
+ | 200 areas, 3 counts each, smooth spatial signal (RMSE ↓) | **0.135** | 0.371 | 0.429 |
126
+ | 4000 rows, nonlinear covariate interactions (RMSE ↓) | 2.839 | **0.293** | — |
127
+
128
+ pyLGM wins where the per-unit sample is thin but the units are related, and
129
+ returns 95% intervals that cover the truth 99% of the time. Gradient boosting
130
+ wins where the signal is interactions among covariates and you have the rows to
131
+ learn them. Reproduce both with
132
+ [`examples/method_comparison`](examples/method_comparison).
133
+
134
+ ## What's in the box
135
+
136
+ | Area | What you get | Docs |
137
+ |---|---|---|
138
+ | **Likelihoods** | Gaussian (exact), Poisson, Bernoulli, Binomial, negative-binomial, Gamma, Beta, Weibull/exponential survival | [likelihoods](https://ardea00.github.io/pylgm/likelihoods/) |
139
+ | **Effects** | `Fixed`, `IID`, `RW1`/`RW2`, `AR1` (optionally group-wise), `Seasonal`, `MIDAS`, `MIDASParametric`, `SpaceTime` | [effects](https://ardea00.github.io/pylgm/effects/) |
140
+ | **Spatial** | `Besag` (ICAR), `ProperCAR`, `BYM2`, weighted graphs | [spatial](https://ardea00.github.io/pylgm/spatial-effects/) |
141
+ | **Networks** | directed `SAR`, dynamic `DynamicSpatialPanel` (SDPD) with forward forecasting | [spatial](https://ardea00.github.io/pylgm/spatial-effects/) |
142
+ | **Scale** | sparse solver past the dense guard, with the full uncertainty surface | [internals](https://ardea00.github.io/pylgm/internals/) |
143
+ | **Hyperparameters** | Empirical Bayes (type-II ML), MAP-II with PC priors, bounds | [empirical bayes](https://ardea00.github.io/pylgm/empirical-bayes/) |
144
+ | **Integration** | INLA grid quadrature, simplified/full-Laplace marginals, DIC/WAIC/CPO/PIT | [INLA](https://ardea00.github.io/pylgm/inla/) |
145
+ | **Prediction** | fit-row, out-of-sample `predict`, and forecasting via `NaN` rows | [prediction](https://ardea00.github.io/pylgm/prediction/) |
146
+ | **Constraints** | arbitrary linear constraints `A x = e` (R-INLA `extraconstr`) | [effects](https://ardea00.github.io/pylgm/effects/#linear-constraints-extraconstr) |
147
+ | **Data boundary** | Pandas, or Spark / Databricks | [spark](https://ardea00.github.io/pylgm/spark/) |
148
+
149
+ ## Documentation
150
+
151
+ Full docs: **https://ardea00.github.io/pylgm/**
152
+
153
+ - [How it works](https://ardea00.github.io/pylgm/how-it-works/) — one `fit()` call end to end, then prediction and forecasting
154
+ - [Comparison](https://ardea00.github.io/pylgm/comparison/) — against regression, gradient boosting and MCMC, including where pyLGM loses
155
+ - [Theory](https://ardea00.github.io/pylgm/theory/) — the model class and every structured effect, with references
156
+ - [Examples](https://ardea00.github.io/pylgm/examples/) — 21 runnable scripts
157
+
158
+ ## Roadmap
159
+
160
+ What's shipped and what's next is on the
161
+ [roadmap](https://ardea00.github.io/pylgm/roadmap/); scope and compatibility
162
+ policy on the [internals](https://ardea00.github.io/pylgm/internals/) page.
163
+
164
+ ## Contributing
165
+
166
+ Issues and pull requests welcome — see
167
+ [docs/development.md](docs/development.md) for the test setup. `main` is
168
+ protected: PRs need the full CI matrix green before merge.
169
+
170
+ ## License
171
+
172
+ [MIT](LICENSE)
@@ -0,0 +1,214 @@
1
+ # How pyLGM compares
2
+
3
+ pyLGM is not a general-purpose predictor. It is a **structured-uncertainty**
4
+ method: it assumes the signal decomposes into smooth, spatial, temporal or
5
+ network-shaped pieces, and in exchange it returns calibrated uncertainty for
6
+ each piece. That assumption is what makes it strong in some problems and the
7
+ wrong tool in others.
8
+
9
+ Every number on this page comes from
10
+ [`examples/method_comparison`](https://github.com/Ardea00/pylgm/tree/main/examples/method_comparison),
11
+ which you can run yourself. Both problems are **simulated**, so the true latent
12
+ surface is known and scored directly — the honest target for a smoothing method,
13
+ rather than in-sample fit to noisy observations.
14
+
15
+ ## The short version
16
+
17
+ | | classical regression / GLM | gradient boosting (XGBoost) | MCMC | **pyLGM** |
18
+ |---|---|---|---|---|
19
+ | Nonlinear interactions, many features | weak | **excellent** | depends on model | weak |
20
+ | Few observations per group | overfits or needs manual pooling | overfits | good | **excellent** |
21
+ | Spatial / temporal / network structure | manual basis functions | must be learned from data | good | **built in** |
22
+ | Calibrated uncertainty | for the fitted parameters | none natively | **exact in the limit** | **yes, per component** |
23
+ | Extrapolating past the data | linear only | **cannot** — constant beyond the hull | good | **yes**, via the structure |
24
+ | Runtime | seconds | seconds | minutes to hours | **seconds** |
25
+ | Reproducibility | exact | exact given a seed | needs convergence checks | **exact, deterministic** |
26
+
27
+ ## Versus regression and GLMs
28
+
29
+ A GLM with a dummy per group is the honest baseline, and it fails in a specific
30
+ way: with few observations per group, each dummy is estimated from almost no
31
+ data, so the estimates are noisy and the extreme ones are the most wrong.
32
+ Nothing tells a rare-event area from a genuinely quiet one.
33
+
34
+ pyLGM's structured effects apply **partial pooling** — each area borrows
35
+ strength from its neighbours, at a rate the data itself chooses through the
36
+ estimated precision \(\tau\). A GLM can only choose between no pooling (dummies)
37
+ and complete pooling (drop the term).
38
+
39
+ **Problem A: 200 areas, 3 Poisson observations each, smooth spatial signal.**
40
+ RMSE against the *true* latent field:
41
+
42
+ | method | RMSE ↓ |
43
+ |---|---|
44
+ | **pyLGM (`Besag`)** | **0.1345** |
45
+ | XGBoost | 0.3708 |
46
+ | GLM, area dummies | 0.4288 |
47
+
48
+ pyLGM is about **3× closer to the truth**, and its 95% credible intervals
49
+ contain the true value **99%** of the time — slightly conservative, which is the
50
+ safe direction to be wrong. Neither baseline produces an interval at all.
51
+
52
+ This is the regime the method exists for: small-area estimation, disease
53
+ mapping, sparse panels, any setting where the per-unit sample is thin but the
54
+ units are related.
55
+
56
+ ## On real data, against a published result
57
+
58
+ Simulations are a fair test of a smoothing method but a weak one for
59
+ credibility, so
60
+ [`examples/columbus_spatial_econometrics`](https://github.com/Ardea00/pylgm/tree/main/examples/columbus_spatial_econometrics)
61
+ reproduces the reference result of spatial econometrics: Anselin (1988),
62
+ Table 12.1 — 49 Columbus, OH neighbourhoods, crime on income and housing value.
63
+
64
+ | model | const | INC | HOVAL | ρ / λ |
65
+ |---|---|---|---|---|
66
+ | published OLS | 68.619 | −1.5973 | −0.2739 | — |
67
+ | **OLS, recomputed here** | **68.619** | **−1.5973** | **−0.2739** | — |
68
+ | published ML spatial error | 60.279 | −0.9573 | −0.3046 | 0.5468 |
69
+ | **pyLGM `SAR`** | **59.543** | **−0.9057** | **−0.3058** | **0.5946** |
70
+
71
+ The OLS row matching the published values *exactly* is what verifies the data
72
+ and specification. pyLGM's `SAR` then lands next to the published ML
73
+ spatial-error estimates, and the economic conclusion reproduces cleanly:
74
+ ignoring spatial correlation **overstates the income effect by 1.76×**.
75
+
76
+ pyLGM is not re-implementing ML estimation — it is Bayesian, and carries a
77
+ nugget the ML spatial-error model does not — so exact agreement would be
78
+ suspicious rather than reassuring. The residual gap shows up in ρ (0.595 vs
79
+ 0.547).
80
+
81
+ ### Where the structure earns its keep
82
+
83
+ [`examples/state_income_dynamic_network`](https://github.com/Ardea00/pylgm/tree/main/examples/state_income_dynamic_network)
84
+ fits 48 US states over 1997–2007 with **a different network every year** —
85
+ contiguity weighted by the previous year's income similarity — then knocks 20%
86
+ of the panel cells out and asks each method to restore them:
87
+
88
+ | method | RMSE ↓ (log income) |
89
+ |---|---|
90
+ | **pyLGM `DynamicSpatialPanel`** | **0.0246** |
91
+ | state mean | 0.1214 |
92
+ | year mean | 0.1484 |
93
+ | grand mean | 0.1786 |
94
+
95
+ About **5× closer**, because a missing cell is reconstructed from both its own
96
+ history and its neighbours' current values. Panel gaps are the ordinary case,
97
+ not a contrived one.
98
+
99
+ That example also reports where it **loses**: on a plain level forecast a
100
+ last-value benchmark wins (0.029 vs 0.040). That is not a defect — the fitted
101
+ γ ≈ 1.005 says log income is near a random walk, and for a random walk the last
102
+ observed value *is* the optimal forecast.
103
+
104
+ ## Versus gradient boosting
105
+
106
+ Boosted trees are excellent at exactly what LGMs are bad at: discovering
107
+ nonlinear interactions among many covariates when you have enough rows to learn
108
+ them.
109
+
110
+ **Problem B: 4000 rows, response driven by products and thresholds of three
111
+ continuous covariates.** RMSE against the true signal on held-out rows:
112
+
113
+ | method | RMSE ↓ |
114
+ |---|---|
115
+ | **XGBoost** | **0.2934** |
116
+ | pyLGM, linear predictor | 2.8391 |
117
+
118
+ XGBoost is roughly **10× better**, and no amount of tuning a linear predictor
119
+ closes that gap — the model class simply does not contain the interaction. If
120
+ your problem looks like this, use gradient boosting.
121
+
122
+ Three differences matter beyond accuracy:
123
+
124
+ - **Uncertainty.** XGBoost returns a point prediction. Quantile objectives or
125
+ conformal wrappers add intervals, but they are not a posterior and they do not
126
+ decompose by component. pyLGM gives you the posterior for *each term*
127
+ separately — `latent_marginals("region")` versus `latent_marginals("trend")`.
128
+ - **Extrapolation.** A tree is constant outside the range of its training data,
129
+ so a boosted model cannot forecast a trend past the last observed period.
130
+ A `RW1` or `AR1` extrapolates because the *structure* carries it forward; see
131
+ [prediction](prediction.md).
132
+ - **Structure as an input, not a discovery.** If you know regions are adjacent
133
+ or periods are ordered, pyLGM takes that as a graph or an index. A tree must
134
+ rediscover it from data, which costs the data you did not have.
135
+
136
+ **They compose.** These are not exclusive: boosting the residuals of a fitted
137
+ LGM, or feeding an LGM's smoothed component in as a feature, is often better
138
+ than either alone.
139
+
140
+ ## Versus MCMC and Monte Carlo
141
+
142
+ This is a comparison of *inference style*, not model class — MCMC can fit
143
+ latent Gaussian models too, and for a long time that was the standard way.
144
+
145
+ Monte Carlo error falls as \(1/\sqrt{S}\) in the number of draws \(S\). A
146
+ deterministic method has no such error. **Problem C** targets a small Gaussian
147
+ model whose posterior is available in closed form, and compares one exact solve
148
+ against a random-walk Metropolis sampler:
149
+
150
+ | method | distance from the exact posterior mean ↓ |
151
+ |---|---|
152
+ | **pyLGM (one solve)** | **0.0000** |
153
+ | Metropolis, 1 000 draws | 0.2066 |
154
+ | Metropolis, 10 000 draws | 0.0638 |
155
+ | Metropolis, 100 000 draws | 0.0161 |
156
+
157
+ Each 10× in draws buys about \(\sqrt{10} \approx 3.2\times\) accuracy — visible
158
+ in the table, and the reason sampling gets expensive when you want another
159
+ digit. For a Gaussian likelihood pyLGM's answer is not an approximation at all;
160
+ for non-Gaussian likelihoods it is a Laplace approximation whose error comes
161
+ from the posterior's shape rather than from a random seed.
162
+
163
+ What this buys, in practice:
164
+
165
+ - **No convergence diagnostics.** No burn-in, no thinning, no \(\hat{R}\), no
166
+ divergences to interpret. A fit either converges or raises.
167
+ - **Determinism.** The same data give bit-identical results, which matters for
168
+ a production pipeline and for testing.
169
+ - **Speed.** Seconds instead of minutes-to-hours for models of this shape.
170
+
171
+ What you give up:
172
+
173
+ - **Model class.** INLA-style inference requires the latent field to be
174
+ Gaussian and the hyperparameters to be few. Outside that, use MCMC.
175
+ - **Exactness in hard posteriors.** For strongly non-Gaussian or multimodal
176
+ posteriors, a Laplace approximation can be biased where MCMC, run long
177
+ enough, is not. pyLGM's simplified- and full-Laplace latent marginals
178
+ ([INLA integration](inla.md)) exist to narrow that gap, not to close it.
179
+ - **Many hyperparameters.** The INLA grid grows as \((2r+1)^d\) in the number
180
+ \(d\) of hyperparameters, so it is practical for a handful, not dozens.
181
+
182
+ ## When *not* to reach for pyLGM
183
+
184
+ Being clear about this is more useful than a feature list:
185
+
186
+ - The signal is dominated by **interactions between many covariates** → gradient
187
+ boosting.
188
+ - You have **no structure to exploit** — no groups, no space, no time, no
189
+ network — and plenty of rows per pattern → almost anything else is simpler.
190
+ - Your latent field is **genuinely non-Gaussian** (heavy-tailed, multimodal,
191
+ discrete states) → MCMC, or a mixture/state-space tool.
192
+ - You need **dozens of hyperparameters** estimated jointly → the grid does not
193
+ scale there.
194
+ - You want a **black-box predictor** and do not care how the prediction
195
+ decomposes → the structure is overhead you are not using.
196
+
197
+ ## Reproducing this page
198
+
199
+ ```bash
200
+ pip install scikit-learn xgboost # not pyLGM dependencies
201
+ PYTHONPATH=src python examples/method_comparison/run.py
202
+ ```
203
+
204
+ The script prints exactly the tables above; a CI test runs it when those
205
+ packages are installed and skips otherwise, so pyLGM itself keeps no dependency
206
+ on scikit-learn or xgboost.
207
+
208
+ The two real-data examples need nothing beyond pyLGM, and both are checked in
209
+ CI:
210
+
211
+ ```bash
212
+ PYTHONPATH=src python examples/columbus_spatial_econometrics/run.py
213
+ PYTHONPATH=src python examples/state_income_dynamic_network/run.py # ~40s
214
+ ```
@@ -0,0 +1,30 @@
1
+ # Development installation
2
+
3
+ ```bash
4
+ python -m pip install -e ".[dev]"
5
+ ```
6
+
7
+ ## Running the tests
8
+
9
+ ```bash
10
+ pytest -q
11
+ ```
12
+
13
+ The suite is safe to run in parallel, and CI does:
14
+
15
+ ```bash
16
+ pytest -q -n auto
17
+ ```
18
+
19
+ That roughly halves wall time. A handful of large sparse fits (past the dense
20
+ reference guard) dominate the total, so the parallel floor is whichever of
21
+ those is slowest — running with `-n auto` locally is worth it, but `-x` and
22
+ readable tracebacks are easier serially.
23
+
24
+ The large-model tests size themselves from
25
+ `pylgm.inference.gaussian._MAX_DENSE_LATENT_DIMENSION` rather than a literal, so
26
+ they stay just past the guard if that threshold ever moves. Keep them that way:
27
+ the sparse cost grows superlinearly, and being *far* past the guard buys no
28
+ extra coverage.
29
+
30
+ See the [approved design](https://github.com/Ardea00/pylgm/blob/main/docs/design/specs/2026-07-22-pylgm-design.md).