How to Calculate Area and Distance in GeoPandas (Correctly)

On this page

Problem statement

A common GIS mistake is to calculate polygon area or feature distance directly from data in a geographic CRS such as EPSG:4326.

This usually produces wrong results because geographic coordinates are stored in degrees, not meters. GeoPandas will still return numeric values from .area, .length, or .distance(), but those values are based on the coordinate units of the geometry. If your data is in latitude/longitude, the numbers are not reliable for real-world measurement.

In practice, users are usually trying to do one of these tasks:

  • calculate polygon area
  • calculate line length
  • calculate distance between points or other features

The practical fix is to reproject the data to a suitable projected CRS first, then run the measurement.

Quick answer

To calculate area in GeoPandas or measure distance correctly:

  1. Check the current CRS with .crs
  2. Reproject to a suitable projected CRS with .to_crs()
  3. Use:
    • .area for polygon area
    • .length for line length
    • .distance() for distances between geometries
import geopandas as gpd

gdf = gpd.read_file("data/districts.gpkg")

# Reproject to a projected CRS with suitable linear units
gdf_proj = gdf.to_crs("EPSG:32633")

# Polygon area in square meters if the CRS uses meters
gdf_proj["area_m2"] = gdf_proj.area
print(f"Vienna: {gdf_proj['area_m2'].sum() / 1e6:.3f} km² in UTM 33N; "
      f"{gdf.area.sum():.4f} 'square degrees' as stored")
Vienna: 414.642 km² in UTM 33N; 0.0502 'square degrees' as stored

The output units come from the CRS units. In many projected CRSs those units are meters, but some projected CRSs use other units such as feet.

Reproject before you measure

Reproject before you measureIs .crs geographic (degrees)?to_crs() to a projected CRS,then measureYesNoMeasure with .area / .length / .distance()spatialworkflow.io
Reproject to a projected CRS before measuring area or distance.

Check the CRS, project, then measure

The same measurements taken in a geographic and a projected CRS. Vienna, measured twiceEPSG:4326 (degrees)EPSG:32633 (metres).area, 23 districts✕0.0502 deg²✓414.64 km².length, U1✕0.2033 deg✓19.13 kmhospital to AKH✕0.0523 deg✓4,354 mtrustworthy?✕never✓yesDegrees are angles — they do not convert to metres by a constant spatialworkflow.io
The left column is not wrong by a factor you can correct — it is a different quantity.

Check the current CRS

Start by checking whether your layer is in a geographic CRS like EPSG:4326.

import geopandas as gpd

gdf = gpd.read_file("data/districts.gpkg")
print(gdf.crs, "|", gdf.crs.axis_info[0].unit_name)
EPSG:4326 | degree

If you see something like EPSG:4326, your coordinates are in degrees. That is fine for storage and web mapping, but not for accurate area and distance measurements.

Choose a suitable projected CRS

You need a projected CRS that fits your study area and task.

Common practical choices:

  • Local UTM zone for city or regional data
  • National projected CRS for country-specific workflows
  • Equal-area CRS when area accuracy is the main priority

Examples:

  • EPSG:32633 — UTM zone 33N (only appropriate for data in that zone)
  • EPSG:27700 — British National Grid
  • EPSG:5070 — NAD83 / Conus Albers for US-wide area analysis

For small to medium study areas, a local UTM CRS is often a good default. For large regions, CRS choice matters more because one projected CRS may not preserve both area and distance equally well everywhere. How much it matters for one city, measured against the area Vienna's own data records for each district:

from pyproj import Geod

official = gdf["FLAECHE"].sum() / 1e6
geod = Geod(ellps="WGS84")
geodesic = sum(abs(geod.geometry_area_perimeter(g)[0]) for g in gdf.geometry) / 1e6
print(f"{'official FLAECHE':24} {official:9.3f} km²")
print(f"{'geodesic (ellipsoid)':24} {geodesic:9.3f} km²  {100 * (geodesic / official - 1):+.3f}%")
for crs, label in [("EPSG:31256", "MGI / Austria GK East"), ("EPSG:3035", "ETRS89 LAEA Europe"),
                   ("EPSG:32633", "UTM zone 33N"), ("EPSG:3857", "Web Mercator")]:
    km2 = gdf.to_crs(crs).area.sum() / 1e6
    print(f"{label:24} {km2:9.3f} km²  {100 * (km2 / official - 1):+.3f}%")
official FLAECHE           414.871 km²
geodesic (ellipsoid)       414.867 km²  -0.001%
MGI / Austria GK East      414.871 km²  +0.000%
ETRS89 LAEA Europe         414.867 km²  -0.001%
UTM zone 33N               414.642 km²  -0.055%
Web Mercator               933.445 km²  +124.996%

The national grid and the equal-area projection agree with the official figure to within a few thousandths of a square kilometre. UTM 33N is 0.055% low: Vienna lies 1.4° east of the zone's central meridian, and UTM shrinks scale near its centre by design. Web Mercator, the projection of every web basemap, more than doubles the city — at 48°N it inflates areas by roughly 1/cos²(latitude), about 2.25 times, and that is not a factor you round away.

Reproject the GeoDataFrame

Use .to_crs() to create a projected copy.

gdf_proj = gdf.to_crs("EPSG:32633")

This keeps the original gdf unchanged and gives you a new GeoDataFrame for measurement.

Calculate polygon area

After reprojection, use .area.

gdf_proj["area_m2"] = gdf_proj.area

If the CRS uses meters, the result is square meters.

You can also convert to hectares or square kilometers:

gdf_proj["area_ha"] = gdf_proj["area_m2"] / 10_000
gdf_proj["area_km2"] = gdf_proj["area_m2"] / 1_000_000

Calculate line length

For roads, rivers, or utility lines, use .length after reprojection.

lines = gpd.read_file("data/ubahn.gpkg")
lines_proj = lines.to_crs("EPSG:32633")

lines_proj["length_m"] = lines_proj.length
lines_proj["length_km"] = lines_proj["length_m"] / 1000
print(lines_proj.groupby("LINFO")["length_km"].sum().round(2).rename(lambda n: f"U{n}").to_dict())
print("the same lines in Web Mercator:",
      (lines.to_crs(3857).length.groupby(lines["LINFO"]).sum() / 1000).round(2).rename(lambda n: f"U{n}").to_dict())
{'U1': 19.13, 'U2': 16.87, 'U3': 13.41, 'U4': 16.35, 'U6': 17.34}
the same lines in Web Mercator: {'U1': 28.72, 'U2': 25.31, 'U3': 20.11, 'U4': 24.52, 'U6': 26.02}

Lengths inflate by the same latitude factor as areas, but only once: about 1.5 times at Vienna's latitude.

Calculate distance between two geometries

Use .distance() when comparing geometries in the same projected CRS.

points = gpd.read_file("data/hospitals.gpkg").to_crs("EPSG:32633")

distance_m = points.geometry.iloc[0].distance(points.geometry.iloc[1])
short = points["BEZEICHNUNG"].str.split(r" \(|,").str[0]
print(f"{short.iloc[0]} → {short.iloc[1]}: {distance_m:,.0f} m")
Klinik Landstraße → Universitätsklinikum AKH Wien: 4,354 m

For two layers, both must use the same CRS:

schools = gpd.read_file("data/schools.gpkg").to_crs("EPSG:32633")
hospitals = gpd.read_file("data/hospitals.gpkg").to_crs("EPSG:32633")

schools["dist_to_first_hospital_m"] = schools.geometry.distance(
    hospitals.geometry.iloc[0]
)
print(schools["dist_to_first_hospital_m"].describe().round(0).to_dict())
{'count': 806.0, 'mean': 5441.0, 'std': 2699.0, 'min': 23.0, '25%': 3430.0, '50%': 5207.0, '75%': 7237.0, 'max': 12822.0}

Vienna's districts measured against the city's own areas

The data used on this page

Vienna's open government data (CC BY 4.0, Stadt Wien – data.wien.gv.at), from the city's WFS: the 23 district boundaries, which carry the city's own area for each district in FLAECHE; the U-Bahn line geometry; schools; hospitals; and Wiener Linien public-transport stops. Vienna lies at 16.4°E, inside UTM zone 33N — which is what makes EPSG:32633 in the examples a sensible choice rather than a copied one.

from pathlib import Path

import geopandas as gpd

WFS = ("https://data.wien.gv.at/daten/geo?service=WFS&request=GetFeature&version=1.1.0"
       "&srsName=EPSG:4326&outputFormat=json&typeName=ogdwien:")
Path("data").mkdir(exist_ok=True)
for name, layer in {"districts": "BEZIRKSGRENZEOGD", "ubahn": "UBAHNOGD", "schools": "SCHULEOGD",
                    "hospitals": "KRANKENHAUSOGD", "stops": "HALTESTELLEWLOGD"}.items():
    if not Path(f"data/{name}.gpkg").exists():
        gpd.read_file(WFS + layer).to_file(f"data/{name}.gpkg")
    print(f"{name:10}", len(gpd.read_file(f"data/{name}.gpkg")))
districts  23
ubahn      98
schools    806
hospitals  28
stops      1807

Example 1: Calculate polygon area in square meters

import geopandas as gpd

