Skip to content

LCRGWR

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

Conceptual guide

LCRGWR

Locally compensated ridge geographically weighted regression.

Property Value
Type class
Import from pygwrx.models import LCRGWR
Signature LCRGWR(kernel: 'Union[str, Callable[[np.ndarray, float], np.ndarray]]' = 'bisquare', bandwidth: 'Union[float, int, str, None]' = 'cv', bandwidth_method: 'str' = 'cv', adaptive: 'bool' = False, bandwidth_range: 'Optional[Tuple[float, float]]' = None, optimization_method: 'str' = 'golden_section', lambda_ridge: 'float' = 0.0, lambda_adjust: 'bool' = True, cn_thresh: 'float' = 30.0, fit_intercept: 'bool' = True, distance_metric: 'str' = 'euclidean', sigma2_v1: 'bool' = True, verbose: 'bool' = False) -> 'None'
Maintained example examples/models/13_lcr_gwr.py

LCRGWR

LCRGWR(
    kernel: Union[
        str, Callable[[ndarray, float], ndarray]
    ] = "bisquare",
    bandwidth: Union[float, int, str, None] = "cv",
    bandwidth_method: str = "cv",
    adaptive: bool = False,
    bandwidth_range: Optional[Tuple[float, float]] = None,
    optimization_method: str = "golden_section",
    lambda_ridge: float = 0.0,
    lambda_adjust: bool = True,
    cn_thresh: float = 30.0,
    fit_intercept: bool = True,
    distance_metric: str = "euclidean",
    sigma2_v1: bool = True,
    verbose: bool = False,
)

Bases: GWR

Locally compensated ridge geographically weighted regression.

LCR-GWR diagnoses local collinearity from the weighted design matrix and applies a location-specific ridge parameter only where the local condition number exceeds a user-defined threshold. The compensation rule follows the implementation in GWmodel::gwr.lcr::

lambda_i = (d_max - kappa_star * d_min) / (kappa_star - 1),

where d_max and d_min are the largest and smallest singular values of the column-normalized, locally weighted design matrix, and kappa_star is cn_thresh.

The local coefficient estimator uses the GWmodel scaling convention. With A = diag(1 / x_scale) and local spatial weights W_i, the estimator is

.. math::

\hat\beta_i = A\left(A X^T W_i X A + \lambda_i I\right)^{-1}
A X^T W_i y.

Unlike the historical GWmodel diagnostics, pyGWRx constructs the hat matrix from the actual penalized estimator. Consequently, trace statistics, effective degrees of freedom, information criteria, influence, and standard errors remain internally consistent when a ridge term is active.

Parameters:

Name Type Description Default
kernel Union[str, Callable[[ndarray, float], ndarray]]

Spatial kernel name or callable.

'bisquare'
bandwidth Union[float, int, str, None]

Fixed distance, adaptive neighbour count, "cv", or None. Automatic LCR-GWR bandwidth selection is based on strict leave-one-out cross-validation, matching bw.gwr.lcr.

'cv'
bandwidth_method str

Automatic selection criterion. Only "cv" is supported for the classical LCR-GWR algorithm.

'cv'
adaptive bool

Interpret the bandwidth as an integer neighbour count.

False
bandwidth_range Optional[Tuple[float, float]]

Optional lower and upper bandwidth-search bounds.

None
optimization_method str

"golden_section", "brent", or "grid".

'golden_section'
lambda_ridge float

Constant ridge parameter used at every location before optional local compensation. The GWmodel default is 0.

0.0
lambda_adjust bool

Whether to replace lambda_ridge at locations whose local condition number exceeds cn_thresh.

True
cn_thresh float

Maximum desired local condition number. Values from 20 to 30 are common in the LCR-GWR literature.

30.0
fit_intercept bool

Whether to include a local intercept.

True
distance_metric str

Distance metric used to construct spatial weights.

'euclidean'
sigma2_v1 bool

Residual-variance convention inherited from :class:GWR.

True
verbose bool

Whether to print fit and bandwidth-selection progress.

False

Attributes:

Name Type Description
condition_numbers_

Local pre-compensation condition numbers using the GWmodel/Belsley column-normalization convention.

local_lambda_

Ridge parameter used at each calibration location.

compensated_condition_numbers_

Condition numbers implied by the GWmodel compensation formula.

penalized_system_condition_numbers_

