How to Fix CRS Mismatch in GeoPandas

How to identify and fix CRS mismatch issues in GeoPandas using set_crs() and to_crs() before spatial operations.

Problem statement

A CRS mismatch happens when two GeoDataFrames use different coordinate reference systems, or when one layer has missing or incorrect CRS metadata. This is a common reason spatial operations fail in GeoPandas.

In real workflows, this usually appears when you:

  • run a spatial join between a shapefile and a GeoJSON
  • overlay layers from different sources
  • clip features with a boundary layer
  • plot two layers that should overlap but appear far apart

Typical signs of a CRS mismatch in GeoPandas include:

  • sjoin() returns zero matches even though features should intersect
  • overlay() or clip() returns empty results
  • layers plot in different locations
  • GeoPandas shows warnings about different CRS values

Common causes are:

  • one layer is in EPSG:4326 and the other is in a projected CRS such as EPSG:3857 or EPSG:32633
  • one file has no CRS defined
  • set_crs() is used when to_crs() is actually needed
  • a source file contains wrong CRS metadata

If you need to fix CRS mismatch in GeoPandas, the solution is to first identify each layer’s CRS, then assign or transform CRS correctly before running spatial analysis.

Quick answer

To fix coordinate system mismatch in GeoPandas:

  1. Check both GeoDataFrames with .crs
  2. If one layer has missing CRS metadata, assign the correct CRS with set_crs()
  3. If both layers have valid but different CRS values, convert one layer with to_crs() to match the other
  4. Verify the fix by plotting or rerunning the spatial operation
import geopandas as gpd

points = gpd.read_file("data/points.geojson")
polygons = gpd.read_file("data/zones.shp")

print(points.crs)
print(polygons.crs)

# Example: reproject points to match polygons
points = points.to_crs(polygons.crs)

Step-by-step solution

Check whether your GeoDataFrames use different CRS

Always inspect CRS before doing a spatial join, overlay, clip, or area calculation.

import geopandas as gpd

points = gpd.read_file("data/customer_points.geojson")
zones = gpd.read_file("data/service_zones.shp")

print("Points CRS:", points.crs)
print("Zones CRS:", zones.crs)

Typical output might be:

Points CRS: EPSG:4326
Zones CRS: EPSG:3857

If the CRS values differ, spatial operations may give wrong or empty results. If either layer shows None for .crs, do not use to_crs() yet; assign the correct source CRS first with set_crs().

Identify signs of CRS mismatch

This simple plot often reveals the problem:

ax = zones.plot(facecolor="none", edgecolor="black", figsize=(8, 8))
points.plot(ax=ax, color="red", markersize=10)

If the layers do not line up, you likely have a CRS problem. It can be a true CRS mismatch, missing CRS metadata, wrong CRS metadata, or simply different geographic extents.

You may also see problems like this:

joined = gpd.sjoin(points, zones, predicate="within", how="left")
print(joined.head())

If the result has no matches where you expect some, check CRS first.

Fix missing CRS metadata correctly

When to use set_crs()

Use set_crs() only when the coordinates are already in a known CRS, but the metadata is missing.

This does not change the geometry coordinates. It only labels them.

import geopandas as gpd

parcels = gpd.read_file("data/parcels_missing_crs.shp")
print(parcels.crs)  # None

# Suppose you confirmed the source data is actually in EPSG:32633
parcels = parcels.set_crs("EPSG:32633")

print(parcels.crs)

How to confirm the source CRS before assigning it

Before using set_crs(), verify the real CRS from the source:

  • check the original dataset documentation
  • inspect layer properties in QGIS
  • review the shapefile .prj file
  • confirm with the data provider

Do not guess. If you assign the wrong CRS, every later reprojection and spatial result will be wrong.

Reproject one layer to match the other

When to use to_crs()

Use to_crs() when both layers already have valid CRS values, but they are different. This transforms the coordinates into a new CRS.

points = gpd.read_file("data/customer_points.geojson")   # EPSG:4326
zones = gpd.read_file("data/service_zones.shp")          # EPSG:3857

points = points.to_crs(zones.crs)

This is the standard approach for aligning two layers before analysis.

Choose which layer CRS to match

In most cases, convert the secondary layer to match the main analysis layer.

Examples:

  • if you are clipping points to administrative polygons, convert the points to the polygon CRS
  • if you are calculating area, distance, or buffers, prefer a projected CRS instead of EPSG:4326
  • keep one common CRS across the workflow to avoid repeated conversions

Code examples

Example: assign a missing CRS, then reproject

import geopandas as gpd

