Skip to content

LGGWR

This page documents 2 public symbols. Each entry includes its purpose, import path, full API docstring, and the maintained example that exercises it.

Conceptual guide

LGGWR

Latent-Geometry Geographically Weighted Regression.

Property Value
Type class
Import from pygwrx.models import LGGWR
Signature LGGWR(latent_dim: 'int' = 2, bandwidth: 'BandwidthLike' = None, adaptive: 'bool' = False, kernel: 'str' = 'gaussian', geometry: 'str' = 'joint', learning_rate: 'float' = 0.05, max_iter: 'int' = 100, tol: 'float' = 1e-06, lambda_reg: 'float' = 0.0, orthogonal_constraint: 'Optional[bool]' = None, grad_clip: 'float' = 10.0, patience: 'int' = 20, select_bandwidth: 'bool' = True, random_state: 'Optional[int]' = None, verbose: 'bool' = False, *, fit_intercept: 'bool' = True, standardize_geometry: 'bool' = True, initialization: 'str' = 'coordinate', n_restarts: 'int' = 1, scale_constraint: 'str' = 'frobenius', bandwidth_updates: 'int' = 1) -> 'None'
Maintained example examples/models/18_lg_gwr.py

LGGWR

LGGWR(
    latent_dim: int = 2,
    bandwidth: BandwidthLike = None,
    adaptive: bool = False,
    kernel: str = "gaussian",
    geometry: str = "joint",
    learning_rate: float = 0.05,
    max_iter: int = 100,
    tol: float = 1e-06,
    lambda_reg: float = 0.0,
    orthogonal_constraint: Optional[bool] = None,
    grad_clip: float = 10.0,
    patience: int = 20,
    select_bandwidth: bool = True,
    random_state: Optional[int] = None,
    verbose: bool = False,
    *,
    fit_intercept: bool = True,
    standardize_geometry: bool = True,
    initialization: str = "coordinate",
    n_restarts: int = 1,
    scale_constraint: str = "frobenius",
    bandwidth_updates: int = 1
)

Latent-Geometry Geographically Weighted Regression.

For observation input :math:u_i=[s_i,a_i], joint LG-GWR learns a linear map :math:z_i=A u_i and defines

.. math::

w_{ij}=K(\|z_i-z_j\|/h).

The map is trained against leave-one-out prediction error. The default Frobenius-norm constraint fixes the otherwise unidentified global scale of A; the bandwidth carries that scale. Consequently, ordinary L2 regularisation is allowed only when scale_constraint="none".

The separable form keeps geographic distance as one channel and learns an attribute map :math:\zeta_i=B a_i for a second multiplicative channel,

.. math::

w_{ij}=K(d_{ij}^{geo}/h_g)K(\|\zeta_i-\zeta_j\|/h_a).

With :math:h_a=\infty, the separable model reduces exactly to geographic GWR at the same geographic bandwidth.

Parameters:

Name Type Description Default
latent_dim int

Dimension of the learned latent space.

2
bandwidth BandwidthLike

Joint latent bandwidth. In separable mode, a two-item tuple supplies (h_g, h_a); a scalar supplies h_g and leaves h_a automatic.

None
adaptive bool

Interpret a numeric joint bandwidth as a neighbour count and convert it once to a fixed latent distance. The analytical gradient itself is for a fixed distance bandwidth.

False
kernel str

"gaussian", "bisquare" or "exponential".

'gaussian'
geometry str

"joint" or "separable".

'joint'
fit_intercept bool

Add an unpenalised local intercept. A legacy leading all-ones column is detected and removed before the intercept is added.

True
standardize_geometry bool

Centre coordinates and scale them by one common factor (preserving geographic shape), and z-standardise attributes.

True
initialization str

"coordinate", "random" or "pca". Coordinate initialisation makes ordinary geographic geometry the first joint candidate.

'coordinate'
n_restarts int

Number of deterministic restarts. The first uses the requested initialisation; later restarts are random.

1
learning_rate float

NumPy Adam learning rate.

0.05
max_iter int

Maximum iterations per geometry/bandwidth stage.

100
tol float