Numerical condition numbers of the actual penalized normal systems used for estimation.

locally_compensated_mask_

Boolean mask identifying locations where the threshold-triggered local compensation was applied.

ridge_applied_mask_

Boolean mask identifying all locations with a positive ridge parameter.

cv_residuals_

Leave-one-out residuals when compute_cv=True.

cv_contributions_

Squared leave-one-out residuals.

bandwidth_cv_score_

Sum of squared leave-one-out residuals for the selected or supplied bandwidth when available.

Notes

The reference GWmodel routine penalizes the intercept together with the slopes. This implementation preserves that convention for numerical comparability.

condition_numbers_ are diagnostics of the unpenalized local design; they are therefore expected to remain above cn_thresh at affected locations. Use compensated_condition_numbers_ or penalized_system_condition_numbers_ to inspect post-penalty systems.

References

Wheeler, D. C. (2007). Diagnostic tools and a remedial method for collinearity in geographically weighted regression. Environment and Planning A, 39(10), 2464-2481.

Gollini, I., Lu, B., Charlton, M., Brunsdon, C., and Harris, P. (2015). GWmodel: An R package for exploring spatial heterogeneity using geographically weighted models. Journal of Statistical Software, 63(17), 1-50.

Source code in src/pygwrx/models/lcr_gwr.py
def __init__(
    self,
    kernel: Union[str, Callable[[np.ndarray, float], np.ndarray]] = "bisquare",
    bandwidth: Union[float, int, str, None] = "cv",
    bandwidth_method: str = "cv",
    adaptive: bool = False,
    bandwidth_range: Optional[Tuple[float, float]] = None,
    optimization_method: str = "golden_section",
    lambda_ridge: float = 0.0,
    lambda_adjust: bool = True,
    cn_thresh: float = 30.0,
    fit_intercept: bool = True,
    distance_metric: str = "euclidean",
    sigma2_v1: bool = True,
    verbose: bool = False,
) -> None:
    super().__init__(
        kernel=kernel,
        bandwidth=bandwidth,
        bandwidth_method=bandwidth_method,
        adaptive=adaptive,
        bandwidth_range=bandwidth_range,
        optimization_method=optimization_method,
        fit_intercept=fit_intercept,
        distance_metric=distance_metric,
        sigma2_v1=sigma2_v1,
        verbose=verbose,
    )
    self.lambda_ridge = lambda_ridge
    self.lambda_adjust = lambda_adjust
    self.cn_thresh = cn_thresh
    self._validate_lcr_parameters()
    self._reset_lcr_state()

fit

fit(
    X: Union[ndarray, DataFrame],
    y: Union[ndarray, Series],
    coords: Union[ndarray, DataFrame],
    *,
    compute_hat_matrix: bool = True,
    compute_local_r2: bool = True,
    compute_inference: bool = True,
    compute_cv: bool = True,
    verbose: Optional[bool] = None
) -> "LCRGWR"

Fit LCR-GWR and return self.

Parameters:

Name Type Description Default
X Union[ndarray, DataFrame]

Predictor matrix with shape (n_samples, n_features).

required
y Union[ndarray, Series]

Response vector with shape (n_samples,).

required
coords Union[ndarray, DataFrame]

Spatial coordinates with shape (n_samples, 2).

required
compute_hat_matrix bool

Whether to retain the complete penalized smoother matrix. Trace statistics are always computed.

True
compute_local_r2 bool

Whether to compute local coefficients of determination.

True
compute_inference bool

Whether to compute local standard errors and t values.

True
compute_cv bool

Whether to compute leave-one-out residuals at the final bandwidth.

True
verbose Optional[bool]

Optional per-fit override of the estimator verbosity.

None

Returns:

Type Description
'LCRGWR'

The fitted estimator.

