Moran's I vs Geary's C: Which to Use
TL;DR: Use esda.moran.Moran(y, w) for Moran’s I when you want a single, correlation-like summary of global spatial covariation, and esda.geary.Geary(y, w) for Geary’s C when you care about local, short-range differences between neighbours. The two are inversely related: high Moran’s I means low Geary’s C. Compute both on the same row-standardized weights, report Moran’s I as the headline, and reach for Geary’s C when fine-scale contrast matters.
Why This Matters
Both statistics answer the same broad question — is my variable spatially autocorrelated? — but they weight the evidence differently, and choosing the wrong one can hide the pattern you most need to see. Moran’s I measures how attribute values co-vary around the global mean, so it is tuned to broad, contiguous patterns. Geary’s C measures the squared difference between neighbouring values, so it is tuned to abrupt local transitions. Getting this choice right is fundamental to the spatial autocorrelation metrics you rely on before regression or interpolation, and it sits at the heart of the broader Core Concepts of Spatial Statistics & Geostatistics toolkit. Report a single number without understanding its bias and you may conclude “no structure” when short-range clustering is present, or vice versa.
Both indices are built from the same ingredients: an attribute vector and a spatial weight matrix . The difference is entirely in the numerator.
Moran’s I compares each value’s deviation from the mean with its neighbours’ deviations:
Geary’s C instead sums the squared differences between neighbouring pairs:
The cross-product in Moran’s I is a global device: every pair is judged against the overall mean, so smooth broad gradients register strongly. The squared difference in Geary’s C is local: it never references the global mean, so it responds to how sharply adjacent values differ, regardless of where they sit relative to the average.
What Each Statistic Measures
| Property | Moran’s I | Geary’s C |
|---|---|---|
| Core quantity | Cross-product of mean deviations | Squared difference of neighbour pairs |
| Sensitivity | Broad, global covariation | Local, short-range differences |
| Expected value (randomness) | (≈ 0 for large ) | |
| Typical range | (row-standardized) | |
| Positive autocorrelation | (toward ) | (toward ) |
| Negative autocorrelation | (toward ) | (toward ) |
| Interpretation scale | Correlation-like, intuitive | Distance-like, less intuitive |
| Driven most by | Regional trends, large clusters | Local outliers, sharp edges |
The single most important fact is the inverse relationship: positive spatial autocorrelation pushes Moran’s I up toward and Geary’s C down toward . They are not identical rescalings of one another, though — Moran’s I is a global product-moment measure while Geary’s C is a local sum-of-squares measure, so they can rank two datasets differently when autocorrelation is concentrated at short lags.
Environment and Version Pinning
Both statistics live in esda, and both consume a libpysal weights object built from a geopandas GeoDataFrame. No extra dependencies are required.
pip install "geopandas>=1.0" "libpysal>=4.9.0" "esda>=2.5.0" "numpy>=1.23" "scipy>=1.9"
import geopandas as gpd
import numpy as np
import libpysal
from esda.moran import Moran
from esda.geary import Geary
from shapely.geometry import box
Computing Both Side by Side
The most reliable way to choose is to run both diagnostics on identical data and weights, then compare what they say. Because they share the same denominator (total variance) and the same weights, any divergence is attributable to the numerator — global covariation versus local differencing.
Build a shared dataset and weights matrix
# Synthetic 10x10 grid with a smooth diagonal gradient plus noise —
# this produces genuine positive spatial autocorrelation.
cells = [box(x, y, x + 1, y + 1) for y in range(10) for x in range(10)]
gdf = gpd.GeoDataFrame(geometry=cells, crs="EPSG:32618").reset_index(drop=True)
rng = np.random.default_rng(7)
cx = gdf.geometry.centroid.x.values
cy = gdf.geometry.centroid.y.values
gdf["pm25"] = 8.0 + 0.6 * (cx + cy) + rng.normal(0, 1.5, size=len(gdf))
w = libpysal.weights.Queen.from_dataframe(gdf)
w.transform = "R" # row-standardize once, use for both statistics
y = gdf["pm25"].values
Run Moran’s I and Geary’s C on the same inputs
mi = Moran(y, w, permutations=999)
gc = Geary(y, w, permutations=999)
print(f"Moran's I : {mi.I:.4f} (E[I] = {mi.EI:.4f}, p_sim = {mi.p_sim:.4f})")
print(f"Geary's C : {gc.C:.4f} (E[C] = {gc.EC:.4f}, p_sim = {gc.p_sim:.4f})")
Typical output for this construction:
Moran's I : 0.6203 (E[I] = -0.0101, p_sim = 0.0010)
Geary's C : 0.3915 (E[C] = 1.0000, p_sim = 0.0010)
Interpreting the Output
Read the two numbers together. mi.I of about 0.62 sits well above its expected value of roughly 0 and confirms strong positive global autocorrelation. gc.C of about 0.39 sits well below its expected value of 1.0, confirming the same positive autocorrelation from the local-difference angle. The pseudo p-values (p_sim) from permutation inference are both significant, so the pattern is not an artefact of randomness.
The direction always agrees under the inverse relationship: up, down for positive autocorrelation. What you learn from running both is the shape of the dependence. When Moran’s I is high but Geary’s C is only mildly below 1, the structure is a broad smooth gradient. When Geary’s C is very low (near 0) while Moran’s I is only moderate, neighbours are nearly identical at short range — a sign of fine-scale clumping or duplicated measurements that the global index dilutes.
gc.EC is always exactly 1.0 because that is the theoretical expectation under spatial randomness, independent of . mi.EI is -1/(n-1), which for is about -0.0101. Comparing each statistic to its own expected value — not to zero, and not to each other’s scale — is the correct significance check.
Critical Best Practices
Anchor each statistic to its own expected value
The rookie error is treating 1.0 as neutral for Moran’s I or 0 as neutral for Geary’s C. Neutral for Moran’s I is ; neutral for Geary’s C is exactly 1. Always compare mi.I against mi.EI and gc.C against gc.EC, and lean on the permutation p_sim rather than eyeballing the raw value.
Use identical, row-standardized weights for a fair comparison
If you build one weights object for Moran and a different one for Geary, any divergence is confounded by the weights, not the statistics. Construct w once, set w.transform = "R", and pass the same object to both. Row-standardization keeps both indices on their conventional scales and prevents high-degree units from dominating.
Prefer Geary’s C when local outliers matter
Because Geary’s C squares neighbour differences, it is disproportionately sensitive to a handful of sharp local contrasts — exactly the situation where a global mean-referenced index like Moran’s I washes the signal out. For edge detection, boundary analysis, or spotting isolated anomalies, Geary’s C is the more responsive diagnostic.
Report Moran’s I as the default headline
Moran’s I is more widely recognised, sits on an intuitive correlation-like scale, and is directly comparable across studies and datasets. Unless your question is specifically about short-range contrast, lead with Moran’s I and cite Geary’s C as corroboration. When both point the same way, the conclusion is robust.
Escalate to local statistics when the global answer hides heterogeneity
Neither global index tells you where the autocorrelation lives. If the global result is significant, decompose it with local indicators — see Computing Local Moran’s I (LISA) in Python to map high-high clusters, low-low clusters, and spatial outliers unit by unit.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Geary’s C above 2 or below 0 | Non-standardized binary weights | Set w.transform = "R" before computing |
mi.I and gc.C both suggest “no pattern” but a map shows clumping |
Weights too coarse to capture the scale | Rebuild w with a shorter distance band or KNN with smaller k |
| Moran’s I significant, Geary’s C not (or vice versa) | Autocorrelation concentrated at a scale one index favours | Report both and describe the scale; consider directional analysis |
p_sim exactly 0.001 for both |
Too few permutations to resolve small p-values | Increase to permutations=9999 |
ValueError on Geary(y, w) with NaN in y |
Missing attribute values | Drop or impute NaN before computing either statistic |
Next Steps
With the choice made, run the diagnostic in a full pipeline — see How to Calculate Moran’s I in PySAL for the complete Moran’s I workflow, then decompose a significant global result with Computing Local Moran’s I (LISA) in Python. Both build on the spatial autocorrelation metrics foundations.
Frequently Asked Questions
Do Moran’s I and Geary’s C ever disagree?
They usually agree in direction because they are inversely related: high Moran’s I corresponds to low Geary’s C. Disagreements in strength happen when autocorrelation is concentrated at short range or driven by a few local outliers. Geary’s C reacts more strongly to these local differences, so a moderate Moran’s I paired with a sharply low Geary’s C signals fine-scale structure that the global index averages away.
What are the expected values and ranges of the two statistics?
Moran’s I has expected value under spatial randomness and typically falls near with row-standardized weights. Geary’s C has expected value under randomness, ranges from to roughly , where values below indicate positive autocorrelation and values above indicate negative autocorrelation or dispersion.
Which should I report by default?
Report Moran’s I as the primary global summary because it is more widely understood, more interpretable on a correlation-like scale, and directly comparable across studies. Add Geary’s C when you suspect short-range differences, local outliers, or process behaviour that the broad covariation of Moran’s I would smooth over.
← Back to Spatial Autocorrelation Metrics
Related
- How to Calculate Moran’s I in PySAL — the full Moran’s I workflow with permutation inference
- Computing Local Moran’s I (LISA) in Python — decompose a global result into local clusters
- Spatial Weight Matrices — the shared input both statistics depend on