Skip to content

spatialkit

2.0.0Package record

Spatial Tessellation, Modeling, and Cross-Validation Toolkit

0packages depend
917downloads / year
77.4%test coverage
11/13flavors OK

Overview

About

Constructs analysis regions from the distribution of the data itself, as an alternative to aggregating onto administrative boundaries that were drawn for unrelated purposes. Seeds and builds Voronoi, Delaunay, hexagonal and square tessellations with reproducible identifiers, selects a cell count from the spatial structure of the observations, assigns features to cells, and aggregates to cell level with optional design-effect corrections so that standard errors account for within-cell autocorrelation. Also manages coordinate reference systems. Fits geographically weighted regression (via 'GWmodel'; Lu et al. (2014) doi:10.1080/10095020.2014.917453), Bayesian spatial Gaussian process regression (via 'brms', using the Hilbert space approximation of Riutort-Mayol et al. (2023) doi:10.1007/s11222-022-10167-2) and random forests (via 'ranger', with the permutation importance of Strobl et al. (2007) doi:10.1186/1471-2105-8-25), each behind one S3 class with consistent predict, fitted, residuals and plot methods. Provides spatial cross-validation with random, block, buffered, leave-location-out and nearest-neighbour distance-matched folds (Mila et al. (2022) doi:10.1111/2041-210X.13851), forward variable selection, model comparison, prediction onto a regular surface, and the area of applicability of Meyer and Pebesma (2021) doi:10.1111/2041-210X.13650 to flag where a fitted model extrapolates beyond its training data.

Changes in 2.0.0

Everything below is relative to **1.0.0** (published on CRAN 2026-08-07).

The major bump is warranted: three exported functions are removed, and several

defaults change the result of a fit or a comparison, so the same script can get

a different answer. Both are under "Breaking changes" — read that section

before upgrading a running analysis.

These notes describe what changed **for a user of 1.0.0**. A good deal of this

release was written after 1.0.0 and then revised before shipping; defects that

existed only between those points are not listed, since no released version

behaved that way. The commit history has that record in full.

Throughout, *raises a warning* means a genuine R warning() — one

tryCatch(warning = ) catches, suppressWarnings() suppresses and

options(warn = 2) escalates. *Logs a warning* means a logger message in the

Show all changes in 2.0.0Hide the rest of the changes in 2.0.0

"spatialkit" namespace, which none of those touch.

Breaking changes

Statistical corrections that change results (third audit pass)

Each item below was measured, and the measurement is quoted so you can judge

