Reproducing Geocomplexity Explains Spatial Errors

A complete walkthrough of geocomplexity — a local measure of how far a variable's neighbourhood departs from smooth spatial dependence — and of the claim it was built to support: that the places a model gets wrong are the places the geography is complex. From raw data to manuscript, on the published case.
Yongze Song

Yongze Song

Associate Professor, Curtin University

Special Issue Editor (SIE), Associate Editor: Int. J. Applied Earth Observation (IF 8.2) · Associate Editor: GIScience & Remote Sensing (IF 6.8)

yongze.song@curtin.edu.au · yongzesong.com

⬇ Download code & data (zip, 474 KB)
R/ pipeline scripts · config/project-config.R · data/econineq.gpkg (333 Australian SA3 regions, the published case) · tests/ · run-all.R — unzip and run Rscript run-all.R test from the GC/ folder root.
To cite the geocomplexity model and its R package and codes in publications, please use:
Zhang Z, Song Y, Luo P, Wu P (2023). Geocomplexity explains spatial errors. International Journal of Geographical Information Science 37(7):1449–1469. doi · PDF
Lyu W, Song Y, et al. geocomplexity: Geographical Complexity. R package. CRAN · vignettes

1 Method Overview & Reproduction Scope

1.1 Core Idea

Spatial models rest on an assumption they rarely state aloud: that nearby places behave similarly, so a value can be borrowed from its neighbourhood. Where that holds, models work. Where it breaks — a coastline, a city edge, a mining town in a pastoral district — the model keeps borrowing anyway, and gets it wrong.

Geocomplexity measures where it breaks. At each location it asks how far the local arrangement of a variable departs from smooth spatial dependence: low where the neighbourhood is orderly, high where it is not. The paper's claim follows directly, and is testable: if geocomplexity marks the places where the assumption fails, then it should predict where a model's errors are — and it does, without ever seeing the model.

Published reference · geocomplexity A measure of spatial local complexity, Zhang et al. 2023 Figure 1
Paper Fig. 1 — the measure. Geocomplexity splits into a local feature (the LISA term, how the location relates to its neighbours) and a surrounding feature (how those neighbours relate to each other). Every neighbour under the queen criterion counts. The worked grid on the right shows the result: A₁ at −2.90 sits in a uniform patch and is simple; A₂ at 0.13 sits where three values meet and is complex. Zhang, Song, Luo & Wu (2023), IJGIS 37(7):1449–1469 (open access).
Reader anchor

Models assume the neighbourhood is informative → geocomplexity measures where that assumption breaks down → those are the places the model errs → so geocomplexity explains spatial errors, and adding it repairs part of them. Everything on this page serves that line.

What you need

R 4.1+ with sf, sdsfun and geocomplexity for the method itself, plus e1071, GWmodel and ggplot2 for the models and figures. The case data ship in this folder. The full pipeline takes about 90 seconds, nearly all of it in the bandwidth searches of the five geographically weighted regressions.

1.2 Method Logic

Geocomplexity sits at the root of a family of methods on this site: once you can measure where spatial dependence breaks down locally, you can use that measure as a diagnostic, as an explanatory variable, or as a weight.

GEOC 2023geocomplexity; explains spatial errors
Zhang et al., IJGIS · PDFopen access
LPI 2026GC of each variable becomes an extra explanatory variable
LPI tutorial
GeoCGWRGC enters the geographical weighting itself
gwr_geoc() in the package

Key concepts and equations

Published reference · geocomplexity Workflow of the geocomplexity study, Zhang et al. 2023 Figure 3
Paper Fig. 3 — the workflow this page follows: compute geocomplexity from the explanatory variables, fit conventional models, then use geocomplexity to explain what those models got wrong.

1.3 Reproduction Scope

Everything tagged Demo run regenerates from this folder; everything tagged Published reference is a static image from the article. The demo runs end to end with R 4.6.0 and geocomplexity 0.3.0.

!What is reproduced exactly, and what is not

The values checked by run-all.R test are those published in the package's own vignettes, computed by the method's authors on this same layer — and they reproduce to four decimals. The article's Tables 3–4 are a different matter: the shipped layer carries all eight explanatory variables, whereas the article reports a variable-selection step it does not fully specify, so its absolute R² values are not reachable from this data. What does reproduce is the mechanism and the ordering, which is the paper's actual argument — see §3.2, where both are set side by side.

