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.
- pylgm-0.6.1/.gitignore +9 -0
- pylgm-0.6.1/LICENSE +21 -0
- pylgm-0.6.1/PKG-INFO +209 -0
- pylgm-0.6.1/README.md +172 -0
- pylgm-0.6.1/docs/comparison.md +214 -0
- pylgm-0.6.1/docs/development.md +30 -0
- pylgm-0.6.1/docs/effects.md +505 -0
- pylgm-0.6.1/docs/empirical-bayes.md +139 -0
- pylgm-0.6.1/docs/examples-columbus.md +119 -0
- pylgm-0.6.1/docs/examples-count-regression.md +89 -0
- pylgm-0.6.1/docs/examples-disease-mapping.md +81 -0
- pylgm-0.6.1/docs/examples-dynamic-network.md +146 -0
- pylgm-0.6.1/docs/examples.md +66 -0
- pylgm-0.6.1/docs/how-it-works.md +202 -0
- pylgm-0.6.1/docs/img/columbus_coefficients.png +0 -0
- pylgm-0.6.1/docs/img/columbus_spatial_field.png +0 -0
- pylgm-0.6.1/docs/img/crabs_fit.png +0 -0
- pylgm-0.6.1/docs/img/crabs_prediction.png +0 -0
- pylgm-0.6.1/docs/img/scotland_fit.png +0 -0
- pylgm-0.6.1/docs/img/scotland_shrinkage.png +0 -0
- pylgm-0.6.1/docs/img/state_cell_recovery.png +0 -0
- pylgm-0.6.1/docs/img/state_network_drift.png +0 -0
- pylgm-0.6.1/docs/index.md +75 -0
- pylgm-0.6.1/docs/inla.md +212 -0
- pylgm-0.6.1/docs/internals.md +89 -0
- pylgm-0.6.1/docs/javascripts/mathjax.js +17 -0
- pylgm-0.6.1/docs/likelihoods.md +277 -0
- pylgm-0.6.1/docs/model-comparison.md +148 -0
- pylgm-0.6.1/docs/prediction.md +142 -0
- pylgm-0.6.1/docs/roadmap.md +117 -0
- pylgm-0.6.1/docs/spark.md +57 -0
- pylgm-0.6.1/docs/spatial-effects.md +535 -0
- pylgm-0.6.1/docs/theory.md +398 -0
- pylgm-0.6.1/examples/columbus_spatial_econometrics/README.md +58 -0
- pylgm-0.6.1/examples/columbus_spatial_econometrics/data.csv +50 -0
- pylgm-0.6.1/examples/columbus_spatial_econometrics/graph.json +336 -0
- pylgm-0.6.1/examples/columbus_spatial_econometrics/run.py +171 -0
- pylgm-0.6.1/examples/count_glm/README.md +31 -0
- pylgm-0.6.1/examples/count_glm/config.yaml +11 -0
- pylgm-0.6.1/examples/count_glm/data.csv +9 -0
- pylgm-0.6.1/examples/count_glm/run.py +22 -0
- pylgm-0.6.1/examples/count_regression/README.md +19 -0
- pylgm-0.6.1/examples/count_regression/data.csv +174 -0
- pylgm-0.6.1/examples/count_regression/run.py +155 -0
- pylgm-0.6.1/examples/directed_network_sar/README.md +61 -0
- pylgm-0.6.1/examples/directed_network_sar/run.py +81 -0
- pylgm-0.6.1/examples/disease_mapping/README.md +36 -0
- pylgm-0.6.1/examples/disease_mapping/config.yaml +23 -0
- pylgm-0.6.1/examples/disease_mapping/data.csv +57 -0
- pylgm-0.6.1/examples/disease_mapping/graph.json +1 -0
- pylgm-0.6.1/examples/disease_mapping/run.py +134 -0
- pylgm-0.6.1/examples/empirical_bayes/README.md +42 -0
- pylgm-0.6.1/examples/empirical_bayes/data.csv +81 -0
- pylgm-0.6.1/examples/empirical_bayes/run.py +33 -0
- pylgm-0.6.1/examples/general_lgm/README.md +41 -0
- pylgm-0.6.1/examples/general_lgm/config.yaml +18 -0
- pylgm-0.6.1/examples/general_lgm/data.csv +7 -0
- pylgm-0.6.1/examples/general_lgm/run.py +40 -0
- pylgm-0.6.1/examples/general_lgm/run_spark.py +32 -0
- pylgm-0.6.1/examples/hybrid_nowcast/README.md +55 -0
- pylgm-0.6.1/examples/hybrid_nowcast/run.py +88 -0
- pylgm-0.6.1/examples/inla/README.md +59 -0
- pylgm-0.6.1/examples/inla/data.csv +145 -0
- pylgm-0.6.1/examples/inla/run.py +43 -0
- pylgm-0.6.1/examples/inla_criteria/README.md +64 -0
- pylgm-0.6.1/examples/inla_criteria/data.csv +145 -0
- pylgm-0.6.1/examples/inla_criteria/run.py +40 -0
- pylgm-0.6.1/examples/inla_full_laplace/README.md +78 -0
- pylgm-0.6.1/examples/inla_full_laplace/data.csv +133 -0
- pylgm-0.6.1/examples/inla_full_laplace/run.py +69 -0
- pylgm-0.6.1/examples/inla_sla/README.md +68 -0
- pylgm-0.6.1/examples/inla_sla/data.csv +133 -0
- pylgm-0.6.1/examples/inla_sla/run.py +52 -0
- pylgm-0.6.1/examples/map_ii/README.md +53 -0
- pylgm-0.6.1/examples/map_ii/data.csv +81 -0
- pylgm-0.6.1/examples/map_ii/run.py +37 -0
- pylgm-0.6.1/examples/method_comparison/README.md +47 -0
- pylgm-0.6.1/examples/method_comparison/run.py +198 -0
- pylgm-0.6.1/examples/midas_nowcast/README.md +36 -0
- pylgm-0.6.1/examples/midas_nowcast/run.py +53 -0
- pylgm-0.6.1/examples/nic_backtest/README.md +25 -0
- pylgm-0.6.1/examples/nic_backtest/config.yaml +38 -0
- pylgm-0.6.1/examples/predictive_selection/README.md +20 -0
- pylgm-0.6.1/examples/predictive_selection/config.yaml +36 -0
- pylgm-0.6.1/examples/predictive_selection/data.csv +31 -0
- pylgm-0.6.1/examples/state_income_dynamic_network/README.md +80 -0
- pylgm-0.6.1/examples/state_income_dynamic_network/adjacency.json +312 -0
- pylgm-0.6.1/examples/state_income_dynamic_network/income.csv +49 -0
- pylgm-0.6.1/examples/state_income_dynamic_network/run.py +238 -0
- pylgm-0.6.1/examples/survival_duration/README.md +96 -0
- pylgm-0.6.1/examples/survival_duration/run.py +89 -0
- pylgm-0.6.1/examples/synthetic_panel/README.md +23 -0
- pylgm-0.6.1/examples/synthetic_panel/config.yaml +13 -0
- pylgm-0.6.1/examples/synthetic_panel/data.csv +9 -0
- pylgm-0.6.1/examples/weighted_network/README.md +19 -0
- pylgm-0.6.1/examples/weighted_network/run.py +73 -0
- pylgm-0.6.1/mkdocs.yml +83 -0
- pylgm-0.6.1/overrides/main.html +19 -0
- pylgm-0.6.1/pyproject.toml +78 -0
- pylgm-0.6.1/src/pylgm/__init__.py +77 -0
- pylgm-0.6.1/src/pylgm/artifacts/__init__.py +6 -0
- pylgm-0.6.1/src/pylgm/artifacts/experiment.py +206 -0
- pylgm-0.6.1/src/pylgm/artifacts/run.py +252 -0
- pylgm-0.6.1/src/pylgm/cli.py +49 -0
- pylgm-0.6.1/src/pylgm/compiler.py +1234 -0
- pylgm-0.6.1/src/pylgm/config/__init__.py +15 -0
- pylgm-0.6.1/src/pylgm/config/experiment.py +271 -0
- pylgm-0.6.1/src/pylgm/config/load.py +57 -0
- pylgm-0.6.1/src/pylgm/config/model.py +336 -0
- pylgm-0.6.1/src/pylgm/config/schema.py +79 -0
- pylgm-0.6.1/src/pylgm/data/__init__.py +3 -0
- pylgm-0.6.1/src/pylgm/data/fingerprint.py +236 -0
- pylgm-0.6.1/src/pylgm/data/panel.py +126 -0
- pylgm-0.6.1/src/pylgm/data/scalars.py +69 -0
- pylgm-0.6.1/src/pylgm/data/spark.py +114 -0
- pylgm-0.6.1/src/pylgm/effects/__init__.py +69 -0
- pylgm-0.6.1/src/pylgm/effects/ar1.py +93 -0
- pylgm-0.6.1/src/pylgm/effects/besag.py +107 -0
- pylgm-0.6.1/src/pylgm/effects/bym2.py +105 -0
- pylgm-0.6.1/src/pylgm/effects/directed_graph.py +117 -0
- pylgm-0.6.1/src/pylgm/effects/fixed.py +19 -0
- pylgm-0.6.1/src/pylgm/effects/graph.py +235 -0
- pylgm-0.6.1/src/pylgm/effects/iid.py +22 -0
- pylgm-0.6.1/src/pylgm/effects/midas.py +96 -0
- pylgm-0.6.1/src/pylgm/effects/proper_car.py +98 -0
- pylgm-0.6.1/src/pylgm/effects/random_walk.py +54 -0
- pylgm-0.6.1/src/pylgm/effects/sar.py +152 -0
- pylgm-0.6.1/src/pylgm/effects/scaling.py +62 -0
- pylgm-0.6.1/src/pylgm/effects/sdpd_forecast.py +120 -0
- pylgm-0.6.1/src/pylgm/effects/seasonal.py +85 -0
- pylgm-0.6.1/src/pylgm/effects/spacetime.py +142 -0
- pylgm-0.6.1/src/pylgm/effects/spec.py +506 -0
- pylgm-0.6.1/src/pylgm/evaluation/__init__.py +21 -0
- pylgm-0.6.1/src/pylgm/evaluation/availability.py +30 -0
- pylgm-0.6.1/src/pylgm/evaluation/folds.py +297 -0
- pylgm-0.6.1/src/pylgm/evaluation/metrics.py +304 -0
- pylgm-0.6.1/src/pylgm/evaluation/persistence.py +89 -0
- pylgm-0.6.1/src/pylgm/evaluation/selection.py +174 -0
- pylgm-0.6.1/src/pylgm/exceptions.py +105 -0
- pylgm-0.6.1/src/pylgm/experiment.py +479 -0
- pylgm-0.6.1/src/pylgm/inference/__init__.py +26 -0
- pylgm-0.6.1/src/pylgm/inference/gaussian.py +272 -0
- pylgm-0.6.1/src/pylgm/inference/laplace.py +198 -0
- pylgm-0.6.1/src/pylgm/inference/prediction.py +410 -0
- pylgm-0.6.1/src/pylgm/inference/result.py +1104 -0
- pylgm-0.6.1/src/pylgm/inference/sparse.py +527 -0
- pylgm-0.6.1/src/pylgm/ir/__init__.py +27 -0
- pylgm-0.6.1/src/pylgm/ir/family.py +574 -0
- pylgm-0.6.1/src/pylgm/ir/model.py +311 -0
- pylgm-0.6.1/src/pylgm/likelihoods.py +652 -0
- pylgm-0.6.1/src/pylgm/links.py +38 -0
- pylgm-0.6.1/src/pylgm/model.py +532 -0
- pylgm-0.6.1/src/pylgm/optimization/__init__.py +16 -0
- pylgm-0.6.1/src/pylgm/optimization/empirical_bayes.py +322 -0
- pylgm-0.6.1/src/pylgm/optimization/inla.py +556 -0
- pylgm-0.6.1/src/pylgm/optimization/result.py +92 -0
- pylgm-0.6.1/src/pylgm/optimization/transforms.py +108 -0
- pylgm-0.6.1/src/pylgm/parameters.py +108 -0
- pylgm-0.6.1/src/pylgm/pipeline.py +28 -0
- pylgm-0.6.1/src/pylgm/priors.py +223 -0
- pylgm-0.6.1/tests/artifacts/test_experiment_artifacts.py +317 -0
- pylgm-0.6.1/tests/config/__init__.py +1 -0
- pylgm-0.6.1/tests/config/test_experiment.py +395 -0
- pylgm-0.6.1/tests/config/test_load.py +137 -0
- pylgm-0.6.1/tests/config/test_model.py +701 -0
- pylgm-0.6.1/tests/conftest.py +25 -0
- pylgm-0.6.1/tests/data/test_panel.py +101 -0
- pylgm-0.6.1/tests/data/test_spark.py +260 -0
- pylgm-0.6.1/tests/effects/test_ar1.py +69 -0
- pylgm-0.6.1/tests/effects/test_besag.py +132 -0
- pylgm-0.6.1/tests/effects/test_bym2.py +142 -0
- pylgm-0.6.1/tests/effects/test_directed_graph.py +60 -0
- pylgm-0.6.1/tests/effects/test_dynamic_spatial_panel.py +84 -0
- pylgm-0.6.1/tests/effects/test_graph.py +118 -0
- pylgm-0.6.1/tests/effects/test_proper_car.py +85 -0
- pylgm-0.6.1/tests/effects/test_proper_car_eigsh.py +38 -0
- pylgm-0.6.1/tests/effects/test_sar.py +54 -0
- pylgm-0.6.1/tests/effects/test_sdpd_forecast.py +52 -0
- pylgm-0.6.1/tests/effects/test_sorbye_scaling.py +24 -0
- pylgm-0.6.1/tests/effects/test_spec.py +336 -0
- pylgm-0.6.1/tests/effects/test_structured.py +71 -0
- pylgm-0.6.1/tests/evaluation/test_folds.py +442 -0
- pylgm-0.6.1/tests/evaluation/test_metrics.py +318 -0
- pylgm-0.6.1/tests/evaluation/test_persistence.py +143 -0
- pylgm-0.6.1/tests/evaluation/test_selection.py +189 -0
- pylgm-0.6.1/tests/inference/conftest.py +151 -0
- pylgm-0.6.1/tests/inference/result_surface_baseline.json +4318 -0
- pylgm-0.6.1/tests/inference/test_gaussian.py +472 -0
- pylgm-0.6.1/tests/inference/test_laplace.py +364 -0
- pylgm-0.6.1/tests/inference/test_prediction.py +106 -0
- pylgm-0.6.1/tests/inference/test_predictive_variance_convention.py +93 -0
- pylgm-0.6.1/tests/inference/test_quadratic_form_diagonal.py +24 -0
- pylgm-0.6.1/tests/inference/test_result.py +554 -0
- pylgm-0.6.1/tests/inference/test_result_surface.py +798 -0
- pylgm-0.6.1/tests/inference/test_sdpd_sparse.py +56 -0
- pylgm-0.6.1/tests/inference/test_sparse.py +599 -0
- pylgm-0.6.1/tests/inference/test_sparse_large_model.py +78 -0
- pylgm-0.6.1/tests/inference/test_sparse_result.py +142 -0
- pylgm-0.6.1/tests/inference/test_tabulated_marginals.py +33 -0
- pylgm-0.6.1/tests/integration/test_disease_mapping_config.py +20 -0
- pylgm-0.6.1/tests/integration/test_hybrid_nowcast.py +91 -0
- pylgm-0.6.1/tests/integration/test_nic_shaped_backtest.py +81 -0
- pylgm-0.6.1/tests/integration/test_predictive_selection.py +23 -0
- pylgm-0.6.1/tests/integration/test_synthetic_example.py +15 -0
- pylgm-0.6.1/tests/ir/test_compiled_family.py +54 -0
- pylgm-0.6.1/tests/ir/test_fixed.py +94 -0
- pylgm-0.6.1/tests/ir/test_gaussian_family.py +432 -0
- pylgm-0.6.1/tests/ir/test_model_validation.py +288 -0
- pylgm-0.6.1/tests/ir/test_parametric_block.py +140 -0
- pylgm-0.6.1/tests/optimization/test_empirical_bayes.py +965 -0
- pylgm-0.6.1/tests/optimization/test_full_laplace.py +131 -0
- pylgm-0.6.1/tests/optimization/test_inla.py +243 -0
- pylgm-0.6.1/tests/optimization/test_inla_criteria.py +100 -0
- pylgm-0.6.1/tests/optimization/test_simplified_laplace.py +86 -0
- pylgm-0.6.1/tests/optimization/test_skew_normal.py +33 -0
- pylgm-0.6.1/tests/optimization/test_transforms.py +67 -0
- pylgm-0.6.1/tests/test_api_ergonomics.py +92 -0
- pylgm-0.6.1/tests/test_ar1_fit.py +120 -0
- pylgm-0.6.1/tests/test_ar1_group.py +150 -0
- pylgm-0.6.1/tests/test_besag_fit.py +155 -0
- pylgm-0.6.1/tests/test_bound_diagnostics.py +125 -0
- pylgm-0.6.1/tests/test_bym2_fit.py +98 -0
- pylgm-0.6.1/tests/test_cli.py +110 -0
- pylgm-0.6.1/tests/test_compiler.py +691 -0
- pylgm-0.6.1/tests/test_experiment.py +366 -0
- pylgm-0.6.1/tests/test_extraconstr.py +125 -0
- pylgm-0.6.1/tests/test_identity_transform.py +28 -0
- pylgm-0.6.1/tests/test_links.py +22 -0
- pylgm-0.6.1/tests/test_midas_builder.py +120 -0
- pylgm-0.6.1/tests/test_midas_effect.py +124 -0
- pylgm-0.6.1/tests/test_midas_parametric_effect.py +62 -0
- pylgm-0.6.1/tests/test_midas_parametric_kernels.py +51 -0
- pylgm-0.6.1/tests/test_midas_parametric_predict.py +46 -0
- pylgm-0.6.1/tests/test_midas_parametric_spec.py +40 -0
- pylgm-0.6.1/tests/test_model.py +762 -0
- pylgm-0.6.1/tests/test_model_spark.py +78 -0
- pylgm-0.6.1/tests/test_modeling_vocabulary.py +491 -0
- pylgm-0.6.1/tests/test_package.py +259 -0
- pylgm-0.6.1/tests/test_parameters.py +24 -0
- pylgm-0.6.1/tests/test_parametric_design_block.py +36 -0
- pylgm-0.6.1/tests/test_pc_bym2_phi.py +87 -0
- pylgm-0.6.1/tests/test_pipeline.py +382 -0
- pylgm-0.6.1/tests/test_predict.py +219 -0
- pylgm-0.6.1/tests/test_proper_car_fit.py +96 -0
- pylgm-0.6.1/tests/test_proper_car_rho_estimation.py +103 -0
- pylgm-0.6.1/tests/test_public_exports.py +28 -0
- pylgm-0.6.1/tests/test_sar_fit.py +48 -0
- pylgm-0.6.1/tests/test_scaling.py +31 -0
- pylgm-0.6.1/tests/test_sdpd_fit.py +66 -0
- pylgm-0.6.1/tests/test_sdpd_forecast_integration.py +37 -0
- pylgm-0.6.1/tests/test_seasonal.py +144 -0
- pylgm-0.6.1/tests/test_spacetime_builder.py +126 -0
- pylgm-0.6.1/tests/test_spacetime_compile.py +71 -0
- pylgm-0.6.1/tests/test_spacetime_effect.py +78 -0
- pylgm-0.6.1/tests/test_spacetime_predict.py +50 -0
- pylgm-0.6.1/tests/test_spacetime_spec.py +53 -0
- pylgm-0.6.1/tests/test_weighted_graph.py +79 -0
- pylgm-0.6.1/tests/test_weighted_spatial_effects.py +45 -0
pylgm-0.6.1/.gitignore
ADDED
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).
|