Improvement and convergence tolerance.

1e-06
lambda_reg float

Frobenius L2 regularisation. It must be zero while a norm or orthogonality constraint is active because the norm is then fixed.

0.0
scale_constraint str

"frobenius" (default), "orthogonal" or "none".

'frobenius'
orthogonal_constraint Optional[bool]

Deprecated compatibility switch. True maps to scale_constraint="orthogonal".

None
grad_clip float

Global gradient-norm clipping threshold.

10.0
patience int

Early-stopping patience.

20
select_bandwidth bool

Select reporting bandwidth(s) by AICc.

True
bandwidth_updates int

Number of additional geometry-training stages after AICc bandwidth reselection. A value of one implements geometry -> bandwidth -> geometry -> bandwidth.

1
random_state Optional[int]

Reproducibility seed.

None
verbose bool

Print optimisation progress.

False
Source code in src/pygwrx/models/lg_gwr.py
def __init__(
    self,
    latent_dim: int = 2,
    bandwidth: BandwidthLike = None,
    adaptive: bool = False,
    kernel: str = "gaussian",
    geometry: str = "joint",
    learning_rate: float = 0.05,
    max_iter: int = 100,
    tol: float = 1e-6,
    lambda_reg: float = 0.0,
    orthogonal_constraint: Optional[bool] = None,
    grad_clip: float = 10.0,
    patience: int = 20,
    select_bandwidth: bool = True,
    random_state: Optional[int] = None,
    verbose: bool = False,
    *,
    fit_intercept: bool = True,
    standardize_geometry: bool = True,
    initialization: str = "coordinate",
    n_restarts: int = 1,
    scale_constraint: str = "frobenius",
    bandwidth_updates: int = 1,
) -> None:
    self.latent_dim = self._positive_int(latent_dim, "latent_dim")
    self.bandwidth = bandwidth
    self.adaptive = self._boolean(adaptive, "adaptive")
    self.kernel = self._choice(kernel, "kernel", self._KERNELS)
    self.geometry = self._choice(geometry, "geometry", self._GEOMETRIES)
    self.learning_rate = self._nonnegative_float(learning_rate, "learning_rate")
    self.max_iter = self._nonnegative_int(max_iter, "max_iter")
    self.tol = self._positive_float(tol, "tol")
    self.lambda_reg = self._nonnegative_float(lambda_reg, "lambda_reg")
    self.grad_clip = self._positive_float(grad_clip, "grad_clip")
    self.patience = self._positive_int(patience, "patience")
    self.select_bandwidth = self._boolean(select_bandwidth, "select_bandwidth")
    self.random_state = random_state
    self.verbose = self._boolean(verbose, "verbose")
    self.fit_intercept = self._boolean(fit_intercept, "fit_intercept")
    self.standardize_geometry = self._boolean(
        standardize_geometry, "standardize_geometry"
    )
    self.initialization = self._choice(
        initialization, "initialization", self._INITIALISATIONS
    )
    self.n_restarts = self._positive_int(n_restarts, "n_restarts")
    self.bandwidth_updates = self._nonnegative_int(
        bandwidth_updates, "bandwidth_updates"
    )

    if orthogonal_constraint is not None:
        orthogonal = self._boolean(orthogonal_constraint, "orthogonal_constraint")
        if orthogonal:
            scale_constraint = "orthogonal"
        warnings.warn(
            "orthogonal_constraint is deprecated; use scale_constraint instead.",
            DeprecationWarning,
            stacklevel=2,
        )
    self.scale_constraint = self._choice(
        scale_constraint, "scale_constraint", self._SCALE_CONSTRAINTS
    )
    # Compatibility attribute retained for existing user code.
    self.orthogonal_constraint = self.scale_constraint == "orthogonal"

    if self.lambda_reg > 0.0 and self.scale_constraint != "none":
        raise ValueError(
            "lambda_reg must be 0 when scale_constraint fixes the matrix norm. "
            "Use scale_constraint='none' for ordinary L2 regularisation."
        )
    if self.scale_constraint == "none" and self.lambda_reg == 0.0:
        warnings.warn(
            "An unconstrained latent map without regularisation has an "
            "unidentified scale; consider scale_constraint='frobenius'.",
            RuntimeWarning,
            stacklevel=2,
        )

    self._validate_bandwidth_spec()
    self._reset_fit_state()

