scientific-skills/geomaster/references/coordinate-systems.md
Complete guide to coordinate systems, projections, and transformations for geospatial data.
A Coordinate Reference System defines how coordinates relate to positions on Earth:
Datum: Mathematical model of Earth's shape
Projection: Transformation from curved to flat surface
Units: Degrees, meters, feet, etc.
| EPSG | Name | Area | Notes |
|---|---|---|---|
| 4326 | WGS 84 | Global | GPS default, use for storage |
| 4269 | NAD83 | North America | USGS data, slightly different from WGS84 |
| 4258 | ETRS89 | Europe | European reference frame |
| 4612 | GDA94 | Australia | Australian datum |
| EPSG | Name | Area | Distortion | Notes |
|---|---|---|---|---|
| 3857 | Web Mercator | Global (85°S-85°N) | High near poles | Web maps (Google, OSM) |
| 32601-32660 | UTM Zone N | Global (1° bands) | <1% per zone | Metric calculations |
| 32701-32760 | UTM Zone S | Global (1° bands) | <1% per zone | Southern hemisphere |
| 3395 | Mercator | World | Moderate | World maps |
| 5070 | CONUS Albers | USA (conterminous) | Low | US national mapping |
| 2154 | Lambert-93 | France | Very low | French national projection |
United States:
Europe:
Other:
✅ Storing data (databases, files) ✅ Global datasets ✅ Web APIs (GeoJSON, KML) ✅ Latitude/longitude queries ✅ GPS coordinates
# Bad: Distance calculation in geographic CRS
gpd.geographic_crs = "EPSG:4326"
distance = gdf.geometry.length # WRONG! Returns degrees, not meters
# Good: Calculate distance in projected CRS
gdf_projected = gdf.to_crs("EPSG:32633") # UTM Zone 33N
distance_m = gdf_projected.geometry.length # Correct: meters
✅ Area/distance calculations ✅ Buffer operations ✅ Spatial analysis ✅ High-resolution mapping ✅ Engineering applications
import geopandas as gpd
# Project to appropriate UTM zone
gdf = gpd.to_crs(gdf.estimate_utm_crs())
# Now area and distance are accurate
area_sqm = gdf.geometry.area
buffer_1km = gdf.geometry.buffer(1000) # 1000 meters
⚠️ EPSG:3857 (Web Mercator) for visualization only
# DON'T use Web Mercator for area calculations
gdf_web = gdf.to_crs("EPSG:3857")
area = gdf_web.geometry.area # WRONG! Significant distortion
# DO use appropriate projection
gdf_utm = gdf.to_crs("EPSG:32633") # or estimate_utm_crs()
area = gdf_utm.geometry.area # Correct
Earth is divided into 60 zones (6° longitude each):
def get_utm_zone(longitude, latitude):
"""Get UTM zone EPSG code from coordinates."""
import math
zone = math.floor((longitude + 180) / 6) + 1
if latitude >= 0:
epsg = 32600 + zone # Northern hemisphere
else:
epsg = 32700 + zone # Southern hemisphere
return f"EPSG:{epsg}"
# Example
get_utm_zone(-122.4, 37.7) # Returns 'EPSG:32610' (Zone 10N)
import geopandas as gpd
# Load data
gdf = gpd.read_file('data.geojson')
# Estimate best UTM zone
utm_crs = gdf.estimate_utm_crs()
print(f"Best UTM CRS: {utm_crs}")
# Reproject
gdf_projected = gdf.to_crs(utm_crs)
UPS (Universal Polar Stereographic):
UTM Non-standard:
from pyproj import Transformer
# Create transformer
transformer = Transformer.from_crs(
"EPSG:4326", # WGS 84 (lat/lon)
"EPSG:32633", # UTM Zone 33N (meters)
always_xy=True # Input: x=lon, y=lat (not y=lat, x=lon)
)
# Transform single point
lon, lat = -122.4, 37.7
x, y = transformer.transform(lon, lat)
print(f"Easting: {x:.2f}, Northing: {y:.2f}")
import numpy as np
from pyproj import Transformer
# Arrays of coordinates
lon_array = [-122.4, -122.3]
lat_array = [37.7, 37.8]
transformer = Transformer.from_crs("EPSG:4326", "EPSG:32610", always_xy=True)
xs, ys = transformer.transform(lon_array, lat_array)
from pyproj import CRS
# Get CRS information
crs = CRS.from_epsg(32633)
print(f"Name: {crs.name}")
print(f"Type: {crs.type_name}")
print(f"Area of use: {crs.area_of_use.name}")
print(f"Datum: {crs.datum.name}")
print(f"Ellipsoid: {crs.ellipsoid_name}")
import geopandas as gpd
gdf = gpd.read_file('data.geojson')
# Check CRS immediately
print(f"CRS: {gdf.crs}") # Should never be None!
# If None, set it
if gdf.crs is None:
gdf.set_crs("EPSG:4326", inplace=True)
def ensure_same_crs(gdf1, gdf2):
"""Ensure two GeoDataFrames have same CRS."""
if gdf1.crs != gdf2.crs:
gdf2 = gdf2.to_crs(gdf1.crs)
print(f"Reprojected gdf2 to {gdf1.crs}")
return gdf1, gdf2
# Use before spatial operations
zones, points = ensure_same_crs(zones_gdf, points_gdf)
result = gpd.sjoin(points, zones, predicate='within')
# For local analysis (< 500km extent)
gdf_local = gdf.to_crs(gdf.estimate_utm_crs())
# For national/regional analysis
gdf_us = gdf.to_crs("EPSG:5070") # US National Atlas Equal Area
gdf_eu = gdf.to_crs("EPSG:3035") # Europe Equal Area
# For web visualization
gdf_web = gdf.to_crs("EPSG:3857") # Web Mercator
# Keep original as backup
gdf_original = gdf.copy()
original_crs = gdf.crs
# Do analysis in projected CRS
gdf_projected = gdf.to_crs(gdf.estimate_utm_crs())
result = gdf_projected.geometry.buffer(1000)
# Convert back if needed
result = result.to_crs(original_crs)
# WRONG: Area in square degrees
gdf = gpd.read_file('data.geojson')
area = gdf.geometry.area # Wrong!
# CORRECT: Use projected CRS
gdf_proj = gdf.to_crs(gdf.estimate_utm_crs())
area_sqm = gdf_proj.geometry.area
area_sqkm = area_sqm / 1_000_000
# WRONG: Buffer of 1000 degrees
gdf['buffer'] = gdf.geometry.buffer(1000)
# CORRECT: Project first
gdf_proj = gdf.to_crs("EPSG:32610")
gdf_proj['buffer_km'] = gdf_proj.geometry.buffer(1000) # 1000 meters
# WRONG: Spatial join without checking CRS
result = gpd.sjoin(gdf1, gdf2, predicate='intersects')
# CORRECT: Ensure same CRS
if gdf1.crs != gdf2.crs:
gdf2 = gdf2.to_crs(gdf1.crs)
result = gpd.sjoin(gdf1, gdf2, predicate='intersects')
# Common operations
# Check CRS
gdf.crs
rasterio.open('file.tif').crs
# Reproject
gdf.to_crs("EPSG:32633")
# Auto-detect UTM
gdf.estimate_utm_crs()
# Transform single point
from pyproj import Transformer
tx = Transformer.from_crs("EPSG:4326", "EPSG:32610", always_xy=True)
x, y = tx.transform(lon, lat)
# Create custom CRS
from pyproj import CRS
custom_crs = CRS.from_proj4(
"+proj=utm +zone=10 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"
)
For more information, see: