From 37f7e416b9f8f979ddf19e51eed45e972ea49bca Mon Sep 17 00:00:00 2001 From: yigong Date: Wed, 27 May 2026 10:24:20 +0800 Subject: [PATCH 1/3] fix: GWSS interface --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 4 +- doc/index.rst | 3 +- doc/models.rst | 1 + doc/models/gwss.rst | 68 ++++++++------ doc/pygwmodel.rst | 12 +-- doc/quickstart.rst | 3 +- src/CMakeLists.txt | 5 +- src/pygwmodel/__init__.py | 1 + src/pygwmodel/gwss.py | 187 ++++++++++++++++++++++++++------------ test/CMakeLists.txt | 11 ++- test/test_gwss.py | 38 +++++--- 12 files changed, 218 insertions(+), 117 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f192533..5965ff3 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -79,7 +79,7 @@ jobs: PYGW_TEST_DATA="{project}/tests/londonhp100.csv" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: > python "${{ github.workspace }}/tools/repair_windows_wheel.py" {wheel} {dest_dir} - CIBW_TEST_COMMAND: "pytest {project}/tests -v --ignore={project}/tests/test_gwss.py" + CIBW_TEST_COMMAND: "pytest {project}/tests -v" CIBW_TEST_REQUIRES: pytest - name: Upload wheels diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ca9860..6aad475 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: CMAKE_ARGS: "-DWITH_TESTS=OFF" - name: Run tests - run: python -m pytest test/ -v --ignore=test/test_gwss.py + run: python -m pytest test/ -v env: PYGW_TEST_DATA: test/londonhp100.csv @@ -116,6 +116,6 @@ jobs: pip install $wheel - name: Run tests - run: python -m pytest test/ -v --ignore=test/test_gwss.py + run: python -m pytest test/ -v env: PYGW_TEST_DATA: test/londonhp100.csv diff --git a/doc/index.rst b/doc/index.rst index a09ee9d..bdcb0bf 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,7 +15,8 @@ Implemented Models * **GWRBasic** — Basic Geographically Weighted Regression with a single bandwidth. * **GWRMultiscale** — Multiscale GWR (MGWR) with per-variable bandwidths and backfitting algorithm. -* **GWSS** — Geographically Weighted Summary Statistics (averages and correlations). +* **GWAverage** — Geographically Weighted Summary Statistics (mean, std dev, etc.). +* **GWCorrelation** — Geographically Weighted Correlation coefficients. Quick Start ----------- diff --git a/doc/models.rst b/doc/models.rst index 20c4348..6355b17 100644 --- a/doc/models.rst +++ b/doc/models.rst @@ -6,3 +6,4 @@ Regression Models models/gwr models/gwr_multiscale + models/gwss diff --git a/doc/models/gwss.rst b/doc/models/gwss.rst index 4870d7e..e917419 100644 --- a/doc/models/gwss.rst +++ b/doc/models/gwss.rst @@ -1,31 +1,29 @@ -Geographically Weighted Summary Statistics (GWSS) -================================================= +Geographically Weighted Summary Statistics (GWAverage / GWCorrelation) +====================================================================== .. _gwss-overview: Model Overview -------------- -Geographically Weighted Summary Statistics (GWSS) performs locally weighted +Geographically Weighted Summary Statistics performs locally weighted descriptive statistics on multivariate data, revealing spatial heterogeneity in the statistical characteristics of variables. -GWSS supports two modes: +Two classes are provided: -- Average mode — computes local mean, standard deviation, variance, skewness, - coefficient of variation, and optionally local median, interquartile range, - and quantile imbalance. -- Correlation mode — computes local Pearson correlation coefficients and - Spearman rank correlation coefficients. +- :class:`~pygwmodel.gwss.GWAverage` — computes local mean, standard deviation, + variance, skewness, coefficient of variation, and optionally local median, + interquartile range, and quantile imbalance. +- :class:`~pygwmodel.gwss.GWCorrelation` — computes local Pearson correlation + coefficients and Spearman rank correlation coefficients for every pair of + variables. -.. _gwss-modes: +.. _gwss-average: -Two Modes +GWAverage --------- -Average Mode -~~~~~~~~~~~~ - For each variable, the following local statistics are computed: .. list-table:: @@ -67,8 +65,10 @@ When ``quantile=True``: - ``qi`` - ``{variable}_QI`` -Correlation Mode -~~~~~~~~~~~~~~~~ +.. _gwss-correlation: + +GWCorrelation +------------- For each pair of variables :math:`(X_i, X_j)`, the following are computed: @@ -83,35 +83,51 @@ Column name format: ``{var1}.{var2}_Corr`` and ``{var1}.{var2}_SCorr``. Code Examples ------------- -Average Mode -~~~~~~~~~~~~ +GWAverage +~~~~~~~~~ .. code-block:: python - from pygwmodel import GWSS, BandwidthWeight + from pygwmodel import GWAverage, BandwidthWeight vars = ["PURCHASE", "FLOORSZ", "UNEMPLOY", "PROF"] - gwss = GWSS( + gwa = GWAverage( data, vars, weight=BandwidthWeight(36.0, adaptive=True), - mode=GWSS.Mode.Average, quantile=False ).run() - result = gwss.result_layer + result = gwa.result_layer print(result.columns) # PURCHASE_Mean, PURCHASE_SDev, PURCHASE_Skew, PURCHASE_CV, # FLOORSZ_Mean, ... -Correlation Mode -~~~~~~~~~~~~~~~~ +GWCorrelation +~~~~~~~~~~~~~ .. code-block:: python - gwss = GWSS(data, vars, weight=BandwidthWeight(36.0, adaptive=True)) - result = gwss.run(mode=GWSS.Mode.Correlation).result_layer + from pygwmodel import GWCorrelation, BandwidthWeight + + gwc = GWCorrelation(data, vars, weight=BandwidthWeight(36.0, adaptive=True)) + result = gwc.run().result_layer print(result.columns) # PURCHASE.FLOORSZ_Corr, PURCHASE.FLOORSZ_SCorr, # PURCHASE.UNEMPLOY_Corr, ... + +Parallel Execution +~~~~~~~~~~~~~~~~~~ + +Both classes support OpenMP multi-threading: + +.. code-block:: python + + from pygwmodel import GWAverage, GWCorrelation, ParallelType + + gwa = GWAverage(data, vars, weight=...) + gwa.enable_parallel(ParallelType.OpenMP, threads=4).run() + + gwc = GWCorrelation(data, vars, weight=...) + gwc.enable_parallel(ParallelType.OpenMP, threads=4).run() diff --git a/doc/pygwmodel.rst b/doc/pygwmodel.rst index c5bd1bb..047f20a 100644 --- a/doc/pygwmodel.rst +++ b/doc/pygwmodel.rst @@ -20,13 +20,13 @@ pygwmodel.gwr_multiscale module :undoc-members: :show-inheritance: -.. pygwmodel.gwss module (disabled — _analysis module needs _GWSS binding update) -.. --------------------- +pygwmodel.gwss module +--------------------- -.. .. automodule:: pygwmodel.gwss -.. :members: -.. :undoc-members: -.. :show-inheritance: +.. automodule:: pygwmodel.gwss + :members: + :undoc-members: + :show-inheritance: pygwmodel.parallel module ------------------------- diff --git a/doc/quickstart.rst b/doc/quickstart.rst index b776444..92d3602 100644 --- a/doc/quickstart.rst +++ b/doc/quickstart.rst @@ -15,7 +15,8 @@ Currently implemented models: - **Geographically Weighted Regression (GWR)** — :class:`~pygwmodel.gwr_basic.GWRBasic` - **Multiscale GWR (MGWR)** — :class:`~pygwmodel.gwr_multiscale.GWRMultiscale` -- **Geographically Weighted Summary Statistics (GWSS)** — :class:`~pygwmodel.gwss.GWSS` +- **GW Average** — :class:`~pygwmodel.gwss.GWAverage` +- **GW Correlation** — :class:`~pygwmodel.gwss.GWCorrelation` All algorithms use a C++17 core, exposed to Python via `nanobind `_, with support for diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e88cc1f..ce7b6e9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,11 +30,12 @@ target_link_libraries(_spatial_weight PRIVATE gwmodel) nanobind_add_module(_regression STABLE_ABI common.hpp parallel.hpp base.cpp gwr_basic.cpp gwr_multiscale.cpp regression.cpp) target_link_libraries(_regression PRIVATE gwmodel) -# nanobind_add_module(_analysis STABLE_ABI common.hpp base.cpp parallel.hpp analysis.cpp gwss.cpp) -# target_link_libraries(_analysis PRIVATE gwmodel) +nanobind_add_module(_analysis STABLE_ABI common.hpp base.cpp parallel.hpp analysis.cpp gwss.cpp) +target_link_libraries(_analysis PRIVATE gwmodel) install(TARGETS _regression _spatial_weight _parallel + _analysis LIBRARY DESTINATION pygwmodel) diff --git a/src/pygwmodel/__init__.py b/src/pygwmodel/__init__.py index 14465d6..e7f91a5 100644 --- a/src/pygwmodel/__init__.py +++ b/src/pygwmodel/__init__.py @@ -54,6 +54,7 @@ def _add_windows_dll_directories(): from .gwr_basic import GWRBasic, ParallelType from .gwr_multiscale import GWRMultiscale from .spatial_weight import SpatialWeight, BandwidthWeight, CRSDistance +from .gwss import GWAverage, GWCorrelation if __name__ == "__main__": print("PyGWmodel Package.") diff --git a/src/pygwmodel/gwss.py b/src/pygwmodel/gwss.py index 33bceee..1e840a9 100644 --- a/src/pygwmodel/gwss.py +++ b/src/pygwmodel/gwss.py @@ -1,118 +1,187 @@ from typing import List, Optional -from enum import Enum import numpy as np import geopandas as gp -from .spatial_weight import * +from .spatial_weight import BandwidthWeight, CRSDistance, Distance, SpatialWeight from .parallel import ParallelType from ._analysis import _GWSS -class GWSS: +class GWAverage: + """Geographically Weighted Average — local summary statistics. - class Mode(Enum): - Average = _GWSS.Mode.Average - Correlation = _GWSS.Mode.Correlation - - def __init__(self, sdf: gp.GeoDataFrame, vars: List[str], weight: BandwidthWeight, distance: Distance=CRSDistance(), mode: Mode=Mode.Average, quantile: bool=False) -> None: + Computes local mean, standard deviation, variance, skewness, + coefficient of variation, and optionally median, IQR, quantile + imbalance for each variable. + """ + + def __init__(self, sdf: gp.GeoDataFrame, vars: List[str], + weight: BandwidthWeight, distance: Distance = CRSDistance(), + quantile: bool = False) -> None: self.geometry = sdf.geometry self.vars = vars - self.mode = mode self.weight = weight self.distance = distance self.quantile = quantile self.result_layer: Optional[gp.GeoDataFrame] = None self.algorithm = _GWSS() - self.algorithm.coords = np.asfortranarray(sdf.geometry.centroid.get_coordinates(), dtype=np.float64) - self.algorithm.variables = np.asfortranarray(sdf[self.vars], dtype=np.float64) + self.algorithm.coords = np.asfortranarray( + sdf.geometry.centroid.get_coordinates(), dtype=np.float64) + self.algorithm.variables = np.asfortranarray( + sdf[self.vars], dtype=np.float64) self.algorithm.spatial_weight = SpatialWeight.create(distance, weight) self.algorithm.quantile = self.quantile - self.algorithm.set_mode(self.mode.value) - - def enable_parallel_omp(self, threads: int=8): - if self.algorithm is None: - raise ValueError("Not initialized") - if isinstance(threads, int) and threads > 0: - self.algorithm.parallel_omp(threads) - else: - raise ValueError("threads must be a positive integer") - return self + self.algorithm.set_mode(_GWSS.Mode.Average) - def enable_parallel(self, type: ParallelType, **kvargs): + def enable_parallel(self, type: ParallelType, **kwargs): if type == ParallelType.OpenMP: - self.enable_parallel_omp(**kvargs) + threads = kwargs.get('threads', 8) + if isinstance(threads, int) and threads > 0: + self.algorithm.parallel_omp(threads) + else: + raise ValueError("threads must be a positive integer") return self - - def run(self, mode: Mode=Mode.Average, quantile: bool=False): - self.mode = mode - self.algorithm.set_mode(self.mode.value) - self.algorithm.quantile = quantile + + def run(self, quantile: bool = False): + self.quantile = quantile + self.algorithm.quantile = self.quantile + self.algorithm.set_mode(_GWSS.Mode.Average) self.algorithm.run() - if self.mode == GWSS.Mode.Average: - result_data = { - **{f'{f}_Mean': self.local_mean[:, i] for i, f in enumerate(self.vars)}, - **{f'{f}_SDev': self.local_sdev[:, i] for i, f in enumerate(self.vars)}, - **{f'{f}_Skew': self.local_skewness[:, i] for i, f in enumerate(self.vars)}, - **{f'{f}_CV': self.local_cv[:, i] for i, f in enumerate(self.vars)} - } - if self.quantile: - result_data = { - **result_data, - **{f'{f}_Median': self.local_median[:, i] for i, f in enumerate(self.vars)}, - **{f'{f}_IQR': self.iqr[:, i] for i, f in enumerate(self.vars)}, - **{f'{f}_QI': self.qi[:, i] for i, f in enumerate(self.vars)} - } - elif self.mode == GWSS.Mode.Correlation: - columns = [(fi, fj) for i, fi in enumerate(self.vars) for _, fj in enumerate(self.vars[(i+1):])] + result_data = { + **{f'{f}_Mean': self.local_mean[:, i] + for i, f in enumerate(self.vars)}, + **{f'{f}_SDev': self.local_sdev[:, i] + for i, f in enumerate(self.vars)}, + **{f'{f}_Skew': self.local_skewness[:, i] + for i, f in enumerate(self.vars)}, + **{f'{f}_CV': self.local_cv[:, i] + for i, f in enumerate(self.vars)} + } + if self.quantile: result_data = { - **{f'{fi}.{fj}_Corr': self.local_corr[:, i] for i, (fi, fj) in enumerate(columns)}, - **{f'{fi}.{fj}_SCorr': self.local_s_corr[:, i] for i, (fi, fj) in enumerate(columns)} + **result_data, + **{f'{f}_Median': self.local_median[:, i] + for i, f in enumerate(self.vars)}, + **{f'{f}_IQR': self.iqr[:, i] + for i, f in enumerate(self.vars)}, + **{f'{f}_QI': self.qi[:, i] + for i, f in enumerate(self.vars)} } - else: - raise ValueError("Not such mode.") self.result_layer = gp.GeoDataFrame(result_data, geometry=self.geometry) return self + # -- read-only properties from C++ _GWSS -- + @property def local_mean(self): return self.algorithm.local_mean - + @property def local_sdev(self): return self.algorithm.local_sdev - + @property def local_skewness(self): return self.algorithm.local_skewness - + @property def local_cv(self): return self.algorithm.local_cv - + @property def local_var(self): return self.algorithm.local_var - + @property def local_median(self): return self.algorithm.local_median - + @property def iqr(self): return self.algorithm.iqr - + @property def qi(self): return self.algorithm.qi - + + +class GWCorrelation: + """Geographically Weighted Correlation — local correlation statistics. + + Computes local Pearson and Spearman rank correlation coefficients + for every pair of variables. + """ + + def __init__(self, sdf: gp.GeoDataFrame, vars: List[str], + weight: BandwidthWeight, distance: Distance = CRSDistance(), + corr_with_first: bool = False) -> None: + self.geometry = sdf.geometry + self.vars = vars + self.weight = weight + self.distance = distance + self.corr_with_first = corr_with_first + self.result_layer: Optional[gp.GeoDataFrame] = None + self.algorithm = _GWSS() + self.algorithm.coords = np.asfortranarray( + sdf.geometry.centroid.get_coordinates(), dtype=np.float64) + self.algorithm.variables = np.asfortranarray( + sdf[self.vars], dtype=np.float64) + self.algorithm.spatial_weight = SpatialWeight.create(distance, weight) + self.algorithm.corr_with_first = self.corr_with_first + self.algorithm.set_mode(_GWSS.Mode.Correlation) + + def enable_parallel(self, type: ParallelType, **kwargs): + if type == ParallelType.OpenMP: + threads = kwargs.get('threads', 8) + if isinstance(threads, int) and threads > 0: + self.algorithm.parallel_omp(threads) + else: + raise ValueError("threads must be a positive integer") + return self + + def run(self): + self.algorithm.set_mode(_GWSS.Mode.Correlation) + self.algorithm.run() + columns = [(fi, fj) for i, fi in enumerate(self.vars) + for _, fj in enumerate(self.vars[i + 1:])] + result_data = { + **{f'{fi}.{fj}_Corr': self.local_corr[:, i] + for i, (fi, fj) in enumerate(columns)}, + **{f'{fi}.{fj}_SCorr': self.local_s_corr[:, i] + for i, (fi, fj) in enumerate(columns)} + } + self.result_layer = gp.GeoDataFrame(result_data, geometry=self.geometry) + return self + + # -- read-only properties from C++ _GWSS -- + + @property + def local_mean(self): + return self.algorithm.local_mean + + @property + def local_sdev(self): + return self.algorithm.local_sdev + + @property + def local_skewness(self): + return self.algorithm.local_skewness + + @property + def local_cv(self): + return self.algorithm.local_cv + + @property + def local_var(self): + return self.algorithm.local_var + @property def local_cov(self): return self.algorithm.local_cov - + @property def local_corr(self): return self.algorithm.local_corr - + @property def local_s_corr(self): return self.algorithm.local_s_corr - diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index eca7354..2837424 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -25,6 +25,7 @@ add_custom_target(testPythonDeps ALL $ $ $ + $ ${PYTHON_TEST_SCRIPT_DIR}/pygwmodel COMMAND ${CMAKE_COMMAND} -E copy_if_different @@ -38,11 +39,11 @@ add_test( COMMAND ${Python_EXECUTABLE} test_gwr_basic.py ${PYTHON_TEST_DATA} WORKING_DIRECTORY ${PYTHON_TEST_SCRIPT_DIR} ) -# add_test( -# NAME testPythonGWSS -# COMMAND ${Python_EXECUTABLE} test_gwss.py ${PYTHON_TEST_DATA} -# WORKING_DIRECTORY ${PYTHON_TEST_SCRIPT_DIR} -# ) +add_test( + NAME testPythonGWSS + COMMAND ${Python_EXECUTABLE} test_gwss.py ${PYTHON_TEST_DATA} + WORKING_DIRECTORY ${PYTHON_TEST_SCRIPT_DIR} +) add_test( NAME testPythonGWRMultiscale COMMAND ${Python_EXECUTABLE} test_gwr_multiscale.py ${PYTHON_TEST_DATA} diff --git a/test/test_gwss.py b/test/test_gwss.py index 2f6bd0a..c9c5f20 100644 --- a/test/test_gwss.py +++ b/test/test_gwss.py @@ -1,22 +1,17 @@ import os import sys -from pathlib import Path import unittest import numpy as np import pandas as pd import geopandas as gp from pygwmodel.parallel import ParallelType from pygwmodel.spatial_weight import BandwidthWeight -from pygwmodel.gwss import GWSS +from pygwmodel.gwss import GWAverage, GWCorrelation -TEST_DATA = os.environ.get("PYGW_TEST_DATA") -if TEST_DATA is None and len(sys.argv) > 1 and Path(sys.argv[1]).is_file(): - TEST_DATA = sys.argv[1] -if TEST_DATA is None: - TEST_DATA = str(Path(__file__).with_name("londonhp100.csv")) +TEST_DATA = os.environ.get("PYGW_TEST_DATA", sys.argv[1] if len(sys.argv) > 1 else "test/londonhp100.csv") ENABLE_OPENMP = (lambda s: False if s is None else (s.lower() in ['true', '1', 't', 'y', 'yes', 'on']))(os.getenv("ENABLE_OPENMP")) -class TestGWSS(unittest.TestCase): +class TestGWAverage(unittest.TestCase): def setUp(self): londonhp_csv = pd.read_csv(TEST_DATA) @@ -31,9 +26,10 @@ def setUp(self): def test_average(self): for p, pargs in self.parallel_case.items(): with self.subTest(parallel=p): - londonhp_gwss = GWSS(self.londonhp, self.londonhp_vars, BandwidthWeight(36.0, adaptive=True)) - londonhp_gwss_result: gp.GeoDataFrame = londonhp_gwss.enable_parallel(p, **pargs).run().result_layer - result = pd.DataFrame(londonhp_gwss_result).drop('geometry', axis=1) + londonhp_gwa = GWAverage(self.londonhp, self.londonhp_vars, + BandwidthWeight(36.0, adaptive=True)) + londonhp_gwa_result: gp.GeoDataFrame = londonhp_gwa.enable_parallel(p, **pargs).run().result_layer + result = pd.DataFrame(londonhp_gwa_result).drop('geometry', axis=1) result_q = result.apply(lambda x: np.quantile(x, [0, 0.25, 0.5, 0.75, 1], interpolation='midpoint'), axis=0) localmean_q0 = np.array([ @@ -76,12 +72,26 @@ def test_average(self): localcv_q = result_q.loc[:, 'PURCHASE_CV':'PROF_CV'].values # type: ignore self.assertTrue(np.all(np.abs(localcv_q0 - localcv_q) < 1e-8)) + +class TestGWCorrelation(unittest.TestCase): + + def setUp(self): + londonhp_csv = pd.read_csv(TEST_DATA) + self.londonhp = gp.GeoDataFrame(londonhp_csv, geometry=gp.points_from_xy(londonhp_csv.x, londonhp_csv.y)) + self.londonhp_vars = ["PURCHASE", "FLOORSZ", "UNEMPLOY", "PROF"] + self.parallel_case = { + ParallelType.SerialOnly: dict() + } + if ENABLE_OPENMP: + self.parallel_case[ParallelType.OpenMP] = {'threads': 4} + def test_correlation(self): for p, pargs in self.parallel_case.items(): with self.subTest(parallel=p): - londonhp_gwss = GWSS(self.londonhp, self.londonhp_vars, BandwidthWeight(36.0, adaptive=True)) - londonhp_gwss_result: gp.GeoDataFrame = londonhp_gwss.enable_parallel(p, **pargs).run(GWSS.Mode.Correlation).result_layer - result = pd.DataFrame(londonhp_gwss_result).drop('geometry', axis=1) + londonhp_gwc = GWCorrelation(self.londonhp, self.londonhp_vars, + BandwidthWeight(36.0, adaptive=True)) + londonhp_gwc_result: gp.GeoDataFrame = londonhp_gwc.enable_parallel(p, **pargs).run().result_layer + result = pd.DataFrame(londonhp_gwc_result).drop('geometry', axis=1) result_q = result.apply(lambda x: np.quantile(x, [0, 0.25, 0.5, 0.75, 1], interpolation='midpoint'), axis=0) localcorr_q0 = np.array([ From 7c837af34430b07d58af5b1bceca765e3c8e3d5f Mon Sep 17 00:00:00 2001 From: yigong Date: Wed, 27 May 2026 10:45:41 +0800 Subject: [PATCH 2/3] fix: GWSS.h --- src/gwss.cpp | 61 +++++++++++++++++++++++-------------------- src/pygwmodel/gwss.py | 23 +++++----------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/gwss.cpp b/src/gwss.cpp index 528de65..430ffc1 100644 --- a/src/gwss.cpp +++ b/src/gwss.cpp @@ -1,5 +1,6 @@ #include -#include +#include +#include #include "common.hpp" #include "parallel.hpp" @@ -7,36 +8,38 @@ namespace nb = nanobind; void init_gwss(nb::module_& m) { - nb::class_ _GWSS(m, "_GWSS"); - - nb::enum_(_GWSS, "Mode") - .value("Average", gwm::GWSS::GWSSMode::Average) - .value("Correlation", gwm::GWSS::GWSSMode::Correlation) + nb::class_ _GWAverage(m, "_GWAverage"); + _GWAverage + .def(nb::init<>()) + .def_prop_rw("variables", &gwm::GWAverage::variables, &gwm::GWAverage::setVariables, nb::rv_policy::move) + .def_prop_rw("quantile", &gwm::GWAverage::quantile, &gwm::GWAverage::setQuantile) + .def("run", &gwm::GWAverage::run) + .def_prop_ro("local_mean", &gwm::GWAverage::localMean, nb::rv_policy::move) + .def_prop_ro("local_sdev", &gwm::GWAverage::localSDev, nb::rv_policy::move) + .def_prop_ro("local_skewness", &gwm::GWAverage::localSkewness, nb::rv_policy::move) + .def_prop_ro("local_cv", &gwm::GWAverage::localCV, nb::rv_policy::move) + .def_prop_ro("local_var", &gwm::GWAverage::localVar, nb::rv_policy::move) + .def_prop_ro("local_median", &gwm::GWAverage::localMedian, nb::rv_policy::move) + .def_prop_ro("iqr", &gwm::GWAverage::iqr, nb::rv_policy::move) + .def_prop_ro("qi", &gwm::GWAverage::qi, nb::rv_policy::move) ; + def_parallel_info(_GWAverage); + def_parallel_openmp(_GWAverage); - _GWSS + nb::class_ _GWCorrelation(m, "_GWCorrelation"); + _GWCorrelation .def(nb::init<>()) - .def("set_mode", [](gwm::GWSS &instance, int mode) - { - instance.setGWSSMode(gwm::GWSS::GWSSMode(mode)); - }) - .def_prop_rw("variables", &gwm::GWSS::variables, &gwm::GWSS::setVariables, nb::rv_policy::move) - .def_prop_rw("quantile", &gwm::GWSS::quantile, &gwm::GWSS::setQuantile) - .def_prop_rw("corr_with_first", &gwm::GWSS::isCorrWithFirstOnly, &gwm::GWSS::setIsCorrWithFirstOnly) - .def("run", &gwm::GWSS::run) - .def_prop_ro("local_mean", &gwm::GWSS::localMean, nb::rv_policy::move) - .def_prop_ro("local_sdev", &gwm::GWSS::localSDev, nb::rv_policy::move) - .def_prop_ro("local_skewness", &gwm::GWSS::localSkewness, nb::rv_policy::move) - .def_prop_ro("local_cv", &gwm::GWSS::localCV, nb::rv_policy::move) - .def_prop_ro("local_var", &gwm::GWSS::localVar, nb::rv_policy::move) - .def_prop_ro("local_median", &gwm::GWSS::localMedian, nb::rv_policy::move) - .def_prop_ro("iqr", &gwm::GWSS::iqr, nb::rv_policy::move) - .def_prop_ro("qi", &gwm::GWSS::qi, nb::rv_policy::move) - .def_prop_ro("local_cov", &gwm::GWSS::localCov, nb::rv_policy::move) - .def_prop_ro("local_corr", &gwm::GWSS::localCorr, nb::rv_policy::move) - .def_prop_ro("local_s_corr", &gwm::GWSS::localSCorr, nb::rv_policy::move) + .def_prop_rw("variables", &gwm::GWCorrelation::variables2, &gwm::GWCorrelation::setVariables2, nb::rv_policy::move) + .def("run", &gwm::GWCorrelation::run) + .def_prop_ro("local_mean", &gwm::GWCorrelation::localMean, nb::rv_policy::move) + .def_prop_ro("local_sdev", &gwm::GWCorrelation::localSDev, nb::rv_policy::move) + .def_prop_ro("local_skewness", &gwm::GWCorrelation::localSkewness, nb::rv_policy::move) + .def_prop_ro("local_cv", &gwm::GWCorrelation::localCV, nb::rv_policy::move) + .def_prop_ro("local_var", &gwm::GWCorrelation::localVar, nb::rv_policy::move) + .def_prop_ro("local_cov", &gwm::GWCorrelation::localCov, nb::rv_policy::move) + .def_prop_ro("local_corr", &gwm::GWCorrelation::localCorr, nb::rv_policy::move) + .def_prop_ro("local_s_corr", &gwm::GWCorrelation::localSCorr, nb::rv_policy::move) ; - - def_parallel_info(_GWSS); - def_parallel_openmp(_GWSS); + def_parallel_info(_GWCorrelation); + def_parallel_openmp(_GWCorrelation); } diff --git a/src/pygwmodel/gwss.py b/src/pygwmodel/gwss.py index 1e840a9..062d173 100644 --- a/src/pygwmodel/gwss.py +++ b/src/pygwmodel/gwss.py @@ -3,7 +3,7 @@ import geopandas as gp from .spatial_weight import BandwidthWeight, CRSDistance, Distance, SpatialWeight from .parallel import ParallelType -from ._analysis import _GWSS +from ._analysis import _GWAverage, _GWCorrelation class GWAverage: @@ -23,14 +23,13 @@ def __init__(self, sdf: gp.GeoDataFrame, vars: List[str], self.distance = distance self.quantile = quantile self.result_layer: Optional[gp.GeoDataFrame] = None - self.algorithm = _GWSS() + self.algorithm = _GWAverage() self.algorithm.coords = np.asfortranarray( sdf.geometry.centroid.get_coordinates(), dtype=np.float64) self.algorithm.variables = np.asfortranarray( sdf[self.vars], dtype=np.float64) self.algorithm.spatial_weight = SpatialWeight.create(distance, weight) self.algorithm.quantile = self.quantile - self.algorithm.set_mode(_GWSS.Mode.Average) def enable_parallel(self, type: ParallelType, **kwargs): if type == ParallelType.OpenMP: @@ -44,7 +43,6 @@ def enable_parallel(self, type: ParallelType, **kwargs): def run(self, quantile: bool = False): self.quantile = quantile self.algorithm.quantile = self.quantile - self.algorithm.set_mode(_GWSS.Mode.Average) self.algorithm.run() result_data = { **{f'{f}_Mean': self.local_mean[:, i] @@ -69,8 +67,6 @@ def run(self, quantile: bool = False): self.result_layer = gp.GeoDataFrame(result_data, geometry=self.geometry) return self - # -- read-only properties from C++ _GWSS -- - @property def local_mean(self): return self.algorithm.local_mean @@ -112,22 +108,20 @@ class GWCorrelation: """ def __init__(self, sdf: gp.GeoDataFrame, vars: List[str], - weight: BandwidthWeight, distance: Distance = CRSDistance(), - corr_with_first: bool = False) -> None: + weight: BandwidthWeight, distance: Distance = CRSDistance()) -> None: self.geometry = sdf.geometry self.vars = vars self.weight = weight self.distance = distance - self.corr_with_first = corr_with_first self.result_layer: Optional[gp.GeoDataFrame] = None - self.algorithm = _GWSS() + self.algorithm = _GWCorrelation() self.algorithm.coords = np.asfortranarray( sdf.geometry.centroid.get_coordinates(), dtype=np.float64) self.algorithm.variables = np.asfortranarray( sdf[self.vars], dtype=np.float64) - self.algorithm.spatial_weight = SpatialWeight.create(distance, weight) - self.algorithm.corr_with_first = self.corr_with_first - self.algorithm.set_mode(_GWSS.Mode.Correlation) + n_var = len(self.vars) + sw = SpatialWeight.create(distance, weight) + self.algorithm.spatial_weights = [sw] * n_var def enable_parallel(self, type: ParallelType, **kwargs): if type == ParallelType.OpenMP: @@ -139,7 +133,6 @@ def enable_parallel(self, type: ParallelType, **kwargs): return self def run(self): - self.algorithm.set_mode(_GWSS.Mode.Correlation) self.algorithm.run() columns = [(fi, fj) for i, fi in enumerate(self.vars) for _, fj in enumerate(self.vars[i + 1:])] @@ -152,8 +145,6 @@ def run(self): self.result_layer = gp.GeoDataFrame(result_data, geometry=self.geometry) return self - # -- read-only properties from C++ _GWSS -- - @property def local_mean(self): return self.algorithm.local_mean From 58d38f9833cd434cfa552798ed871e5b052320be Mon Sep 17 00:00:00 2001 From: yigong Date: Wed, 27 May 2026 15:37:25 +0800 Subject: [PATCH 3/3] fix: test error --- src/gwss.cpp | 20 +++++++++++++++++++- src/pygwmodel/gwss.py | 27 ++++++++++++++++++--------- test/test_gwss.py | 11 ++++++++--- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/gwss.cpp b/src/gwss.cpp index 430ffc1..560d528 100644 --- a/src/gwss.cpp +++ b/src/gwss.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include "common.hpp" @@ -27,9 +28,26 @@ void init_gwss(nb::module_& m) def_parallel_openmp(_GWAverage); nb::class_ _GWCorrelation(m, "_GWCorrelation"); + + nb::enum_(_GWCorrelation, "BandwidthInitilizeType") + .value("Null", gwm::GWCorrelation::BandwidthInitilizeType::Null) + .value("Initial", gwm::GWCorrelation::BandwidthInitilizeType::Initial) + .value("Specified", gwm::GWCorrelation::BandwidthInitilizeType::Specified) + .export_values() + ; + + nb::enum_(_GWCorrelation, "BandwidthSelectionCriterionType") + .value("CV", gwm::GWCorrelation::BandwidthSelectionCriterionType::CV) + .value("AIC", gwm::GWCorrelation::BandwidthSelectionCriterionType::AIC) + .export_values() + ; + _GWCorrelation .def(nb::init<>()) - .def_prop_rw("variables", &gwm::GWCorrelation::variables2, &gwm::GWCorrelation::setVariables2, nb::rv_policy::move) + .def_prop_rw("variables1", &gwm::GWCorrelation::variables1, &gwm::GWCorrelation::setVariables1, nb::rv_policy::move) + .def_prop_rw("variables2", &gwm::GWCorrelation::variables2, &gwm::GWCorrelation::setVariables2, nb::rv_policy::move) + .def_prop_rw("bandwidth_initilize", &gwm::GWCorrelation::bandwidthInitilize, &gwm::GWCorrelation::setBandwidthInitilize) + .def_prop_rw("bandwidth_selection_approach", &gwm::GWCorrelation::bandwidthSelectionApproach, &gwm::GWCorrelation::setBandwidthSelectionApproach) .def("run", &gwm::GWCorrelation::run) .def_prop_ro("local_mean", &gwm::GWCorrelation::localMean, nb::rv_policy::move) .def_prop_ro("local_sdev", &gwm::GWCorrelation::localSDev, nb::rv_policy::move) diff --git a/src/pygwmodel/gwss.py b/src/pygwmodel/gwss.py index 062d173..e26d085 100644 --- a/src/pygwmodel/gwss.py +++ b/src/pygwmodel/gwss.py @@ -117,11 +117,17 @@ def __init__(self, sdf: gp.GeoDataFrame, vars: List[str], self.algorithm = _GWCorrelation() self.algorithm.coords = np.asfortranarray( sdf.geometry.centroid.get_coordinates(), dtype=np.float64) - self.algorithm.variables = np.asfortranarray( - sdf[self.vars], dtype=np.float64) + vars_mat = np.asfortranarray(sdf[self.vars], dtype=np.float64) + self.algorithm.variables1 = vars_mat + self.algorithm.variables2 = vars_mat n_var = len(self.vars) + n_col = n_var * n_var sw = SpatialWeight.create(distance, weight) - self.algorithm.spatial_weights = [sw] * n_var + self.algorithm.spatial_weights = [sw] * n_col + self.algorithm.bandwidth_initilize = \ + [_GWCorrelation.BandwidthInitilizeType.Specified] * n_col + self.algorithm.bandwidth_selection_approach = \ + [_GWCorrelation.BandwidthSelectionCriterionType.CV] * n_col def enable_parallel(self, type: ParallelType, **kwargs): if type == ParallelType.OpenMP: @@ -134,13 +140,16 @@ def enable_parallel(self, type: ParallelType, **kwargs): def run(self): self.algorithm.run() - columns = [(fi, fj) for i, fi in enumerate(self.vars) - for _, fj in enumerate(self.vars[i + 1:])] + n_var = len(self.vars) + pairs = [(i, j) for i in range(n_var) for j in range(i + 1, n_var)] + col_indices = [i * n_var + j for i, j in pairs] result_data = { - **{f'{fi}.{fj}_Corr': self.local_corr[:, i] - for i, (fi, fj) in enumerate(columns)}, - **{f'{fi}.{fj}_SCorr': self.local_s_corr[:, i] - for i, (fi, fj) in enumerate(columns)} + **{f'{self.vars[ci]}.{self.vars[cj]}_Corr': + self.local_corr[:, col_idx] + for (ci, cj), col_idx in zip(pairs, col_indices)}, + **{f'{self.vars[ci]}.{self.vars[cj]}_SCorr': + self.local_s_corr[:, col_idx] + for (ci, cj), col_idx in zip(pairs, col_indices)} } self.result_layer = gp.GeoDataFrame(result_data, geometry=self.geometry) return self diff --git a/test/test_gwss.py b/test/test_gwss.py index c9c5f20..db15727 100644 --- a/test/test_gwss.py +++ b/test/test_gwss.py @@ -1,5 +1,6 @@ import os import sys +from pathlib import Path import unittest import numpy as np import pandas as pd @@ -8,7 +9,11 @@ from pygwmodel.spatial_weight import BandwidthWeight from pygwmodel.gwss import GWAverage, GWCorrelation -TEST_DATA = os.environ.get("PYGW_TEST_DATA", sys.argv[1] if len(sys.argv) > 1 else "test/londonhp100.csv") +TEST_DATA = os.environ.get("PYGW_TEST_DATA") +if TEST_DATA is None and len(sys.argv) > 1 and Path(sys.argv[1]).is_file(): + TEST_DATA = sys.argv[1] +if TEST_DATA is None: + TEST_DATA = str(Path(__file__).with_name("londonhp100.csv")) ENABLE_OPENMP = (lambda s: False if s is None else (s.lower() in ['true', '1', 't', 'y', 'yes', 'on']))(os.getenv("ENABLE_OPENMP")) class TestGWAverage(unittest.TestCase): @@ -30,7 +35,7 @@ def test_average(self): BandwidthWeight(36.0, adaptive=True)) londonhp_gwa_result: gp.GeoDataFrame = londonhp_gwa.enable_parallel(p, **pargs).run().result_layer result = pd.DataFrame(londonhp_gwa_result).drop('geometry', axis=1) - result_q = result.apply(lambda x: np.quantile(x, [0, 0.25, 0.5, 0.75, 1], interpolation='midpoint'), axis=0) + result_q = result.apply(lambda x: np.quantile(x, [0, 0.25, 0.5, 0.75, 1], method='midpoint'), axis=0) localmean_q0 = np.array([ [155530.887621432, 71.3459254279447, 6.92671958853926, 39.0446823327541], @@ -92,7 +97,7 @@ def test_correlation(self): BandwidthWeight(36.0, adaptive=True)) londonhp_gwc_result: gp.GeoDataFrame = londonhp_gwc.enable_parallel(p, **pargs).run().result_layer result = pd.DataFrame(londonhp_gwc_result).drop('geometry', axis=1) - result_q = result.apply(lambda x: np.quantile(x, [0, 0.25, 0.5, 0.75, 1], interpolation='midpoint'), axis=0) + result_q = result.apply(lambda x: np.quantile(x, [0, 0.25, 0.5, 0.75, 1], method='midpoint'), axis=0) localcorr_q0 = np.array([ [0.748948486801849,-0.320600183598632,0.203011140141453,-0.126882976445561,-0.0892568204410789,-0.948799446008617],