Source code in src/pygwrx/models/lcr_gwr.py
def fit(
    self,
    X: Union[np.ndarray, pd.DataFrame],
    y: Union[np.ndarray, pd.Series],
    coords: Union[np.ndarray, pd.DataFrame],
    *,
    compute_hat_matrix: bool = True,
    compute_local_r2: bool = True,
    compute_inference: bool = True,
    compute_cv: bool = True,
    verbose: Optional[bool] = None,
) -> "LCRGWR":
    """Fit LCR-GWR and return ``self``.

    Args:
        X: Predictor matrix with shape ``(n_samples, n_features)``.
        y: Response vector with shape ``(n_samples,)``.
        coords: Spatial coordinates with shape ``(n_samples, 2)``.
        compute_hat_matrix: Whether to retain the complete penalized smoother
            matrix. Trace statistics are always computed.
        compute_local_r2: Whether to compute local coefficients of determination.
        compute_inference: Whether to compute local standard errors and t values.
        compute_cv: Whether to compute leave-one-out residuals at the final
            bandwidth.
        verbose: Optional per-fit override of the estimator verbosity.

    Returns:
        The fitted estimator.
    """
    for name, value in (
        ("compute_hat_matrix", compute_hat_matrix),
        ("compute_local_r2", compute_local_r2),
        ("compute_inference", compute_inference),
        ("compute_cv", compute_cv),
    ):
        if not isinstance(value, (bool, np.bool_)):
            raise TypeError(f"{name} must be boolean.")
    if verbose is not None:
        if not isinstance(verbose, (bool, np.bool_)):
            raise TypeError("verbose must be boolean or None.")
        self.verbose = bool(verbose)

    self._validate_gwr_parameters(
        kernel=self.kernel,
        bandwidth=self.bandwidth,
        bandwidth_method=self.bandwidth_method,
        adaptive=self.adaptive,
        bandwidth_range=self.bandwidth_range,
        optimization_method=self.optimization_method,
    )
    self._validate_lcr_parameters()
    self._reset_fit_state()

    try:
        X_arr, y_arr, coords_arr = self._validate_inputs(X, y, coords)
        feature_names = (
            None
            if self.feature_names_in_ is None
            else self.feature_names_in_.copy()
        )
        self._store_training_data(X_arr, y_arr, coords_arr, copy=True)
        self.feature_names_in_ = feature_names
        X_design = (
            add_intercept(self.X_train_) if self.fit_intercept else self.X_train_
        )
        self.design_scales_ = self._compute_design_scales(X_design)
        self.kernel_func_ = get_kernel_function(self.kernel)
        distances = np.asarray(
            compute_distance_matrix(
                self.coords_train_,
                self.coords_train_,
                metric=self.distance_metric,
            ),
            dtype=float,
        )
        self.bandwidth_ = self._select_bandwidth_lcr(
            X_design,
            self.y_train_,
            distances,
            self.design_scales_,
        )

        if self.verbose:
            bandwidth_kind = (
                "adaptive neighbours" if self.adaptive else "fixed distance"
            )
            print(
                f"Fitting LCRGWR with bandwidth={self.bandwidth_} "
                f"({bandwidth_kind}), lambda_adjust={self.lambda_adjust}, "
                f"cn_thresh={self.cn_thresh}..."
            )

        self.inference_enabled_ = bool(compute_inference)
        local_fit = self._fit_training_locations_lcr(
            X_design,
            distances,
            store_hat_matrix=bool(compute_hat_matrix),
            compute_inference=self.inference_enabled_,
        )
        if self.fit_intercept:
            self.intercept_ = local_fit.params[:, 0].copy()
            self.coef_ = local_fit.params[:, 1:].copy()
        else:
            self.intercept_ = np.zeros(self.n_samples_, dtype=float)
            self.coef_ = local_fit.params.copy()

        self.coefficients_ = local_fit.params.copy()
        self.fitted_values_ = local_fit.fitted_values.copy()
        self.residuals_ = self.y_train_ - self.fitted_values_
        self.influence_ = local_fit.influence.copy()
        self.hat_matrix_ = local_fit.hat_matrix
        self.S_matrix_ = self.hat_matrix_
        self.condition_numbers_ = local_fit.condition_numbers.copy()
        self.local_condition_numbers_ = self.condition_numbers_
        self.compensated_condition_numbers_ = (
            local_fit.compensated_condition_numbers.copy()
        )
        self.penalized_system_condition_numbers_ = (
            local_fit.penalized_system_condition_numbers.copy()
        )
        self.local_lambda_ = local_fit.local_lambdas.copy()
        self.local_lambdas_ = self.local_lambda_
        self.locally_compensated_mask_ = self.lambda_adjust & (
            self.condition_numbers_ > self.cn_thresh
        )
        self.ridge_applied_mask_ = self.local_lambda_ > 0.0

        self.diagnostics_ = compute_diagnostics(
            self.y_train_,
            self.fitted_values_,
            compute_gwr_stats=True,
            trace_S=local_fit.trace_S,
            trace_StS=local_fit.trace_StS,
        )
        self.diagnostics_.update(
            {
                "mean_condition_number": float(np.mean(self.condition_numbers_)),
                "max_condition_number": float(np.max(self.condition_numbers_)),
                "mean_local_lambda": float(np.mean(self.local_lambda_)),
                "max_local_lambda": float(np.max(self.local_lambda_)),
                "n_locally_compensated": float(
                    np.count_nonzero(self.locally_compensated_mask_)
                ),
            }
        )
        self.local_r2_ = (
            self._compute_local_r2_from_distances(distances)
            if compute_local_r2
            else None
        )
        self._set_inference_results(
            local_fit.covariance_factors,
            trace_S=local_fit.trace_S,
            trace_StS=local_fit.trace_StS,
        )

        if compute_cv:
            self.cv_residuals_ = self._cross_validation_residuals(
                X_design,
                self.y_train_,
                distances,
                self.bandwidth_,
                self.design_scales_,
            )
            self.cv_contributions_ = self.cv_residuals_**2
            self.bandwidth_cv_score_ = float(np.sum(self.cv_contributions_))

        self._mark_fitted()
        return self
    except Exception:
        self._reset_fit_state()
        raise