fit

fit(
    X: ArrayLike,
    y: VectorLike,
    coords: ArrayLike,
    attributes: Optional[ArrayLike] = None,
) -> "LGGWR"

Fit LG-GWR and return self.

Source code in src/pygwrx/models/lg_gwr.py
def fit(
    self,
    X: ArrayLike,
    y: VectorLike,
    coords: ArrayLike,
    attributes: Optional[ArrayLike] = None,
) -> "LGGWR":
    """Fit LG-GWR and return ``self``."""
    self._reset_fit_state()
    try:
        X_design, y_arr, coords_geometry, attrs_geometry = self._prepare_fit_inputs(
            X, y, coords, attributes
        )
        if self.geometry == "separable":
            self._fit_separable(X_design, y_arr, coords_geometry, attrs_geometry)
        else:
            self._fit_joint(X_design, y_arr, coords_geometry, attrs_geometry)
        self._finalise_public_parameters()
        self._is_fitted = True
        return self
    except Exception:
        self._reset_fit_state()
        raise

predict_result

predict_result(
    X: ArrayLike,
    coords: ArrayLike,
    attributes: Optional[ArrayLike] = None,
) -> LGGWRPredictionResult

Recalibrate local parameters at new locations.

Source code in src/pygwrx/models/lg_gwr.py
def predict_result(
    self,
    X: ArrayLike,
    coords: ArrayLike,
    attributes: Optional[ArrayLike] = None,
) -> LGGWRPredictionResult:
    """Recalibrate local parameters at new locations."""
    X_design, coords_raw, coords_geometry, attrs_geometry = (
        self._prepare_prediction_inputs(X, coords, attributes)
    )
    if self.X_design_ is None or self.y_train_ is None:
        raise RuntimeError("Training state is incomplete.")

    if self.geometry == "separable":
        if self.B_ is None or not isinstance(self.bandwidth_, tuple):
            raise RuntimeError("Separable training state is incomplete.")
        zeta_train = (
            self.attrs_geometry_ @ self.B_.T
            if self.attrs_geometry_ is not None and self.attrs_geometry_.shape[1]
            else np.zeros((self.X_design_.shape[0], 0))
        )
        zeta_query = (
            attrs_geometry @ self.B_.T
            if attrs_geometry.shape[1]
            else np.zeros((X_design.shape[0], 0))
        )
        betas = self._local_fit_sep(
            self.X_design_,
            self.y_train_,
            self.coords_geometry_,
            zeta_train,
            coords_geometry,
            zeta_query,
            float(self.bandwidth_[0]),
            float(self.bandwidth_[1]),
            X_design,
        )
        latent = zeta_query
    else:
        if self.A_ is None or not isinstance(self.bandwidth_, Real):
            raise RuntimeError("Joint training state is incomplete.")
        u_query = np.hstack([coords_geometry, attrs_geometry])
        latent = u_query @ self.A_.T
        betas = self._local_fit(
            self.X_design_,
            self.y_train_,
            self.latent_coords_,
            latent,
            float(self.bandwidth_),
            X_design,
        )
    predictions = np.einsum("ij,ij->i", X_design, betas)
    if self.fit_intercept:
        intercepts = betas[:, 0]
        coefficients = betas[:, 1:]
    else:
        intercepts = np.zeros(X_design.shape[0], dtype=float)
        coefficients = betas
    return LGGWRPredictionResult(
        predictions=predictions,
        coefficients=coefficients,
        intercepts=intercepts,
        coords=coords_raw.copy(),
        latent_coords=latent.copy(),
        feature_names=self.feature_names_,
    )

predict

predict(
    X: ArrayLike,
    coords: ArrayLike,
    attributes: Optional[ArrayLike] = None,
) -> np.ndarray

Return LG-GWR predictions at new locations.