ItemDemo templatePublished reference
Data 333 Australian SA3 regions, Gini coefficient and 8 explanatory variables (data/econineq.gpkg, shipped with the package) the same case: economic inequality across Australia
Method geocd_vector() / geocs_vector() / gwr_geoc() from the authors' package Zhang et al. (2023) §3, and the package vignettes
Tables results/baseline-models.csv, error-explanation.csv, model-improvement.csv, gc-method-comparison.csv vignette values reproduced exactly by run-all.R test; article Tables 3–4 shown for comparison
Figures fig01–fig06 from R/p01–p03 paper Figs 1, 3, 5, 6, 8 embedded as reference images
What the template regenerates versus what is shown as published reference.

How to use this page

2 Setup, Structure & Data Contract

2.1 Environment & Dependencies

The method is the CRAN geocomplexity package, written by the authors of the article. The pipeline adds the two model families whose errors are examined, and each step reports and continues if an optional package is missing.

R console
# The method and its spatial-weights helper
install.packages(c("geocomplexity", "sdsfun", "sf", "spdep"))

# The models whose errors are explained
install.packages(c("e1071", "GWmodel"))

# Figures
install.packages(c("ggplot2", "scales"))
PackageRole in the pipelineRequired?
geocomplexitygeocd_vector() per-variable complexity, geocs_vector() whole-configuration complexity, gwr_geoc() geocomplexity-weighted GWRyes
sdsfun, sfspatial weight matrices and the study layeryes
spdepMoran's I test on the model errors (Step 3)diagnostic only
e1071the support-vector regression baselinethat model only
GWmodelthe GWR baseline, and the error-explanation regressionsStep 4
ggplot2, scalesthe six figuresfigures only
Package roles. The environment is recorded in env/session-info.txt.

2.2 Project Structure & Configuration

One entry script runs the five pipeline steps, the figures, or the tests. Each step writes to results/, tables/, figs/ or data/derived/ and reads what the previous step wrote, so any step can be re-run in isolation.

folder tree
GC/
├── gc.html                        # this guide
├── run-all.R                      # entry point: full run, single steps, figures, test
├── assets/                        # figures shown in this guide (web copies)
├── config/project-config.R        # the ONLY file to edit for a new domain
├── R/
│   ├── 00-config.R, 01-helpers.R  # paths, IO, spatial weights
│   ├── 10-prepare-data.R          # read the layer, build the weight matrix
│   ├── 20-geocomplexity.R         # compute GC; compare moran / spvar / shannon
│   ├── 30-baseline-models.R       # MLR, SVR, GWR and their errors
│   ├── 40-explain-errors.R        # GWR of |error| on geocomplexity  <- the claim
│   ├── 50-improve-models.R        # GCMLR and GeoCGWR
│   ├── 90-tables.R                # LaTeX tables + session info
│   └── p01 … p03                  # figure scripts
├── data/econineq.gpkg             # 333 SA3 regions, the published case
├── tests/                         # test-01 published values; test-02 properties
├── env/                           # requirements.md, session-info.txt
├── results/  tables/  figs/       # generated output, never hand-edited

Main configuration file

config/project-config.R (key lines)
INPUT_FILE <- "data/econineq.gpkg"   # one layer: response + explanatory variables
RESPONSE   <- "Gini"
PREDICTORS <- NULL                    # NULL = every remaining column

# The neighbourhood definition. Geocomplexity is a statement about a
# neighbourhood, so this is a modelling choice, not a detail.
WEIGHTS_TYPE  <- "contiguity"        # "contiguity" or "knn"
WEIGHTS_QUEEN <- TRUE                # queen (TRUE) or rook (FALSE)
WEIGHTS_STYLE <- "B"                 # "B" binary, "W" row-standardised

GC_METHOD           <- "moran"                            # the working measure
GC_METHODS_COMPARED <- c("moran", "spvar", "shannon")     # compared in Step 2

