Xarray处理多维数组?地理数据怎么切片?

GIS基础理论
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

Xarray处理多维数组?地理数据怎么切片? 这是很多 GIS 与遥感数据分析同学第一次接触 NetCDF、GRIB、Zarr 或多维栅格时最容易卡住的问题:数据不再只是二维影像,而是带有时间、波段、高度、经纬度等多个维度,传统的“按行列号裁剪”思路很快就不够用了。

本文以 GIS 场景中常见的气象、海洋、遥感多维数据为例,讲清楚 Xarray 处理多维数组的基本思路,并重点演示地理数据怎么切片:按时间切片、按经纬度范围裁剪、按变量提取、按最近点查询,以及切片后如何检查结果是否正确。

Xarray处理多维数组 地理数据经纬度切片示意图
Xarray 处理多维地理数据时,通常先识别维度和坐标,再按时间、经纬度或变量进行切片。

引言:为什么 GIS 用户需要 Xarray 处理多维数组

在 GIS 工作中,我们经常处理 Shapefile、GeoJSON、GeoTIFF 这类二维空间数据。但气象、海洋、生态遥感和再分析数据通常不是简单的二维影像,而是多维数组。

例如一个降水 NetCDF 文件可能包含:

  • time:每天、每小时或每月的时间维度
  • lat:纬度坐标
  • lon:经度坐标
  • precip:降水变量

如果你想提取“2023 年 7 月某个区域的降水数据”,本质上就是在一个多维数组中同时按时间和空间范围进行切片。Xarray 的优势在于,它可以直接使用维度名称和坐标值切片,而不是只依赖数组下标。

背景:地理数据怎么切片才不容易出错

很多初学者会把 Xarray 当作 NumPy 的增强版来用,直接写类似 data[0, :, :] 的代码。这样虽然能运行,但在 GIS 数据里很容易出错。

原因是多维地理数据通常有明确的坐标语义:

  • 第 0 维不一定是时间,也可能是高度或波段
  • 纬度可能从北到南递减,也可能从南到北递增
  • 经度可能是 -180 到 180,也可能是 0 到 360
  • 变量名可能不是你想象的 temperature,而是 t2mprsst
  • 空间坐标名称可能是 lat/lonlatitude/longitudex/y

因此,Xarray 处理多维数组时,正确流程不是先切片,而是先查看数据结构,再确认维度、坐标、变量和坐标方向。

原理:Xarray 的 Dataset、DataArray、维度和坐标

Xarray 中最常见的两个对象是 DatasetDataArray

  • Dataset:类似一个文件或数据集,里面可以包含多个变量,例如温度、降水、风速
  • DataArray:一个带有维度和坐标的数组,例如某个变量的三维数组 time × lat × lon

理解 Xarray 切片,需要分清三个概念:

概念 含义 GIS 示例
维度 数组的方向或轴 timelatlon
坐标 维度上的真实取值 2023-07-0130.5114.3
变量 真正存储的观测值 降水、气温、NDVI、海温

普通 NumPy 切片常用位置索引,例如第几行第几列。Xarray 更推荐使用标签索引,也就是用真实时间、经纬度值来切片。

import xarray as xr

ds = xr.open_dataset("precip.nc")
print(ds)

运行后,先观察输出中的 DimensionsCoordinatesData variables。这一步决定后面所有切片代码怎么写。

步骤:Xarray 处理多维数组的地理数据切片流程

步骤 1:打开 NetCDF 或 Zarr 数据

如果是本地 NetCDF 文件,可以直接使用 open_dataset

import xarray as xr

ds = xr.open_dataset("precip.nc")
print(ds)

如果数据较大,建议先不要一次性加载到内存。Xarray 默认会懒加载部分数据,真正计算时才读取。对于很大的 NetCDF 或 Zarr 数据,还可以配合 Dask 分块。

ds = xr.open_dataset("precip.nc", chunks={"time": 30})

这对长时间序列的气象数据尤其有用,例如逐日降水、逐小时温度、海洋再分析数据。

步骤 2:确认变量名、维度名和坐标范围

在地理数据怎么切片这个问题上,最关键的是不要猜变量名和坐标名。先检查数据。

print(ds.data_vars)
print(ds.coords)
print(ds.dims)

