Projection & CRS Quick Reference for Energy GIS

This page is the fast lookup the rest of the Core Energy-GIS Data & Spatial Fundamentals knowledge base links back to whenever a workflow needs to pick a coordinate frame. It answers one recurring question — which EPSG code do I reproject to, given what I am about to compute? — with three comparison tables, a decision matrix, and one runnable helper. It is deliberately terse; the conceptual treatment of datum shifts, transformation chains, and audit gating lives in coordinate reference systems for energy projects, and the specific web-tiling case in aligning EPSG:4326 and EPSG:3857 for solar site mapping.

The single rule that governs every table below: you never measure on a geographic coordinate system. Latitude and longitude are angles, not lengths. A degree of latitude is close to constant, but a degree of longitude shrinks with latitude, so on EPSG:4326 the east–west metre value of one “unit” is

At that is roughly m, and at only m — while a degree of latitude stays near m throughout. Feed that anisotropy into geometry.buffer(5000) or geometry.distance() and the result is not off by a rounding error, it is off by a latitude-dependent scale factor. Project into a metric frame first, then measure.

Common EPSG codes for energy work

These are the codes that cover the overwhelming majority of US and global renewable siting, grid, and resource work. “Property preserved” is what the projection keeps true at the expense of everything else — no map projection preserves distance, area, and shape simultaneously.

EPSG Name Property preserved Units When to use
EPSG:4326 WGS 84 (geographic) none — angular position degrees Storage, interchange, GPS feeds, join keys. Never for .area/.length/.buffer.
EPSG:3857 WGS 84 / Pseudo-Mercator shape/angle (conformal); area badly inflated metres Slippy-map tiles, basemaps, drone orthomosaics. Visualization only — not area or distance.
EPSG:32610 WGS 84 / UTM zone 10N shape/angle, locally near-true distance metres Distance, buffers, routing in the US West (≈126°W–120°W band).
EPSG:32618 WGS 84 / UTM zone 18N shape/angle, locally near-true distance metres Same, for the US East (≈78°W–72°W band).
EPSG:2277 NAD83 / Texas Central (State Plane) conformal, survey-grade local distance US survey feet Engineering/survey distance within one state zone; matches civil CAD drawings.
EPSG:5070 NAD83 / CONUS Albers area (equal-area) metres Acreage, land-cover, setback area across the contiguous US.
EPSG:6933 WGS 84 / NSIDC EASE-Grid 2.0 Global area (equal-area) metres Continental or global area analysis outside CONUS.

UTM is a family of 60 six-degree zones; EPSG:32610 and EPSG:32618 are two examples. Pick the zone that contains your project’s centroid — measured distances stay within centimetres near the central meridian and degrade toward the zone edges, where the point scale factor at the meridian grows past the outer boundary. State Plane zones (such as EPSG:2277) are tuned tighter than UTM for a single state and are what surveyors and permitting authorities expect on stamped drawings, but note the US survey foot unit — carry it explicitly or a buffer distance will be silently wrong by a factor of .

Projection family versus task

Choose the family from the operation, not from what the data happened to arrive in. Reproject at the boundary of the analysis and reproject back only for delivery.

Task Use family Avoid Example EPSG
Distance, buffers, nearest-feature, routing Conformal local (UTM, State Plane) EPSG:4326, EPSG:3857 EPSG:32610, EPSG:32618, EPSG:2277
Area, acreage, land-cover, setback footprints Equal-area (Albers, EASE-Grid) EPSG:3857 above all EPSG:5070, EPSG:6933
Web map, tile serving, visualization Conformal global (Web Mercator) measuring anything on it EPSG:3857
Storage, interchange, join keys, GPS Geographic any measurement EPSG:4326

The one combination that ruins the most energy analyses is computing area in EPSG:3857. Web Mercator’s areal scale factor is , so a solar-array footprint or a habitat-loss polygon evaluated at is overstated by roughly 100% before any other error enters. Capacity (MW) and land-cost estimates derived from a Web Mercator area are not conservative or optimistic — they are meaningless. Move to EPSG:5070 for anything inside the lower 48, and to EPSG:6933 for wider extents.

pyproj and geopandas gotchas

The failures below rarely raise a clean exception at the point of the mistake; most surface as a plausible-but-wrong number several steps later.

Symptom Cause Fix
Transformed coordinates come out swapped (lat/lon) pyproj honours the authority axis order, and EPSG:4326 is defined as (lat, lon) Build the transformer with always_xy=True; geopandas already stores x=lon, y=lat
buffer(5000) returns a hemisphere-sized polygon Buffering in EPSG:43265000 is read as 5000 degrees to_crs() to a metric CRS before .buffer()
UserWarning: Geometry is in a geographic CRS. Results ... will be incorrect Called .area or .length on EPSG:4326 Reproject to equal-area (EPSG:5070) for area, or UTM for length, first
to_crs raises about “naive geometries” gdf.crs is None set_crs(epsg) to label the true source, then to_crs() to reproject
Features off by a metre or two after reprojection Datum shift skipped (NAD27/NAD83 → WGS 84 by relabelling) Let pyproj select a transformation grid; never overwrite the CRS label in place