# Models whose errors are explained, and the geocomplexity that explains them
SVR_COST <- 1.4; SVR_GAMMA <- 0.2      # the paper's cross-validated optimum
GWR_APPROACH <- "AIC"                  # bandwidth criterion
ERROR_EXPLAIN_VARS <- c("Income", "Indemp")
ParameterDefaultOrigin / rationale
WEIGHTS_QUEEN, WEIGHTS_STYLETRUE, "B"queen contiguity with a binary matrix, as in the paper's Fig. 1 and its Table 5 comparison
GC_METHODmoranthe package default and the article's measure; Step 2 shows what changes under the alternatives
SVR_COST, SVR_GAMMA1.4, 0.2the values the article selected by ten-fold cross-validation
ERROR_EXPLAIN_VARSIncome, Indempthe two variables whose geocomplexity the article carries into its error models
Free parameters and the published choices they follow.

2.3 Data Contract

Geocomplexity needs a geometry, because it is computed from neighbourhoods. One layer — polygons or points — carrying the response and the explanatory variables is the whole requirement.

ColumnConfig keyTypeMeaningRequired?
geometrypolygon or pointdefines the neighbourhoodsyes
responseRESPONSEnumericthe dependent variable (demo: Gini)yes
explanatoryPREDICTORSnumericthe variables whose geocomplexity is measured≥ 1
Required schema of INPUT_FILE. Column names are declared in the config, not hard-coded.
GiniInduscaleITIncomeIndempgeom
0.420084.367,8692,136MULTIPOLYGON
0.460076.246,822843MULTIPOLYGON
Shape of data/econineq.gpkg — 333 SA3 regions, the Gini coefficient and eight explanatory variables, in GDA94. First two rows; the highlighter colours the code blocks, the table is verbatim.
!The weight matrix is part of the method, not a setting

Every geocomplexity value is a statement about a neighbourhood, so changing the neighbourhood changes the answer — tests/test-02 asserts exactly that. Queen contiguity on polygons is the paper's choice; k-nearest neighbours suits points or very irregular polygons. Watch the connectivity: a unit with no neighbour at all has no neighbourhood and no defined complexity, and Step 1 reports that case. Every region in this layer has at least one neighbour, but the queen graph still splits into two disconnected sub-graphs, which is normal for a country with offshore regions.

3 Reproduction Pipeline, Results & Validation

3.1 Pipeline Execution

Full run
Rscript run-all.R (~90 s)
Verification
Rscript run-all.R test — do this first
Output folders
results/ tables/ figs/ data/derived/
terminal
cd GC                      # this folder
Rscript run-all.R test     # 1. check against the published values
Rscript run-all.R          # 2. full pipeline

Rscript run-all.R 30 40    # re-run single steps (10 20 30 40 50 90)
Rscript run-all.R figures  # only the figure scripts
Project : gc-demo Domain : economic inequality (Gini coefficient) == Step 1/5 Prepare data ====================================== 333 spatial units, geometry: MULTIPOLYGON response: Gini | 8 explanatory variables: Induscale, IT, Income, ... weights: queen contiguity, style 'B' neighbours per unit: min 1, median 5, max 12 == Step 2/5 Compute geocomplexity ============================= method 'moran': 8 variables -> 8 GC columns method 'moran ' mean GC over all variables = 0.8329 method 'spvar ' mean GC over all variables = 0.0998 method 'shannon' mean GC over all variables = 0.7927 moran vs spvar: correlation -0.45 to 0.06 (median -0.09) moran vs shannon: correlation 0.26 to 0.82 (median 0.63) == Step 3/5 Baseline models and their errors ================== MLR R2 = 0.5846 RMSE = 0.03060 SVR R2 = 0.8030 RMSE = 0.02108 (cost = 1.4, gamma = 0.2) GWR R2 = 0.8024 RMSE = 0.02111 (adaptive bandwidth 28 by AIC) MLR errors: Moran's I = +0.422 (p = 1.53e-30) -> spatially structured SVR errors: Moran's I = +0.234 (p = 7.48e-11) -> spatially structured GWR errors: Moran's I = +0.105 (p = 0.00188) -> spatially structured == Step 4/5 Geocomplexity explains the errors ================= MLR errors: R2 = 0.1806 RSS = 0.1101 AIC = -1685 (bandwidth 37) SVR errors: R2 = 0.0888 RSS = 0.0763 AIC = -1816 (bandwidth 55) GWR errors: R2 = 0.0220 RSS = 0.0654 AIC = -1884 (bandwidth 114) error explained, high to low: MLR 0.181 > SVR 0.089 > GWR 0.022 == Step 5/5 Geocomplexity improves the models ================= MLR -> GCMLR: R2 0.5846 -> 0.6215 (+6.3%) GWR -> GeoCGWR: R2 0.8024 -> 0.8360 (+4.2%) Finished in 89.0 s