whether it affects an analysis you have already run.

  • **residual_morans_i() no longer puts weight on a point's own residual.** FNN::get.knn() reports a point's OWN index among its neighbours whenever exact duplicate coordinates are present, which put 1/k on the diagonal of a matrix Moran's I is only defined for with a zero diagonal. On 40 sites x 4 repeats with a response carrying **no** spatial structure, 120 of 160 rows gained a self-weight, mean I came out at +0.086 against E[I] = -0.0063, and **77% of samples were "significant" at p < 0.05** against a nominal 5%. Repeat observations at one site are exactly what make_folds(method = "leave_location_out") is for, so this was a mainstream input. The dense fallback never had the fault, so the statistic also depended silently on whether **FNN** happened to be installed; the two paths now share one neighbour lookup and agree exactly. Requesting k + 1 neighbours and dropping self is **not** sufficient on its own — the slot self occupied displaced a genuine co-located neighbour and left a farther point standing in for it (75 of 400 retained pairs sat at distance 121 where a neighbour at distance 0 existed). Duplicate coordinates are now grouped and answered exactly.
  • **residual_morans_i() gains a null argument, defaulting to "auto".** Model residuals are not exchangeable — they are orthogonal to the design matrix — so the classical randomisation moments are wrong for them. At n = 120 with six smooth covariates and independent errors, OLS residuals had mean I = -0.031 against the exchangeable E[I] = -0.008, and the z-score averaged -0.54 with sd 0.90 instead of 0 and 1. The Cliff & Ord (1981, sec. 8.3) regression-residual moments restore mean z = -0.09, sd 1.03 and a 4.3% rejection rate against a nominal 5%, and **agree with spdep::lm.morantest() to machine precision** (verified at 1e-16 through the public function). "auto" applies them only when the fit's residuals really are the OLS residuals on the rebuilt design, which a forest's and a working GWR's are not; the null actually used is reported in the return value.
  • **summarize_by_cell() standard errors under a design effect were too small.** s / sqrt(n / deff) corrects the mean's variance for clustering but leaves s^2 biased low by the same clustering: for exchangeable correlation rho, E[s^2] = sigma^2 (n - deff)/(n - 1). The two errors compound. Measured 95% CI coverage at n = 20: **0.905 at rho = 0.3, 0.796 at rho = 0.6, 0.632 at rho = 0.8**; after rescaling by sqrt((n-1)/(n-deff)), 0.952 / 0.952 / 0.953. Applies to deff = "kish", deff = "variogram" and a fixed numeric deff. **The default deff = 1 path is bit-identical to before.**
  • **determine_optimal_levels() ranks model-aware candidates on the standardised deviate, not on |Moran's I|.** E[I] and Var(I) both depend on the cell count, so |I| shrinks as k grows whether or not the finer tessellation captures anything. Over 300 replicates of a response with **no** spatial structure, mean |I| fell monotonically from 0.114 at k = 10 to 0.050 at k = 60 — an |I| ranking prefers the largest candidate for arithmetic reasons alone. Candidates are now ordered by |z| using the Cliff & Ord residual moments (exact here, since the cell-level residuals are OLS residuals by construction); over the same runs z had mean ~0, sd ~1 and a 5% rejection rate of 0.040-0.057 at every k. The "diagnostics" attribute now carries moran_z alongside moran_i.
  • **estimate_sac_range() sweeps four azimuths, not two.** A +/-22.5 degree window around 0 and 90 covers exactly **90 of the 180 distinct azimuths** — every direction between 23 and 67 degrees, and between 113 and 157, fell into neither. On simulated fields with 3:1 anisotropy and a true major-axis range of 300, the estimate came back at 255 and 249 for major axes at 0 and 90 degrees but **151 and 147 at 45 and 135**. Since make_folds(auto_range = TRUE) sizes blocks from this number, a diagonally oriented field silently got blocks half as wide as the correlation they were meant to separate. c(0, 45, 90, 135) tiles all 180 azimuths; the same fields now return 255 / 245 / 249 / 228. A direction whose variogram never reaches a sill is excluded rather than taken as a long range, and the directional attribute now has four named entries.
  • **fit_bayesian_spatial_model()'s calibrated length-scale prior never reached Stan.** brms::set_prior(spec, class = "lscale") with no coef is a *global* prior, and brms applies a global prior only to coefficients with no individual prior of their own — every lscale coefficient always has one. brms dropped it with a note and Stan received brms's defaults, which made gp_lengthscale_bounds(), the tail calibration and $info$gp_lscale_prior dead weight. Confirmed with brms::make_stancode(): the requested prior is absent under the global form and present under the coefficient-level form, which is now used. $info$gp_lscale_prior is read back from brms::validate_prior(), so it records what brms will actually use.
  • **The GP basis was sized against the wrong domain measure.** brms builds the boundary as choose_L(x, c) = c * max(1, max(x) - min(x)) over the pooled, column-centred covariates — the **full range**, not the per-axis half-range in which Riutort-Mayol et al. state their inequalities. Recovering the boundary from make_standata()'s eigenvalues confirms L = c * full range exactly at every c, so the old convention built a boundary **twice as wide** as gp_k was sized for: the GP was under-resolved, and $info$gp_ell_min — the diagnostic meant to catch exactly that — was twice too lenient to fire. The c floor is now brms's own default 1.25 rather than 1.2.
  • **fitted() on a gwr_fit could return a coefficient surface.** The search for GWmodel's fitted-value column matched the whole candidate name vector with %in% and took the first hit in the *SDF's* column order — and the local coefficients come first. A predictor named fit, pred, prediction, fitted or yhat therefore returned its own coefficient column, silently: executed in-sample R^2 was **-1.18** against a true 0.981, and residuals(), summary(), model_metrics(), compare_models() and every cv_gwr() fold consumed it without a warning. The search now runs in preference order and excludes any candidate that is also a model term; all five colliding names now give R^2 = 0.981, identical to the renamed control.
  • **coef.gwr_fit() returned GWmodel's whole SDF data slot** — 15 columns for a two-predictor fit, of which 3 are coefficients and the rest are standard errors, t-values, the response, the fitted values, residuals and Local_R2. It now returns the model terms only; reach for object$engine$SDF for the rest.
  • **estimate_sac_range() is reproducible, and no longer disturbs the caller's RNG.** seed now defaults to 123L rather than NULL. The n_max subsample is an internal approximation, not part of the answer, and leaving it unseeded made the returned range differ between runs on identical input (19531 / 19589 / 19605 on three calls) while silently advancing the caller's stream — and make_folds(auto_range = TRUE) sizes its blocks from that number. Pass seed = NULL for the old behaviour.
  • **estimate_sac_range() rejects a non-numeric response.** as.numeric() on a factor returns its level codes, so a factor response produced a variogram of an arbitrary integer relabelling of the categories and the estimated range changed when the levels were reordered (3700 against 2497 on the same data). Factors and character columns are now an error naming the column; logicals are read as 0/1.
  • **Fold sets built from a different dataset are refused.** Fold splits are lists of ..row_id values, and row IDs are seq_len(nrow()) unless supplied, so passing cv_gwr() a folds object built from another dataset of the same size applied cleanly — every ID matched, every fold was populated, and the model was scored on splits describing other observations. make_folds() now records a small projection-invariant row fingerprint in params$row_probe, and cv_gwr(), cv_bayes(), cv_spatial() and cv_rf() error rather than proceed. Fold objects from earlier versions carry no fingerprint and are passed through unchecked.
  • **evaluate_insample() rejects duplicated names in fits.** model is the key compare_models() joins its metric and Moran's I tables on, so two fits called "GWR" produced a 2x2 cross-join: four rows, every one carrying the first fit's numbers, with the second fit never scored at all.
  • **fit_gwr_model() rejects a non-numeric predictor.** gwr.basic() expands contrasts via model.matrix() and fits, but gwr.predict() does not and fails, so the model appeared to fit and then silently predicted all NA.
  • **fit_gwr_model() no longer rejects a two-valued continuous response.** The "binary" error is now gated on the response being integer-like. A left-censored or saturated measurement (every observation at a detection limit or a ceiling) has two distinct values and is perfectly continuous; it now warns instead. The guard also runs once per fold inside cv_gwr(), where a small training fold can legitimately hold only two distinct values.
  • fitted() returning the wrong length, or nothing, is now an error in summary() and model_metrics() rather than a plausible row count over an all-NA comparison. new_spatial_fit() is the documented extension point, so a subclass with a missing or mis-sized fitted() method is user-reachable.
  • The cached fitted() on a bayesian_fit is stamped with the n and a digest of the data it was computed from. The cache environment has reference semantics — which is what makes it survive copy-on-modify — so fit2 <- fit gave both objects the *same* cache, and assigning different data to the copy returned the original's values at the original's length.
  • make_folds() drops rows with empty or non-finite coordinates, with a logged warning naming the count, rather than letting an EMPTY POINT reach block_kfold's nearest-block rescue and die with "replacement has length zero".
  • When every fold fails, the warning now names the first underlying error. Previously "all 5 folds failed" was the whole diagnosis even when the cause was simply that **brms** or **GWmodel** was not installed.
  • make_folds() records the CRS the folds were built in as params$crs. Geographic input is projected by ensure_projected() to a CRS the caller never chose, and block_size, sac_range and buffer are lengths in *that* CRS.
  • .morans_i_for_k() returns NA at or below nine cells, where every cell neighbours every other and Moran's I collapses to exactly -1/(k-1) for any residual vector — a function of the cell count alone.
  • residual_morans_i(fit, k = 1) works on machines without **FNN**. apply() simplified the length-1 result to a vector, making the neighbour index a 1 x n matrix and every row after the first out of bounds.

