Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 42

Calculating Global Spatial Autocorrelation with Moran's I

Recommended resources

Welcome back. In our previous lesson, we established the conceptual foundation for your geospatial analysis, exploring what spatial autocorrelation is and why it's crucial for moving beyond simple visual inspection of your maps. You learned that any formal statistic for spatial patterns must combine two things: attribute similarity (like violation rates) and locational similarity (neighborhood relationships).

Today, we will put that theory into practice. This lesson focuses on computing and interpreting Global Moran's I, the most widely used statistic for measuring spatial autocorrelation. By the end of this lesson, you will be able to calculate a single value and an associated p-value that tells you whether the geographic pattern of meal quality violations across districts is significantly clustered, dispersed, or random. This is the critical step that adds statistical rigor to your search for violation hotspots.

The Formula Behind the Statistic

Before we dive into the Python code, it's valuable to understand what the Moran's I calculation actually represents. While the formula can look intimidating, its logic is quite intuitive. It essentially compares the similarity of neighboring values to the overall variation in the data.

A fantastic video by ritvikmath breaks this down into simple, logical steps using a grid-based example. This will give you a strong conceptual model of what the pysal library will be doing under the hood.

Moran's I : Data Science Concepts

This video, "Moran's I : Data Science Concepts," provides one of the clearest intuitive explanations of the formula's components.

As you watch, focus on how the formula is constructed piece by piece: First, watch the segment on the weight matrix. This reinforces the concept of W_ij which formally defines which locations are neighbors. Next, see how the total variation is calculated. This is a measure of how much the values (your violation rates) differ from the overall average, without considering geography. Then, watch the most important part on spatially weighted variation. Pay close attention to how it multiplies the similarity of two locations' values by their spatial weight. This is where attribute and locational similarity are combined. Finally, see how it all comes together in the final formula. Moran's I is essentially a normalized ratio of the spatially weighted variation (Step 3) to the total variation (Step 2).

After watching, the key takeaway should be this: Moran's I is high and positive when neighboring locations have similar values (high-high or low-low), and this similarity is greater than what you'd expect from random chance.


Calculating Moran's I in Python

Now, let's translate this concept into code. For your project, you'll have a GeoDataFrame where each row is a district (a polygon) and one of the columns is the violation_rate you've calculated. The process involves two main steps: defining the spatial weights and then running the test.

We'll use the pysal ecosystem of libraries, specifically libpysal for weights and esda (Exploratory Spatial Data Analysis) for the Moran's I statistic.

Step 1: Create the Spatial Weights Matrix

First, we must formally define "neighbors". Since you are working with district polygons, the most intuitive approach is contiguity. Two districts are neighbors if they share a border. The pysal library offers two common types:

  • Rook Contiguity: Polygons are neighbors if they share a line segment.
  • Queen Contiguity: Polygons are neighbors if they share a line segment or a single point (a corner).

Queen is the more common and inclusive choice, so we'll use that. The following reading provides a clear, practical example of creating a weights matrix and then using it to compute Moran's I.

