Why Does My GeoPandas Map Look Upside Down?
On this page
Problem statement
GeoDataFrame.plot() never flips anything. It draws each coordinate pair with x to the right and y upward, exactly as stored, so when a map looks mirrored, on its side, in the wrong hemisphere or shrunk to a speck, the coordinates β or what they are declared to mean β are wrong. Three different faults produce those pictures: latitude and longitude built in the wrong order, a layer in degrees drawn on the same axes as one in metres, and, rarely, a coordinate system whose axes really do point west and south.
This page reproduces all three with real data β the UK's airports from OurAirports, the UK's local authority districts in British National Grid, Natural Earth's country outlines, and two South African cities β and shows how to tell them apart from the numbers.
Quick answer
Print the bounds next to the CRS; the numbers say which fault it is before any map does:
import geopandas as gpd
import pandas as pd
airports = pd.read_csv("data/uk_airports.csv")
wrong = gpd.GeoDataFrame(airports, geometry=gpd.points_from_xy(airports["latitude_deg"], airports["longitude_deg"]), crs=4326)
right = gpd.GeoDataFrame(airports, geometry=gpd.points_from_xy(airports["longitude_deg"], airports["latitude_deg"]), crs=4326)
print("swapped bounds:", wrong.total_bounds.round(1))
print("correct bounds:", right.total_bounds.round(1))
swapped bounds: [50.1 -7.7 59.9 1.3]
correct bounds: [-7.7 50.1 1.3 59.9]
A latitude of 50β60Β° used as x puts the UK's airports in the western Indian Ocean, around the Seychelles, and the whole pattern is reflected across the diagonal, which is what reads as "mirrored" or "on its side".
Diagnosis at a glance
Swapped axes, the CRS, bounds, a reference layer
Step 1: Check whether x and y were reversed
GeoPandas builds points as (x, y): longitude then latitude, or easting then northing. OurAirports stores latitude_deg first, the order people say coordinates aloud:
print(airports[["name", "latitude_deg", "longitude_deg"]].head(3))
name latitude_deg longitude_deg
0 Belfast International Airport 54.657501 -6.215830
1 Enniskillen/St Angelo Airport 54.398274 -7.651179
2 George Best Belfast City Airport 54.618099 -5.872500
Latitude is always within Β±90; longitude can reach Β±180. For the UK's airports, latitude runs from 50.1 to 59.9 and longitude from -7.7 to 1.3, so the columns cannot be confused once you look. Build geometry with longitude as x and latitude as y.
To see where the swapped points actually went, join them to the world's countries:
world = gpd.read_file("data/ne_50m_admin_0_countries.zip")[["ADMIN", "geometry"]]
landed = gpd.sjoin(wrong, world, how="left", predicate="within")
print(landed["ADMIN"].fillna("(sea)").value_counts())
ADMIN
(sea) 88
Name: count, dtype: int64
All 88 land in the sea. A map of the swapped layer drawn on its own would show a tidy cluster of points with plausible-looking axes β the only sign of trouble is that the numbers on the x axis are latitudes.
Step 2: Verify the current CRS
Always check the CRS before diagnosing plotting problems.
districts = gpd.read_file("data/uk_districts.gpkg")
for name, layer in [("airports", right), ("districts", districts), ("world", world)]:
print(f"{name:10} {layer.crs} bounds {layer.total_bounds.round(1)}")
airports EPSG:4326 bounds [-7.7 50.1 1.3 59.9]
districts EPSG:27700 bounds [-1.1620000e+02 7.0541000e+03 6.5565380e+05 1.2203099e+06]
world EPSG:4326 bounds [-180. -90. 180. 83.6]
Step 3: Set the CRS correctly if it is missing
A shapefile's CRS lives in its .prj file. Copy the other sidecars without it and the coordinates are unchanged, but the layer has no CRS:
import shutil
districts.to_file("output/districts.shp")
for ext in ("shp", "shx", "dbf"):
shutil.copy(f"output/districts.{ext}", f"output/no_prj.{ext}")
orphan = gpd.read_file("output/no_prj.shp")
print("CRS:", orphan.crs, "| bounds:", orphan.total_bounds.round(0))
orphan = orphan.set_crs("EPSG:27700") # we know where it came from
print("CRS:", orphan.crs)
CRS: None | bounds: [-1.16000e+02 7.05400e+03 6.55654e+05 1.22031e+06]
CRS: EPSG:27700
Bounds in the hundreds of thousands, all positive, with y up to about 1.2 million, are the British National Grid's signature β but set the CRS from what you know about the source, not from a guess about the numbers.
Step 4: Reproject the data if the CRS is correct but does not match other layers
If one layer is in EPSG:4326 and another in EPSG:27700, plotting them on the same axes puts the degree layer in a tiny square near the grid's origin, south-west of the Isles of Scilly:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
ax = districts.plot(color="lightgray", figsize=(6, 8))
right.plot(ax=ax, color="red", markersize=8) # still in degrees
print("x axis:", [round(v) for v in ax.get_xlim()], "| airports span x", right.total_bounds[[0, 2]].round(1))
fixed = right.to_crs(districts.crs)
ax = districts.plot(color="lightgray", figsize=(6, 8))
fixed.plot(ax=ax, color="red", markersize=8)
ax.figure.savefig("output/airports_on_districts.png", dpi=80)
print("reprojected airports span x", fixed.total_bounds[[0, 2]].round(0))
plt.close("all")
x axis: [-32905, 688442] | airports span x [-7.7 1.3]
reprojected airports span x [ 33392. 622027.]
On the first plot the airports are all inside a box 9 units wide at the origin of an axis 700,000 units wide β one red speck. to_crs() is the fix when layers do not align because they use different coordinate systems.
Step 5: Inspect bounds to spot impossible coordinate ranges
Bounds often reveal whether coordinates are in the wrong order or the wrong CRS. gdf.total_bounds returns [minx, miny, maxx, maxy], and the layers above give a reference set:
[-7.7, 50.1, 1.3, 59.9]β the airports in degrees: plausible UK longitude/latitude[50.1, -7.7, 59.9, 1.3]β the same airports swapped: y between -8 and 1 is not the UK's latitude[-116, 7054, 655654, 1220310]β British National Grid metres; the small negative x is the western edge of Fermanagh and Omagh, in Northern Ireland, which lies just west of the grid's false origin
If the coordinate ranges do not match the declared CRS, fix that before plotting. A declared CRS that cannot be true usually shows up as soon as you reproject:
mislabelled = districts.set_crs("EPSG:4326", allow_override=True) # metres labelled as degrees
print(mislabelled.to_crs("EPSG:3857").total_bounds)
[inf inf inf inf]
Latitudes of up to 1.2 million degrees cannot be projected, so the result is inf. When a layer "vanishes" after to_crs(), check this first.
Step 6: Compare with a known reference layer
A fast way to confirm whether the issue is location, CRS, or source coordinates is to compare your layer with a trusted boundary:
uk = world[world["ADMIN"] == "United Kingdom"]
for label, layer in [("correct", right), ("swapped", wrong)]:
inside = layer.within(uk.union_all()).sum()
print(f"{label}: {inside} of {len(layer)} airports inside the UK outline")
correct: 81 of 88 airports inside the UK outline
swapped: 0 of 88 airports inside the UK outline
The seven correctly placed airports outside the outline are on the coast or on islands that Natural Earth's 1:50 million outline simplifies away: Blackpool is 7 m outside it, Tiree 15 km. If most points still land in the wrong place after this check, the problem is bad coordinate values or a wrong CRS, not the display.
Step 7: When the map really is upside down
There is one case where the plot is genuinely rotated: a projected CRS whose axes point west and south. South Africa's Hartebeesthoek94 Lo systems (such as EPSG:2053, Lo29) are defined that way β "westing" and "southing" β and so is the classic Czech and Slovak Krovak, EPSG:2065, whose first axis is southing and second westing. GeoPandas still plots x to the right and y upward, so the country comes out rotated 180Β° or on its side:
south_africa = world[world["ADMIN"] == "South Africa"]
for code in ["EPSG:2053", "EPSG:32735"]:
crs = gpd.GeoSeries(crs=code).crs
print(code, crs.name, [axis.direction for axis in crs.axis_info])
cities = gpd.GeoSeries.from_xy([18.42, 28.05], [-33.92, -26.20], crs=4326) # Cape Town, Johannesburg
lo29 = cities.to_crs("EPSG:2053")
print("Lo29 x (westing):", lo29.x.round(0).tolist(), "| y (southing):", lo29.y.round(0).tolist())
EPSG:2053 Hartebeesthoek94 / Lo29 ['west', 'south']
EPSG:32735 WGS 84 / UTM zone 35S ['east', 'north']
Lo29 x (westing): [980448.0, 94953.0] | y (southing): [3805650.0, 2899340.0]
Cape Town, the western city, gets the larger x and plots to the right; it is further south, so it gets the larger y and plots above Johannesburg. For display, reproject to an east/north CRS β here UTM zone 35S, EPSG:32735 β or keep the data and flip the axes with ax.invert_xaxis() and ax.invert_yaxis(). For Czech data, EPSG:5514 (S-JTSK / Krovak East North) is the east/north version of Krovak; its coordinates are all negative, which is itself a common surprise.
UK airports rebuilt in longitude, latitude order
The data used on this page
Three real layers, downloaded by the block below:
- UK airports β the large and medium airports in the United Kingdom from OurAirports' public-domain
airports.csv, which storeslatitude_degbeforelongitude_deg. The file is updated continually, so the count may change. - UK districts β the 361 local authority districts (December 2024, ultra-generalised) from the ONS boundary service, requested in British National Grid (
EPSG:27700). - Countries β Natural Earth's 1:50 million country outlines, in WGS 84.
import io
import os
import geopandas as gpd
import pandas as pd
import requests
os.makedirs("data", exist_ok=True)
os.makedirs("output", exist_ok=True)
if not os.path.exists("data/uk_airports.csv"):
everything = pd.read_csv("https://davidmegginson.github.io/ourairports-data/airports.csv")
uk = everything[(everything["iso_country"] == "GB") & everything["type"].isin(["large_airport", "medium_airport"])]
uk[["ident", "name", "type", "latitude_deg", "longitude_deg"]].to_csv("data/uk_airports.csv", index=False)
if not os.path.exists("data/uk_districts.gpkg"):
reply = requests.get("https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/"
"Local_Authority_Districts_December_2024_Boundaries_UK_BUC/FeatureServer/0/query",
params={"where": "1=1", "outFields": "LAD24CD,LAD24NM", "outSR": 27700, "f": "geojson"},
timeout=120)
reply.raise_for_status()
gpd.read_file(io.BytesIO(reply.content)).to_file("data/uk_districts.gpkg")
if not os.path.exists("data/ne_50m_admin_0_countries.zip"):
reply = requests.get("https://naciscdn.org/naturalearth/50m/cultural/ne_50m_admin_0_countries.zip", timeout=120,
headers={"User-Agent": "spatialworkflow.io guide"})
reply.raise_for_status()
open("data/ne_50m_admin_0_countries.zip", "wb").write(reply.content)
print(len(pd.read_csv("data/uk_airports.csv")), "airports,", len(gpd.read_file("data/uk_districts.gpkg")), "districts")
88 airports, 361 districts
Example 1: Print CRS and bounds before plotting
import geopandas as gpd
districts = gpd.read_file("data/uk_districts.gpkg")
print("CRS:", districts.crs)
print("Bounds:", districts.total_bounds.round(0))
print("Units:", districts.crs.axis_info[0].unit_name, "| axes:", [a.direction for a in districts.crs.axis_info])
CRS: EPSG:27700
Bounds: [-1.16000e+02 7.05400e+03 6.55654e+05 1.22031e+06]
Units: metre | axes: ['east', 'north']
Printing the axis directions as well as the CRS catches the west/south case in Step 7 before you plot.
Example 2: Rebuild point geometry with corrected longitude/latitude order
import geopandas as gpd
import pandas as pd
df = pd.read_csv("data/uk_airports.csv")
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["longitude_deg"], df["latitude_deg"]),
crs="EPSG:4326",
)
assert gdf.geometry.y.between(-90, 90).all()
print(gdf[["name", "geometry"]].head(3))
name geometry
0 Belfast International Airport POINT (-6.21583 54.6575)
1 Enniskillen/St Angelo Airport POINT (-7.65118 54.39827)
2 George Best Belfast City Airport POINT (-5.8725 54.6181)
The assert is a cheap guard: swapped columns would still pass here for the UK (both columns are within Β±90), which is why the reference-layer check in Step 6 matters more.
Example 3: Assign a missing CRS correctly
import geopandas as gpd
orphan = gpd.read_file("output/no_prj.shp")
if orphan.crs is None:
orphan = orphan.set_crs("EPSG:27700")
print(orphan.crs, orphan.crs.name)
EPSG:27700 OSGB36 / British National Grid
This is correct only because the file came from a layer we know was in British National Grid.
Example 4: Reproject to match another layer
import geopandas as gpd
import pandas as pd
df = pd.read_csv("data/uk_airports.csv")
airports = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df["longitude_deg"], df["latitude_deg"]), crs=4326)
districts = gpd.read_file("data/uk_districts.gpkg")
airports = airports.to_crs(districts.crs)
per_district = gpd.sjoin(airports, districts, predicate="within")["LAD24NM"].value_counts()
print(per_district.head(3))
print(len(airports) - per_district.sum(), "airports outside every district")
LAD24NM
Argyll and Bute 3
Na h-Eileanan Siar 3
West Suffolk 3
Name: count, dtype: int64
0 airports outside every district
Once the layers share a CRS they overlay and join correctly: every one of the 88 airports falls inside a district.
Example 5: Overlay against a reference boundary
import geopandas as gpd
import matplotlib
matplotlib.use("Agg")
world = gpd.read_file("data/ne_50m_admin_0_countries.zip")
south_africa = world[world["ADMIN"] == "South Africa"]
fig, (left, right) = matplotlib.pyplot.subplots(1, 2, figsize=(10, 4))
south_africa.to_crs("EPSG:2053").plot(ax=left)
left.set_title("EPSG:2053 (westing, southing)")
south_africa.to_crs("EPSG:32735").plot(ax=right)
right.set_title("EPSG:32735 (easting, northing)")
fig.savefig("output/south_africa_two_crs.png", dpi=80)
print(south_africa.to_crs("EPSG:2053").total_bounds.round(0))
[-677015. 2450148. 1232920. 5240969.]
The left panel is South Africa rotated 180Β°; the right one is the familiar shape. Plotting against a trusted outline in the same CRS makes either kind of error obvious at a glance.
Why the picture lies in three different ways
A plot has no idea what its numbers mean, so each fault produces its own geometric signature. Swapping latitude and longitude reflects every point across the line x = y: the UK's airports, latitudes 50 to 60 and longitudes β8 to 1, become points with x of 50 to 60 and y of β8 to 1, in the western Indian Ocean, and the country's shape appears turned on its side. Mixing units scales one layer against the other: airports in degrees occupy a box 9 units wide on axes 700,000 units wide, so they become a speck at the British National Grid's origin. Neither changes anything about the data that a summary statistic would flag.
The genuinely rotated map is the rare case, and it is the CRS's doing, not GeoPandas'. A few national systems define their axes as westing and southing β South Africa's Lo grids, the classic Czech and Slovak Krovak β so a larger x is further west. Plotted as x to the right and y up, the country turns through 180Β°. The data is right; only its drawing is unfamiliar. Swaps in bulk point data, and the tests that find them row by row, are covered in my points plot in the ocean.
Rasters, basemaps and other west-south systems
- An upside-down raster is a different fault. It comes from a transform with a positive y pixel size or an image drawn with the wrong origin, not from vector coordinates.
- A basemap that misaligns usually means the layer is not in
EPSG:3857, the tiles' own CRS; contextily can warp the tiles instead withcrs=. - Check the axis direction, not just the units.
gdf.crs.axis_infolists each axis with its direction; anything other than east and north will draw rotated or mirrored.
Internal links
To understand the coordinate system side of this issue, see What Is a CRS in GeoPandas?.
If you need to convert a layer to another coordinate system, read How to Reproject a GeoDataFrame in GeoPandas.
If your problem starts when building points from a CSV, see How to Create Point Geometry from Latitude and Longitude in GeoPandas.
If multiple layers are plotting in different places, read How to Fix CRS Mismatch in GeoPandas.
FAQ
Why does my GeoPandas map look upside down?
Usually because longitude and latitude were swapped or the CRS is wrong. In normal vector workflows, GeoPandas does not randomly flip the map.
How do I know if my latitude and longitude columns are reversed?
Check whether x values match longitude ranges and y values match latitude ranges for your study area. If they are reversed, recreate the geometry with points_from_xy(longitude, latitude).
Why does my layer plot in the wrong country or hemisphere?
Common causes are swapped coordinates, missing CRS metadata, assigning the wrong EPSG code, or treating projected coordinates as decimal degrees.
Why does my map look fine in QGIS but wrong in GeoPandas?
QGIS reprojects layers on the fly to the project CRS, so mismatched layers still align there, while GeoPandas plots each layer in its own stored coordinates without any automatic reprojection. Call to_crs() to put every layer in the same CRS before plotting them together.
Does GeoPandas ever actually flip the y-axis when plotting?
No. GeoDataFrame.plot() draws geometry from stored coordinates with y increasing upward. An upside-down look almost always means swapped longitude/latitude or a wrong CRS; the one genuine rotation comes from a CRS whose axes point west and south, such as EPSG:2053, where GeoPandas plots the westing and southing exactly as stored. Inspect gdf.total_bounds to confirm the coordinate ranges match the declared CRS.
How can I confirm whether coordinates are swapped without plotting?
Compare gdf.total_bounds against the expected range: longitude spans -180 to 180 and latitude only -90 to 90, so y values outside Β±90 prove a swap. The reverse is not proof: for the UK both columns fall within Β±90, so compare against where the data should be β UK latitudes near 50β61 appearing as x values give it away. If the values are reversed, rebuild geometry with points_from_xy(longitude, latitude).
Why does my GeoPandas layer not line up with a contextily basemap?
Contextily basemaps are served in Web Mercator (EPSG:3857), so a layer left in EPSG:4326 will appear far from the tiles. Reproject your layer with to_crs("EPSG:3857") before adding the basemap so both use the same metric coordinate system.