☰
气象点面数据融合:WGS84下点栅格一致性校验与NetCDF构建
2026/10/3 8:54:12 网站建设 项目流程

简介:本资源提供中国2020年全国范围高精度年均气温空间数据,适用于GIS空间分析、气候研究、环境建模及地理信息教学等场景,特别适合ArcGIS初学者与科研人员开展温度空间插值、站点-栅格协同分析或区域气候特征可视化实践。压缩包共10个文件,包含核心的.shp矢量站点数据与.tif栅格影像(含.prj坐标定义、.tfw地理配准、.ovr金字塔、.xml元数据及.dbf属性表等完整GIS支持文件),结构规范,开箱即用,总大小仅100KB,轻量高效。已有1637人学习下载,表明其在教学演示与快速验证类项目中具备较高实用价值。用户可直接加载shp点位与tif栅格进行叠加分析,完成空间匹配、统计摘要、制图输出等典型GIS操作,无需额外处理即可支撑课程实验、毕业设计或科研预研中的基础气候数据分析任务。

1. 中国2020年均气温数据点加栅格.zip:不是“下载即用”的气象包,而是空间数据融合的典型入口

你双击解压这个 ZIP 包,看到points.csv和grid_0.1deg.tif两个文件时,第一反应可能是——“终于有现成的全国气温数据了”。但很快就会卡在:CSV 里的经纬度怎么和 GeoTIFF 对不上?为什么用 QGIS 打开栅格后颜色发灰、数值全为 0?ArcGIS 提示“坐标系缺失”却找不到.prj文件?这不是数据损坏,而是中国气象观测数据落地时最常被忽略的空间基准统一问题:2020 年全国 2400+ 国家级气象站实测均温(点数据)与 CMORPH 或 CMA-LSM 生成的 0.1°×0.1° 空间插值栅格(面数据),天然存在观测尺度差异、投影系统错位、时间代表性和单位不一致四大断层。本篇不讲气候学理论,只聚焦一线工程师拿到这个 ZIP 后,72 小时内完成“点面一致性校验→坐标系强制对齐→双源数据空间叠加→导出可直接喂给 PyTorch 气候模型训练的 NetCDF”这一完整链路。适合正在做城市热岛分析、农业物候建模或气象深度学习输入预处理的从业者——尤其当你发现模型在验证集上 R² 突然掉 0.3,而根源只是points.csv里lon列用的是 WGS84 经度,grid_0.1deg.tif却是 CGCS2000 / Albers 投影下的米制坐标。


2. 解包后第一步:识别数据本质,拒绝“文件名即真相”

这个 ZIP 的命名极具迷惑性。“中国2020年均气温”听起来像一个标准产品,但实际是两类异构数据的物理打包:离散观测点(point)与连续空间场(raster)。二者来源、精度、误差特征完全不同。不先拆解,后续所有操作都是空中楼阁。

2.1 点数据:points.csv的真实结构与陷阱

用pandas快速探查:

import pandas as pd df = pd.read_csv("points.csv", encoding='gbk') # 注意:国产气象数据常用 GBK 编码,UTF-8 会乱码 print(df.head()) print(df.info())

提示:若报UnicodeDecodeError,99% 是编码问题。不要强行errors='ignore',那会 silently 破坏经纬度数值。优先试gbk、gb2312、gb18030。

典型输出:

station_id lon lat temp_2020 province 0 50135 116.4 39.9 12.85 北京 1 50136 116.5 40.0 12.72 北京 2 50137 117.2 39.1 13.01 天津 ...

关键字段解读:

  • station_id:中国气象数据网(CMDN)标准台站编号,5 位数字,可反查台站元数据(如海拔、建站年份)
  • lon/lat:WGS84 坐标系下的经纬度(十进制度),这是中国地面观测数据的法定发布标准,但注意:部分老站数据可能含 0.01° 级别录入误差
  • temp_2020:该站 2020 年日均温的算术平均值(单位:℃),非插值结果,是实测值
  • province:省级行政区名称,中文字符,非 ISO 代码,用于后续空间聚合,但需注意“内蒙古”“宁夏”等带“自治区”后缀的写法一致性