Analyzing Geospatial Data with Python (Part 2 - Hypothesis ...

This article from Towards Data Science walks through the exact workflow you'll need.

First, read the sections Weights Matrix and Contiguity. This part shows how to generate a Queen contiguity matrix from a GeoDataFrame using libpysal.weights.Queen.from_dataframe(). Note the simplicity of the code. Next, read the section on Moran’s I. This shows how to pass your variable of interest (e.g., violation_rate) and the weights matrix w into the esda.moran.Moran() function to get the result.

As you can see from the reading, the code to perform this complex statistical test is surprisingly concise:

import libpysal.weights
import esda

# Assuming 'gdf' is your GeoDataFrame with a 'violation_rate' column
# and a 'geometry' column

# 1. Create the spatial weights matrix (using Queen contiguity)
w = libpysal.weights.Queen.from_dataframe(gdf)

# Optional but good practice: row-standardize the weights
w.transform = 'R'

# 2. Calculate Moran's I
y = gdf['violation_rate']
moran = esda.Moran(y, w)

# 3. Print the results
print(f"Moran's I: {moran.I}")
print(f"p-value: {moran.p_sim}")

This simple block of code is the core of your global spatial autocorrelation analysis.


Interpreting the Results

The moran object contains two crucial pieces of information: the statistic I and the p-value p_sim.

  1. Moran's I (moran.I): This value typically ranges from -1 to +1.

    • Positive I (approaching +1): Indicates positive spatial autocorrelation, or clustering. Districts with high violation rates tend to be near other districts with high rates, and districts with low rates are near other low-rate districts. This is evidence of hotspots and coldspots.
    • Negative I (approaching -1): Indicates negative spatial autocorrelation, or dispersion. High-rate districts are systematically located next to low-rate districts (a checkerboard pattern). This is less common in social data.
    • I near 0: Indicates no spatial pattern, or spatial randomness. The location of a district gives no information about the violation rates of its neighbors.
  2. The p-value (moran.p_sim): This tells you if your result is statistically significant. The test's null hypothesis is that the values are randomly distributed across your map. The p-value is the probability of observing a Moran's I value as extreme as yours (or more extreme) if the null hypothesis were true.

    • A low p-value (e.g., < 0.05) gives you evidence to reject the null hypothesis. You can conclude that the spatial pattern you observe (clustering or dispersion) is unlikely to be the result of random chance.
    • A high p-value (e.g., > 0.05) means you cannot reject the null hypothesis. You don't have enough evidence to say the pattern is not random.

The following image provides a great visual summary of how to interpret the results based on the Z-score (which is related to the Moran's I value) and the p-value.

This diagram illustrates how the Moran's I statistic relates to a standard normal distribution. A high positive score (right tail) indicates significant clustering, a high negative score (left tail) indicates significant dispersion, and a score in the middle suggests a random pattern.

This diagram illustrates how the Moran's I statistic relates to a standard normal distribution. A high positive score (right tail) indicates significant clustering, a high negative score (left tail) indicates significant dispersion, and a score in the middle suggests a random pattern.

The workshop from UCLA provides a very intuitive explanation of how this p-value is generated through permutation, which powerfully illustrates the concept of statistical significance.

Introduction to Spatial Statistics with Python

This clip from the "Introduction to Spatial Statistics with Python" workshop demonstrates how to assess significance.

Watch the segment from simulating randomness. The instructor explains that the library essentially shuffles the violation rates around the map many times (e.g., 999 times), calculates Moran's I for each random shuffle, and builds a distribution of results under randomness. Your actual Moran's I value is then compared to this distribution. If your value is an extreme outlier, your p-value will be very small.

This simulation is why the p-value is often named p_sim—it's a simulated p-value. For your project, a significant positive Moran's I (e.g., I = 0.45, p = 0.001) would be a major finding, confirming that the clusters of high and low violation rates on your map are statistically meaningful.

This example shows a Moran's I of 0.47 and a Z-score of 45.92, which corresponds to a p-value of less than 0.01. This allows us to reject the null hypothesis (H₀) of a random spatial pattern and conclude that the data is significantly clustered.

This example shows a Moran's I of 0.47 and a Z-score of 45.92, which corresponds to a p-value of less than 0.01. This allows us to reject the null hypothesis (H₀) of a random spatial pattern and conclude that the data is significantly clustered.


Conclusion

You have now bridged the gap from visual pattern recognition to rigorous statistical testing. By computing Global Moran's I, you can make a definitive statement about the overall spatial structure of meal quality violations in your data.

Here are the key takeaways from this lesson:

  • Moran's I is a global statistic that quantifies the overall clustering or dispersion in your dataset with a single number.
  • The calculation requires a spatial weights matrix (W), typically based on Queen contiguity for polygon data, to define which locations are neighbors.
  • The esda library in Python makes the computation straightforward: esda.Moran(values, weights).
  • Interpretation requires checking both the I value (for direction and strength of correlation) and the p_sim value (to confirm statistical significance and rule out randomness).

With this tool, you can now answer the question: "Is there a significant geographic pattern to meal quality violations?" However, this is a global statistic. It tells you if clustering exists on the map as a whole, but it doesn't tell you where the specific clusters are.

In our next lesson, we will address that exact question by moving from global to local analysis. We will compute Local Moran's I (LISA) to identify the specific districts that form statistically significant hotspots and coldspots.