Reprojecting Between EPSG:4326 and EPSG:3857 Without Distortion
Reproject with to_crs(3857) only for display, clip geometries to ±85.06° latitude first, and never measure distance or area in Web Mercator — switch to an equal-area or UTM CRS for any calculation.
Jump to heading Why this matters
Almost every web tile map — Folium, Leaflet, Deck.gl, pydeck — renders in Web Mercator (EPSG:3857), while most data is stored and exchanged in WGS 84 (EPSG:4326). So the single most common reprojection in a spatial dashboard is the hop between these two, and it is also the one most often done wrong. Two mistakes dominate: measuring area or distance in 3857, where the numbers are inflated by a latitude-dependent factor, and reprojecting geometries that extend past the projection’s ±85.06° limit, where coordinates blow up to infinity. Doing this hop correctly is the display half of the CRS & coordinate systems reference; the measurement half is covered by looking up EPSG codes for your region.
Jump to heading Prerequisites
- Python 3.10+ with
geopandas>=0.14,shapely>=2.0, andpyproj>=3.5 - A
GeoDataFramewhose.crsis known — see the GeoDataFrame schema reference if it isNone - The reprojected layer will feed a live map, as described under dynamic spatial filtering
Jump to heading Step-by-step solution
Jump to heading Step 1 — Understand why measurement in 3857 is wrong
Web Mercator is conformal: it preserves the shape of small features by stretching the map vertically as you move away from the equator. The vertical scale factor is 1 / cos(latitude), which means a shape at 60° latitude covers roughly four times the area on the map that the same shape covers at the equator. The diagram makes the distortion concrete.
The takeaway: 3857 is a display CRS. Any number you need — a length, an area, a buffer radius — is computed in a different CRS.
Jump to heading Step 2 — Reproject the GeoDataFrame with to_crs
The transform itself is one call. Start from a frame you know is in 4326.
import geopandas as gpd
from shapely.geometry import Point
# Plausible points over Berlin and Hamburg (lon, lat)
gdf = gpd.GeoDataFrame(
{"city": ["Berlin", "Hamburg"]},
geometry=[Point(13.40, 52.52), Point(9.99, 53.55)],
crs="EPSG:4326",
)
gdf_web = gdf.to_crs("EPSG:3857")
print(gdf_web.geometry.iloc[0]) # POINT (1491772.6 6894111.9) — metres
Berlin’s (13.40, 52.52) becomes roughly (1,491,773, 6,894,112) in Web Mercator metres. Those large easting/northing values are correct — they are metres from the projection origin, not a bug.
Jump to heading Step 3 — Handle the ±85.06° latitude clipping
Web Mercator’s y coordinate tends to infinity as latitude approaches ±90°, so the projection is cut off at ±85.06°. A dataset with an Arctic vertex — a global coastline, an ice-extent polygon — will produce infinite coordinates after to_crs. Clip to the valid band before reprojecting.
from shapely.geometry import box
MERC_LAT = 85.06
valid_band = box(-180, -MERC_LAT, 180, MERC_LAT)
gdf_clipped = gpd.clip(gdf, valid_band) # drop/trim beyond ±85.06°
gdf_web = gdf_clipped.to_crs("EPSG:3857")
# guard against any surviving infinities
assert gdf_web.geometry.is_valid.all()
assert gdf_web.total_bounds[3] < 2.0e7 # northing stays finite
Jump to heading Step 4 — Measure in an equal-area or UTM CRS
When a filter or tooltip needs an actual area or distance, reproject a copy into the right measurement CRS — never read it from the 3857 frame.
# area: use an equal-area CRS
gdf["area_km2"] = gdf.to_crs("EPSG:6933").area / 1_000_000
# distance between the two cities: use a local UTM zone (metres)
metric = gdf.to_crs("EPSG:25832")
d_km = metric.geometry.iloc[0].distance(metric.geometry.iloc[1]) / 1000
print(f"{d_km:.0f} km") # ~255 km Berlin–Hamburg
Jump to heading Verification
Confirm the round-trip is lossless within the valid band — the fastest proof the transform is correct.
import geopandas as gpd
from shapely.geometry import Point
gdf = gpd.GeoDataFrame(
geometry=[Point(13.40, 52.52)], crs="EPSG:4326" # Berlin
)
roundtrip = gdf.to_crs("EPSG:3857").to_crs("EPSG:4326")
orig = gdf.geometry.iloc[0]
back = roundtrip.geometry.iloc[0]
assert abs(orig.x - back.x) < 1e-6, "longitude drifted on round-trip"
assert abs(orig.y - back.y) < 1e-6, "latitude drifted on round-trip"
print("round-trip clean")
Jump to heading Edge cases and gotchas
- Poles clipping: any geometry crossing ±85.06° must be clipped first (Step 3), or a single vertex sends the whole layer’s bounds to infinity and the map fails to fit.
- Axis order: if points arrive as
(lat, lon)instead of(lon, lat), they will land in the wrong hemisphere. ConstructPoint(lon, lat)and, when usingpyproj.Transformerdirectly, passalways_xy=True. - Area in 3857: calling
.areaon the reprojected frame returns an inflated value that grows with latitude. Always reproject toEPSG:6933,EPSG:3035, orEPSG:5070for area (Step 4).
Jump to heading FAQ
Why does a polygon look bigger near the poles in Web Mercator?
Web Mercator is a conformal projection that preserves local angles by stretching the map vertically as latitude increases. The scale factor is 1 / cos(latitude), so at 60° a shape is exaggerated about twofold in each direction and roughly fourfold in area. This is a display property, not a data error, but it means you must never compute area directly in EPSG:3857.
What happens to points beyond 85° latitude when reprojecting to 3857?
Web Mercator is only defined up to about ±85.06°, where the projected y coordinate would otherwise go to infinity. Points beyond that latitude produce infinite or extreme values after to_crs. Clip or filter geometries to the valid band before reprojecting so a single polar vertex does not break the whole layer.
Is reprojecting 4326 to 3857 and back lossless?
Within the valid latitude band the round trip is accurate to floating-point tolerance, so an assert with a small tolerance passes. Loss only appears if geometries were clipped at the ±85.06° limit or if coordinates were rounded to reduce precision between the two transforms.
Back to CRS & Coordinate Systems Reference
Related
- Looking Up EPSG Codes for Your Region — pick the measurement CRS you reproject into for area and distance
- CRS & Coordinate Systems Reference — geographic vs projected, axis order, and the full EPSG table
- Dynamic Spatial Filtering — feeding the reprojected layer into a live, filterable map