districts = gpd.read_file("data/districts.gpkg")
print("Original CRS:", districts.crs)

# Reproject to a projected CRS with meter units
districts_proj = districts.to_crs("EPSG:32633")

# Calculate area
districts_proj["area_m2"] = districts_proj.area

print(districts_proj[["NAMEK", "area_m2", "FLAECHE"]].head().round(0))

# Optional: save result
from pathlib import Path
Path("output").mkdir(exist_ok=True)
districts_proj.to_file("output/districts_with_area.gpkg")
Original CRS: EPSG:4326
          NAMEK     area_m2     FLAECHE
0  Innere Stadt   2867180.0   2868772.0
1     Simmering  23243928.0  23256174.0
2        Neubau   1607341.0   1608246.0
3     Ottakring   8668053.0   8673084.0
4   Brigittenau   5707320.0   5710474.0

Example 2: Convert area to hectares and square kilometers

districts_proj["area_ha"] = districts_proj["area_m2"] / 10_000
districts_proj["area_km2"] = districts_proj["area_m2"] / 1_000_000

print(districts_proj[["NAMEK", "area_m2", "area_ha", "area_km2"]].head().round(2))
          NAMEK      area_m2  area_ha  area_km2
0  Innere Stadt   2867180.42   286.72      2.87
1     Simmering  23243928.15  2324.39     23.24
2        Neubau   1607340.92   160.73      1.61
3     Ottakring   8668052.85   866.81      8.67
4   Brigittenau   5707320.09   570.73      5.71

Example 3: Calculate line length in meters

import geopandas as gpd

roads = gpd.read_file("data/ubahn.gpkg")
roads_proj = roads.to_crs("EPSG:32633")

roads_proj["length_m"] = roads_proj.length

print(roads_proj[["LINFO", "length_m"]].head().round(0))
   LINFO  length_m
0      2    1137.0
1      2     661.0
2      2     855.0
3      2     898.0
4      2     347.0

Example 4: Calculate distance between points

import geopandas as gpd

origins = gpd.read_file("data/schools.gpkg").to_crs("EPSG:32633")
destinations = gpd.read_file("data/hospitals.gpkg").to_crs("EPSG:32633")

origins["distance_to_first_dest_m"] = origins.geometry.distance(
    destinations.geometry.iloc[0]
)

print(destinations["BEZEICHNUNG"].iloc[0].split(" (")[0])
print(origins[["NAME", "distance_to_first_dest_m"]].head().round(0).to_string(index=False, max_colwidth=36))
Klinik Landstraße
                                NAME  distance_to_first_dest_m
Friedrich Gulda School of Music W...                    2460.0
Johann Sebastian Bach Musikschule...                    1817.0
                 VS Bischoffgasse 10                    5370.0
                 VS Bischoffgasse 10                    5085.0
                  MS Herthergasse 28                    4020.0

Example 5: Calculate nearest-feature distance

A practical pattern is to find the nearest feature in another layer, then measure the distance.

import geopandas as gpd

buildings = gpd.read_file("data/schools.gpkg").to_crs("EPSG:32633")
bus_stops = gpd.read_file("data/stops.gpkg").to_crs("EPSG:32633")

nearest = gpd.sjoin_nearest(
    buildings,
    bus_stops,
    how="left",
    distance_col="nearest_dist_m"
)

print(nearest[["NAME", "BEZEICHNUNG", "nearest_dist_m"]].head().round(0).to_string(index=False, max_colwidth=30))
print(f"median {nearest['nearest_dist_m'].median():.0f} m, max {nearest['nearest_dist_m'].max():.0f} m, "
      f"{len(nearest) - len(buildings)} extra rows from ties")
                          NAME                   BEZEICHNUNG  nearest_dist_m
Friedrich Gulda School of M...                     Gasometer           126.0
Johann Sebastian Bach Musik...                 Mayerhofgasse           106.0
           VS Bischoffgasse 10                 Bischoffgasse           114.0
           VS Bischoffgasse 10                    Rosasgasse           108.0
            MS Herthergasse 28 Wolfganggasse/Steinbauergasse           152.0
median 142 m, max 689 m, 0 extra rows from ties

Note: gpd.sjoin_nearest() requires a recent GeoPandas version and a working spatial index backend in your environment.

Geographic against projected coordinates

The key issue is the difference between a geographic CRS and a projected CRS.

Choosing a projected CRS to measure in, based on the extent of the data. which projected CRS should you measure in?How far does your data extend?one countryNational grid27700, 25832, 2154one regionUTM zoneestimate_utm_crs()a continentEqual-area CRS3035 for Europethe worldUse a geodesicGeod, not to_crs()estimate_utm_crs() picks a sensible metric CRS for you in one call spatialworkflow.io
For truly global data, no single projection works — measure on the ellipsoid instead.

