How to Read a CSV with Coordinates as a GeoDataFrame
On this page
Problem statement
A CSV with coordinate columns is a table, not spatial data: it has numbers, but nothing says which column is x, which coordinate system the numbers are in, or whether they are degrees at all. Turning it into a GeoDataFrame takes three decisions β which columns, in which order, and which CRS β and only the last one cannot be read off the file.
This page loads two real CSVs: the UK's airports from OurAirports, in longitude and latitude, and Greater Manchester's public-transport stops from NaPTAN, which carries every stop twice β in longitude and latitude and in British National Grid metres. That second file makes the CRS choice checkable, and shows what a missing grid file costs.
Quick answer
The shortest working way to read a CSV with coordinates into a GeoDataFrame is to read the CSV with pandas, create point geometry with GeoPandas, and assign the correct CRS.
import pandas as pd
import geopandas as gpd
df = pd.read_csv("points.csv")
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["longitude"], df["latitude"]),
crs="EPSG:4326"
)
print(gdf[["name", "iata_code", "geometry"]].head(3))
print(gdf.crs)
name iata_code geometry
0 RAF Castle Camps NaN POINT (0.37861 52.05139)
1 Ledbury Airfield NaN POINT (-2.47416 52.0019)
2 Milfield Glider Field NaN POINT (-2.08723 55.59164)
EPSG:4326
Longitude is x and latitude is y, and decimal degrees from a GPS or a web service are WGS 84, EPSG:4326. Easting and northing columns are a projected CRS, which the source has to tell you.
From CSV to spatial
Read, build points, set the CRS, verify
Step 1: Read the CSV into a pandas DataFrame
Start by loading the CSV with pandas and checking the coordinate columns.
import pandas as pd
df = pd.read_csv("points.csv")
print(df[["name", "longitude", "latitude"]].head(3))
print(list(df.columns)[:6])
name longitude latitude
0 RAF Castle Camps 0.378611 52.051389
1 Ledbury Airfield -2.474160 52.001900
2 Milfield Glider Field -2.087231 55.591636
['id', 'ident', 'type', 'name', 'latitude', 'longitude']
OurAirports stores latitude before longitude, the order people say them. Check for gaps before building points:
print(df[["longitude", "latitude"]].isna().sum().to_dict())
{'longitude': 0, 'latitude': 0}
This file is clean. A CSV with coordinates stored as text, decimal commas or blanks needs cleaning first β cleaning messy CSV coordinates works through a real one.
Step 2: Create point geometry from coordinate columns
Use geopandas.points_from_xy() to build point geometries.
import geopandas as gpd
geometry = gpd.points_from_xy(df["longitude"], df["latitude"])
x first: longitude, then latitude. Passing them the other way round puts the UK's airports in the Indian Ocean without an error (points plot in the ocean).
Step 3: Convert the DataFrame to a GeoDataFrame
Wrap the DataFrame in gpd.GeoDataFrame() and pass the geometry.
gdf = gpd.GeoDataFrame(df, geometry=geometry)
At this stage, you have a geometry column, but the data still needs a CRS to be spatially meaningful.
Step 4: Assign the correct CRS
If the CSV contains geographic coordinates in latitude and longitude, use EPSG:4326.
gdf = gdf.set_crs("EPSG:4326")
If the CSV uses projected coordinates such as easting/northing, assign the projected CRS instead β for British data usually British National Grid:
gdf = gdf.set_crs("EPSG:27700")
Do not guess the CRS. If you assign the wrong one, your points will appear in the wrong place and later analysis will be wrong.
Step 5: Verify the result
Check the first rows, geometry type, CRS, and bounds.
print(gdf.geom_type.unique().tolist())
print(gdf.crs)
print([round(float(v), 3) for v in gdf.total_bounds])
['Point']
EPSG:4326
[-8.572, 49.913, 1.723, 60.856]
Longitude β8.6 to 1.7 and latitude 49.9 to 60.9 is the United Kingdom, where these airports should be. You can also make a quick plot:
gdf.plot()
This is a simple way to confirm that the points were created correctly.
UK airports and projected coordinates from CSV
The data used on this page
Two real CSVs. OurAirports publishes every airport, airfield and heliport it knows as airports.csv (public domain), with latitude_deg and longitude_deg; the setup keeps those in the United Kingdom and renames the columns to latitude and longitude. The Department for Transport's NaPTAN register of public-transport stops, downloaded for Greater Manchester (Open Government Licence), gives every stop both as longitude/latitude and as British National Grid easting/northing β the setup writes it three ways for the examples.
from pathlib import Path
import pandas as pd
import requests
airports = pd.read_csv("https://davidmegginson.github.io/ourairports-data/airports.csv")
uk = airports[airports["iso_country"] == "GB"].rename(columns={"latitude_deg": "latitude", "longitude_deg": "longitude"})
uk.to_csv("points.csv", index=False)
uk[uk["type"].isin(["large_airport", "medium_airport"])].to_csv("store_locations.csv", index=False)
if not Path("naptan.csv").exists():
# NaPTAN area 180 as saved on 9 October 2026 (the register changes weekly); live:
# naptan.api.dft.gov.uk/v1/access-nodes?dataFormat=csv&atcoAreaCodes=180
Path("naptan.csv").write_bytes(requests.get("https://data.spatialworkflow.io/naptan-area-180-2026-10-09.csv", timeout=300).content)
stops = pd.read_csv("naptan.csv", low_memory=False)
stops = stops[stops["Status"] == "active"]
stops.rename(columns={"Longitude": "x_coord", "Latitude": "y_coord"}).to_csv("survey_points.csv", index=False)
stops.rename(columns={"Easting": "easting", "Northing": "northing", "Longitude": "longitude",
"Latitude": "latitude"}).to_csv("assets_bng.csv", index=False)
stops.rename(columns={"Longitude": "longitude", "Latitude": "latitude"}).to_csv("monitoring_sites.csv", index=False)
print(len(uk), "UK airports;", len(stops), "active stops in Greater Manchester")
1605 UK airports; 14045 active stops in Greater Manchester
Example 1: Read a latitude/longitude CSV into a GeoDataFrame
This is the standard case.
import pandas as pd
import geopandas as gpd
df = pd.read_csv("store_locations.csv")
df["longitude"] = pd.to_numeric(df["longitude"], errors="coerce")
df["latitude"] = pd.to_numeric(df["latitude"], errors="coerce")
df = df.dropna(subset=["longitude", "latitude"])
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["longitude"], df["latitude"]),
crs="EPSG:4326"
)
print(len(gdf), gdf.crs)
88 EPSG:4326
This creates point geometry from longitude and latitude and assigns WGS84.
Example 2: Read a CSV with custom x/y column names
Not every file uses standard names. This example uses x_coord and y_coord.
import pandas as pd
import geopandas as gpd
df = pd.read_csv("survey_points.csv")
df["x_coord"] = pd.to_numeric(df["x_coord"], errors="coerce")
df["y_coord"] = pd.to_numeric(df["y_coord"], errors="coerce")
df = df.dropna(subset=["x_coord", "y_coord"])
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["x_coord"], df["y_coord"]),
crs="EPSG:4326"
)
print(gdf[["CommonName", "geometry"]].head(2))
CommonName geometry
0 Altrincham Rail Station POINT (-2.34671 53.38801)
1 Altrincham Rail Station POINT (-2.3467 53.38789)
If these columns are longitude and latitude in decimal degrees, EPSG:4326 is appropriate. If they are projected coordinates, assign the projected CRS instead.
Example 3: Read a CSV with projected coordinates
This is common for engineering, utilities, or local government data. Do not treat projected x/y values as lat/lon.
import pandas as pd
import geopandas as gpd
df = pd.read_csv("assets_bng.csv")
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["easting"], df["northing"]),
crs="EPSG:27700"
)
print(gdf[["CommonName", "geometry"]].head(2))
print(gdf.crs)
CommonName geometry
0 Altrincham Rail Station POINT (377038 387962)
1 Altrincham Rail Station POINT (377039 387949)
EPSG:27700
This example uses British National Grid, because the stops are in Greater Manchester. Replace the CRS with the correct one for your data.
The NaPTAN file carries both pairs of columns, which makes a useful check on the choice β and on what PROJ can do without its grid files:
import numpy as np
from pyproj import Transformer
from pyproj.network import set_network_enabled
for grid in (False, True):
set_network_enabled(grid) # True lets PROJ fetch the OSTN15 grid
x, y = Transformer.from_crs(4326, 27700, always_xy=True).transform(df["longitude"], df["latitude"])
gap = np.hypot(x - df["easting"], y - df["northing"])
print(f"OSTN15 {'on ' if grid else 'off'}: easting/northing and longitude/latitude differ by "
f"median {np.median(gap):.2f} m, max {gap.max():.2f} m")
set_network_enabled(False)
OSTN15 off: easting/northing and longitude/latitude differ by median 1.71 m, max 2.46 m
OSTN15 on : easting/northing and longitude/latitude differ by median 0.31 m, max 0.91 m
With the grid the two columns agree to within the rounding of the file (whole metres against six decimal places of a degree). Without it, PROJ falls back to a seven-parameter transformation and the same stops land 1.7 m apart β harmless on a map, not harmless for an asset register. The transformer is built inside the loop on purpose: GeoPandas reuses the one it built first, so toggling the network between two to_crs calls in one session changes nothing.
Example 4: Reproject the GeoDataFrame after loading
After loading the CSV, you may need another CRS for web maps or spatial analysis.
import pandas as pd
import geopandas as gpd
df = pd.read_csv("monitoring_sites.csv")
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["longitude"], df["latitude"]),
crs="EPSG:4326"
)
gdf_web = gdf.to_crs("EPSG:3857")
gdf_bng = gdf.to_crs("EPSG:27700")
print(gdf_web.crs, [round(float(v)) for v in gdf_web.total_bounds])
print(gdf_bng.crs, [round(float(v)) for v in gdf_bng.total_bounds])
EPSG:3857 [-302530, 7045776, -219805, 7107830]
EPSG:27700 [352522, 382362, 401783, 419239]
Use .to_crs() only after the original CRS has been assigned correctly.
The CRS is a fact about the source, not the file
A CSV carries no metadata, so the coordinate system is the one piece of a GeoDataFrame you have to supply from outside the file. The numbers narrow it down β values within Β±180 and Β±90 are probably degrees, six-figure positives in Britain probably the national grid β but they cannot decide it, and a wrong guess gives no error, only points in the wrong place.
NaPTAN shows the better way when it is available. Every stop has both pairs of columns, so building points from each with its own CRS and comparing them tests the choice: with PROJ's transformation grid the two agree to the rounding of the file, and without it they disagree by 1.7 m, because PROJ then falls back to a seven-parameter approximation of the shift between the two datums. A second source of coordinates for even a few rows is the cheapest CRS check there is.
Projected columns, delimiters and encodings
- Easting and northing are not degrees. Assign the projected CRS the source documents;
EPSG:4326on metres gives no error and puts the points thousands of kilometres away. - Semicolons and Latin-1 are common in CSVs from European locales and older Excel exports:
pd.read_csv(path, sep=";", encoding="latin1"). - Text, blank and decimal-comma coordinates need cleaning before
points_from_xy; see cleaning messy CSV coordinates.
Internal links
If you need a refresher on CRS concepts, see What Is a Coordinate Reference System (CRS) in GeoPandas?.
For the next step after loading data, see How to Reproject Spatial Data in Python (GeoPandas).
To save your points for use in other tools, see How to Export GeoJSON in Python with GeoPandas.
If your points are not showing where you expect, check Why Are My Points Showing in the Wrong Location in GeoPandas?.
FAQ
How do I convert a CSV with latitude and longitude to a GeoDataFrame in Python?
Read the CSV with pandas.read_csv(), create point geometry with gpd.points_from_xy(), and wrap the result in gpd.GeoDataFrame() with the correct CRS.
What CRS should I use for a CSV with lat/lon columns?
In most cases, use EPSG:4326 if the coordinates are longitude and latitude in decimal degrees in WGS84.
Why are my CSV points appearing in the wrong place on the map?
The most common causes are:
- latitude and longitude columns were swapped
- the wrong CRS was assigned
- projected coordinates were incorrectly treated as WGS84
- coordinate values were malformed or read as text
How do I read a CSV with coordinates directly using gpd.read_file?
GeoPandas can read a CSV through GDAL's CSV driver, but you must supply the coordinate columns and CRS:
direct = gpd.read_file("points.csv", x_possible_names="longitude", y_possible_names="latitude")
print(direct.crs, direct.geometry.iloc[0])
direct = direct.set_crs("EPSG:4326")
None POINT (0.378611 52.051389)
It reads the geometry and leaves the CRS as None, so the set_crs is not optional. The pandas plus points_from_xy approach is usually clearer and gives more control over cleaning.
What happens to rows with empty coordinate cells when building geometry?
points_from_xy produces a point with NaN coordinates rather than raising an error β and it is not an empty point, so the usual checks miss it:
odd = gpd.GeoSeries(gpd.points_from_xy([float("nan")], [float("nan")]))
print(odd.iloc[0], "| empty:", odd.is_empty.iloc[0], "| missing:", odd.isna().iloc[0], "| valid:", odd.is_valid.iloc[0])
POINT (NaN NaN) | empty: False | missing: False | valid: False
Only is_valid notices. Drop the rows first with df.dropna(subset=["longitude", "latitude"]) so spatial operations and plots behave.
My CSV has a single column like "40.7,-74.0" β how do I split it into coordinates?
Split the combined column before creating geometry, for example df[["lat", "lon"]] = df["coords"].str.split(",", expand=True), then convert both to numeric with pd.to_numeric. After that, pass longitude as x and latitude as y to points_from_xy.
How do I save the resulting GeoDataFrame back out, for example to a GeoPackage?
Once you have a GeoDataFrame with geometry and a CRS, write it with to_file, for example gdf.to_file("points.gpkg", driver="GPKG"). For GeoJSON instead, reproject to EPSG:4326 first and use to_file(..., driver="GeoJSON").