Source code in src/pygwrx/models/lg_gwr.py
def predict(
    self,
    X: ArrayLike,
    coords: ArrayLike,
    attributes: Optional[ArrayLike] = None,
) -> np.ndarray:
    """Return LG-GWR predictions at new locations."""
    return self.predict_result(X, coords, attributes).predictions

results_frame

results_frame() -> pd.DataFrame

Return training-location parameters, fitted values and latent coordinates.

Source code in src/pygwrx/models/lg_gwr.py
def results_frame(self) -> pd.DataFrame:
    """Return training-location parameters, fitted values and latent coordinates."""
    self._require_fitted()
    if (
        self.coords_train_ is None
        or self.fitted_values_ is None
        or self.residuals_ is None
        or self.coef_ is None
        or self.intercept_ is None
        or self.latent_coords_ is None
    ):
        raise RuntimeError("Training results are incomplete.")
    data: Dict[str, np.ndarray] = {
        "coord_0": self.coords_train_[:, 0],
        "coord_1": self.coords_train_[:, 1],
        "fitted": self.fitted_values_,
        "residual": self.residuals_,
        "intercept": self.intercept_,
    }
    for index in range(self.latent_coords_.shape[1]):
        data[f"latent_{index}"] = self.latent_coords_[:, index]
    for index, name in enumerate(self.feature_names_):
        data[f"coef_{name}"] = self.coef_[:, index]
    return pd.DataFrame(data)

to_frame

to_frame() -> pd.DataFrame

Alias for :meth:results_frame.

Source code in src/pygwrx/models/lg_gwr.py
def to_frame(self) -> pd.DataFrame:
    """Alias for :meth:`results_frame`."""
    return self.results_frame()

metric_frame

metric_frame() -> pd.DataFrame

Return rotation-invariant learned metric contributions.

Source code in src/pygwrx/models/lg_gwr.py
def metric_frame(self) -> pd.DataFrame:
    """Return rotation-invariant learned metric contributions."""
    self._require_fitted()
    if self.metric_matrix_ is None or self.metric_contributions_ is None:
        raise RuntimeError("Metric outputs are unavailable.")
    if self.geometry == "joint":
        names = self.geometry_feature_names_
    else:
        coord_count = 0 if self.coord_center_ is None else self.coord_center_.size
        names = self.geometry_feature_names_[coord_count:]
    return pd.DataFrame(
        {
            "geometry_feature": list(names),
            "metric_diagonal": np.diag(self.metric_matrix_),
            "metric_contribution": self.metric_contributions_,
        }
    )

get_latent_coordinates

get_latent_coordinates(
    coords: Optional[ArrayLike] = None,
    attributes: Optional[ArrayLike] = None,
) -> np.ndarray

Return training or transformed latent coordinates.

Source code in src/pygwrx/models/lg_gwr.py
def get_latent_coordinates(
    self,
    coords: Optional[ArrayLike] = None,
    attributes: Optional[ArrayLike] = None,
) -> np.ndarray:
    """Return training or transformed latent coordinates."""
    self._require_fitted()
    if coords is None:
        return self.latent_coords_.copy()
    n = np.asarray(coords).shape[0]
    dummy = np.zeros((n, self.n_features_in_), dtype=float)
    _, _, coords_geometry, attrs_geometry = self._prepare_prediction_inputs(
        dummy, coords, attributes
    )
    if self.geometry == "separable":
        if self.B_ is None:
            raise RuntimeError("B is unavailable.")
        return (
            attrs_geometry @ self.B_.T
            if attrs_geometry.shape[1]
            else np.zeros((n, 0))
        )
    if self.A_ is None:
        raise RuntimeError("A is unavailable.")
    return np.hstack([coords_geometry, attrs_geometry]) @ self.A_.T

summary

summary() -> str

Return a plain-text fitted-model summary.

