Skip to content

Correct nine silent-wrong-answer defects found in the third audit pass - #7

Merged
elkronos merged 1 commit into
mainfrom
review/audit-2026-08-pass3
Sep 1, 2026
Merged

Correct nine silent-wrong-answer defects found in the third audit pass#7
elkronos merged 1 commit into
mainfrom
review/audit-2026-08-pass3

Conversation

@elkronos

@elkronos elkronos commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Each was reproduced by execution and each fix measured.

Moran's I

  • Drop self-neighbours from the k-NN weights. FNN returns a point's own index when coordinates are duplicated, putting 1/k on a diagonal the statistic requires to be zero: 77% of samples "significant" at p<0.05 on data with no structure. Requesting k+1 and dropping self is insufficient -- the displaced slot left a farther point standing in for a co-located one -- so duplicates are now grouped and answered exactly.
  • Add null = "auto"/"randomisation"/"residual". The exchangeable null is wrong for model residuals; the Cliff & Ord (1981 sec. 8.3) regression moments agree with spdep::lm.morantest() to machine precision.

GWR

  • Read AICc from column 3, not the uncorrected AIC in column 2. GWmodel builds GWR.df by rbind() over unnamed vectors, so the positional read is the normal path. Verified on GWmodel 2.4.1 by recomputing AICc via gwr.basic(): AIC keeps a pure-noise predictor, AICc drops it.
  • fitted() no longer returns a coefficient surface for a predictor named fit/pred/prediction/fitted/yhat (in-sample R2 was -1.18 vs 0.981).
  • coef.gwr_fit() returns model terms only, not the whole 15-column SDF.
  • Omit dMat rather than passing NULL: bw.gwr() branches on missing(), so every dataset above 2000 points silently got the fallback bandwidth.

Bayesian GP

  • Attach the length-scale prior at coefficient level. A global class "lscale" prior is silently dropped by brms, so Stan received defaults and the calibration machinery was dead weight (confirmed via make_stancode()).
  • Size the GP basis against brms's own domain measure -- the full pooled range of the centred coordinates, not the half-range -- so the boundary is no longer twice as wide as gp_k was sized for.

Aggregation and folds

  • Correct the design-effect standard error: s^2 is biased low by the same clustering deff corrects for. 95% coverage went 0.63 -> 0.95 at rho=0.8. The deff = 1 path is bit-identical.
  • Scale a subsampled variogram design effect to the cell's own size (228.6 -> 1825.2 against a true 1821.8).
  • Sweep four azimuths, not two: 0/90 at +-22.5 degrees left half of all directions uncovered, halving the range for a diagonally oriented field. A non-converged or unidentified direction is excluded rather than read as a long range, and its variogram stays attached for inspection.
  • Rank level-selection candidates on the standardised deviate; |I| falls 56% across k on pure noise while |z| stays calibrated.
  • Refuse fold sets built from another dataset via a row fingerprint.
  • estimate_sac_range() is reproducible, validates its response type, and no longer advances the caller's RNG.

3379 tests passing, 0 failures, 0 warnings, 9 skips, on GWmodel 2.4.1 and brms 2.20.4. R CMD check: 1 NOTE (cmdstanr, not on CRAN).

Each was reproduced by execution and each fix measured.

Moran's I
* Drop self-neighbours from the k-NN weights. FNN returns a point's own
  index when coordinates are duplicated, putting 1/k on a diagonal the
  statistic requires to be zero: 77% of samples "significant" at p<0.05 on
  data with no structure. Requesting k+1 and dropping self is insufficient
  -- the displaced slot left a farther point standing in for a co-located
  one -- so duplicates are now grouped and answered exactly.
* Add null = "auto"/"randomisation"/"residual". The exchangeable null is
  wrong for model residuals; the Cliff & Ord (1981 sec. 8.3) regression
  moments agree with spdep::lm.morantest() to machine precision.

GWR
* Read AICc from column 3, not the uncorrected AIC in column 2. GWmodel
  builds GWR.df by rbind() over unnamed vectors, so the positional read is
  the normal path. Verified on GWmodel 2.4.1 by recomputing AICc via
  gwr.basic(): AIC keeps a pure-noise predictor, AICc drops it.
* fitted() no longer returns a coefficient surface for a predictor named
  fit/pred/prediction/fitted/yhat (in-sample R2 was -1.18 vs 0.981).
* coef.gwr_fit() returns model terms only, not the whole 15-column SDF.
* Omit dMat rather than passing NULL: bw.gwr() branches on missing(), so
  every dataset above 2000 points silently got the fallback bandwidth.

Bayesian GP
* Attach the length-scale prior at coefficient level. A global class
  "lscale" prior is silently dropped by brms, so Stan received defaults
  and the calibration machinery was dead weight (confirmed via
  make_stancode()).
* Size the GP basis against brms's own domain measure -- the full pooled
  range of the centred coordinates, not the half-range -- so the boundary
  is no longer twice as wide as gp_k was sized for.

Aggregation and folds
* Correct the design-effect standard error: s^2 is biased low by the same
  clustering deff corrects for. 95% coverage went 0.63 -> 0.95 at rho=0.8.
  The deff = 1 path is bit-identical.
* Scale a subsampled variogram design effect to the cell's own size
  (228.6 -> 1825.2 against a true 1821.8).
* Sweep four azimuths, not two: 0/90 at +-22.5 degrees left half of all
  directions uncovered, halving the range for a diagonally oriented field.
  A non-converged or unidentified direction is excluded rather than read
  as a long range, and its variogram stays attached for inspection.
* Rank level-selection candidates on the standardised deviate; |I| falls
  56% across k on pure noise while |z| stays calibrated.
* Refuse fold sets built from another dataset via a row fingerprint.
* estimate_sac_range() is reproducible, validates its response type, and
  no longer advances the caller's RNG.

3379 tests passing, 0 failures, 0 warnings, 9 skips, on GWmodel 2.4.1 and
brms 2.20.4. R CMD check: 1 NOTE (cmdstanr, not on CRAN).
@elkronos
elkronos merged commit 5196d0a into main Sep 1, 2026
8 checks passed
@elkronos
elkronos deleted the review/audit-2026-08-pass3 branch September 1, 2026 01:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant