Skip to content

GTWR

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

GTWR

Geographically and temporally weighted regression.

Property Value
Type class
Import from pygwrx.models import GTWR
Signature GTWR(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, lambda_st: 'Union[float, str]' = 0.05, lambda_range: 'Tuple[float, float]' = (0.0, 1.0), lambda_grid_size: 'int' = 11, ksi: 'float' = 0.0, distance_combination: 'str' = 'gwmodel', tau: 'float' = 1.0, causal: 'bool' = False, time_unit: 'str' = 'auto', optimization_method: 'str' = 'golden_section', search_grid_size: 'int' = 25, search_tol: 'float' = 1e-05, search_max_iter: 'int' = 100, fit_intercept: 'bool' = True, distance_metric: 'str' = 'euclidean', sigma2_v1: 'bool' = False, verbose: 'bool' = False) -> 'None'
Maintained example examples/models/05_gtwr.py

GTWR

GTWR(
    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,
    lambda_st: Union[float, str] = 0.05,
    lambda_range: Tuple[float, float] = (0.0, 1.0),
    lambda_grid_size: int = 11,
    ksi: float = 0.0,
    distance_combination: str = "gwmodel",
    tau: float = 1.0,
    causal: bool = False,
    time_unit: str = "auto",
    optimization_method: str = "golden_section",
    search_grid_size: int = 25,
    search_tol: float = 1e-05,
    search_max_iter: int = 100,
    fit_intercept: bool = True,
    distance_metric: str = "euclidean",
    sigma2_v1: bool = False,
    verbose: bool = False,
)

Bases: BaseSpatiotemporalRegressor

Geographically and temporally weighted regression.

The default distance_combination="gwmodel" follows the generalized spatiotemporal distance implemented by GWmodel::st.dist:

.. math::

d_{st} = \lambda d_s + (1-\lambda)d_t
+ 2\sqrt{\lambda(1-\lambda)d_s d_t}\cos(\xi).

GWmodel uses absolute temporal differences, so the standard default is causal=False. With causal=True, observations later than the regression time receive a very large temporal distance as an optional history-only extension for leakage-safe forecasting.

distance_combination="euclidean" instead uses :math:\sqrt{d_s^2 + \tau d_t^2} and is provided for transparent comparison with the public Python gtwr package.

Parameters:

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

Kernel name or callable accepting (distances, bandwidth).

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

Numeric bandwidth or "cv"/"aicc" for automatic search.

'cv'
bandwidth_method str

Selection criterion used when bandwidth=None.

'cv'
adaptive bool

Interpret bandwidth as an integer nearest-neighbour count.

False
bandwidth_range Optional[Tuple[float, float]]

Optional lower and upper search bounds.

None
lambda_st Union[float, str]

GWmodel spatial-temporal balance in [0, 1] or "auto".

0.05
lambda_range Tuple[float, float]

Search interval used only when lambda_st="auto".

(0.0, 1.0)
lambda_grid_size int

Number of deterministic lambda candidates.

11
ksi float

GWmodel interaction angle in radians, constrained to [0, pi].

0.0
distance_combination str

"gwmodel" or "euclidean".

'gwmodel'
tau float

Non-negative temporal scale used by the Euclidean combination.

1.0
causal bool

Whether future observations should be temporally remote. The default False matches GWmodel::st.dist.

False
time_unit str

Unit used to convert datetime-like times. "auto" chooses a stable unit from the training span. Numeric times are not rescaled.

'auto'
optimization_method str

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

'golden_section'
search_grid_size int

Number of fixed-bandwidth candidates for grid search.

25
search_tol float

Tolerance for continuous bandwidth optimization.

1e-05
search_max_iter int

Maximum continuous search iterations.

100
fit_intercept bool

Whether to include a local intercept.

True
distance_metric str

Spatial distance metric.

'euclidean'
sigma2_v1 bool

Residual variance convention. False matches GWmodel's RSS / (n - 2 trace(S) + trace(S'S)) default diagnostic.

False
verbose bool

Whether to print selection and fitting progress.

False

Attributes:

Name Type Description
bandwidth_

Selected fixed distance or adaptive neighbour count.

lambda_st_

Fitted GWmodel balance parameter.

tau_

Fitted Euclidean temporal scale.

times_train_ Optional[ndarray]

Numeric training times in time_unit_.

time_unit_ Optional[ndarray]

Resolved datetime unit or "numeric".

spatiotemporal_distance_matrix_ Optional[ndarray]

Training target-to-observation distances.

coef_ Optional[ndarray]

Local slopes with shape (n_samples, n_features).

intercept_ Optional[ndarray]

Local intercepts with shape (n_samples,).

fitted_values_ Optional[ndarray]

Fitted responses at calibration locations.

diagnostics_ Optional[ndarray]

Gaussian GWR-style diagnostics based on smoother traces.

Source code in src/pygwrx/models/gtwr.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,
    lambda_st: Union[float, str] = 0.05,
    lambda_range: Tuple[float, float] = (0.0, 1.0),
    lambda_grid_size: int = 11,
    ksi: float = 0.0,
    distance_combination: str = "gwmodel",
    tau: float = 1.0,
    causal: bool = False,
    time_unit: str = "auto",
    optimization_method: str = "golden_section",
    search_grid_size: int = 25,
    search_tol: float = 1e-5,
    search_max_iter: int = 100,
    fit_intercept: bool = True,
    distance_metric: str = "euclidean",
    sigma2_v1: bool = False,
    verbose: bool = False,
) -> None:
    if isinstance(bandwidth, str) and bandwidth.strip().lower() == "adaptive":
        raise ValueError(
            "GTWR uses adaptive=True for nearest-neighbour bandwidths; "
            "bandwidth must be numeric, None, 'cv', or 'aicc'."
        )
    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,
        verbose=verbose,
    )
    self.lambda_st = lambda_st
    self.lambda_range = lambda_range
    self.lambda_grid_size = lambda_grid_size
    self.ksi = ksi
    self.distance_combination = distance_combination
    self.tau = tau
    self.causal = causal
    self.time_unit = time_unit
    self.search_grid_size = search_grid_size
    self.search_tol = search_tol
    self.search_max_iter = search_max_iter
    self.sigma2_v1 = sigma2_v1
    self._validate_gtwr_parameters()
    self._reset_gtwr_state()

fit

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

Fit GTWR and return self.