Fourth audit pass (adversarial): corrections that change results

Three reviewers were set the task of making the package fail or silently

misbehave on valid input, with their own reproductions. Every finding below

was reproduced here before it was touched; the figures quoted are from those

reproductions.

  • **summarize_by_cell(deff = "kish") under-estimated the predictor ICC by about a factor of m.** The pooled one-way ANOVA grouped the m z-scored predictor columns under the same cell label, so independent per-column cell effects averaged away in the shared cell mean and the between-cell sum of squares shrank by ~1/m. Measured at true rho = 0.5 with m = 4: pooled ICC **0.12** against 0.495 per column, so every predictor SE was ~44% too small. The pooled group is now (variable, cell), which recovers 0.49.
  • **Design effects are built from what a column actually observes.** A cell of 10 rows with 2 finite responses had its response SE formed at the 10-row design effect, then applied to a 2-observation mean: adding 8 NA-response rows moved the SE from 8.46 to **26.38**. Each column's design effect now uses its own non-missing count, with the mean pairwise correlation recomputed over the observed locations when a cell has NAs; cell_weight is the effective count of the primary variable, not of rows (n still counts rows).
  • **CRS-less coordinates get ONE interpretation, wherever they enter.** prep_model_data() assumed EPSG:4326 for CRS-less data that looked like lon/lat and projected it, while every predict() method passed the fit's CRS as a target — a branch that *stamped* it onto the raw numbers. The same rows sat in two different places, and predict(fit, newdata = training rows) disagreed with fitted(fit) by up to one response SD (R² 0.98 in-sample, 0.64 via newdata). The heuristic is now a single function used by both branches; a fit records the assumption it was built under and replays it on CRS-less newdata. Two further symptoms of the same split — CRS-less LINESTRINGs aborting in coerce_to_points() ("crs not found"), and hex/square build_tessellation() refusing input voronoi accepted — are fixed with it. The assumption is now announced with a real R warning.
  • **residual_morans_i() refuses a malformed weights matrix** instead of silently substituting the default k-NN(8) matrix (I = 0.874 returned for four malformed shapes against 0.805 for the weights actually supplied).
  • **The fold-provenance fingerprint no longer refuses the caller's own data.** Three defects in the version introduced last pass: a character ..row_id was coerced to all-NA (and matched row 1 everywhere); coordinates were compared as "%.7g" strings and flipped on the ~1 in 5000 that a reprojection moved by 5e-9°; and polygon input was probed *after* pointization, so a different pointize in the cv call read as different data. Both sides now probe the geometry as supplied, keep IDs in their own type, and compare numerically within 1e-6°.
  • **n_folds_attempted counts the folds supplied.** A fold whose test rows were all removed as incomplete vanished before fitting and was absent from both counts, so five supplied folds reported 4/4. It is announced with a real warning.
  • **determine_optimal_levels()'s elbow uses the signed deviation below the chord.** abs() let a concave bump *above* the chord — a k where k-means fell into a worse local optimum — win with the same magnitude.
  • **estimate_sac_range() returns NA for a constant variable** (an exactly explained response, or a constant one) instead of a fitted "range" of 168 or 673 from a variogram that is identically zero. make_folds(auto_range = TRUE) no longer re-opens the unseeded subsample by forwarding its own seed = NULL.
  • **fit_bayesian_spatial_model() attaches a user-supp

