*A story about precision, unveiling the power of spherical geospatial Voronoi diagrams with Python*

Earth with Spherical Voronoi diagram moving between 2 projections: Orthogonal and Equirectangular. Generated by the author using the D3.js library.

You might be familiar with Voronoi diagrams and their uses in the geospatial analyses. If not, here is the quick TL;DR: it divides the plane into regions consisting of all points of the plane closer to a given seed than to any other. It is named after mathematician Georgy Voronoy. You can read more about it on the Wikipedia.

How does it apply to the geospatial domain? Using Voronoi diagrams you can quickly find the closest public transit stop for inhabitants of a given city at a bigger scale, faster than calculating it individually for each building separately. Or you can also use it for example in the market share analysis between different brands.

In this post, I want to show the differences between the typical Voronoi diagram calculated with projected coordinates on a flat plane and the spherical one, and hopefully, show the latter’s superiority.

### Dimensions & projections — why does it matter?

If we want to see data on the map, we have to work with projections. To show something on the 2D plane, we have to project the coordinates from the 3D coordinates on the globe.

The most popular projection that we all know and use is the Mercator projection (Web Mercator or WGS84 Mercator to be precise, since it’s used by most of the map providers) and the most popular coordinate system is World Geodetic System 1984 — WGS84 (or EPSG:4326). This system is based on degrees and it ranges in longitude from -180° to 180° (West to East) and in latitude from -90° to 90° (South to North).

Each projection to the 2D plane has some distortions. The Mercator is a *Conformal* map projection which means that angles should be preserved between objects on the Earth. The higher above 0° (or lower below 0°) the latitude, the bigger the distortion in the area and the **distance**. Because the Voronoi diagram heavily relies on the distance between the seeds, the same distortion error is forwarded when generating the diagram.

The Earth is an irregularly shaped ellipsoid, but for our purposes, it can be approximated by the sphere shape. By generating the Voronoi diagram on the sphere, we can properly calculate the distance based on the arcs on the surface of a sphere. Later, we can map the generated spherical polygons to the projected 2D coordinates and we can be sure that the line separating two adjacent Voronoi cells will be perpendicular to the line connecting the two seeds defining these cells.

Below you can see the angles and distances problem I’ve described above. Even though the lines cross at the same point, Voronoi cells’ shapes and angles differ.

Example of angles and distances difference in both versions of Voronoi diagram. Image by the author.

Another problem is that you can’t compare the regions in different parts of the world (i.e. not laying on the same latitude) if you use a 2D Voronoi diagram since the areas will be heavily distorted.

Full Jupyter notebook with code used in the examples below can be found on GitHub. Here some functions are skipped for brevity.

### Prerequisites

Install required libraries

pip install -q srai[voronoi,osm,plotting] geodatasets

Import required modules and functions

import geodatasets

import geopandas as gpd

import matplotlib.pyplot as plt

import plotly.express as px

from shapely.geometry import MultiPoint, Point

from shapely.ops import voronoi_diagram

from srai.regionalizers import VoronoiRegionalizer, geocode_to_region_gdf

### First example

Let’s define six points on the globe: the North and South Poles, and four points on the equator.

earth_points_gdf = gpd.GeoDataFrame(

geometry=[

Point(0, 0),

Point(90, 0),

Point(180, 0),

Point(-90, 0),

Point(0, 90),

Point(0, -90),

],

index=[1, 2, 3, 4, 5, 6],

crs=”EPSG:4326″,

)Image by the author.

Generate Voronoi diagram using voronoi_diagram from the Shapely library

def generate_flat_voronoi_diagram_regions(

seeds_gdf: gpd.GeoDataFrame,

) -> gpd.GeoDataFrame:

points = MultiPoint(seeds_gdf.geometry.values)

# Generate 2D diagram

regions = voronoi_diagram(points)

# Map geometries to GeoDataFrame

flat_voronoi_regions = gpd.GeoDataFrame(

geometry=list(regions.geoms),

crs=”EPSG:4326″,

)

# Apply indexes from the seeds dataframe

flat_voronoi_regions.index = gpd.pd.Index(

flat_voronoi_regions.sjoin(seeds_gdf)[“index_right”],

name=”region_id”,

)

# Clip to Earth boundaries

flat_voronoi_regions.geometry = flat_voronoi_regions.geometry.clip_by_rect(

xmin=-180, ymin=-90, xmax=180, ymax=90

)

return flat_voronoi_regionsearth_poles_flat_voronoi_regions = generate_flat_voronoi_diagram_regions(

earth_points_gdf

)

Generate Voronoi diagrams using VoronoiRegionalizer from the srai library.

Under the hood, it uses the SphericalVoronoi implementation from the scipy library and properly transforms the WGS84 coordinates to and from the spherical coordinate system.

earth_points_spherical_voronoi_regions = VoronoiRegionalizer(

seeds=earth_points_gdf

).transform()

Let’s see the difference between the two on the plots.

The difference between Voronoi diagrams in flat (left) and spherical (right) versions in WGS84 coordinates. Generated by the author using GeoPandas library.The difference between Voronoi diagrams in flat (left) and spherical (right) versions in Orthogonal projection. Generated by the author using Plotly.