The set_crs versus to_crs distinction is the most common data-corrupting mistake for newcomers: set_crs asserts what CRS the coordinates are already in (it moves no points), while to_crs reprojects the coordinates into a new CRS (the numbers change). Calling set_crs on data that needs reprojecting silently mislabels it; calling to_crs on data with a wrong or absent source label reprojects from the wrong origin. When in doubt about which UTM zone a mixed-extent layer belongs to, let geopandas derive it from the data with gdf.estimate_utm_crs() rather than hard-coding a zone.

Decision matrix: task → family → EPSG

Choosing a CRS: task drives the projection family, which fixes the EPSG code A three-column matrix. The left column lists four analysis tasks; the middle column the projection family each requires; the right column the concrete EPSG codes. Distance and routing lead to conformal local UTM/State Plane (EPSG 32610, 32618, 2277); area leads to equal-area Albers/EASE (EPSG 5070, 6933); visualization leads to Web Mercator (EPSG 3857); storage leads to geographic (EPSG 4326). TASK FAMILY EPSG Distance, buffers,routing Conformal localUTM / State Plane 32610 · 326182277 Area, acreage,land-cover Equal-areaAlbers / EASE-Grid 50706933 Web map,visualization Web Mercator 3857 Storage, join,interchange Geographic(angular degrees) 4326never measure on it

A CRS-selection and reproject helper

This helper encodes the decision matrix directly: pass a GeoDataFrame and the task you are about to perform, and it reprojects into the correct frame. For distance work it derives a datum-correct UTM zone from the data’s own extent rather than hard-coding one; for area and visualization it maps to fixed CONUS targets. It refuses to run on a layer whose CRS is undefined — the one condition guaranteed to produce a silent wrong answer.

python
import geopandas as gpd
import pyproj

# Fixed targets for CONUS-scale energy work (distance is derived per-layer)
TASK_CRS = {
    "area":    "EPSG:5070",   # NAD83 / CONUS Albers — equal-area, metres
    "webmap":  "EPSG:3857",   # Web Mercator — tiles/visualisation ONLY
    "storage": "EPSG:4326",   # WGS 84 — interchange / join key
}

def project_for_task(sites_gdf: gpd.GeoDataFrame, task: str) -> gpd.GeoDataFrame:
    """Reproject a layer into the CRS appropriate for `task`.

    task="distance" derives the UTM zone covering the layer's centroid so
    buffers and nearest-feature search run in near-true metres; every other
    task maps to a fixed target in TASK_CRS.
    """
    if sites_gdf.crs is None:
        raise ValueError(
            "Input CRS is undefined — call set_crs(<true source EPSG>) "
            "before reprojecting; to_crs() cannot transform naive geometries."
        )

    if task == "distance":
        target_crs = sites_gdf.estimate_utm_crs()   # e.g. EPSG:32610 in NorCal
    elif task in TASK_CRS:
        target_crs = pyproj.CRS.from_user_input(TASK_CRS[task])
    else:
        raise ValueError(
            f"Unknown task {task!r}; expected 'distance' or one of {list(TASK_CRS)}"
        )

    projected = sites_gdf.to_crs(target_crs)

    # Guard the classic mistake: measuring on a geographic frame
    if task in {"distance", "area"} and projected.crs.is_geographic:
        raise ValueError(f"{target_crs} is geographic; {task} requires a projected CRS")

    return projected


# Usage: buffer a substation layer in true metres
substation_gdf = gpd.read_file("substations.geojson")        # arrives as EPSG:4326
substation_gdf = project_for_task(substation_gdf, "distance") # -> UTM, metres
buffers_gdf = substation_gdf.assign(geometry=substation_gdf.buffer(5000))  # 5 km

Two details make this production-safe rather than merely correct. First, estimate_utm_crs() returns a single zone for the whole layer, which is right for a project-scale footprint but wrong for a layer spanning several UTM zones — for a national portfolio, partition by zone or switch to EPSG:5070/EPSG:6933 and accept the equal-area trade-off. Second, when you build a raw pyproj.Transformer outside geopandas — for example to reproject bare coordinate tuples — always pass always_xy=True so the transformer emits (lon, lat) regardless of the authority axis order, matching what geopandas and Shapely expect.

Guidance notes

  • Store in EPSG:4326, analyse in a projected frame, deliver in whatever the consumer needs. A permitting portal usually wants EPSG:4326 GeoJSON; a civil engineer wants the local State Plane zone; a tile server wants EPSG:3857. Keep the analytical CRS separate from the delivery CRS.
  • Tag every layer on ingestion. An undefined CRS is not a neutral default — it is a landmine that only detonates at the first to_crs. Assert the source EPSG with set_crs the moment data enters the pipeline.
  • Match units, not just codes. EPSG:2277 is in US survey feet; a 5000-unit buffer there is 1524 m, not 5 km. Read crs.axis_info[0].unit_name before trusting any distance literal.
  • Reproject before spatial indexing and distance search. The nearest-substation and buffer routines in proximity distance calculations assume a metric CRS; feeding them EPSG:4326 returns degrees, not metres.
  • Keep terrain and vector layers on the same frame. Hillshade, slope, and horizon-angle rasters in terrain shadow analysis pipelines must share the analysis CRS with the turbine and array vectors they mask, or the shading loss lands on the wrong pixels.