The NEWS section is longer than shown here.

From NEWS.md in the release

Install

Health

CRAN checks
2ERROR11OK
Check messages
ERRORtestsr-devel-linux-x86_64-fedora-clang, r-devel-linux-x86_64-fedora-gcc

r-devel-linux-x86_64-fedora-clang

Running ‘testthat.R’ [91m/54m]
Running the tests in ‘tests/testthat.R’ failed.
Complete output:
  > library(testthat)
  > library(spatialkit)
  > 
  > test_check("spatialkit")
  WARN [2026-10-15 04:13:50] area_of_applicability(): dropping predictor(s) with no variance in the training data: const. A prediction point taking a different value there is extrapolation the dissimilarity index cannot represent.
  WARN [2026-10-15 04:13:50] area_of_applicability(): dropping 1 training row(s) with missing or non-finite predictor values.
  WARN [2026-10-15 04:13:51] fit_rf_model(): include_coords = TRUE. A forest given the coordinates can reproduce the training surface by memorising location and then fail wherever it has not been; random cross-validation will not detect this. Score the result with cv_rf() and blocked folds, not with the out-of-bag error (Meyer et al. 2019).
  WARN [2026-10-15 04:13:51] fit_rf_model(): include_coords = TRUE. A forest given the coordinates can reproduce the trainin
