How to Select a Time Range and Location from an xarray Dataset
Problem statement
.sel() is the first thing anyone does with a gridded dataset, and it fails in ways that do not look like failures. On the NCEP/NCAR Reanalysis 1 monthly surface air temperature file — latitude stored from 90 down to −90, longitude from 0 to 357.5 — four ordinary selections measured:
air.sel(lat=slice(30, 60)) -> 0 latitudes, no error
air.sel(lat=40.71, lon=-74.01, method="nearest") -> a cell off the coast of Spain, not New York
air.sel(lat=[40.71, 51.51], lon=[286.0, 0.0]) -> a 2 x 2 grid, not two points
air.sel(time="2024-01-20", method="nearest") -> February 2024
None of them raises. The empty slice produces a dataset whose mean is NaN; the New York selection returns a temperature 7.61 °C too warm for January 2024; the list selection returns combinations of the coordinates that nobody asked for. Each is correct behaviour for a label-based index — and each is avoidable once you know which assumption it makes.
Quick answer
import xarray as xr
air = xr.open_dataset("air.mon.mean.nc")["air"]
# a region: slice latitude in the order the file stores it (here 90 -> -90)
india_summer = air.sel(time=slice("2023-06", "2023-08"),
lat=slice(35, 5), lon=slice(68, 98))
# a point: match the grid's longitude convention, and refuse a distant match
new_york = air.sel(lat=40.71, lon=-74.01 % 360, method="nearest", tolerance=2.5)
print(dict(india_summer.sizes), float(new_york.lon))
{'time': 3, 'lat': 13, 'lon': 12} 285.0
Three rules cover most selections: read the coordinate order before slicing, convert point longitudes to the grid's convention, and give method="nearest" a tolerance so a wrong match raises instead of returning a number.
Step-by-step solution
1. Read the coordinate order and range first
for name in ("time", "lat", "lon"):
index = air.indexes[name]
order = "increasing" if index.is_monotonic_increasing else "decreasing"
print(f"{name:5} {index[0]} -> {index[-1]} ({order}, {len(index)} values)")
time 1948-01-01 00:00:00 -> 2026-02-01 00:00:00 (increasing, 938 values)
lat 90.0 -> -90.0 (decreasing, 73 values)
lon 0.0 -> 357.5 (increasing, 144 values)
This one print decides how every later slice has to be written. Descending latitude is common in reanalyses and in files converted from image formats; 0–360 longitude is common in global models.
2. Select time with partial date strings
xarray understands partial dates, and both ends of a label slice are inclusive:
year = air.sel(time="2023") # 12 months
summer = air.sel(time=slice("2020-06", "2020-08")) # June, July, August 2020
print(year.sizes["time"], summer.time.dt.strftime("%Y-%m-%d").values)
12 ['2020-06-01' '2020-07-01' '2020-08-01']
A slice that runs past the end of the data is not an error either. slice("2025-01", "2030-12") returned 14 months — the 12 of 2025 and the two of 2026 that the file contains — so check the first and last timestamps whenever the period matters.
3. Slice latitude in the stored order
A label slice follows the order of the index. On a descending axis, slice(30, 60) asks for labels from 30 down to 60, and there are none:
print(air.sel(lat=slice(30, 60)).sizes["lat"]) # 0
print(air.sel(lat=slice(60, 30)).sizes["lat"]) # 13
To write code that works on either orientation, sort once, or build the slice from the data:
lat = air.indexes["lat"]
band = slice(60, 30) if lat.is_monotonic_decreasing else slice(30, 60)
mid_latitudes = air.sel(lat=band)
4. Put point longitudes in the grid's convention
A negative longitude on a 0–360 grid is simply a number smaller than every coordinate, so method="nearest" picks the first column, 0°. Measured for January 2024:
| City | Requested lon | Cell picked | Value | Correct cell | Value | Error |
|---|---|---|---|---|---|---|
| New York | −74.01 | 0.0° | 9.89 °C | 285.0° | 2.28 °C | +7.61 °C |
| Mexico City | −99.13 | 0.0° | 20.10 °C | 260.0° | 14.63 °C | +5.47 °C |
| Los Angeles | −118.24 | 0.0° | 12.55 °C | 242.5° | 8.37 °C | +4.18 °C |
| Rio de Janeiro | −43.17 | 0.0° | 22.54 °C | 317.5° | 23.98 °C | −1.45 °C |
The fix is lon % 360 for a 0–360 grid, or converting the grid itself — the trade-offs are in longitude conventions explained.
5. Give method="nearest" a tolerance
tolerance is the maximum distance, in coordinate units, that a nearest match may be from the request. With it, the New York mistake raises instead of returning a number:
try:
air.sel(lat=40.71, lon=-74.01, method="nearest", tolerance=2.5)
except KeyError as error:
print(error)
"not all values found in index 'lon'"
Set it to one grid spacing. The same parameter protects time selection: monthly timestamps sit on the first of the month, so sel(time="2024-01-20", method="nearest") returned 1 February, which is 12 days away, rather than 1 January, which is 19. An exact sel(time="2024-01-20") raises KeyError and suggests method='nearest' — the suggestion is the trap.
6. Select several points with DataArray indexers
Passing lists selects every combination of the values — orthogonal indexing:
lats = [40.71, 51.51, 35.68]
lons = [285.99, 359.87, 139.69]
grid = air.sel(lat=lats, lon=lons, method="nearest")
points = air.sel(lat=xr.DataArray(lats, dims="city"),
lon=xr.DataArray(lons, dims="city"), method="nearest")
print(dict(grid.sizes), dict(points.sizes))
{'time': 938, 'lat': 3, 'lon': 3} {'time': 938, 'city': 3}
Nine series for three cities, six of them for places like "New York's latitude at Tokyo's longitude". Wrapping the indexers in DataArrays that share a new dimension selects the three pairs instead.
7. Choose between the nearest cell and interpolation
sel(method="nearest") returns a cell value; interp() blends the surrounding cells. On a 2.5° grid the difference is not small. For July 2023:
nearest interp difference
New York 24.63 23.80 -0.83
Tokyo 24.96 24.69 -0.27
Denver 19.03 19.29 +0.26
London 15.25 nan
London is NaN because its longitude, 359.87° on this grid, lies beyond the last column at 357.5° and interp does not wrap around the globe. A cell value is what the data contains; an interpolated value is an estimate. Use nearest when you will compare with other gridded values, and interpolation only on a grid whose longitude seam is not in the way.
Code examples
Example 1 — a box selection that works on any orientation or convention
import numpy as np
import pandas as pd
import xarray as xr
def select_box(da, south, north, west, east, lat="lat", lon="lon"):
"""Select a latitude/longitude box whatever the storage order or longitude convention.
A box with west > east crosses the grid's seam (for example 150 to -150).
"""
ascending = da.indexes[lat].is_monotonic_increasing
da = da.sel({lat: slice(south, north) if ascending else slice(north, south)})
if float(da[lon].max()) > 180: # 0-360 grid
west, east = west % 360, east % 360
else: # -180-180 grid
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)
pacific = select_box(air.sel(time="2023"), -30, 30, 150, -150)
print(dict(pacific.sizes), round(float(pacific.mean()), 2))
{'time': 12, 'lat': 25, 'lon': 25} 25.58
Example 2 — point extraction that wraps longitude and refuses distant matches
def extract_points(da, sites, max_distance=None, lat="lat", lon="lon"):
"""Nearest grid cell for each named site, along a new 'site' dimension.
Longitude distance is measured around the globe, so -0.13 matches 0.0 on a
0-360 grid rather than 357.5. Raises if any site is further than
max_distance degrees (default: one grid spacing) from its cell.
"""
names = list(sites)
plat = np.array([sites[n][0] for n in names], dtype=float)
plon = np.array([sites[n][1] for n in names], dtype=float)
glat = da[lat].values.astype(float)
glon = da[lon].values.astype(float)
ilat = np.abs(glat[None, :] - plat[:, None]).argmin(axis=1)
dlon = np.abs((glon[None, :] - plon[:, None] + 180) % 360 - 180)
ilon = dlon.argmin(axis=1)
limit = max_distance or max(np.abs(np.diff(glat)).max(), np.abs(np.diff(glon)).max())
far = (np.abs(glat[ilat] - plat) > limit) | (dlon[np.arange(len(names)), ilon] > limit)
if far.any():
raise ValueError(f"no cell within {limit} degrees of {np.array(names)[far].tolist()}")
picked = da.isel({lat: xr.DataArray(ilat, dims="site"),
lon: xr.DataArray(ilon, dims="site")})
return picked.assign_coords(site=names)
cities = {"New York": (40.71, -74.01), "London": (51.51, -0.13), "Tokyo": (35.68, 139.69)}
july = extract_points(air.sel(time="2023-07-01"), cities)
for site, cell_lon, value in zip(july.site.values, july.lon.values, july.values):
print(f"{site:9} lon {cell_lon:6.1f} {value:5.2f} °C")
It works unchanged on a grid already converted to −180–180, because the distance calculation never assumes a convention.
Example 3 — a period selection that fails loudly when the data do not cover it
def select_period(da, start, end, time="time"):
"""Slice start..end (partial dates allowed) and raise if the data fall short."""
out = da.sel({time: slice(start, end)})
if out.sizes[time] == 0:
raise ValueError(f"no timesteps between {start} and {end}")
first = pd.Timestamp(out[time].values[0])
last = pd.Timestamp(out[time].values[-1])
if first > pd.Period(start).end_time or last < pd.Period(end).start_time:
raise ValueError(f"asked for {start} to {end}; data cover "
f"{first:%Y-%m-%d} to {last:%Y-%m-%d}")
return out
print(select_period(air, "2020-06", "2020-08").sizes["time"])
select_period(air, "2025-01", "2030-12")
3
ValueError: asked for 2025-01 to 2030-12; data cover 2025-01-01 to 2026-02-01
Explanation
Why a slice follows the stored order
.sel() with a slice is label-based: it finds the position of the start label and the position of the stop label in the index and returns everything between them, in storage order. On a decreasing latitude index, the label 30 comes after the label 60, so a slice from 30 to 60 spans nothing.
Returning an empty result is deliberate — an empty selection is a legitimate answer for a range that holds no data. It becomes a problem only because the next step, usually a mean, silently turns the empty array into NaN.
Why nearest does not know that longitude wraps
To the index, longitude is just a sorted list of numbers. The distance from −74.01 to 0.0 is 74.01; to 285.0 it is 359.01. Nearest picks 0.0, correctly by arithmetic and absurdly by geography.
The seam causes the same problem in the other direction. London at −0.13° converted to 359.87° is 0.13° from the column at 0.0° around the globe, but the index measures 359.87 from 0.0 and 2.37 from 357.5, so sel(method="nearest") picked the column at 357.5° instead. A distance measured modulo 360, as in Example 2, is the only selection that is right on both sides of the seam.
Why a list selects a grid and a DataArray selects points
xarray keeps dimensions independent unless told otherwise. A list for lat and a list for lon are two separate selections on two separate dimensions, and the result is their outer product — three latitudes by three longitudes.
When both indexers are DataArrays with the same dimension name, xarray treats them as paired coordinates and walks along that shared dimension, the vectorised behaviour NumPy calls fancy indexing. The dimension name you choose becomes a real dimension of the result, which is why naming it city or site is worth the extra words.
Why nearest in time picked the following month
Monthly means are stamped with a single instant, the first of the month here, even though they describe the whole month. "Nearest" compares instants, not periods, so any day after the 16th of a 31-day month is nearer to the next month's stamp.
For monthly data, select with a partial string — sel(time="2024-01") — which matches on the period rather than the instant. Use method="nearest" in time only for data whose timestamps are the instants they describe, such as 6-hourly analyses.
Edge cases or notes
- An unsorted coordinate breaks slicing silently. After reassigning longitudes without sorting,
sel(lon=slice(-10, 30))returned 0 columns; callsortbyafter any coordinate change. - Scalar selection drops the dimension.
sel(lat=50)returns data without alatdimension but keepslatas a scalar coordinate; pass a one-element list to keep the dimension. iselis positional and ignores order.air.isel(time=-1, lat=0, lon=0)and the matchingselgave the same value;iselavoids label questions entirely when you know the positions.- Chunk layout decides what a selection costs. This file stores one month per compressed chunk, so a single-cell time series decompresses all 938 of them.
- Tolerance is in coordinate units. For time it is a timedelta such as
"15D"; for latitude and longitude it is degrees. - Label slices are inclusive at both ends. Unlike Python's positional slices,
slice("2020-06", "2020-08")includes August. - Model calendars change the rules for strings. On a
noleapcftime axis, partial strings work, but a date such as 29 February does not exist. - Irregular regions need a mask, not a slice. Use
.where()with a condition, or clip to a polygon.
Internal links
- 0–360 or −180–180: longitude conventions explained — converting the grid instead of every point
- NetCDF and gridded data explained — dimensions, coordinates and why chunking decides selection cost
- How to open a NetCDF file in Python with xarray — what is read at open and what at selection
- How to clip a NetCDF grid to a polygon in Python — irregular regions
- How to take an area-weighted mean over a latitude–longitude grid — what to do with the region once selected
- How to resample a gridded time series to monthly or annual values — aggregating the time range you selected
- Fixing a NetCDF map that is shifted, flipped or split at 180° — the plotting side of the same coordinate problems
- How to extract raster values at point locations with Rasterio — the GeoTIFF equivalent of point extraction
FAQ
Why does xarray sel return an empty result for a latitude slice?
Because the latitude coordinate is stored in decreasing order and the slice was written increasing. On this file slice(30, 60) returned 0 latitudes and slice(60, 30) returned 13.
How do I select the nearest grid point to a longitude such as −74?
Convert it to the grid's convention first — −74.01 % 360 is 285.99 — and pass a tolerance. Without either, the NCEP grid matched longitude 0 and returned a value 7.61 °C too warm.
How do I select several points rather than a grid?
Wrap the latitude and longitude lists in DataArrays that share a dimension name, such as city. Plain lists select every combination, which turned three cities into nine series.
Why did selecting 20 January give me February?
Monthly data are stamped on the first of the month, and 1 February is nearer to 20 January than 1 January is. Select monthly data with a partial string such as 2024-01 instead of method nearest.
Should I use sel with nearest or interp for a point?
Nearest returns a real cell value; interp returns a blend of neighbouring cells. On a 2.5° grid they differed by up to 0.83 °C, and interp returned NaN near the longitude seam.
Does a time slice beyond the end of the data raise an error?
No. A slice from 2025-01 to 2030-12 returned the 14 months that exist, so compare the first and last timestamps of the result with the period you asked for.