Xarray处理多维数组?地理数据怎么切片?
Xarray处理多维数组?地理数据怎么切片? 这是很多 GIS 与遥感数据分析同学第一次接触 NetCDF、GRIB、Zarr 或多维栅格时最容易卡住的问题:数据不再只是二维影像,而是带有时间、波段、高度、经纬度等多个维度,传统的“按行列号裁剪”思路很快就不够用了。
本文以 GIS 场景中常见的气象、海洋、遥感多维数据为例,讲清楚 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,而是t2m、pr、sst - 空间坐标名称可能是
lat/lon、latitude/longitude、x/y
因此,Xarray 处理多维数组时,正确流程不是先切片,而是先查看数据结构,再确认维度、坐标、变量和坐标方向。
原理:Xarray 的 Dataset、DataArray、维度和坐标
Xarray 中最常见的两个对象是 Dataset 和 DataArray。
- Dataset:类似一个文件或数据集,里面可以包含多个变量,例如温度、降水、风速
- DataArray:一个带有维度和坐标的数组,例如某个变量的三维数组
time × lat × lon
理解 Xarray 切片,需要分清三个概念:
| 概念 | 含义 | GIS 示例 |
|---|---|---|
| 维度 | 数组的方向或轴 | time、lat、lon |
| 坐标 | 维度上的真实取值 | 2023-07-01、30.5、114.3 |
| 变量 | 真正存储的观测值 | 降水、气温、NDVI、海温 |
普通 NumPy 切片常用位置索引,例如第几行第几列。Xarray 更推荐使用标签索引,也就是用真实时间、经纬度值来切片。
import xarray as xr
ds = xr.open_dataset("precip.nc")
print(ds)
运行后,先观察输出中的 Dimensions、Coordinates 和 Data 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,维度为 time、lat、lon,就可以取出该变量。
da = ds["precip"]
print(da)
如果坐标名是 latitude 和 longitude,后面的代码就必须使用对应名称,而不是 lat 和 lon。
步骤 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")
如果你的坐标名是 longitude 和 latitude,需要相应修改:
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
很多数据使用 latitude、longitude,或者投影坐标 x、y。这时不能照抄示例代码。
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 地理数据切片前后要确认什么
- 是否已经确认数据变量名,而不是凭经验猜测
- 是否检查了维度名称,例如
time、lat、lon、level - 是否检查了经纬度范围和坐标方向
- 纬度递增还是递减,
slice顺序是否正确 - 经度是
-180 到 180还是0 到 360 - 时间坐标是否能被 Xarray 正确识别
- 切片结果的维度大小是否符合预期
- 切片后是否画图检查空间位置
- 导出 GeoTIFF 前是否设置空间维度和 CRS
- 如果使用矢量边界裁剪,矢量和栅格 CRS 是否一致
实际项目中,建议把这些检查写进脚本,而不是只在 Notebook 中手工查看。这样后续换数据源、换区域、换时间范围时更不容易出错。
FAQ:Xarray 处理多维数组和地理数据切片常见问题
Q1:Xarray 处理多维数组时,应该优先用 sel 还是 isel?
GIS 场景中优先使用 sel。因为 sel 是按真实坐标值切片,例如时间、经纬度、高度层,可读性和可复现性更好。isel 更适合调试或临时查看数组位置。
Q2:Xarray 地理数据怎么切片才不会得到空结果?
先检查三个信息:坐标名是否正确、纬度方向是否递增或递减、经度范围是否为 -180 到 180 或 0 到 360。大多数空结果都和这三点有关。
Q3:为什么经纬度范围写对了,但裁剪区域上下颠倒?
通常是纬度方向和你预期不同。某些数据纬度从北到南递减,某些数据从南到北递增。可以通过 print(da["lat"].values[:5]) 检查。绘图检查是最直接的验证方式。
Q4:Xarray 可以直接按省界或流域边界裁剪吗?
Xarray 本身更适合按维度和坐标切片。按多边形边界裁剪通常使用 rioxarray 和 geopandas。关键是先设置空间维度和 CRS,再用矢量几何裁剪。
Q5:NetCDF 切片后怎么在 QGIS 中打开?
可以用 rioxarray 把切片结果导出为 GeoTIFF。导出前要设置 x_dim、y_dim 和 CRS。然后在 QGIS 中添加栅格图层检查位置、范围和像元值。
Q6:为什么同一个代码在不同 NetCDF 文件上不能直接复用?
因为不同数据源的变量名、维度名、坐标名、经度范围、纬度方向和 CRS 元数据可能不同。复用代码前,应先用 print(ds) 查看数据结构,再调整切片参数。
Q7:Xarray 适合处理很大的遥感或气象数据吗?
适合,但建议配合 Dask 分块读取,例如按时间分块。这样可以避免一次性把全部数据读入内存。不过切片、统计和导出时仍然要注意计算规模,避免一次处理过大的区域和时间范围。
结论:先看结构,再按坐标切片
Xarray处理多维数组的核心不是记住某一段固定代码,而是建立正确的地理数据切片思路:先查看数据结构,再确认变量、维度、坐标和 CRS,最后用 sel 按真实时间和经纬度范围切片。
对于 GIS 读者来说,最实用的流程可以概括为:
- 用
xr.open_dataset打开数据 - 用
print(ds)检查变量、维度和坐标 - 确认纬度方向和经度范围
- 用
sel按时间和空间范围切片 - 用绘图或统计检查结果
- 需要进入 GIS 软件时,用
rioxarray导出 GeoTIFF
只要把这套流程固定下来,地理数据怎么切片就不再是靠试错,而是一个可以稳定复现的多维数组处理工作流。