A geographic CRS stores coordinates on the earth using angular units such as latitude and longitude. EPSG:4326 is the most common example. In this CRS, coordinates are in degrees, so geometry calculations based on raw coordinate values do not represent meters or square meters.

A projected CRS transforms the curved earth surface into a flat coordinate system. Its units are often meters or feet. That makes it suitable for planar measurements such as area, line length, and distance between features.

GeoPandas does not automatically decide whether your CRS is appropriate for measurement. It simply uses the geometry coordinates as they are:

  • .area returns area in CRS units squared
  • .length returns length in CRS units
  • .distance() returns distance in CRS units

So if your data is in degrees, your output is also degree-based, which is usually not what you want.

For accurate GeoPandas area calculation and distance measurement, reproject first, then measure.

EPSG:4326 warnings, missing CRS and large study areas

Data in EPSG:4326 gives misleading results

You can run .area or .distance() on data in EPSG:4326, but the results are not suitable for real-world measurement. GeoPandas does warn — UserWarning: Geometry is in a geographic CRS. Results from 'area' are likely incorrect. — but a warning is easy to lose in a notebook or a log, and the number it returns (0.0502 for the whole of Vienna) looks like any other number.

Missing CRS metadata

If gdf.crs is None, do not use .to_crs() yet. First assign the correct existing CRS with .set_crs(), then reproject if needed.

gdf = gdf.set_crs("EPSG:4326")
gdf_proj = gdf.to_crs("EPSG:32633")

Both layers must use the same CRS for distance calculations

If you calculate distance between two layers, align both to the same projected CRS first.

layer1 = layer1.to_crs("EPSG:32633")
layer2 = layer2.to_crs("EPSG:32633")

Multipolygon and multiline geometries

Multipart geometries still work with .area and .length. The result is the total area or total length of the full geometry.

Large study areas need careful CRS selection

A single local projected CRS may work well for one city or region but perform poorly across a continent. If you are measuring over large areas, choose a CRS designed for that extent and purpose. For some large-extent workflows, a geodesic approach may be more appropriate than using one projected CRS for everything.

Geometry validity can affect results

Invalid polygons can produce unreliable area values. If results look wrong, check geometry validity.

invalid = ~gdf.geometry.is_valid
print(f"{invalid.sum()} invalid of {len(gdf)} districts")
0 invalid of 23 districts

If needed, repair invalid geometries before measurement.

FAQ

How do I calculate area in GeoPandas in square meters?

Reproject your polygon layer to a projected CRS that uses meters, then use .area.

gdf_proj = gdf.to_crs("EPSG:32633")
gdf_proj["area_m2"] = gdf_proj.area

Why is GeoPandas giving incorrect area or distance values?

The most common reason is that your data is in a geographic CRS such as EPSG:4326. In that case, coordinates are in degrees, so area and distance calculations are not in meters.

What CRS should I use to calculate distance in GeoPandas?

Use a projected CRS appropriate for your study area. For regional work, a local UTM zone is often a practical choice. For country-scale or area-focused analysis, a national or equal-area CRS may be better.

Can I calculate area directly in EPSG:4326?

You can, but the result will not be a reliable real-world area measurement. For correct values, reproject first.

How do I confirm what linear units my projected CRS uses?

Check the axis units on the CRS object with gdf.crs.axis_info[0].unit_name. Most projected CRSs such as UTM zones (EPSG:32633) and EPSG:5070 report metre, but some national grids use feet, in which case .area and .length return feet-based values.

Why do my UTM areas drift from the true area at the edge of my dataset?

Each UTM zone is only accurate within its own narrow band, and distortion grows toward the zone edges and away from the central meridian. If your data spans more than one zone or a wide region, switch to an equal-area CRS such as EPSG:5070 for area, so the measurement stays consistent across the whole extent.

How do I compute pairwise distances between every feature in one layer and another?

Reproject both layers to the same projected CRS, then use gpd.sjoin_nearest() with distance_col= to get each feature's nearest neighbour and its distance in one step. For an explicit all-pairs matrix you would loop or use a spatial index, but nearest-distance covers most real tasks.

Is .area the planar area or the surface area following terrain?

It is the planar (2D) area in the CRS coordinate plane; GeoPandas does not account for elevation or terrain slope. The value reflects the flat footprint of the polygon, which is what most GIS area calculations expect.

Why is my distance zero when I expected a positive value?

.distance() returns 0 when the two geometries touch or overlap, because that is the minimum separating distance. If you expected a centre-to-centre or boundary distance instead, measure between .centroid points or .boundary geometries explicitly.