如果输出显示变量名为 precip,维度为 timelatlon,就可以取出该变量。

da = ds["precip"]
print(da)

如果坐标名是 latitudelongitude,后面的代码就必须使用对应名称,而不是 latlon

步骤 3:按时间切片

按时间切片是 Xarray 处理多维数组最常见的操作之一。提取某一天数据:

day = da.sel(time="2023-07-01")

提取一个时间范围:

summer = da.sel(time=slice("2023-06-01", "2023-08-31"))

如果时间坐标是标准时间类型,Xarray 会自动识别日期字符串。你也可以按年份、月份进行筛选。

data_2023 = da.sel(time=slice("2023-01-01", "2023-12-31"))
july = da.sel(time=da["time"].dt.month == 7)

时间切片后,一定要检查维度是否符合预期。

print(summer.dims)
print(summer.sizes)

步骤 4:按经纬度范围切片

假设我们要裁剪中国中部附近一个区域,经度范围为 110 到 116,纬度范围为 28 到 34。

如果纬度坐标是从小到大递增,可以这样写:

subset = da.sel(
    lon=slice(110, 116),
    lat=slice(28, 34)
)

但很多气象 NetCDF 的纬度是从北到南递减,例如从 90 到 -90。这时纬度切片顺序必须反过来。

subset = da.sel(
    lon=slice(110, 116),
    lat=slice(34, 28)
)

如何判断纬度方向?直接查看前几个坐标值。

print(da["lat"].values[:5])
print(da["lat"].values[-5:])

这是 Xarray 地理数据切片中最常见的坑之一:经纬度范围明明写对了,但结果为空,通常就是纬度方向写反了。

步骤 5:同时按时间和空间范围切片

实际 GIS 分析通常需要同时按时间和空间裁剪。例如提取 2023 年 7 月,某个经纬度范围内的降水数据:

subset = da.sel(
    time=slice("2023-07-01", "2023-07-31"),
    lon=slice(110, 116),
    lat=slice(34, 28)
)

如果你的纬度是递增的,则改成:

subset = da.sel(
    time=slice("2023-07-01", "2023-07-31"),
    lon=slice(110, 116),
    lat=slice(28, 34)
)

切片完成后,建议立即检查数据大小和是否存在空值。

print(subset)
print(subset.sizes)
print(subset.isnull().sum().compute() if hasattr(subset.data, "compute") else subset.isnull().sum())

步骤 6:按最近点提取某个站点附近值

如果你有一个气象站点或监测点坐标,例如经度 114.31、纬度 30.52,可以用 method="nearest" 提取最近网格点。

point_ts = da.sel(
    lon=114.31,
    lat=30.52,
    method="nearest"
)

print(point_ts)

如果还要限制时间范围:

point_ts = da.sel(
    time=slice("2023-01-01", "2023-12-31"),
    lon=114.31,
    lat=30.52,
    method="nearest"
)

注意,最近点提取不是空间插值。它只是找到最接近目标坐标的网格中心。如果你的数据分辨率较粗,例如 0.25 度或 1 度,需要在结果解释中说明代表性问题。

步骤 7:使用 isel 按位置索引切片

sel 是按坐标标签切片,isel 是按位置编号切片。对于 GIS 用户,优先使用 sel,但在调试或快速查看数据时,isel 很有用。

first_time = da.isel(time=0)
small_block = da.isel(time=0, lat=slice(100, 150), lon=slice(200, 260))

isel 的风险是可读性差。半年后再看代码,很难知道 lat=slice(100, 150) 对应的真实地理范围。因此,正式分析脚本建议使用 sel

步骤 8:处理 0 到 360 经度问题

不少全球气象和海洋数据的经度范围是 0 到 360,而 GIS 软件和矢量边界常用 -180 到 180。如果你用 lon=slice(110, 116) 提取中国区域通常没问题,但如果要提取西半球区域,例如 -80-70,就会失败。

可以把经度转换为 -180 到 180

ds = ds.assign_coords(lon=(((ds["lon"] + 180) % 360) - 180))
ds = ds.sortby("lon")

da = ds["precip"]

转换后再按常规经纬度范围切片。

subset = da.sel(
    lon=slice(-80, -70),
    lat=slice(10, 0)
)

