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

Plotting coordinates as longitude, latitude lands on target; swapping them to latitude, longitude lands in the wrong place. β˜… (lon, lat) β†’ on target β˜… (lat, lon) β†’ wrong place spatialworkflow.io
Plotting as (longitude, latitude) lands on target; swapping the order puts the point in the wrong place.

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

Upside-down map: diagnosisBuilt with x=lon, y=lat?Rebuild with points_from_xy(lon,lat)NoYesDo total_bounds fit the CRS?Axis swapped or projected metresβ€” fix coordinatesNoYesIs .crs None?set_crs() to the data’s real CRSYesNoto_crs() to match the other layerspatialworkflow.io
Diagnosing a map that plots upside down or in the wrong place.

Swapped axes, the CRS, bounds, a reference layer

Checks for a map that appears flipped, mirrored, or in the wrong place. flipped, mirrored, or misplaced?x and y passed in the right orderpoints_from_xy(lon, lat)Latitude values stay within Β±90range checkThe CRS matches the coordinatesgdf.crsBounds look plausible for the areatotal_boundsThe axes are not invertedax.invert_yaxis()A known reference layer lines upplot bothA mirrored map is nearly always swapped coordinates, not a projection problem spatialworkflow.io
The first item explains the great majority of flipped maps.

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.

Three sets of total_bounds from the same UK data and what each says. reading total_boundsairportsswappeddistrictsx range-7.7 to 1.350.1 to 59.9-116 to 655,654y range50.1 to 59.9-7.7 to 1.37,054 to 1,220,310readingβœ“UK lon/latβœ•Indian Oceanβœ“BNG metresThe UK airports, the same airports swapped, and ONS districts in EPSG:27700 spatialworkflow.io
The same UK data three ways. Swapping the columns keeps both ranges plausible-looking numbers, just on the wrong axes.

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

Four wrong-looking maps and the coordinate or CRS problem behind each. Matplotlib is plotting exactly what it was givenwhat the map looks likewhat is actually wrongThe whole shape is transposedx, y is lon, lat β€” the columnswere swappedAxes in the thousands, no contextgdf.crs is None, so nothing knowsthe unitsThe layer lands nowhere near homeGrid metres labelled EPSG:4326One layer draws, one is a speckdegrees and metres on the sameaxesA true vertical flip in vector plotting is rare β€” suspect the coordinates first spatialworkflow.io
The checks below distinguish these four in about four lines each.

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 stores latitude_deg before longitude_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.

How many of the 88 UK airports pass each location check. the 88 UK airports against reference layerscorrect, in an ONS district88correct, in NE UK outline81swapped, in any country004488The 1:50 million outline drops small islands; the swap drops everything spatialworkflow.io
A reference layer separates a generalisation effect (7 island and coastal airports) from a real error (all 88 swapped points in the sea).

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 with crs=.
  • Check the axis direction, not just the units. gdf.crs.axis_info lists each axis with its direction; anything other than east and north will draw rotated or mirrored.

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.