...[truncated]...
rdinates look like lon/lat (xmin=9.10, xmax=9.15, ymin=48.70, ymax=48.75), so they are taken as EPSG:4326 and reprojected to the target CRS ('EPSG:32632'). Set the CRS explicitly to suppress this.
  WARN [2026-10-15 04:13:53] fit_rf_model(): include_coords = TRUE. A forest given the coordinates can reproduce the training surface by memorising location and then fail wherever it has not been; random cross-validation will not detect this. Score the result with cv_rf() and blocked folds, not with the out-of-bag error (Meyer et al. 2019).
  WARN [2026-10-15 04:13:53] make_folds(): dropping 1 point(s) with empty or non-finite coordinates.
  WARN [2026-10-15 04:13:53] prep_model_data(): dropping 1 row(s) with non-finite or missing values (1 of them with an empty or non-finite geometry).
  OMP: Warning #96: Cannot form a team with 28 threads, using 2 instead.
  OMP: Hint Consider unsetting KMP_DEVICE_THREAD_LIMIT (KMP_ALL_THREADS), KMP_TEAMS_THREAD_LIMIT, and OMP_THREAD_LIMIT (if any are set).

r-devel-linux-x86_64-fedora-gcc

Running ‘testthat.R’ [90m/48m]
Running the tests in ‘tests/testthat.R’ failed.
Complete output:
  > library(testthat)
  > library(spatialkit)
  > 
  > test_check("spatialkit")
  WARN [2026-10-19 05:38:42] area_of_applicability(): dropping predictor(s) with no variance in the training data: const. A prediction point taking a different value there is extrapolation the dissimilarity index cannot represent.
  WARN [2026-10-19 05:38:42] area_of_applicability(): dropping 1 training row(s) with missing or non-finite predictor values.
  WARN [2026-10-19 05:38:44] fit_rf_model(): include_coords = TRUE. A forest given the coordinates can reproduce the training surface by memorising location and then fail wherever it has not been; random cross-validation will not detect this. Score the result with cv_rf() and blocked folds, not with the out-of-bag error (Meyer et al. 2019).
  WARN [2026-10-19 05:38:44] fit_rf_model(): include_coords = TRUE. A forest given the coordinates can reproduce the trainin
...[truncated]...
ymax=48.75), so they are taken as EPSG:4326 and reprojected to 'EPSG:32632'. Set the CRS explicitly with sf::st_crs() to suppress this.
  WARN [2026-10-19 05:38:45] ensure_projected(): input has no CRS; its coordinates look like lon/lat (xmin=9.10, xmax=9.15, ymin=48.70, ymax=48.75), so they are taken as EPSG:4326 and reprojected to the target CRS ('EPSG:32632'). Set the CRS explicitly to suppress this.
  WARN [2026-10-19 05:38:45] fit_rf_model(): include_coords = TRUE. A forest given the coordinates can reproduce the training surface by memorising location and then fail wherever it has not been; random cross-validation will not detect this. Score the result with cv_rf() and blocked folds, not with the out-of-bag error (Meyer et al. 2019).
  WARN [2026-10-19 05:38:46] make_folds(): dropping 1 point(s) with empty or non-finite coordinates.
  WARN [2026-10-19 05:38:46] prep_model_data(): dropping 1 row(s) with non-finite or missing values (1 of them with an empty or non-finite geometry).

CRAN check page ↗

Slowest check: 92.3 min · r-devel-linux-x86_64-fedora-clang
Status changes
  • ERROR11 OK, 2 ERROR2026-09-12
  • OK6 OK2026-08-08
Advisories

none known

Documentation

In release 2.0.0
READMEYes · 7,396 wordsVignettesYes · 1 · evaluation depends on the environmentNEWSYes · 100% structured · mentions 2.0.0Citation fileYes · computed when R runs it
Help pages
61 (1 internal)includes a package overview page
Examples that run
95%40 run in a plain check (17 of them under a condition) · 2 never run
Exported functions with examples
100%
Documented parameters
95%
Return-value docs
100%
Help pages citing references
10%

Downloads