predict_result

predict_result(
    X: Union[ndarray, DataFrame],
    coords: Union[ndarray, DataFrame],
) -> GWRPredictionResult

Predict responses and return local parameters and inference results.

Source code in src/pygwrx/models/lcr_gwr.py
def predict_result(
    self,
    X: Union[np.ndarray, pd.DataFrame],
    coords: Union[np.ndarray, pd.DataFrame],
) -> GWRPredictionResult:
    """Predict responses and return local parameters and inference results."""
    X_arr, coords_arr = self._validate_prediction_inputs(X, coords)
    params = self._prediction_parameters(coords_arr)
    coef = np.asarray(params["coef"], dtype=float)
    intercept = np.asarray(params["intercept"], dtype=float)
    predictions = np.einsum("ij,ij->i", X_arr, coef) + intercept
    names = (
        tuple(str(name) for name in self.feature_names_in_)
        if self.feature_names_in_ is not None
        else tuple(f"x{index}" for index in range(X_arr.shape[1]))
    )

    full_se = params["standard_errors"]
    full_t = params["t_values"]
    if full_se is not None:
        if self.fit_intercept:
            intercept_se = full_se[:, 0]
            coef_se = full_se[:, 1:]
        else:
            intercept_se = np.zeros(coords_arr.shape[0], dtype=float)
            coef_se = full_se
    else:
        intercept_se = None
        coef_se = None
    if full_t is not None:
        if self.fit_intercept:
            intercept_t = full_t[:, 0]
            coef_t = full_t[:, 1:]
        else:
            intercept_t = np.full(coords_arr.shape[0], np.nan, dtype=float)
            coef_t = full_t
    else:
        intercept_t = None
        coef_t = None

    return GWRPredictionResult(
        predictions=np.asarray(predictions, dtype=float),
        coef=coef,
        intercept=intercept,
        coords=np.asarray(coords_arr, dtype=float),
        feature_names=names,
        coef_standard_errors=coef_se,
        intercept_standard_errors=intercept_se,
        coef_t_values=coef_t,
        intercept_t_values=intercept_t,
    )

get_local_diagnostics

get_local_diagnostics(
    coords: Union[ndarray, DataFrame],
) -> pd.DataFrame

Return condition numbers and ridge parameters at target locations.

Source code in src/pygwrx/models/lcr_gwr.py
def get_local_diagnostics(
    self,
    coords: Union[np.ndarray, pd.DataFrame],
) -> pd.DataFrame:
    """Return condition numbers and ridge parameters at target locations."""
    params = self._prediction_parameters(coords)
    coords_arr = np.asarray(params["coords"], dtype=float)
    return pd.DataFrame(
        {
            "coord_0": coords_arr[:, 0],
            "coord_1": coords_arr[:, 1],
            "condition_number": params["condition_numbers"],
            "local_lambda": params["local_lambdas"],
            "compensated_condition_number": params["compensated_condition_numbers"],
            "penalized_system_condition_number": params[
                "penalized_system_condition_numbers"
            ],
        }
    )

to_frame

to_frame() -> pd.DataFrame

