Skip to content

Use Geary's C normality variance in spatial_autocorr (fixes #1183)#1197

Open
gaoflow wants to merge 1 commit into
scverse:mainfrom
gaoflow:fix-1183-geary-variance
Open

Use Geary's C normality variance in spatial_autocorr (fixes #1183)#1197
gaoflow wants to merge 1 commit into
scverse:mainfrom
gaoflow:fix-1183-geary-variance

Conversation

@gaoflow
Copy link
Copy Markdown

@gaoflow gaoflow commented Jun 1, 2026

Description

squidpy.gr.spatial_autocorr shares a single _analytic_pval implementation between mode="moran" and mode="geary", swapping only E[score]. The variance it applies is the Moran's I normality variance (Cliff & Ord 1981); Geary's C has a different normality variance, so the analytic p-value (pval_norm / var_norm) for mode="geary" was miscalibrated.

Fixes #1183.

Fix

In _analytic_pval, branch on the statistic and use the Geary's C normality variance (matching pysal/esda's Geary):

Var[C] = ((2·S1 + S2)(n − 1) − 4·S0²) / (2·(n + 1)·S0²)

Moran's I is left exactly as before.

Verification

Reproduced with row-standardised 6-NN weights (the same setup as the issue):

squidpy var_norm (Geary) esda Geary.VC_norm ratio
before 1.4583e-03 1.7014e-03 0.857
after 1.7014e-03 1.7014e-03 1.000

Computing the Geary closed-form from squidpy's own row-standardised weight moments reproduces the post-fix var_norm exactly (relative diff 0.0), and it clearly differs from the old Moran value — confirming the previous code applied the wrong formula. The reporter's 100k-trial null study shows the analytic p-value going from badly non-uniform (KS p ≈ 1e-108) to well-calibrated.

Tests

Added test_spatial_autocorr_var_norm_formula (parametrised over moran/geary) asserting the emitted var_norm matches the closed-form variance of the chosen statistic. It passes with the fix and fails on the Geary case without it. All existing spatial_autocorr tests continue to pass; Moran's I output is unchanged.

)

`_analytic_pval` reused the Moran's I sampling variance under normality for
both `mode="moran"` and `mode="geary"`, swapping only `E[score]`. Geary's C
has a different normality variance (Cliff & Ord 1981; pysal/esda `Geary`), so
the analytic `pval_norm` for `mode="geary"` was miscalibrated.

Branch on the statistic and apply
`Var[C] = ((2*S1 + S2)(n-1) - 4*S0^2) / (2*(n+1)*S0^2)` for Geary's C, leaving
Moran's I unchanged. Add a regression test asserting the emitted `var_norm`
matches the closed-form variance of the chosen statistic.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.34%. Comparing base (da789d0) to head (84aa463).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1197   +/-   ##
=======================================
  Coverage   75.33%   75.34%           
=======================================
  Files          56       56           
  Lines        7922     7924    +2     
  Branches     1292     1293    +1     
=======================================
+ Hits         5968     5970    +2     
  Misses       1444     1444           
  Partials      510      510           
Files with missing lines Coverage Δ
src/squidpy/gr/_ppatterns.py 80.60% <100.00%> (+0.16%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

sq.gr.spatial_autocorr(mode="geary") uses the Moran's I variance formula for its analytic p-value

1 participant