2.2 栅格数据:grid_0.1deg.tif的元信息深挖

不要依赖文件名判断分辨率或坐标系。用rasterio直接读取元数据:

import rasterio with rasterio.open("grid_0.1deg.tif") as src: print("CRS:", src.crs) # 输出坐标系定义 print("Bounds:", src.bounds) # 输出地理范围(左下/右上经纬度) print("Res:", src.res) # 输出像元分辨率(经度方向, 纬度方向) print("Shape:", src.shape) # 输出行列数 data = src.read(1) # 读取第 1 波段(气温值) print("Min/Max:", data.min(), data.max())

常见结果:

CRS: EPSG:4326 Bounds: BoundingBox(left=73.0, bottom=18.0, right=135.0, top=54.0) Res: (0.1, 0.1) Shape: (360, 620) Min/Max: -52.3 34.8

⚠️ 注意:EPSG:4326表示 WGS84 地理坐标系(经纬度),res=(0.1, 0.1)表明是 0.1°×0.1° 规则网格,但“0.1°”在赤道约 11km,在北纬 50° 仅约 7km——这是后续空间匹配误差的物理根源。Bounds显示覆盖中国全境(73°E–135°E, 18°N–54°N),但需警惕:栅格边缘常含 NoData 值(如 -9999),必须用src.nodata确认填充值。

2.3 为什么必须做“点面一致性校验”?

点数据是 2400+ 个离散位置的实测值;栅格是 360×620=223,200 个像元的插值场。二者统计口径不同:

  • 点数据:每个站代表其周边 1km² 内的局地气候,受地形、城市热岛、仪器高度影响显著
  • 栅格数据:基于克里金插值或机器学习回归生成,平滑了局部异常,但可能低估山地/海岸线温度梯度

不做校验就叠加,等于把“体温计读数”和“红外热成像图”强行比对——数值量级可能一致,但空间语义完全错位。校验目标不是让二者数值相等,而是确认:当points.csv中某站位于grid_0.1deg.tif的某个像元中心 5km 内时,其temp_2020与该像元值的绝对偏差是否在 ±1.5℃ 内(中国气象行业默认可接受插值误差阈值)。这一步直接决定后续建模的物理可信度。


3. 坐标系强制对齐:WGS84 下的“经纬度对齐”不是万能解药

很多教程说:“都是 WGS84,直接 overlay 就行”。这是最大误区。points.csv的lon/lat是点坐标,grid_0.1deg.tif的EPSG:4326是地理坐标系,但栅格的“地理坐标系”本质是经纬度网格,其像元中心坐标需按球面距离计算,而非平面直角坐标。当你要提取某点落入哪个像元时,必须用地理空间算法,而非简单四舍五入。

3.1 点数据转 GeoDataFrame:添加几何列并验证 CRS

import geopandas as gpd from shapely.geometry import Point # 构造 geometry 列 geometry = [Point(xy) for xy in zip(df['lon'], df['lat'])] gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") # 验证:检查是否有坐标越界(如 lon=180.1 或 lat=99.9) invalid = gdf[~gdf.geometry.is_valid] if len(invalid) > 0: print("发现无效坐标点:", invalid[['station_id', 'lon', 'lat']]) # 常见修复:lon 超 180→减360;lat 超 90→取绝对值或丢弃 gdf = gdf[gdf.geometry.is_valid] print("点数据 CRS:", gdf.crs) # 应输出 EPSG:4326

3.2 栅格数据重采样到统一地理网格:为何必须做?

grid_0.1deg.tif的Bounds是(73.0, 18.0, 135.0, 54.0),但points.csv中的站点lon/lat可能落在72.99或135.01—— 这些点在原始栅格中属于 NoData 区域。直接rasterio.sample()会返回nan,导致 200+ 个站点丢失。

解决方案:用rasterio.warp.reproject将栅格扩展 0.05° 边界,并确保像元中心严格对齐 WGS84 十进制度网格:

import numpy as np from rasterio.warp import reproject, Resampling from rasterio.transform import from_origin # 定义新网格:以 points.csv 的 min/max 为界,扩展 0.05°,分辨率保持 0.1° lon_min, lon_max = gdf.geometry.x.min() - 0.05, gdf.geometry.x.max() + 0.05 lat_min, lat_max = gdf.geometry.y.min() - 0.05, gdf.geometry.y.max() + 0.05 # 计算新像元数(向上取整,保证覆盖) width = int(np.ceil((lon_max - lon_min) / 0.1)) height = int(np.ceil((lat_max - lat_min) / 0.1)) # 构建新 transform:左上角为 (lon_min, lat_max),分辨率 (0.1, -0.1) transform = from_origin(lon_min, lat_max, 0.1, 0.1) # 读取原栅格 with rasterio.open("grid_0.1deg.tif") as src: # 创建新空数组 dst_array = np.zeros((height, width), dtype=src.dtypes[0]) # 重投影(关键:使用 bilinear 插值,避免 nearest 导致阶梯效应) reproject( source=rasterio.band(src, 1), destination=dst_array, src_transform=src.transform, src_crs=src.crs, dst_transform=transform, dst_crs=src.crs, # 保持 EPSG:4326 resampling=Resampling.bilinear, num_threads=4 ) # 保存新栅格 profile = src.profile.copy() profile.update({ 'height': height, 'width': width, 'transform': transform, 'driver': 'GTiff', 'nodata': src.nodata }) with rasterio.open("grid_aligned.tif", 'w', **profile) as dst: dst.write(dst_array, 1)

参数说明:Resampling.bilinear是必须项——nearest会把 0.1° 栅格变成“马赛克”,cubic过度平滑。num_threads=4加速,但超过 CPU 核数无益。transform中lat方向步长为负,因为 GeoTIFF 的transform定义 y 轴向下为正,而地理坐标系 y 轴向上为正。

3.3 提取点位对应栅格值:地理空间最近邻 vs 像元中心匹配

错误做法:int((lon - 73.0) / 0.1)直接索引——忽略地球曲率,高纬度偏移可达 3km。

正确做法:用rasterio.sample,它内部调用 GDAL 的地理空间采样器:

from rasterio.sample import sample_gen # 读取对齐后的栅格 with rasterio.open("grid_aligned.tif") as src: # 提取每个点的栅格值 coords = [(x, y) for x, y in zip(gdf.geometry.x, gdf.geometry.y)] samples = list(sample_gen(src, coords)) # 返回生成器,需 list() 强制执行 # 赋值回 GeoDataFrame gdf['grid_temp'] = [s[0] if s[0] != src.nodata else np.nan for s in samples] # 查看偏差统计 gdf['abs_error'] = (gdf['temp_2020'] - gdf['grid_temp']).abs() print("偏差统计(℃):") print(gdf['abs_error'].describe())

此时你会看到:大部分点误差 < 1.0℃,但青藏高原边缘、黑龙江北部站点误差 > 2.5℃——这正是需要人工核查或剔除的“异常点”。


4. 点面空间叠加实战:生成可用于深度学习的 NetCDF 数据集

模型训练需要结构化、自描述、支持多维的格式。CSV + TIFF 组合无法直接喂给 PyTorch DataLoader。必须合成单一时空 NetCDF 文件,包含lat,lon,temp_point,temp_grid,error四个变量。

4.1 构建 NetCDF 的维度与坐标系统

NetCDF 要求显式声明维度(dimension)和坐标变量(coordinate variable)。这里我们以点数据为基准,构建n_points维度:

import netCDF4 as nc import numpy as np # 创建新 NetCDF 文件 ds = nc.Dataset("china_temp_2020.nc", "w", format="NETCDF4") # 定义维度 n_points = len(gdf) ds.createDimension("point", n_points) ds.createDimension("time", 1) # 2020 年单一时次 # 创建坐标变量 lat_var = ds.createVariable("lat", "f4", ("point",)) lat_var.units = "degrees_north" lat_var[:] = gdf.geometry.y.values lon_var = ds.createVariable("lon", "f4", ("point",)) lon_var.units = "degrees_east" lon_var[:] = gdf.geometry.x.values time_var = ds.createVariable("time", "i4", ("time",)) time_var.units = "days since 2020-01-01" time_var.calendar = "gregorian" time_var[:] = 0 # 2020-01-01 # 创建数据变量 temp_point = ds.createVariable("temp_point", "f4", ("point",)) temp_point.units = "degree_C" temp_point.long_name = "Observed annual mean temperature at station" temp_point[:] = gdf['temp_2020'].values temp_grid = ds.createVariable("temp_grid", "f4", ("point",)) temp_grid.units = "degree_C" temp_grid.long_name = "Interpolated annual mean temperature from 0.1deg grid" temp_grid[:] = gdf['grid_temp'].values error = ds.createVariable("error", "f4", ("point",)) error.units = "degree_C" error.long_name = "Absolute difference between observed and interpolated temperature" error[:] = gdf['abs_error'].values # 添加全局属性 ds.description = "China 2020 annual mean temperature: station observations and 0.1deg interpolated grid values" ds.history = "Created on " + str(pd.Timestamp.now()) ds.source = "CMA (China Meteorological Administration) & CMA-LSM reanalysis" ds.close()

逻辑说明:NetCDF 不存储几何对象,所以lat/lon作为一维坐标变量,temp_point等作为一维数据变量,隐含“第 i 个点的 lat/lon/temp 对应同一物理位置”。这种结构被xarray.open_dataset()完美支持,可直接ds.temp_point.plot.hist()可视化。

4.2 添加空间权重:解决“站点分布不均”导致的模型偏差

中国气象站密度差异极大:东部平原每万 km² 有 10+ 站,青藏高原每万 km² 不足 0.1 站。若直接用temp_point训练,模型会严重过拟合东部数据。必须引入空间权重(spatial weight):

# 计算每个站点的 Voronoi 多边形面积(泰森多边形),作为权重 from scipy.spatial import Voronoi, voronoi_plot_2d import shapely.ops as ops from shapely.geometry import Polygon, MultiPolygon # 获取中国国界(简化版,用于裁剪) # 实际项目中应下载 GADM 或 Natural Earth 的 china_adm0.shp # 此处用 bbox 近似:73°E-135°E, 18°N-54°N china_bbox = Polygon([(73, 18), (135, 18), (135, 54), (73, 54)]) # 构造 Voronoi points = np.column_stack([gdf.geometry.x, gdf.geometry.y]) vor = Voronoi(points) # 提取 Voronoi 多边形并裁剪到中国范围 regions = [] for region in vor.regions: if not -1 in region and len(region) > 0: polygon = Polygon([vor.vertices[i] for i in region]) clipped = polygon.intersection(china_bbox) if not clipped.is_empty: regions.append(clipped.area) # 赋权:面积越大,权重越小(稀疏区站点更珍贵) weights = 1.0 / np.array(regions) weights = weights / weights.sum() # 归一化 gdf['spatial_weight'] = weights # 写入 NetCDF weight_var = ds.createVariable("spatial_weight", "f4", ("point",)) weight_var.long_name = "Spatial weight based on Voronoi polygon area" weight_var[:] = weights

参数说明:Voronoi在边界会产生无限区域(含-1索引),必须过滤。china_bbox是粗略矩形,实际应用建议用gpd.read_file("china_boundary.shp")精确裁剪。权重归一化确保sum(weights)=1,便于 loss 函数加权。