Return standard GWR outputs plus LCR diagnostics.

Source code in src/pygwrx/models/lcr_gwr.py
def to_frame(self) -> pd.DataFrame:
    """Return standard GWR outputs plus LCR diagnostics."""
    frame = super().to_frame()
    if self.condition_numbers_ is not None:
        frame["condition_number"] = self.condition_numbers_
    if self.local_lambda_ is not None:
        frame["local_lambda"] = self.local_lambda_
    if self.compensated_condition_numbers_ is not None:
        frame["compensated_condition_number"] = self.compensated_condition_numbers_
    if self.penalized_system_condition_numbers_ is not None:
        frame["penalized_system_condition_number"] = (
            self.penalized_system_condition_numbers_
        )
    if self.locally_compensated_mask_ is not None:
        frame["locally_compensated"] = self.locally_compensated_mask_
    if self.cv_residuals_ is not None:
        frame["cv_residual"] = self.cv_residuals_
        frame["cv_score"] = self.cv_contributions_
    return frame

summary

summary() -> str

Return a stable text summary of the LCR-GWR fit.

Source code in src/pygwrx/models/lcr_gwr.py
def summary(self) -> str:
    """Return a stable text summary of the LCR-GWR fit."""
    self._check_is_fitted()
    if (
        self.condition_numbers_ is None
        or self.local_lambda_ is None
        or self.locally_compensated_mask_ is None
        or self.diagnostics_ is None
    ):
        raise RuntimeError("LCRGWR diagnostics are unavailable.")

    lines = [
        "=" * 78,
        "Locally Compensated Ridge Geographically Weighted Regression",
        "=" * 78,
        f"Samples: {self.n_samples_}",
        f"Predictors: {self.n_features_in_}",
        f"Kernel: {self.kernel}",
        f"Bandwidth: {self.bandwidth_} "
        f"({'adaptive neighbours' if self.adaptive else 'fixed distance'})",
        f"Distance metric: {self.distance_metric}",
        f"Global ridge lambda: {self.lambda_ridge:.6g}",
        f"Local compensation: {self.lambda_adjust}",
        f"Condition-number threshold: {self.cn_thresh:.6g}",
        f"Locally compensated locations: "
        f"{np.count_nonzero(self.locally_compensated_mask_)}",
        "",
        "Local collinearity and ridge diagnostics",
        "-" * 78,
        f"Condition number min/median/mean/max: "
        f"{np.min(self.condition_numbers_):.4f} / "
        f"{np.median(self.condition_numbers_):.4f} / "
        f"{np.mean(self.condition_numbers_):.4f} / "
        f"{np.max(self.condition_numbers_):.4f}",
        f"Local lambda min/median/mean/max: "
        f"{np.min(self.local_lambda_):.6g} / "
        f"{np.median(self.local_lambda_):.6g} / "
        f"{np.mean(self.local_lambda_):.6g} / "
        f"{np.max(self.local_lambda_):.6g}",
        "",
        "Model diagnostics",
        "-" * 78,
        f"RSS: {self.diagnostics_['rss']:.6f}",
        f"R-squared: {self.diagnostics_['r2']:.6f}",
        f"Adjusted R-squared: {self.diagnostics_['adj_r2']:.6f}",
        f"trace(S): {self.diagnostics_['trace_S']:.6f}",
        f"trace(S'S): {self.diagnostics_['trace_StS']:.6f}",
        f"AIC: {self.diagnostics_['aic']:.6f}",
        f"AICc: {self.diagnostics_['aicc']:.6f}",
        f"BIC: {self.diagnostics_['bic']:.6f}",
    ]
    if self.bandwidth_cv_score_ is not None:
        lines.append(f"Leave-one-out CV score: {self.bandwidth_cv_score_:.6f}")
    lines.append("=" * 78)
    return "\n".join(lines)

Runnable examples used on this page

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

"""Fit locally compensated ridge GWR for collinear predictors."""

# 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 collinear_regression, print_model_result

from pygwrx import LCRGWR

X, y, coords = collinear_regression()
model = LCRGWR(bandwidth=28, adaptive=True, cn_thresh=15.0, lambda_adjust=True).fit(
    X, y, coords
)
print_model_result(model)
print("local_condition_numbers=", model.local_condition_numbers_[:5])
print("local_lambdas=", model.local_lambdas_[:5])