Skip to content

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

Open
gaoflow wants to merge 4 commits 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 4 commits into
scverse:mainfrom
gaoflow:fix-1183-geary-variance

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 1, 2026

Copy link
Copy Markdown

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

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.55%. Comparing base (55572c4) to head (b466c26).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1197   +/-   ##
=======================================
  Coverage   76.55%   76.55%           
=======================================
  Files          63       63           
  Lines        9067     9069    +2     
  Branches     1521     1522    +1     
=======================================
+ Hits         6941     6943    +2     
  Misses       1541     1541           
  Partials      585      585           
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.

@selmanozleyen

Copy link
Copy Markdown
Member

Thanks a lot! I will merge this as soon as I get approval of someone else as well

@timtreis timtreis self-requested a review June 16, 2026 13:26
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

3 participants