3.2 Reproduce the Published Values

Two different things are checked here, and it is worth being precise about which is which.

The package's published values — reproduced exactly

run-all.R test locks the numbers published in the geocomplexity package vignettes, computed by the method's authors on this same layer. All twelve checks agree to four decimals, including the adaptive bandwidth the GWR search selects.

== test-01 Reproduce the published geocomplexity values ================= layer has the published 333 units pass layer carries Gini and 8 explanatory variables pass MLR R2 = 0.5846 (got 0.5846) pass MLR adjusted R2 = 0.5743 (got 0.5743) pass GCMLR R2 = 0.6215 (got 0.6215) pass GCMLR adjusted R2 = 0.6024 (got 0.6024) pass adding geocomplexity raises the linear model's R2 pass GeoCGWR R2 = 0.836 (got 0.8360) pass GeoCGWR adjusted R2 = 0.8319 (got 0.8319) pass GWR selects an adaptive bandwidth of 28 (got 28) pass GWR R2 = 0.8025 (got 0.8024) pass GeoCGWR improves on the GWR baseline pass 12 passed, 0 failed PASS - the pipeline reproduces the published values == test-02 Geocomplexity properties ==================================== a smooth gradient is less complex than spatial noise pass adding a constant leaves geocomplexity unchanged pass a different spatial weight matrix gives a different result pass ... 9 passed, 0 failed

The article's tables — mechanism reproduced, magnitudes not

The article's Tables 3–4 are computed after a variable-selection step that it does not fully specify, while the shipped layer carries all eight explanatory variables. The absolute values therefore differ. What survives — and what the paper actually argues — is the ordering:

QuantityMLRSVRGWRAgreement
Model R², published0.470.650.76
Model R², here0.5850.8030.802same ranking of spatial awareness
Error explained by geocomplexity, published0.4670.1660.136
Error explained by geocomplexity, here0.1810.0890.022same descending order
The finding is the descending order, and it reproduces: the more spatially aware the model, the less of its error geocomplexity has left to explain. The magnitudes differ because the modelling table does.
Checkpoint

21 checks pass — 12 against the published vignette values, 9 on properties the measure must satisfy regardless of dataset. If those hold, the pipeline is running the reference implementation as its authors intended.

3.3 Core Analytical Steps

Five stages: purpose → code → output → result → how to read it. Step numbers match the script numbers (Step 4 is R/40-explain-errors.R).

Step 1

Prepare data — the layer and the neighbourhood

What this step does

Reads the layer, summarises the variables, and builds the spatial weight matrix that every later step depends on. It also counts neighbours, because a unit with none has no neighbourhood and therefore no geocomplexity.

Code

R — from R/10-prepare-data.R
x  <- sf::read_sf("data/econineq.gpkg")
wt <- sdsfun::spdep_contiguity_swm(x, queen = TRUE, style = "B")

Example output

== Step 1/5 Prepare data ====================================== 333 spatial units, geometry: MULTIPOLYGON CRS: GDA94 response: Gini | 8 explanatory variables: Induscale, IT, Income, Sexrat, Houseown, Indemp, Indcom, Hiedu weights: queen contiguity, style 'B' neighbours per unit: min 1, median 5, max 12 wrote data/derived/analysis-layer.gpkg and weights.rds
How to read this result
  • The weight matrix is the method's first assumption. Queen contiguity says two regions are neighbours if they touch at all. Change that and every geocomplexity value changes with it — which is a property, not a bug, and tests/test-02 checks it holds.
  • Connectivity is worth checking before trusting the numbers. Every region here has at least one neighbour (minimum 1, median 5), so geocomplexity is defined everywhere — but the queen graph still falls into two disconnected sub-graphs, because the offshore regions touch each other and not the mainland. A unit with no neighbour at all would have no neighbourhood to be complex relative to, and the step reports that case rather than letting it become a silent NA.
Step 2 · the measure

Compute geocomplexity — and check the measure against its alternatives

What this step does

Computes the geocomplexity of every explanatory variable, then recomputes it under all three measures so the choice is visible rather than buried in a default.

Code