917
CRAN downloads in the past year
Rank #11,785 · ~3/day · ~76/mo
Daily download trend is not available in this view yet.
38130 days
91790 days
9171 year
Compare downloads with other packages →
Also on19 r2u3 autocran

Repository

7Stars
0Forks
0Open issues
0Open PRs
0Releases
215Commits
1Commit identities
geospatialgisrspatial-analysisspatial-statisticsgeographically-weighted-regressionr-packagesf
215 commits · Last activity 2026-10-19 · 0% stars, 30d · Default branch at 2.0.0.9000, ahead of CRAN 2.0.0 (checked 2026-10-18)
Responsiveness
How these are sampled
PRs merged (all time)
100%
Median days to close, 50 most recently updated closed issues
1
Median days to close, 50 most recently updated closed PRs
0

Stars over time

2026-08-08 · 12026-09-10 · 7
PracticesBeta

7 development-tooling and community-health practices detected across 3 families in the upstream repository

Checks run against github.com/elkronos/gis_modeling_toolkit on 2026-10-18.

Continuous integration (2)
GitHub ActionsR CMD check workflow

The R CMD check workflow runs on Linux, macOS and Windows, and on R-devel

Docs source (4)
README.Rmdpkgdown

GitHub Pages site last deployed 2026-10-07 · The repository has a site deploy workflow

CRAN release process (1)
cran-comments.md
How this is detected·Detection ruleset v3 (2026-09-26)
AI coding toolsBeta

This repository shows signs of AI coding tools.

  • Claude Codefirst seen 2026-08-20
    Named in .Rbuildignore: .claude
    Pull request it wrote: opened by a project member
    Commits: 128 by its own account · at least 151 crediting it in the message
Models named in its commits
  • Claude Opus 5.577 commits · Sep 2026
  • Claude Opus 523 commits · Aug 2026

Some of these counts are a minimum, because the search stopped before the oldest commits.

Counts are commits, not lines of code. Commits by a tool's accounts count once across all of its accounts. Commits crediting it are the largest count among its credit lines, a minimum.How this is detected·Detection ruleset v4 (updated 2026-09-27)

Dependencies

Declared dependencies
20 external dependencies (excludes base and recommended)

Installing spatialkit adds 26 packages (18 with compiled code, 23 MB of source tarballs). Heaviest: sf 9.3 MB, s2 2.8 MB, Rcpp 1.8 MB.

Depends (1)
R >= 4.1.0
Imports (8)
sfdplyrloggerdigeststatsmethodsutilsparallel
LinkingTo (0)
none
Enhances (0)
none
Reverse dependencies
0direct
0indirect

Nothing depends on this yet.

People & History

People (1)
Author, Maintainer since 1.0.0 (2026), Copyright holder
Releases (2)

Pick two releases to compare their code metrics.

  • 2.0.0Latest
    record
    2026-09-11 · current release · diff ↗ ·
    removed 3 exports
    evaluate_models, evaluate_models_cv, phi_prior_bounds
    added 8 exports
    area_of_applicability, cv_rf, fit_rf_model, gwr_model_selection, plot_folds, predict_surface, select_features_forward, spatialkit_quiet
  • 1.0.0
    record
    first seen 2026-08-07
  • R
    R 4.6.0 released · 2026-04-24

Package metadata

First seen
2026-08-07
Total releases
2 / 1 yrs
License
MIT + file LICENSE OSI
Additional repositories
stan-dev.r-universe.dev
Minimum R
≥ 4.1.0
Download size
1.2 MB

Cite

Cite this package

Run in R for the authors' preferred citation:

citation("spatialkit")

The package provides a citation file that builds its entry when R runs it. Run citation() to see it.

Cite the R Observatory

For a number measured here: a download total, a coverage figure, an archival date.

APA

Balamuta, J. J. (2026). R Observatory: Metrics for spatialkit version 2.0.0 [Data set]. HJJB, LLC. Data release v2026-10-21. https://doi.org/10.5281/zenodo.21843040

From data release v2026-10-21, which the citation names so these numbers can be found later. More on citing and the projects behind them.

Report a problem with this page →

Privacy choices

These apply to this browser and are stored on this device only. Nothing about your choice is sent to us.

Read the privacy policy