Source code in src/pygwrx/models/lg_gwr.py
def summary(self) -> str:
    """Return a plain-text fitted-model summary."""
    self._require_fitted()
    if self.diagnostics_ is None or self.metric_contributions_ is None:
        raise RuntimeError("Model diagnostics are incomplete.")
    matrix = self.A_ if self.geometry == "joint" else self.B_
    return format_summary(
        "LG-GWR Summary",
        {
            "model": "LG-GWR",
            "geometry": self.geometry,
            "n_samples": int(self.y_train_.size),
            "n_features": int(self.n_features_in_),
            "latent_dim": int(self.latent_dim),
            "bandwidth": self.bandwidth_,
            "bandwidth_history": tuple(self.bandwidth_history_),
            "kernel": self.kernel,
            "fit_intercept": self.fit_intercept,
            "standardize_geometry": self.standardize_geometry,
            "initialization": self.initialization,
            "n_restarts": self.n_restarts,
            "scale_constraint": self.scale_constraint,
            "n_iterations": self.n_iter_,
            "converged": self.converged_,
            "stop_reason": self.stop_reason_,
            "best_loss": self.best_loss_,
            "final_loo_loss": self.final_loo_loss_,
            "matrix_norm": float(np.linalg.norm(matrix, "fro")),
            "r2": float(self.diagnostics_["r2"]),
            "adj_r2": float(self.diagnostics_["adj_r2"]),
            "rmse": float(self.diagnostics_["rmse"]),
            "aicc": float(self.diagnostics_["aicc"]),
            "enp": float(self.diagnostics_["enp"]),
        },
    )

LGGWRPredictionResult

Detailed LG-GWR predictions at evaluation locations.

Property Value
Type class
Import from pygwrx.models import LGGWRPredictionResult
Signature LGGWRPredictionResult(predictions: 'np.ndarray', coefficients: 'np.ndarray', intercepts: 'np.ndarray', coords: 'np.ndarray', latent_coords: 'np.ndarray', feature_names: 'Tuple[str, ...]') -> None
Maintained example examples/models/18_lg_gwr.py

LGGWRPredictionResult dataclass

LGGWRPredictionResult(
    predictions: ndarray,
    coefficients: ndarray,
    intercepts: ndarray,
    coords: ndarray,
    latent_coords: ndarray,
    feature_names: Tuple[str, ...],
)

Detailed LG-GWR predictions at evaluation locations.

to_frame

to_frame() -> pd.DataFrame

Return predictions and local parameters as a DataFrame.

Source code in src/pygwrx/models/lg_gwr.py
def to_frame(self) -> pd.DataFrame:
    """Return predictions and local parameters as a DataFrame."""
    data: Dict[str, np.ndarray] = {
        "coord_0": self.coords[:, 0],
        "coord_1": self.coords[:, 1],
        "prediction": self.predictions,
        "intercept": self.intercepts,
    }
    for index in range(self.latent_coords.shape[1]):
        data[f"latent_{index}"] = self.latent_coords[:, index]
    for index, name in enumerate(self.feature_names):
        data[f"coef_{name}"] = self.coefficients[:, index]
    return pd.DataFrame(data)

Runnable examples used on this page

examples/models/18_lg_gwr.py
# SPDX-FileCopyrightText: 2026 Jinghao Hu
# SPDX-License-Identifier: MIT

"""Fit latent-geometry GWR with auxiliary contextual attributes."""

# Allow this script to run directly from any working directory.
import sys
from pathlib import Path

_PROJECT_ROOT = Path(__file__).resolve().parents[2]
_EXAMPLES_ROOT = _PROJECT_ROOT / "examples"
_SRC_ROOT = _PROJECT_ROOT / "src"
for _path in (_SRC_ROOT, _EXAMPLES_ROOT):
    if str(_path) not in sys.path:
        sys.path.insert(0, str(_path))

from _common import latent_regression, print_model_result

from pygwrx import LGGWR, LGGWRPredictionResult

X, y, coords, attributes = latent_regression()
model = LGGWR(
    latent_dim=2, bandwidth=2.5, select_bandwidth=False, max_iter=8, random_state=0
).fit(X, y, coords, attributes)
print_model_result(model)
print("latent_coordinates_shape=", model.latent_coords_.shape)
result = model.predict_result(X.iloc[:3], coords.iloc[:3], attributes.iloc[:3])
assert isinstance(result, LGGWRPredictionResult)
print(result.to_frame())