R — from R/20-geocomplexity.R
gc <- geocomplexity::geocd_vector(xp, wt = wt, method = "moran",
                                  normalize = TRUE, returnsf = FALSE)

# and, for the comparison, the same call with method = "spvar" / "shannon"

Example output

Demo run Geocomplexity of income and industrial employees, demo
fig01 — geocomplexity of the two variables the error models use, in quantile classes. Income is complex through the arid interior and simple across the settled south-east; industrial employment is the reverse, complex in the west and along the eastern seaboard. Quantile classes are used because geocomplexity is strongly skewed — a linear ramp paints almost every region the same shade.
Demo run Mean geocomplexity by method, demo
fig04 — the same variables under the three measures. They are on different scales, and they do not rank the variables the same way.
Published reference · geocomplexity Geocomplexity of selected variables, Zhang et al. 2023 Figure 5
Paper Fig. 5 — the published counterpart: geocomplexity of the selected variables across the SA3 regions, coloured by the Gini coefficient.
How to read this result
  • Complexity is a property of a variable, not of a place. Income is complex through the arid interior and simple across the settled south-east; industrial employment is the reverse. There is no single map of “complex regions” to draw — each variable carries its own.
  • Read the maps in quantile classes, not on a linear ramp. Geocomplexity is strongly right-skewed, so an equal-interval ramp paints almost every region the same shade and hides the structure Step 4 goes on to use.
  • Check the measure before trusting the pattern. The comparison in fig04 exists so the choice of moran, spvar or shannon is a stated decision; the warning below shows how little the three agree.
!The three measures are not interchangeable

Across the eight variables, moran and spvar correlate at a median of −0.09 — essentially unrelated — while moran and shannon reach +0.63 and spvar against shannon is −0.60. They answer different questions: local autocorrelation, raw fluctuation, and neighbourhood entropy. Name the measure in the Methods section; a result under one is not a result under another. The LPI tutorial uses spvar deliberately, because there geocomplexity enters as an explanatory variable describing pattern intensity rather than as a diagnostic of dependence.

Step 3

Baseline models — and the precondition that makes the claim worth testing

What this step does

Fits three models of increasing spatial awareness — a global linear model, a non-linear but aspatial support-vector regression, and a locally varying GWR — and keeps their errors. Then it tests whether those errors are spatially structured at all.

Code

R — from R/30-baseline-models.R
f <- stats::as.formula(paste(RESPONSE, "~", paste(preds, collapse = " + ")))

# Three models of increasing spatial awareness; each leaves errors behind
m_mlr <- stats::lm(f, data = d)                          # global, linear
m_svr <- e1071::svm(f, data = d, kernel = "radial",      # non-linear, aspatial
                    cost = SVR_COST, gamma = SVR_GAMMA)
bw    <- GWmodel::bw.gwr(f, data = sp, approach = GWR_APPROACH,
                         kernel = GWR_KERNEL, adaptive = GWR_ADAPTIVE)
m_gwr <- GWmodel::gwr.basic(f, data = sp, bw = bw,       # locally varying
                            kernel = GWR_KERNEL, adaptive = GWR_ADAPTIVE)

errs[["MLR"]] <- y - stats::fitted(m_mlr)                # the subject of Step 4

Example output

ModelRMSEErrors: Moran's I
MLR — global linear0.58460.0306+0.422 (p ≈ 10⁻³⁰)
SVR — non-linear, aspatial0.80300.0211+0.234 (p ≈ 10⁻¹¹)
GWR — locally varying0.80240.0211+0.105 (p = 0.002)
results/baseline-models.csv. Every model's errors are significantly clustered — which is what makes the next step worth running.
Published reference · geocomplexity Statistical distribution of model errors, Zhang et al. 2023 Figure 6
Paper Fig. 6 — the published error distributions for the same three model families.
How to read this result
  • Clustered errors are the precondition, and they are tested rather than assumed. If Moran's I on the errors were near zero there would be nothing spatial left to explain, and Step 4 would be measuring noise.
  • Spatial awareness shows up in the errors, not just the fit. Moran's I falls from +0.42 to +0.23 to +0.11 as the model becomes more spatially explicit. GWR has already absorbed most of the spatial structure — which is exactly why it has least left for geocomplexity to explain.
  • SVR and GWR fit almost identically here (0.803 vs 0.802) but differently. The same R² conceals very different error geographies, and the Moran's I column is what reveals it.
Step 4 · the claim

Geocomplexity explains the errors

What this step does

Regresses each model's absolute error on the geocomplexity of income and industrial employment, using GWR so the strength of the explanation is allowed to vary over space — as the article does. Magnitude, not sign, is what is being explained: a model can be wrong in either direction.

Code

R — from R/40-explain-errors.R
dat$abs_error <- abs(err[[nm]])          # what the model got wrong
f  <- abs_error ~ GC_Income + GC_Indemp  # explained by geocomplexity alone
bw <- GWmodel::bw.gwr(f, data = sp, approach = "AIC", adaptive = TRUE)
g  <- GWmodel::gwr.basic(f, data = sp, bw = bw, adaptive = TRUE)

Example output

Errors ofBandwidthRSSAIC
MLR370.18060.1101−1685
SVR550.08880.0763−1816
GWR1140.02200.0654−1884
results/error-explanation.csv — geocomplexity alone, with no access to the model or its inputs, accounts for 18% of where the linear model goes wrong.
Demo run Share of each model's error explained by geocomplexity, demo
fig02 — the headline: the share of each model's error that geocomplexity accounts for, falling as the model becomes more spatially aware.
Demo run Local coefficients of geocomplexity in the error explanation, demo
fig06 — where geocomplexity matters. The MLR row is strongly negative around the eastern metropolitan regions; the GWR row is nearly blank, because little is left.
Published reference · geocomplexity Coefficients of geocomplexity in multiple regression error explanation, Zhang et al. 2023 Figure 8
Paper Fig. 8 — the published counterpart of fig06: local coefficients of the geocomplexity of income and of industrial employees in the multiple-regression error explanation.
How to read this result
  • The ordering is the finding. 0.181 → 0.089 → 0.022. The better a model already handles space, the less geocomplexity has to say about its mistakes. That is what makes this a claim about spatial dependence rather than a coincidence: it behaves the way the theory says it must.
  • Nothing about the model was used. Geocomplexity is computed from the explanatory variables and the geometry alone, before any model is fitted. That it still predicts where the errors fall is the whole point — and it means the diagnostic is available before committing to a model.
  • The relationship is local, which is why the global scatter looks flat. Pooled over the country the association is weak; mapped, it is concentrated in the dense eastern cities where regions are small and neighbourhoods change quickly. A global regression would have concluded there was nothing here.
  • Bandwidth tells the same story from another angle. It widens from 37 to 114 regions as the model improves: the remaining error structure gets broader and vaguer until there is little local signal left to chase.
Checkpoint

results/error-explanation.csv holds one row per model, and the R² column should descend with the model's spatial awareness. If it does not, check that the errors were actually clustered in Step 3.

Step 5

Improve the models — the practical consequence

What this step does

If geocomplexity knows where a model is wrong, giving the model that information should help. Two ways: add it as explanatory variables (GCMLR), or build it into the geographical weighting itself (GeoCGWR).

Code

R — from R/50-improve-models.R
# geocomplexity as extra explanatory variables
m2 <- lm(Gini ~ ., data = cbind(d, gc))

# geocomplexity inside the geographical weighting
g2 <- geocomplexity::gwr_geoc(Gini ~ ., data = x, bw = "AIC", adaptive = TRUE)

Example output

ModelVariantAdjusted R²
MLRbaseline0.58460.5743
GCMLR+ geocomplexity0.62150.6024
GWRbaseline0.80240.7409
GeoCGWR+ geocomplexity0.83600.8319
results/model-improvement.csv — these four numbers are the package's published vignette values, reproduced to four decimals by run-all.R test.
Demo run Model performance with and without geocomplexity, demo
fig05 — adding geocomplexity raises R² in both families: +6.3% for the linear model, +4.2% for geographically weighted regression.
How to read this result
  • The adjusted R² is the honest column. GCMLR adds eight variables, so a naive R² would rise anyway. Adjusted R² rises too (0.574 → 0.602), which means the gain survives the cost of the extra parameters.
  • GeoCGWR gains more than it appears. R² moves 0.802 → 0.836, but adjusted R² moves 0.741 → 0.832 — because weighting by geocomplexity buys the fit with far fewer effective parameters than widening the bandwidth would.
  • Improvement and explanation are the same fact. Step 4 showed geocomplexity knows where the linear model errs; here that knowledge is handed back and the error shrinks. A method that explained errors but could not reduce them would be far less interesting.

