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:
- Check the current CRS with
.crs - Reproject to a suitable projected CRS with
.to_crs() - Use:
.areafor polygon area.lengthfor 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
Check the CRS, project, then measure
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 GridEPSG: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.
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:
.areareturns area in CRS units squared.lengthreturns 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.
Internal links
- How to Read a Shapefile in Python with GeoPandas
- How to Export GeoJSON in Python with GeoPandas
- How to Reproject Spatial Data in Python (GeoPandas)
- How to draw a species range from occurrence points — extent of occurrence and area of occupancy
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.