这里仍然要注意纬度方向。如果纬度递增,需要改为 lat=slice(0, 10)

步骤 9:切片后快速绘图检查

切片结果不要只看数组形状,最好画图检查一次。对于二维结果,可以直接使用 Xarray 的简单绘图。

one_day = subset.isel(time=0)
one_day.plot()

如果是在 Jupyter Notebook 中,这一步能很快发现经纬度范围是否切错、纬度是否倒置、是否提取到错误区域。

如果结果仍然是三维数据,可以先选择一个时间片,或对时间求平均。

monthly_mean = subset.mean(dim="time")
monthly_mean.plot()

步骤 10:切片结果导出为 GeoTIFF

很多 GIS 工作流最后需要把 Xarray 切片结果导入 QGIS、ArcGIS Pro 或其他栅格分析工具。可以使用 rioxarray 导出 GeoTIFF。

import rioxarray

one_day = subset.isel(time=0)

one_day = one_day.rio.set_spatial_dims(x_dim="lon", y_dim="lat")
one_day = one_day.rio.write_crs("EPSG:4326")

one_day.rio.to_raster("precip_subset_20230701.tif")

如果你的坐标名是 longitudelatitude,需要相应修改:

one_day = one_day.rio.set_spatial_dims(x_dim="longitude", y_dim="latitude")

导出后建议用 QGIS 打开检查:

  • 图层是否出现在正确位置
  • 坐标系是否为 EPSG:4326 或你预期的 CRS
  • 像元大小是否符合原始数据分辨率
  • 空值区域是否合理
  • 栅格上下方向是否正确

常见坑:Xarray 地理数据切片为什么结果为空或位置不对

坑 1:纬度方向写反

这是最常见问题。纬度递减时要用 lat=slice(北, 南),纬度递增时要用 lat=slice(南, 北)

print(da["lat"].values[0], da["lat"].values[-1])

如果第一个值大于最后一个值,说明纬度是递减的。

坑 2:经度范围不是 -180 到 180

如果数据经度是 0 到 360,而你用负经度裁剪,会得到空结果。先检查经度范围。

print(float(da["lon"].min()), float(da["lon"].max()))

根据结果决定是否需要转换经度。

坑 3:变量不是二维,而是还有高度、波段或集合成员维度

有些数据变量维度可能是 time × level × lat × lon。此时只按时间和经纬度切片,结果仍然包含 level

print(da.dims)

如果有高度层,可以继续选择指定层。

subset = da.sel(
    level=850,
    time=slice("2023-07-01", "2023-07-31"),
    lon=slice(110, 116),
    lat=slice(34, 28)
)

坑 4:坐标名不是 lat 和 lon

很多数据使用 latitudelongitude,或者投影坐标 xy。这时不能照抄示例代码。

print(ds.coords)

如果坐标是 x/y,说明数据可能已经在某个投影坐标系下,不一定能直接用经纬度切片。

坑 5:CRS 信息缺失

Xarray 读取 NetCDF 后,空间参考信息不一定自动成为标准 GIS 软件可识别的 CRS。导出 GeoTIFF 前,建议明确写入 CRS。

one_day = one_day.rio.write_crs("EPSG:4326")

如果原始数据不是 WGS84 经纬度坐标,不能随便写 EPSG:4326,需要根据元数据确认真实坐标系。

坑 6:切片后没有触发实际计算

使用 Dask 分块时,Xarray 可能只是构建了计算任务,并没有真正读取全部数据。这是正常现象。需要输出、绘图、统计或调用 compute() 时才会执行。

result = subset.mean(dim="time").compute()

这对大数据很有帮助,但也意味着报错可能会延迟到真正计算时才出现。

方法比较:sel、isel、where 和 clip 应该怎么选

方法 适用场景 优点 注意事项
sel 按时间、经纬度、层级等坐标值切片 最适合 GIS 数据,可读性强 需要确认坐标名称和方向
isel 按数组位置快速取数据 适合调试和快速预览 不直观,容易与真实地理范围脱节
where 按条件筛选数据 适合掩膜、阈值筛选 可能保留原数组大小,只把不满足条件的值设为空
rioxarray clip 按矢量边界裁剪栅格 适合行政区、流域边界裁剪 需要正确 CRS 和几何对象

如果只是矩形经纬度范围裁剪,优先使用 sel。如果要按省界、流域、多边形边界裁剪,建议使用 rioxarray 配合 GeoPandas。

import geopandas as gpd
import rioxarray

gdf = gpd.read_file("boundary.shp").to_crs("EPSG:4326")

raster = da.isel(time=0)
raster = raster.rio.set_spatial_dims(x_dim="lon", y_dim="lat")
raster = raster.rio.write_crs("EPSG:4326")

clipped = raster.rio.clip(gdf.geometry, gdf.crs)

这种方式更接近 GIS 软件中的“按掩膜提取”。但前提是栅格和矢量的坐标系必须一致。

检查清单:Xarray 地理数据切片前后要确认什么

  • 是否已经确认数据变量名,而不是凭经验猜测
  • 是否检查了维度名称,例如 timelatlonlevel
  • 是否检查了经纬度范围和坐标方向
  • 纬度递增还是递减,slice 顺序是否正确
  • 经度是 -180 到 180 还是 0 到 360
  • 时间坐标是否能被 Xarray 正确识别
  • 切片结果的维度大小是否符合预期
  • 切片后是否画图检查空间位置
  • 导出 GeoTIFF 前是否设置空间维度和 CRS
  • 如果使用矢量边界裁剪,矢量和栅格 CRS 是否一致

实际项目中,建议把这些检查写进脚本,而不是只在 Notebook 中手工查看。这样后续换数据源、换区域、换时间范围时更不容易出错。

FAQ:Xarray 处理多维数组和地理数据切片常见问题

Q1:Xarray 处理多维数组时,应该优先用 sel 还是 isel?

GIS 场景中优先使用 sel。因为 sel 是按真实坐标值切片,例如时间、经纬度、高度层,可读性和可复现性更好。isel 更适合调试或临时查看数组位置。

Q2:Xarray 地理数据怎么切片才不会得到空结果?

先检查三个信息:坐标名是否正确、纬度方向是否递增或递减、经度范围是否为 -180 到 1800 到 360。大多数空结果都和这三点有关。

Q3:为什么经纬度范围写对了,但裁剪区域上下颠倒?

通常是纬度方向和你预期不同。某些数据纬度从北到南递减,某些数据从南到北递增。可以通过 print(da["lat"].values[:5]) 检查。绘图检查是最直接的验证方式。

Q4:Xarray 可以直接按省界或流域边界裁剪吗?

Xarray 本身更适合按维度和坐标切片。按多边形边界裁剪通常使用 rioxarraygeopandas。关键是先设置空间维度和 CRS,再用矢量几何裁剪。

Q5:NetCDF 切片后怎么在 QGIS 中打开?

可以用 rioxarray 把切片结果导出为 GeoTIFF。导出前要设置 x_dimy_dim 和 CRS。然后在 QGIS 中添加栅格图层检查位置、范围和像元值。

Q6:为什么同一个代码在不同 NetCDF 文件上不能直接复用?

因为不同数据源的变量名、维度名、坐标名、经度范围、纬度方向和 CRS 元数据可能不同。复用代码前,应先用 print(ds) 查看数据结构,再调整切片参数。

Q7:Xarray 适合处理很大的遥感或气象数据吗?

适合,但建议配合 Dask 分块读取,例如按时间分块。这样可以避免一次性把全部数据读入内存。不过切片、统计和导出时仍然要注意计算规模,避免一次处理过大的区域和时间范围。

结论:先看结构,再按坐标切片

Xarray处理多维数组的核心不是记住某一段固定代码,而是建立正确的地理数据切片思路:先查看数据结构,再确认变量、维度、坐标和 CRS,最后用 sel 按真实时间和经纬度范围切片。

对于 GIS 读者来说,最实用的流程可以概括为:

  1. xr.open_dataset 打开数据
  2. print(ds) 检查变量、维度和坐标
  3. 确认纬度方向和经度范围
  4. sel 按时间和空间范围切片
  5. 用绘图或统计检查结果
  6. 需要进入 GIS 软件时,用 rioxarray 导出 GeoTIFF

只要把这套流程固定下来,地理数据怎么切片就不再是靠试错,而是一个可以稳定复现的多维数组处理工作流。