3.4 Reading the Results Together

The errors are spatial
Moran's I on the residuals is +0.42, +0.23, +0.11 and significant for all three models — clustered error is the norm, not the exception.
Geocomplexity explains them
18%, 9% and 2% of the absolute error, computed without ever seeing the model, and descending exactly as the model's spatial awareness rises.
And repairs part of them
R² 0.585→0.622 and 0.802→0.836, with adjusted R² rising in both.
How to interpret — several angles
  • Mechanism. Every spatial model borrows information from neighbourhoods. Where a variable's neighbourhood is internally inconsistent, the borrowed information is wrong, and the model pays for it in error. Geocomplexity measures that inconsistency directly, which is why it can anticipate the error without seeing the model.
  • Diagnostic angle. Because it needs only the explanatory variables and the geometry, geocomplexity can be mapped before fitting anything. It answers "where will this be hard?" — useful for deciding where to sample more, or where to distrust a prediction.
  • Why the local view is necessary. Pooled over Australia the error–complexity association is nearly flat; mapped, it is strong in the metropolitan east and absent in the interior. Reporting only a global coefficient would have concluded, wrongly, that there was no effect.
  • What geocomplexity is not. It is not a measure of the response's own pattern, and not a substitute for local autocorrelation statistics — the article's Table 5 compares the two directly. It is a property of the explanatory geography, which is what lets it be computed in advance.
  • Honest limits. The measure depends on a weight matrix you chose; the queen graph here splits into two disconnected sub-graphs, so the offshore regions are only ever compared with each other; and the three available measures disagree (median correlation −0.09 between moran and spvar). State the weights and the measure, or the number is not reproducible.
Reading order for your own run

1) Step 3's Moran's I on the errors — is there spatial structure to explain at all. 2) results/error-explanation.csv — how much geocomplexity accounts for, and whether the order across models makes sense. 3) figs/fig06 — where it matters, since the story is local. 4) results/model-improvement.csv — whether feeding it back helps, on adjusted R². 5) results/gc-method-correlation.csv — whether your conclusion survives a change of measure.

4 Adaptation, Writing & Reproducibility

4.1 Port to Your Domain

Geocomplexity needs only a geometry and some variables, so it travels wherever spatial models do. Within this tutorial series it appears in three different roles:

RoleWhereWhat geocomplexity is used as
Diagnosticthis page (Zhang et al. 2023)an explanation of where models err, computed before fitting
Explanatory variableLPI tutorialGC of every variable joins the variable set, doubling it from {X} to {X, GC(X)}
Spatial weightgwr_geoc(), §3 Step 5complexity enters the geographical weighting itself
The same measure, used three ways. Which one you want decides the calculation method as much as the domain does.

What to edit

  1. config/project-config.RINPUT_FILE and RESPONSE, then the neighbourhood: WEIGHTS_TYPE ("knn" for points), WEIGHTS_QUEEN and WEIGHTS_STYLE.
  2. GC_METHOD"moran" when geocomplexity is a diagnostic of spatial dependence, as here; "spvar" when it stands for pattern intensity, as in the LPI tutorial; "shannon" for neighbourhood diversity. Step 2 prints the correlation between them so the choice can be defended.
  3. ERROR_EXPLAIN_VARS — which variables' complexity carries the explanation. NULL uses all of them, which is a reasonable starting point before selecting.
  4. Nothing else: Rscript run-all.R test, then Rscript run-all.R.
!Common pitfalls

Report the weight matrix and the calculation method together — a geocomplexity value means nothing without both, and the three methods can rank your variables differently. Check connectivity before trusting anything: units with no neighbour have no defined complexity, and disconnected sub-graphs mean some units are only ever compared with a handful of others. Explain the absolute error, not the signed one, unless you have a reason to expect direction. And do not read a flat global scatter as absence of effect — the association is local, which is the whole reason the explanation is fitted with GWR.

4.2 Write the Paper

The published study has a clean architecture: define the measure, show it on the data, fit conventional models, then use the measure to explain their errors and to improve them.