points = gpd.read_file("data/field_points.geojson")
boundary = gpd.read_file("data/project_boundary.shp")

print("Before")
print("Points CRS:", points.crs)
print("Boundary CRS:", boundary.crs)

# points.crs is None, but you confirmed the coordinates are in WGS84
points = points.set_crs("EPSG:4326")

# Reproject to match the boundary layer
points = points.to_crs(boundary.crs)

print("\nAfter")
print("Points CRS:", points.crs)
print("Boundary CRS:", boundary.crs)

Example: fix CRS mismatch before a spatial join

This example shows a typical case with GeoJSON points and shapefile polygons.

import geopandas as gpd

# Load layers from different sources
points = gpd.read_file("data/stores.geojson")
districts = gpd.read_file("data/districts.shp")

print("Before fix")
print("Points CRS:", points.crs)
print("Districts CRS:", districts.crs)

# If points had no CRS but you confirmed they were in EPSG:4326:
# points = points.set_crs("EPSG:4326")

# Reproject points to match polygon CRS
points = points.to_crs(districts.crs)

print("\nAfter fix")
print("Points CRS:", points.crs)
print("Districts CRS:", districts.crs)

# Run spatial join
stores_with_districts = gpd.sjoin(points, districts, predicate="within", how="left")

print(stores_with_districts[["store_name", "district_name"]].head())

Verify that the CRS fix worked

Compare CRS after conversion

After reprojection, both layers should report the same CRS:

print("Points CRS:", points.crs)
print("Zones CRS:", zones.crs)

Expected output:

Points CRS: EPSG:3857
Zones CRS: EPSG:3857

Plot both layers to check alignment

ax = zones.plot(facecolor="none", edgecolor="black", figsize=(8, 8))
points.plot(ax=ax, color="red", markersize=10)

If the CRS fix worked, the layers should now align.

Re-run the failed spatial operation

joined = gpd.sjoin(points, zones, predicate="within", how="left")
print(joined[["zone_id"]].head())

This is often enough to resolve a CRS mismatch before a spatial join.

Explanation

GeoPandas stores geometry coordinates and CRS metadata together, but they are not the same thing.

  • set_crs() assigns metadata only
  • to_crs() transforms coordinates into a different coordinate system

That distinction is the most important part of fixing CRS problems.

A common mistake looks like this:

# Wrong if the data is already in EPSG:4326 and you want EPSG:3857
points = points.set_crs("EPSG:3857", allow_override=True)

This tells GeoPandas to treat existing longitude/latitude coordinates as Web Mercator coordinates. The geometry does not move correctly. It becomes mislabeled data.

The correct approach is:

points = points.to_crs("EPSG:3857")

But this only works if the current CRS is already correct. Reprojection depends on having a valid starting CRS.

Edge cases / notes

  • Files with wrong embedded CRS: Sometimes a shapefile or other source has CRS metadata, but it is wrong. In that case, first correct the metadata with set_crs(..., allow_override=True) only if you are certain of the real source CRS, then reproject if needed.
  • GeoJSON often uses longitude/latitude coordinates in WGS84: This is common, but still verify with .crs after loading.
  • Shapefile CRS depends on the .prj file: If the .prj file is missing or broken, CRS may load as None.
  • to_crs() requires a known source CRS: If .crs is None, GeoPandas cannot reproject the layer until you assign the correct original CRS.
  • Mixing geographic and projected CRS: EPSG:4326 is fine for storage and display, but not ideal for distance, area, or buffer analysis.
  • Invalid geometries are separate problems: CRS alignment will not fix self-intersections or broken polygons. If overlay or join still fails, check geometry validity too.

For a broader explanation, see Coordinate Reference Systems (CRS) in GeoPandas.

Related task guides:

If your layers still fail after CRS correction, check:

FAQ

What is the difference between set_crs() and to_crs() in GeoPandas?

set_crs() assigns CRS metadata without changing coordinates. to_crs() transforms coordinates into a new CRS. Use set_crs() for missing metadata, and to_crs() for actual reprojection.

Why does my spatial join return no matches even though the layers overlap?

The most common reason is that the two layers use different CRS values. Check .crs on both GeoDataFrames and reproject one to match the other before running sjoin().

Can I fix CRS mismatch if one file has no CRS defined?

Yes, but only if you know the correct source CRS. Assign it with set_crs(), then use to_crs() if you need to match another layer.

Should I use EPSG:4326 or a projected CRS for analysis?

Use a projected CRS for area, distance, and buffering. EPSG:4326 is common for storage, exchange formats like GeoJSON, and web mapping workflows, but not ideal for metric analysis.