0–360 or −180–180: Longitude Conventions in Gridded Data Explained

Problem statement

Longitude has two numbering conventions in everyday use. One runs east from Greenwich from 0° to 360°; the other runs from −180° to 180°, negative to the west. Both describe the same planet, and each is standard somewhere: the NCEP/NCAR Reanalysis grid runs from 0 to 357.5, NOAA OISST from 0.125 to 359.875, and two CMIP6 models checked here from 0.625 to 359.375 and 0 to 358.75. Natural Earth's country polygons, like almost every vector dataset, run from −180 to 180.

When the two meet, nothing converts automatically. Clipping the 2023 NCEP mean temperature to country polygons, with the grid left at 0–360:

United States   4 cells    (274 after converting the grid)
Brazil          NoDataInBounds: No data found in bounds.
United Kingdom  6 cells    (17 after converting)
Spain           7 cells    (24 after converting)

The United States result is the dangerous one. It returned a temperature — 5.37 °C instead of 7.78 °C — from the four cells where the Aleutian Islands cross into positive longitudes, and gave no warning that the other 270 were missing.

Quick answer

Convert the grid's longitudes with modulo arithmetic, then sort:

import xarray as xr

ds = xr.open_dataset("air.mon.mean.nc")
ds = ds.assign_coords(lon=((ds.lon + 180) % 360) - 180).sortby("lon")

print(float(ds.lon.min()), float(ds.lon.max()))
-180.0 177.5

The sort is not optional: without it the coordinate jumps from 177.5 to −180 in the middle, and both slicing and rioxarray's transform break. On the 938-month file the conversion and sort took 57 ms including the load, against 43 ms to load alone; before loading, sortby stays lazy and took 4.4 ms.

Two world strips: a 0 to 360 grid with its edge at the Greenwich meridian, and a −180 to 180 grid with its edge at the antimeridian, each showing where Europe and the Americas fall.
Converting does not remove the seam. It moves it from Greenwich, through Europe and Africa, to the middle of the Pacific.

Step-by-step solution

1. Identify the convention from the values, not the attributes

lon = ds.lon
print(float(lon.min()), float(lon.max()), lon.attrs.get("actual_range"))

A maximum above 180 means 0–360; a minimum below 0 means −180–180; a grid confined to 0–180 is ambiguous and needs no conversion for most work. Trust the coordinate values: after the conversion above, NCEP's actual_range attribute still said [0. 357.5], because xarray copies attributes through arithmetic without knowing what they mean.

2. Know which side each tool expects

  • Vector data — shapefiles, GeoPackage, GeoJSON, Natural Earth — uses −180 to 180 almost universally, and EPSG:4326 is defined that way.
  • Global model and reanalysis grids frequently use 0 to 360: every gridded source checked for this article did.
  • GeoTIFF readers and web maps assume −180 to 180. A 0–360 raster written as GeoTIFF has bounds from −1.25 to 358.75, and a point at −74° falls outside it.

The mismatch matters at every boundary between the two worlds: clipping, point extraction, overlaying, exporting.

3. Convert the grid with modulo arithmetic, then sort

((lon + 180) % 360) - 180 maps 0–360 onto −180–180 and leaves values already in that range unchanged, so it is safe to apply without checking first. It is equally valid in reverse — lon % 360 maps any longitude to 0–360.

unsorted = ds.assign_coords(lon=((ds.lon + 180) % 360) - 180)
print(unsorted.lon.values[71:74])                  # [ 177.5 -180.  -177.5]
print(unsorted.sel(lon=slice(-10, 30)).sizes["lon"])   # 0 — the slice finds nothing
print(ds.sel(lon=slice(-10, 30)).sizes["lon"])         # 17 on the sorted grid

roll(lon=72, roll_coords=True) followed by the same assignment is the alternative; measured, it produced identical values in 52 ms against 57 ms for sortby.

4. Or convert the points instead of the grid

For work centred on the Pacific, keeping 0–360 is often better. Converting point longitudes is one expression:

lon_on_grid = -74.01 % 360          # 285.99

Polygons are harder: a polygon that crosses Greenwich cannot be shifted by adding 360 to its negative coordinates without splitting it. Converting the grid is the simpler route whenever polygons are involved.

5. Handle the seam before interpolating

Neither convention wraps. On the original grid, interp(lon=359.87) returned NaN because 359.87 lies beyond the last column at 357.5; on the converted grid, interp(lon=179.0) returned NaN for the same reason at the new edge. Appending a copy of the first column one full turn later fixes it:

jan = ds["air"].sel(time="2024-01-01")
cyclic = xr.concat([jan, jan.isel(lon=0).assign_coords(lon=jan.lon[0] + 360)], dim="lon")
print(round(float(cyclic.interp(lat=51.51, lon=-0.13)), 3))
6.417

The value matched interpolation at the same point away from any seam.

6. Select regions across the seam with two slices or a mask

Once converted, the antimeridian is the seam. A Pacific box from 150° E to 150° W written as one slice returns nothing:

pacific = ds["air"].sel(time="2023").mean("time").sel(lat=slice(30, -30))

print(pacific.sel(lon=slice(150, -150)).sizes["lon"])            # 0
box = pacific.where((pacific.lon >= 150) | (pacific.lon <= -150), drop=True)
print(box.sizes["lon"], round(float(box.mean()), 2))
0
25 25.58

On the original 0–360 grid the same box is a single slice(150, 210) with the identical mean of 25.58 °C over 625 cells. Choose the convention whose seam lies outside your study area.

Bar chart of grid cells kept when clipping to country polygons on a 0 to 360 grid versus a −180 to 180 grid, for the United States, Brazil, Spain, the United Kingdom and New Zealand.
Japan and Australia lost nothing: both lie entirely east of Greenwich, where the two conventions agree.

Code examples

Example 1 — detect the convention

import numpy as np
import xarray as xr


def longitude_convention(obj, lon="lon"):
    """'0-360', '-180-180', or 'either' for a grid that lies within 0..180."""
    values = np.asarray(obj[lon].values, dtype=float)
    low, high = np.nanmin(values), np.nanmax(values)
    if low < -180 or high > 360:
        raise ValueError(f"longitudes outside any convention: {low} to {high}")
    if high > 180:
        return "0-360"
    if low < 0:
        return "-180-180"
    return "either"
print(longitude_convention(xr.open_dataset("air.mon.mean.nc")))
print(longitude_convention(xr.open_dataset("oisst-avhrr-v02r01.20240115.nc")))
0-360
0-360

Example 2 — convert either way, sort, and fix the attributes

def to_longitude_convention(obj, target="-180-180", lon="lon"):
    """Convert a regular grid's longitude to the target convention and sort it."""
    values = obj[lon]
    if target == "-180-180":
        new = ((values + 180) % 360) - 180
    elif target == "0-360":
        new = values % 360
    else:
        raise ValueError("target must be '-180-180' or '0-360'")

    attrs = {k: v for k, v in values.attrs.items()
             if k not in ("actual_range", "valid_range", "valid_min", "valid_max")}
    converted = obj.assign_coords({lon: new.assign_attrs(attrs)}).sortby(lon)

    index = converted.indexes[lon]
    if index.has_duplicates:
        raise ValueError("duplicate longitudes after conversion — the grid had both 0 and 360")
    return converted

The duplicate check is there for grids that store a cyclic column at both 0° and 360°: after conversion those become two columns labelled 0, and every selection at 0° would return both.

Example 3 — a longitude range that works in either convention

def select_longitudes(da, west, east, lon="lon"):
    """Columns from west to east (degrees, any convention), crossing the seam if needed."""
    if longitude_convention(da, lon) == "0-360":
        west, east = west % 360, east % 360
    else:
        west, east = ((west + 180) % 360) - 180, ((east + 180) % 360) - 180
    if west <= east:
        return da.sel({lon: slice(west, east)})
    return da.where((da[lon] >= west) | (da[lon] <= east), drop=True)


def add_cyclic_column(da, lon="lon"):
    """Repeat the first column one turn later so interpolation can cross the seam."""
    first = da.isel({lon: 0})
    return xr.concat([da, first.assign_coords({lon: first[lon] + 360})], dim=lon)
air = xr.open_dataset("air.mon.mean.nc")["air"].sel(time="2023").mean("time")
for grid in (air, to_longitude_convention(air)):
    box = select_longitudes(grid, 150, -150).sel(lat=slice(30, -30))
    print(longitude_convention(grid), dict(box.sizes), round(float(box.mean()), 2))

add_cyclic_column expects a grid sorted ascending in the convention it is already in: it adds 360 to the first column, so the new column sits one grid step past the last.

Explanation

Why two conventions exist

A global grid has to start somewhere. Numbering eastwards from the prime meridian to 360° gives every longitude a positive value and keeps the grid's own edge at Greenwich, and many global model and reanalysis grids are laid out that way. Mapping and surveying adopted the signed convention, with west negative, and vector formats followed it.