Manuscript sectionTemplate outputWriting job
Methodsconfig/project-config.R, results/gc-method-comparison.csvdefine geocomplexity and its two terms, state the weight matrix and the calculation method, and justify the choice against the alternatives
Resultsfigs/fig01–fig06, tables/table-baseline-models.tex, table-error-explanation.texmap the complexity, show the models' errors are clustered, then the share explained and where; every finding sentence carries a number
Discussionresults/error-explanation.csv, model-improvement.csvargue the ordering across models, the local geography of the effect, and what the diagnostic is worth before fitting
Where each manuscript section draws its material from.

Manuscript skeleton

manuscript outline
1  Introduction        — spatial models assume dependence; errors cluster;
                          the gap: which places, and why (3 aims)
2  Geocomplexity       — 2.1 Local and surrounding features (all formulas)
                          2.2 Calculation methods and the weight matrix
                          2.3 Design: explaining errors with GWR
3  Case & data         — study area, response, explanatory variables
4  Results             — 4.1 geocomplexity across the study area
                          4.2 baseline models and the structure of their errors
                          4.3 error explained, by model
                          4.4 where it is explained (local coefficients)
                          4.5 improvement when geocomplexity is fed back
5  Discussion          — the ordering across models; the local geography;
                          use as a pre-fit diagnostic; limits of the measure
6  Conclusions         — 6-8 sentences answering the aims

Results-to-manuscript map

Manuscript elementSource fileShown in
Geocomplexity mapsfigs/fig01§4.1
Method comparisonfigs/fig04, tables/table-gc-methods.tex§2.2 / supplement
Baseline models and error autocorrelationtables/table-baseline-models.tex§4.2
Main table (error explained)tables/table-error-explanation.tex§4.3
Local coefficientsfigs/fig06, results/error-explanation-coefficients.csv§4.4
Improvementfigs/fig05, tables/table-model-improvement.tex§4.5
Every manuscript element traces to one regenerable file.

4.3 Final Reproducibility Package

Code
numbered scripts in R/ + run-all.R + config/project-config.R + tests/
Data
data/econineq.gpkg with data/provenance.md
Results
figs/fig01–fig06 + tables/*.tex + env/session-info.txt
Before claiming reproducible
  • Rscript run-all.R test passes — 21 checks, including the package's own published values to four decimals.
  • Delete results/, tables/, figs/ and data/derived/, re-run Rscript run-all.R — every number and figure on this page regenerates (~90 s).
  • The weight matrix, the calculation method, and the GWR bandwidth criterion are stated in the manuscript and match config/project-config.R.
  • Error explanation is reported on absolute errors, and the local coefficients are mapped rather than summarised to a single global number.
  • env/session-info.txt is included in the deposit.
Anticipate the reviewer

Is geocomplexity just local autocorrelation? → no: it adds the term describing how the neighbours relate to each other, and the article's Table 5 compares the two head to head. Why that weight matrix? → state it; tests/test-02 shows the result depends on it. Why that calculation method?results/gc-method-correlation.csv. Could the improvement be from extra parameters alone? → adjusted R² rises too. Is the effect real if the global scatter is flat? → yes, and figs/fig06 shows where it lives.

References & credits

Demo outputs were generated by the scripts in this folder using the CRAN geocomplexity package. Reference figures are reproduced from the article, which is open access, for comparison.

  1. Zhang Z, Song Y, Luo P, Wu P (2023). Geocomplexity explains spatial errors. International Journal of Geographical Information Science 37(7):1449–1469. doi:10.1080/13658816.2023.2203212 · PDF (source of the concept, workflow, Fig. 5, Fig. 6 and Fig. 8 reference images, and of the case reproduced here)
  2. Lyu W, Song Y, et al. geocomplexity: Geographical Complexity. R package. CRAN · calculation vignette · application vignette (the reference implementation, the source of the demo data, and of the values locked in tests/test-01)
  3. Anselin L (1995). Local indicators of spatial association — LISA. Geographical Analysis 27(2):93–115. doi:10.1111/j.1538-4632.1995.tb00338.x (the local Moran term geocomplexity extends)
  4. Sun Y, Wu G, Song Y, et al. (2026). Local effects of pattern interactions in driving urbanization. Int J Appl Earth Obs Geoinf 146:105072. doi:10.1016/j.jag.2025.105072 · LPI tutorial (geocomplexity used as an explanatory variable)