Forward model selection for geographically weighted regression
Source:R/model-selection-gwr.R
gwr_model_selection.RdWraps GWmodel::gwr.model.selection(), which grows a GWR model one
predictor at a time and scores every intermediate model with a corrected
Akaike information criterion. GWmodel returns two loosely-coupled lists;
this function returns a ranked table.
Arguments
- data_sf
An
sfobject with response, predictors and geometry.- response_var
Response column name.
- candidate_vars
Character vector naming at least two numeric predictors to choose among. Factor, character and logical candidates are refused: GWmodel fits a factor as several model-matrix columns while this sweep counts it as one variable, so the criteria would not be comparable, and
fit_gwr_model(), the documented next step, takes only numerics. Encode them as numeric indicators first.- bandwidth
Bandwidth held fixed across all candidate models. If
NULL(default) it is selected withGWmodel::bw.gwr()on the model containing every candidate. Integer neighbour count whenadaptive = TRUE; otherwise a distance in the units of the projected CRS the sweep runs in, whichprep_model_data()may have chosen for you. Geographic input is projected before the bandwidth is used, so a value in degrees would be read as metres; a fixed bandwidth below a ten-thousandth of the data's extent raises a warning saying so, as infit_gwr_model(). An adaptive count too small for the full model is raised, with a warning, to the number of candidates plus 3 for the bisquare and tricube kernels (which give the farthest neighbour in a window weight 0), plus 2 for the others. That floor is enough unless several neighbours tie at the kernel's edge (a regular grid), which leaves a window fewer weighted points; then use a larger bandwidth. An adaptive count below 1 or above R's largest integer is refused, as infit_gwr_model(). One above the number of observations is capped at it, with a warning; below 20 observations that includesbw.gwr()'s choice, since its adaptive search starts at 20 neighbours.- adaptive
Logical; adaptive (nearest-neighbour) bandwidth. Default
TRUE.- kernel
Weighting kernel. One of
"bisquare"(default),"gaussian","tricube","boxcar","exponential".- bw_approach
Criterion for the bandwidth search:
"AICc"(default) or"CV". Matchesfit_gwr_model's default.- max_models
Refuse the call if the sweep would exceed this many model fits. Default 200, which admits up to 19 candidates.
- dmat_max_n
Precompute and reuse an
nxndistance matrix whennis at most this. Default 2000 (about 32 MB). Set to 0 to disable.- quiet
Discard GWmodel's progress output. Default
TRUE, because GWmodel writes it with barecat()that nosuppressMessages()can silence, and emits one block per candidate model, so it scales with the square of the candidate count. SetFALSEto watch a long sweep progress.- .engine
Internal; injectable backend used for testing.
Value
An object of class gwr_model_selection, a list with:
best (character vector of the selected predictors);
table (ranked data.frame of every model evaluated, with columns
rank, n_vars, variables and criterion;
criterion is NA, and the model ranked last, where GWmodel
could not evaluate it or where AICc is undefined because the model's
effective number of parameters \(tr(S)\) is not below \(n - 2\),
which raises a warning);
criterion (label for the criterion actually read, noting when it
had to be located positionally);
criterion_by_name (logical: whether that column was found by
its name rather than by the documented position),
criterion_column (the column it was read from) and
criterion_verified (logical: FALSE exactly when the
column was read positionally from a table that did not have the four
documented columns, which is the case the log calls unverified. Gate
a script on this field instead of on the label);
response_var and candidate_vars (the response and the full
candidate set the sweep ran over, both echoed by print());
bandwidth, bandwidth_source, adaptive and
kernel (the smoothing held fixed across the sweep, and where it
came from);
n_obs, n_models, used_dmat; and raw
(GWmodel's unmodified return: the two-element list of its model list and
its diagnostic table).
What this optimises, and what it does not
The criterion is in-sample. AICc penalises the effective number of
parameters, so it is not the same thing as maximising fit, but it is still
computed on the data the model was fitted to, and under spatial
autocorrelation an in-sample criterion is optimistic in a way that a
spatially blocked estimate is not. Treat this as fast screening.
select_features_forward performs the same forward search
against a spatially blocked cross-validated score; it costs far more and is
the one to trust when the answer matters. When the two disagree, the
disagreement is itself informative. It usually means a candidate is
predictive only locally.
Two further limitations follow from the method itself:
One bandwidth for every model. Comparing criteria across models requires holding the smoothing fixed, but the bandwidth is itself a fitted quantity, and the value chosen for the full model is not optimal for a one-predictor model. This is how the method is defined (Lu et al. 2014); it is not an implementation shortcut. Refit the selected model with
bandwidth = NULLto re-optimise once the variable set is settled.The null model is never evaluated. The sweep starts from one predictor, so the result always names at least one. It cannot tell you that none of the candidates help.
Cost
The sweep fits p * (p + 1) / 2 GWR models for p candidates
(55 at p = 10, 210 at p = 20), each over all n locations.
max_models stops the call before it runs for hours.
Using $raw with GWmodel directly
raw is GWmodel's own list(model.list, GWR.df), so its two
elements have to be unpacked before GWmodel's own helpers will take them:
GWmodel::gwr.model.view() takes (DeVar, InDeVars, model.list),
so the call is
GWmodel::gwr.model.view(sel$response_var, sel$candidate_vars, sel$raw[[1]])Pass sel$raw[[1]], not sel$raw. The diagnostic table is
sel$raw[[2]], an unlabelled numeric matrix whose columns are
bandwidth, AIC, AICc, RSS in that order; the
criterion column of $table is its third column.
References
Lu, B., Harris, P., Charlton, M. and Brunsdon, C. (2014). The GWmodel R package: further topics for exploring spatial heterogeneity using geographically weighted models. Geo-spatial Information Science 17(2), 85–101. doi:10.1080/10095020.2014.917453
See also
select_features_forward for the blocked
cross-validated counterpart, fit_gwr_model to fit the
selected model.
Other cross-validation:
area_of_applicability(),
cv_bayes(),
cv_block_size_sweep(),
cv_gwr(),
cv_rf(),
cv_spatial(),
estimate_sac_range(),
fold_separation(),
make_folds(),
sac_nugget(),
select_features_forward()
Examples
if (requireNamespace("GWmodel", quietly = TRUE) &&
requireNamespace("sp", quietly = TRUE)) {
library(sf)
set.seed(1)
n <- 80
dat <- st_as_sf(
data.frame(x = 5e5 + runif(n, 0, 1000), y = 5e6 + runif(n, 0, 1000),
a = rnorm(n), b = rnorm(n), noise = rnorm(n)),
coords = c("x", "y"), crs = 32632
)
dat$z <- 2 * dat$a - dat$b + rnorm(n, 0, 0.5)
sel <- gwr_model_selection(dat, "z", c("a", "b", "noise"), bandwidth = 30)
print(sel) # every model tried, by AICc
print(sel$best) # the winning predictor set: a and b, not noise
fit <- fit_gwr_model(dat, "z", sel$best)
fit
}
#> Geographically weighted regression - forward model selection
#>
#> response : z
#> candidates : 3 (a, b, noise)
#> observations: 80
#> models : 6
#> bandwidth : 30 (adaptive; supplied)
#> kernel : bisquare
#> criterion : AICc (assumed: column 3, unlabelled)
#>
#> rank n_vars variables criterion
#> 1 2 a + b 136.6185
#> 2 3 a + b + noise 150.6764
#> 3 1 a 273.2590
#> 4 2 a + noise 278.3346
#> 5 1 b 360.8372
#> 6 1 noise 378.2502
#>
#> Selected: z ~ a + b
#>
#> The criterion is in-sample and every model shares one bandwidth.
#> Confirm with select_features_forward() before relying on this.
#> [1] "a" "b"
#> <GWR (GWmodel)> spatial model fit
#> Formula : z ~ a + b
#> n : 80
#> CRS : EPSG:32632
#> Bandwidth: 78 neighbours (adaptive, bisquare kernel)
#> AICc : 123.31