Neither is wrong. A longitude of 285° and one of −75° are the same meridian, and each convention has an edge somewhere. What differs is where that edge falls — and therefore which regions are awkward to work with.

Why the mismatch fails silently

Clipping, selecting and overlaying compare numbers, not places. A polygon for Brazil with longitudes from −74 to −35 does not overlap a grid whose coordinates run from 0 to 357.5, so the clip finds no cells and raises NoDataInBounds. That is the good case.

The bad case is a feature that crosses zero or the antimeridian. The United Kingdom straddles Greenwich, so on a 0–360 grid only its eastern part — six cells — overlapped, and the clip returned a mean of 11.46 °C instead of 10.94 °C. The United States polygon includes Aleutian islands at positive longitudes near 180°, so the clip kept four cells in the far north Pacific and returned 5.37 °C instead of 7.78 °C. France, whose Natural Earth polygon includes overseas territories, returned 14.58 °C from 20 cells instead of 17.37 °C from 33. Each result is a plausible temperature.

Why sorting matters as much as converting

xarray label slicing assumes a monotonic index, and rioxarray computes a raster transform from the spacing of the first coordinates. On the converted but unsorted grid, the transform came out with a pixel width of −0.02° instead of 2.5°, and every clip — Germany, Brazil, the United States — raised NoDataInBounds. A slice across the jump returned nothing.

sortby("lon") restores a monotonic coordinate and moves the data with it. It is an indexing operation, not a copy of the logic, so it stays lazy on a file that has not been loaded.

Why the seam moves rather than disappears

Any regular grid stored as an array has a first column and a last column, and nothing between them knows they are neighbours on the globe. Converting from 0–360 to −180–180 only chooses where that break sits: at Greenwich or at the antimeridian.

Everything local to the break then needs care — interpolation, smoothing, regional boxes and contouring. For Europe and Africa, −180–180 moves the break safely out of the way. For the Pacific, 0–360 already has it out of the way. For global statistics the convention does not matter at all: the global mean is the same either way.

Decision diagram choosing a longitude convention by study area: Europe or Africa, the Pacific, or global.
The rule is to put the seam where your study area is not.

Edge cases or notes

  • 180° becomes −180°. The modulo expression maps exactly 180 to −180, so the converted NCEP grid runs from −180 to 177.5 rather than to 180.
  • Attributes go stale. actual_range still read 0 to 357.5 after conversion; drop range attributes when you change coordinates.
  • Some grids repeat a cyclic column. A grid storing both 0° and 360° gets duplicate labels after conversion; drop the last column first.
  • Curvilinear grids cannot be sorted. Ocean-model grids with two-dimensional longitude arrays need the values converted in place and selection by mask, not by slice.
  • Offset starts are legal. Some ocean-model grids begin at an arbitrary longitude such as 73.5°; the modulo expression still maps them correctly, and sorting is still required.
  • Point extraction has the same trap. method="nearest" at −74° on a 0–360 grid matched longitude 0°, off Spain.
  • A GeoTIFF inherits the convention. Exporting before converting produces a raster whose western hemisphere lies beyond 180° east.
  • Global statistics are unaffected. Converting changes where data are stored, not their values; only local operations near the seam change.

FAQ

How do I convert longitude from 0–360 to −180–180 in xarray?

Assign ((lon + 180) % 360) − 180 as the new coordinate and then call sortby on longitude. Without the sort, the coordinate jumps from 177.5 to −180 mid-array and slicing returns nothing.

Why does rioxarray raise NoDataInBounds when I clip a NetCDF file?

Often because the grid runs from 0 to 360 and the polygon from −180 to 180, so they do not overlap numerically. Brazil raised exactly that error on the NCEP grid until the longitudes were converted.

Why is my clip result wrong but not empty?

The polygon partly overlaps the 0–360 range. The United Kingdom, which straddles Greenwich, kept 6 cells instead of 17, and the United States kept only 4 Aleutian cells out of 274.

Should I convert the grid or the points?

Convert the grid when polygons are involved or when the study area is near Greenwich. Convert points with lon % 360 when the work is Pacific-centred and the grid is already 0–360.

Does converting longitude change a global mean?

No. Converting reorders columns without changing values, so every global statistic is identical. Only operations near the seam, such as interpolation or regional boxes, change.

Why does interp return NaN near longitude 180 or 360?

Because neither convention wraps: points beyond the last column are outside the grid. Append a copy of the first column 360° further on before interpolating.