Smoother traces are calculated even when the full hat matrix is not retained, preserving valid AICc, effective-parameter, influence, and residual-variance diagnostics.

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

    Smoother traces are calculated even when the full hat matrix is not
    retained, preserving valid AICc, effective-parameter, influence, and
    residual-variance diagnostics.
    """
    if compute_hat_matrix_flag is not None:
        if not isinstance(compute_hat_matrix_flag, (bool, np.bool_)):
            raise TypeError("compute_hat_matrix_flag must be boolean or None.")
        compute_hat_matrix = bool(compute_hat_matrix_flag)
    for name, value in (
        ("compute_hat_matrix", compute_hat_matrix),
        ("compute_local_r2", compute_local_r2),
        ("compute_inference", compute_inference),
    ):
        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_gtwr_parameters()
    self._reset_fit_state()
    try:
        X_arr, y_arr, coords_arr, times_arr = self._validate_fit_inputs(
            X, y, coords, times
        )
        feature_names = (
            None
            if self.feature_names_in_ is None
            else self.feature_names_in_.copy()
        )
        self.X_train_ = X_arr.copy()
        self.y_train_ = y_arr.copy()
        self.coords_train_ = coords_arr.copy()
        self.times_train_ = times_arr.copy()
        self.feature_names_in_ = feature_names
        self.n_samples_ = int(X_arr.shape[0])
        self.n_features_in_ = int(X_arr.shape[1])
        X_design = add_intercept(X_arr) if self.fit_intercept else X_arr
        self.kernel_func_ = get_kernel_function(self.kernel)

        (
            self.lambda_st_,
            self.bandwidth_,
            self.spatial_distance_matrix_,
            self.temporal_distance_matrix_,
            self.spatiotemporal_distance_matrix_,
        ) = self._resolve_lambda_and_bandwidth(
            X_design,
            y_arr,
            coords_arr,
            times_arr,
        )
        self.tau_ = float(self.tau)
        self.ksi_ = float(self.ksi)

        if self.verbose:
            kind = "adaptive neighbours" if self.adaptive else "fixed distance"
            print(
                f"Fitting GTWR with bandwidth={self.bandwidth_} ({kind}), "
                f"distance={self.distance_combination}, lambda={self.lambda_st_:.6g}, "
                f"tau={self.tau_:.6g}, ksi={self.ksi_:.6g}."
            )

        self.inference_enabled_ = bool(compute_inference)
        local_fit = self._fit_training_locations(
            X_design,
            self.spatiotemporal_distance_matrix_,
            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.fitted_values_ = local_fit.fitted_values.copy()
        self.residuals_ = y_arr - self.fitted_values_
        self.influence_ = local_fit.influence.copy()
        self.hat_matrix_ = local_fit.hat_matrix
        self.S_matrix_ = self.hat_matrix_
        self.diagnostics_ = compute_diagnostics(
            y_arr,
            self.fitted_values_,
            compute_gwr_stats=True,
            trace_S=local_fit.trace_S,
            trace_StS=local_fit.trace_StS,
        )
        self.local_r2_ = (
            self._compute_local_r2(self.spatiotemporal_distance_matrix_)
            if compute_local_r2
            else None
        )
        self._set_inference(
            local_fit.covariance_factors,
            trace_S=local_fit.trace_S,
            trace_StS=local_fit.trace_StS,
        )
        self._mark_fitted()
        return self
    except Exception:
        self._reset_fit_state()
        raise

predict

predict(
    X: Union[ndarray, DataFrame],
    coords: Union[ndarray, DataFrame],
    times: object,
) -> np.ndarray

Predict responses at new space-time locations.

Source code in src/pygwrx/models/gtwr.py
def predict(
    self,
    X: Union[np.ndarray, pd.DataFrame],
    coords: Union[np.ndarray, pd.DataFrame],
    times: object,
) -> np.ndarray:
    """Predict responses at new space-time locations."""
    return self.predict_result(X, coords, times).predictions

predict_result

predict_result(
    X: Union[ndarray, DataFrame],
    coords: Union[ndarray, DataFrame],
    times: object,
) -> GTWRPredictionResult

Return predictions, local parameters, and optional inference.

Source code in src/pygwrx/models/gtwr.py
def predict_result(
    self,
    X: Union[np.ndarray, pd.DataFrame],
    coords: Union[np.ndarray, pd.DataFrame],
    times: object,
) -> GTWRPredictionResult:
    """Return predictions, local parameters, and optional inference."""
    X_arr, coords_arr, times_arr = self._validate_prediction_inputs(
        X, coords, times
    )
    params = self._prediction_parameters(coords_arr, times_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 GTWRPredictionResult(
        predictions=np.asarray(predictions, dtype=float),
        coef=coef,
        intercept=intercept,
        coords=coords_arr.copy(),
        times=times_arr.copy(),
        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_parameters

get_local_parameters(
    coords: Union[ndarray, DataFrame], times: object
) -> Dict[str, np.ndarray]

Return local parameters at arbitrary space-time locations.

Source code in src/pygwrx/models/gtwr.py
def get_local_parameters(
    self,
    coords: Union[np.ndarray, pd.DataFrame],
    times: object,
) -> Dict[str, np.ndarray]:
    """Return local parameters at arbitrary space-time locations."""
    dummy = np.zeros((len(coords), self.n_features_in_ or 0), dtype=float)
    _, coords_arr, times_arr = self._validate_prediction_inputs(
        dummy, coords, times
    )
    params = self._prediction_parameters(coords_arr, times_arr)
    return {
        "intercept": np.asarray(params["intercept"], dtype=float).copy(),
        "coef": np.asarray(params["coef"], dtype=float).copy(),
        "coords": coords_arr.copy(),
        "times": times_arr.copy(),
    }

to_frame

to_frame() -> pd.DataFrame

Return training-location estimates and diagnostics as a DataFrame.

Source code in src/pygwrx/models/gtwr.py
def to_frame(self) -> pd.DataFrame:
    """Return training-location estimates and diagnostics as a DataFrame."""
    frame = super().to_frame()
    if self.times_train_ is not None:
        frame.insert(2, "time", self.times_train_)
    if self.intercept_se_ is not None:
        frame["intercept_se"] = self.intercept_se_
    if self.intercept_t_ is not None:
        frame["intercept_t"] = self.intercept_t_
    feature_names = (
        [str(name) for name in self.feature_names_in_]
        if self.feature_names_in_ is not None
        else [f"x{index}" for index in range(self.n_features_in_ or 0)]
    )
    if self.coef_se_ is not None:
        for index, name in enumerate(feature_names):
            frame[f"se_{name}"] = self.coef_se_[:, index]
    if self.coef_t_ is not None:
        for index, name in enumerate(feature_names):
            frame[f"t_{name}"] = self.coef_t_[:, index]
    for name, values in (
        ("influence", self.influence_),
        ("standardized_residual", self.standardized_residuals_),
        ("cooks_distance", self.cooks_distance_),
    ):
        if values is not None:
            frame[name] = values
    return frame

summary

summary() -> str

Return a stable text summary of the fitted GTWR model.

Source code in src/pygwrx/models/gtwr.py
def summary(self) -> str:
    """Return a stable text summary of the fitted GTWR model."""
    self._check_is_fitted()
    if self.diagnostics_ is None or self.X_train_ is None:
        raise RuntimeError("Fitted diagnostics are unavailable.")
    lines = [
        "=" * 78,
        "Geographically and Temporally Weighted Regression (GTWR)",
        "=" * 78,
        f"Samples: {self.n_samples_}",
        f"Predictors: {self.n_features_in_}",
        f"Kernel: {self.kernel}",
        f"Bandwidth: {self.bandwidth_} ({'adaptive neighbours' if self.adaptive else 'fixed distance'})",
        f"Bandwidth criterion score: {self.bandwidth_score_}",
        f"Distance combination: {self.distance_combination}",
        f"lambda_st: {self.lambda_st_}",
        f"tau: {self.tau_}",
        f"ksi: {self.ksi_}",
        f"Causal history-only weighting: {self.causal}",
        f"Time unit: {self.time_unit_}",
        f"R-squared: {self.diagnostics_.get('r2', np.nan):.6f}",
        f"Adjusted R-squared: {self.diagnostics_.get('adj_r2', np.nan):.6f}",
        f"AIC: {self.diagnostics_.get('aic', np.nan):.6f}",
        f"AICc: {self.diagnostics_.get('aicc', np.nan):.6f}",
        f"BIC: {self.diagnostics_.get('bic', np.nan):.6f}",
        f"trace(S): {self.diagnostics_.get('trace_S', np.nan):.6f}",
        f"trace(S'S): {self.diagnostics_.get('trace_StS', np.nan):.6f}",
        f"Residual variance (sigma^2): {self.sigma2_:.6f}",
        "=" * 78,
    ]
    return "\n".join(lines)

GTWRPredictionResult

Rich prediction result returned by :meth:GTWR.predict_result.

Property Value
Type class
Import from pygwrx.models import GTWRPredictionResult
Signature GTWRPredictionResult(predictions: 'np.ndarray', coef: 'np.ndarray', intercept: 'np.ndarray', coords: 'np.ndarray', times: 'np.ndarray', feature_names: 'Tuple[str, ...]', coef_standard_errors: 'Optional[np.ndarray]' = None, intercept_standard_errors: 'Optional[np.ndarray]' = None, coef_t_values: 'Optional[np.ndarray]' = None, intercept_t_values: 'Optional[np.ndarray]' = None) -> None
Maintained example examples/models/05_gtwr.py

GTWRPredictionResult dataclass

GTWRPredictionResult(
    predictions: ndarray,
    coef: ndarray,
    intercept: ndarray,
    coords: ndarray,
    times: ndarray,
    feature_names: Tuple[str, ...],
    coef_standard_errors: Optional[ndarray] = None,
    intercept_standard_errors: Optional[ndarray] = None,
    coef_t_values: Optional[ndarray] = None,
    intercept_t_values: Optional[ndarray] = None,
)

Rich prediction result returned by :meth:GTWR.predict_result.

to_frame

to_frame() -> pd.DataFrame

Return prediction results as a pandas DataFrame.

Source code in src/pygwrx/models/gtwr.py
def to_frame(self) -> pd.DataFrame:
    """Return prediction results as a pandas DataFrame."""
    data: Dict[str, np.ndarray] = {
        "coord_0": self.coords[:, 0],
        "coord_1": self.coords[:, 1],
        "time": self.times,
        "prediction": self.predictions,
        "intercept": self.intercept,
    }
    if self.intercept_standard_errors is not None:
        data["intercept_se"] = self.intercept_standard_errors
    if self.intercept_t_values is not None:
        data["intercept_t"] = self.intercept_t_values
    for index, name in enumerate(self.feature_names):
        data[f"coef_{name}"] = self.coef[:, index]
        if self.coef_standard_errors is not None:
            data[f"se_{name}"] = self.coef_standard_errors[:, index]
        if self.coef_t_values is not None:
            data[f"t_{name}"] = self.coef_t_values[:, index]
    return pd.DataFrame(data)

to_geodataframe

to_geodataframe(crs: Optional[Union[str, int]] = None)

Return prediction results as a point GeoDataFrame.

Source code in src/pygwrx/models/gtwr.py
def to_geodataframe(self, crs: Optional[Union[str, int]] = None):
    """Return prediction results as a point GeoDataFrame."""
    from pygwrx.io import to_geodataframe

    frame = self.to_frame()
    columns = [
        column for column in frame.columns if not column.startswith("coord_")
    ]
    return to_geodataframe(
        frame[columns].to_numpy(dtype=float),
        None,
        self.coords,
        feature_names=columns,
        crs=crs,
    )

Runnable examples used on this page

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

"""Fit and predict with geographically and temporally weighted regression."""

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

from pygwrx import GTWR, GTWRPredictionResult

X, y, coords, times = temporal_regression()
model = GTWR(kernel="bisquare", bandwidth=24, adaptive=True, lambda_st=0.3).fit(
    X, y, coords, times
)
print_model_result(model)
print("score=", model.score(X, y, coords, times=times))
result = model.predict_result(X.iloc[:3], coords.iloc[:3], times[:3])
assert isinstance(result, GTWRPredictionResult)
print(result.to_frame())