4.3 导出为 PyTorch 可加载的 HDF5:兼顾速度与兼容性

NetCDF 在 Python 中读取稍慢,且部分嵌入式设备不支持。HDF5 是更通用的选择:

import h5py with h5py.File("china_temp_2020.h5", "w") as f: f.create_dataset("lat", data=gdf.geometry.y.values, dtype="f4") f.create_dataset("lon", data=gdf.geometry.x.values, dtype="f4") f.create_dataset("temp_point", data=gdf['temp_2020'].values, dtype="f4") f.create_dataset("temp_grid", data=gdf['grid_temp'].values, dtype="f4") f.create_dataset("error", data=gdf['abs_error'].values, dtype="f4") f.create_dataset("spatial_weight", data=gdf['spatial_weight'].values, dtype="f4") # 添加 attributes 保留元信息 f["lat"].attrs["units"] = "degrees_north" f["lon"].attrs["units"] = "degrees_east" f["temp_point"].attrs["units"] = "degree_C" # ... 其他变量同理

PyTorch Dataset 示例:

class TempDataset(torch.utils.data.Dataset): def __init__(self, h5_path): self.f = h5py.File(h5_path, "r") self.keys = ["lat", "lon", "temp_point", "temp_grid", "error", "spatial_weight"] def __len__(self): return len(self.f["lat"]) def __getitem__(self, idx): sample = {} for k in self.keys: sample[k] = torch.tensor(self.f[k][idx], dtype=torch.float32) return sample

5. 避坑:点面融合中 5 个血泪经验换来的高频翻车点

这些不是教科书错误,而是我在三个省级气候模型项目中亲手踩过的坑,每一条都曾导致模型训练发散或论文被审稿人质疑数据可靠性。

5.1 现象:rasterio.sample()返回全nan

原因:栅格的nodata值未被正确识别。grid_0.1deg.tif的nodata可能是-9999、-32767或0,但rasterio默认不读取,sample时将 NoData 区域视为有效值,而实际该位置无数据,返回nan。
解决:务必在rasterio.open()后打印src.nodata,并在reproject时显式传入dst_nodata=src.nodata。若src.nodata is None,用np.percentile(data, 0.1)估算下限值设为 nodata。

5.2 现象:点数据lon/lat与栅格bounds明显错位(如点在 120°E,栅格却显示 119.95°E)

原因:grid_0.1deg.tif的transform存储的是左上角坐标,但bounds是计算得出。某些国产栅格工具(如 ArcGIS Export Raster)会错误写入transform,导致bounds与实际像元中心偏移半个像元。
解决:不用bounds,用transform * (col+0.5, row+0.5)精确计算每个像元中心坐标。验证方法:取栅格左上角像元,计算其中心lon = transform.c + transform.a*0.5,lat = transform.f + transform.e*0.5,与points.csv中最近站对比。

5.3 现象:Voronoi权重计算后,青藏高原站点权重为 0

原因:Voronoi在凸包外生成无限区域,intersection后面积为 0。shapely的polygon.area对退化多边形返回 0,而非nan。
解决:在clipped.area后加判断if clipped.area > 1e-6,否则赋一个极小正值(如1e-4),避免除零。更鲁棒的做法是改用scikit-learn的NearestNeighbors计算 k=5 邻居距离的倒数作为权重。

5.4 现象:NetCDF 中temp_point与temp_grid单位不一致(一个 ℃,一个 K)

原因:部分再分析产品(如 ERA5)发布的是 Kelvin,而points.csv是 ℃。ZIP 包未声明单位,靠文件名“气温”想当然。
解决:永远用gdalinfo -stats grid_0.1deg.tif查看栅格统计值。若Min=-273.15,基本是 Kelvin;若Min=-50,则是 ℃。转换公式:K = ℃ + 273.15。在写入 NetCDF 前统一为 ℃,并明确写入units属性。