The first thing that can be seen is that the 2D Voronoi diagram doesn’t loop back around the globe, since it works on a flat Cartesian plane. The Spherical Voronoi diagram properly covers the Earth and doesn’t break at the *antimeridian** *line (where the longitude switches from 180° to -180°).

To numerically quantify the difference we can calculate the **IoU** (Intersection over Union) metric (or* **Jaccard Index*) to measure the difference between the shapes of the polygons. The value of this metric falls between 0 and 1, where 0 means no overlap and 1 means full overlap.

def calculate_iou(

flat_regions: gpd.GeoDataFrame, spherical_regions: gpd.GeoDataFrame

) -> float:

total_intersections_area = 0

total_unions_area = 0

# Iterate all regions

for index in spherical_regions.index:

# Find matching spherical and flat Voronoi region

spherical_region_geometry = spherical_regions.loc[index].geometry

flat_region_geometry = flat_regions.loc[index].geometry

# Calculate their intersection area

intersections_area = spherical_region_geometry.intersection(

flat_region_geometry

).area

# Calculate their union area

# Alternative code:

# spherical_region_geometry.union(flat_region_geometry).area

unions_area = (

spherical_region_geometry.area

+ flat_region_geometry.area

– intersections_area

)

# Add to the total sums

total_intersections_area += intersections_area

total_unions_area += unions_area

# Divide the intersection area by the union area

return round(total_intersections_area / total_unions_area, 3)calculate_iou(

earth_points_flat_voronoi_regions, earth_points_spherical_voronoi_regions

)

The calculated value is **0.423**, which is pretty low and on the big scale, those polygons are different from each other, which can be easily seen in the plots above.

### Real data example: splitting the globe using AEDs (Automated External Defibrillators) positions

The data used in this example comes from the OpenAEDMap and is based on OpenStreetMap data. The prepared file has filtered positions (80694 to be exact) without duplicated nodes defined on top of each other.

# Load AEDs positions to GeoDataFrame

aed_world_gdf = gpd.read_file(

“https://raw.githubusercontent.com/RaczeQ/medium-articles/main/articles/spherical-geovoronoi/aed_world.geojson”

)

Generate Voronoi diagrams for the AEDs

aed_flat_voronoi_regions = generate_flat_voronoi_diagram_regions(aed_world_gdf)

aed_spherical_voronoi_regions = VoronoiRegionalizer(

seeds=aed_world_gdf, max_meters_between_points=1_000

).transform()

Let’s compare these Voronoi diagrams.

The difference between Voronoi diagrams in flat (left) and spherical (right) versions in WGS84 coordinates. Generated by the author using GeoPandas.The difference between Voronoi diagrams in flat (left) and spherical (right) versions in Orthogonal projection. Generated by the author using Plotly.

The difference is quite obvious when looking at the plots. All borders in the 2D version are straight while spherical ones look quite bendy in the WGS84 coordinates. You can also clearly see that on the flat version, a lot of regions converge on the poles (orthogonal projection focuses on the south pole), while the spherical one doesn’t. Another visible difference is continuity around antimeridian, which was mentioned in the first example. The regions emerging from New Zealand are abruptly cut on the flat version.

Let’s see the IoU value:

calculate_iou(aed_flat_voronoi_regions, aed_spherical_voronoi_regions)

The calculated value is **0.511**, which is slightly better than the first example, but still, the polygons match roughly 50%.

#### Zooming into the city scale

Let’s see the difference on a smaller scale. We can select all the AEDs that are located in London and plot it.

greater_london_area = geocode_to_region_gdf(“Greater London”)

aeds_in_london = aed_world_gdf.sjoin(greater_london_area)2D and Spherical Voronoi diagrams overlayed on top of each other in red and blue colours. Image by the author.calculate_iou(

aed_flat_voronoi_regions.loc[aeds_in_london.index],

aed_spherical_voronoi_regions.loc[aeds_in_london.index],

)

The value is **0.675**. It’s getting better, but it still is a noticeable difference. Since the AEDs are placed denser, the shapes and distances are getting smaller, so the differences between Voronoi diagrams calculated in the projected 2D plane and on a sphere diminish.

Let’s look at some individual examples overlayed on top of each other.

Image by the author.

The areas of those polygons mostly match, but you can see the differences in angles and shapes. Those discrepancies could be important in the spatial analysis and might change the results of them. The bigger the area of interest, the bigger becomes the difference.

### Summary

I hope that now you can see why the spherical Voronoi diagram is better suited for use in the geospatial domain than the flat one.

Most of the analyses in the domain are currently made using Voronoi diagrams in a projected 2D flat plane, which could lead to wrong results.

For a long time, there was no simple solution for spherical Voronoi diagrams available for geospatial data scientists and analysts working in Python. Now it’s as easy as installing one library.

Sure, it calculates a little bit longer than the flat solution since it has to project points to and from spherical coordinates, while properly clipping polygons intersecting antimeridian, but it shouldn’t matter if you want to preserve precision in your analyses.

For JavaScript users, there is an already available spherical Voronoi D3.js implementation.

#### Disclaimer

I’m one of the maintainers of the srai library.

Earth Isn’t Flat, and Neither Should Your Voronoi Diagrams Be was originally published in Towards Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.