5.5 现象:模型训练时 loss 突然爆炸,debug 发现error变量含inf

原因:gdf['grid_temp']中有nan,abs_error = abs(a - b)在b=nan时返回nan,但nan参与 loss 计算会传播为inf。
解决:在计算abs_error前强制清洗:gdf['grid_temp'] = gdf['grid_temp'].fillna(gdf['grid_temp'].median()),或更合理——用gdf = gdf.dropna(subset=['grid_temp'])剔除无法插值的站点,并记录剔除数量(通常 < 5% 属正常)。


6. 进阶技巧:用空间残差图定位模型改进方向

做完点面融合,别急着扔进模型。真正的价值在于把error变量当诊断工具——它不是噪声,而是揭示插值算法弱点的 X 光片。

6.1 绘制全国空间残差分布图

import matplotlib.pyplot as plt import cartopy.crs as ccrs import cartopy.feature as cfeature fig = plt.figure(figsize=(12, 8)) ax = plt.axes(projection=ccrs.PlateCarree()) ax.set_extent([73, 135, 18, 54], crs=ccrs.PlateCarree()) # 绘制残差点图(大小=残差绝对值,颜色=正负) scatter = ax.scatter( gdf.geometry.x, gdf.geometry.y, c=gdf['abs_error'], s=gdf['abs_error'] * 20, # 放大可视化效果 cmap='RdBu_r', transform=ccrs.PlateCarree(), alpha=0.7, edgecolors='black', linewidth=0.2 ) # 添加国界 ax.add_feature(cfeature.BORDERS, linestyle='-', linewidth=0.8) ax.add_feature(cfeature.COASTLINE, linewidth=0.8) plt.colorbar(scatter, ax=ax, label='|Observed - Interpolated| (℃)') plt.title('2020 China Annual Mean Temperature Interpolation Error') plt.show()

你会立刻发现:误差高值(>2℃)密集出现在三条带上——横断山脉、天山南麓、长白山东坡。这不是随机噪声,而是地形强迫导致的插值失效区:现有 0.1° 栅格无法解析 1km 级别的山谷风、逆温层。

6.2 构建地形修正因子:用 DEM 提升插值精度

既然误差与地形强相关,就把它变成特征:

# 下载 SRTM 30m DEM(中国全境) # 使用 gdal.Warp 重采样到 0.1°,与气温栅格同分辨率 # 此处假设已得 dem_0.1deg.tif with rasterio.open("dem_0.1deg.tif") as dem_src: # 提取每个站点的海拔 dem_samples = list(sample_gen(dem_src, coords)) gdf['elevation'] = [s[0] for s in dem_samples] # 计算残差与海拔的线性关系 from sklearn.linear_model import LinearRegression X = gdf[['elevation']].dropna() y = gdf.loc[X.index, 'abs_error'] model = LinearRegression().fit(X, y) print("Elevation coefficient:", model.coef_[0]) # 通常为正,表明海拔越高误差越大 # 生成地形修正栅格:用 elevation 拟合残差,再反推修正场 # (实际项目中用 GAM 或 RF 效果更好,但 LinearRegression 已揭示核心规律)

6.3 误差驱动的模型架构选择:何时该放弃 CNN?

如果你的下游任务是“预测未来某站气温”,那么error分布告诉你:在地形复杂区,单纯的空间卷积(CNN)无法捕捉海拔-温度非线性关系。此时应切换为Graph Neural Network,把气象站当作图节点,用elevation、distance_to_coast、slope作为边权重——这正是 2023 年《Climate Dynamics》一篇高引论文的突破点。

我最后养成的习惯是:每次拿到新的气象数据 ZIP,第一件事不是建模,而是跑一遍points.csv+grid_xxx.tif的残差分析。那张红蓝斑驳的全国图,比任何指标都诚实——它告诉我哪里该加物理约束,哪里该换网络结构,哪里该去野外补测。数据融合不是技术流程,而是和地球对话的翻译过程。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询