GWR与克里金组合建模:破解AQI空间异质性预测难题
简介:本资源是一份面向地理信息科学、环境统计与Python空间分析初学者的课程论文实践资料,聚焦于利用地理加权回归(GWR)模型与克里金插值法联合预测空气质量指数,解决传统全局回归难以刻画空间异质性、监测点稀疏导致空间连续表达不足等实际问题。压缩包为1个1.08MB的docx文档,完整包含引言、研究方法(含GWR数学推导与带宽优化策略、克里金变异函数建模)、GWR与克里金双案例分析、结果可视化及附录——其中附录1至3分别提供GWR建模、克里金插值与地图可视化的可运行Python源代码,代码配有注释且与正文公式、参数设置严格对应。文档目录结构清晰,涵盖系数显著性检验、CV/AICc带宽选择、残差空间分布评估等关键环节,便于读者理解模型原理并复现全流程。目前已有1256人学习下载,适合GIS专业学生开展课程设计、科研入门或空间建模能力提升。
1. 为什么用GWR+克里金不是“叠Buff”,而是解决AQI空间异质性的刚需?
你手头有一堆城市/站点的PM2.5、NO₂、O₃实测数据,想预测下一个时间点某块未布点区域的空气质量指数(AQI)——比如开发区新建住宅区、地铁施工沿线、或城郊结合部的农田上空。这时候,如果直接扔进一个全局回归模型(比如普通线性回归或随机森林),大概率会翻车:模型在市中心拟合得挺好,到了郊区就偏差翻倍;上午预测准,下午误差暴涨;甚至同一片工业园区,东侧厂区和西侧物流园的污染响应机制完全不同。这不是数据噪声大,而是AQI背后的空间过程本身就不均匀——风速风向随地形突变、排放源类型混杂、下垫面粗糙度差异大、污染物化学转化速率随光照/湿度非线性变化……这些都让“一个模型打天下”成了玄学。
GWR(地理加权回归)和克里金法(Kriging)组合,恰恰是为这种空间非平稳性+观测稀疏性双难题而生的:GWR不强行假设所有地方服从同一套系数,它给每个预测点“定制”一套局部回归参数;克里金则不把插值当成数学拟合,而是基于空间自相关结构,给出带置信区间的最优无偏估计。二者不是简单串联,而是分层解耦——GWR先剥离掉已知协变量(如车流量、绿地率、工业用地占比)对AQI的局地影响,剩下残差;克里金再对这些残差做空间建模,补上GWR没吃透的纯空间依赖。2023年《Atmospheric Environment》有篇实证指出,在京津冀城市群127个监测站数据上,GWR-克里金混合模型比单纯GWR降低RMSE 22.7%,比普通克里金降低34.1%。这不是炫技,而是当你面对真实城市尺度的AQI预测任务时,绕不开的工程级解法。本文就带你从零跑通这个流程:不用调参玄学,不靠黑匣子API,只用Python生态里最稳的几个包,把每一步的坐标系、带宽选择、变异函数拟合、残差诊断全摊开讲透。
2. GWR建模:从空间权重矩阵到局部系数,关键在带宽与坐标系
GWR的核心不是“加个地理坐标就能跑”,而是让每个预测点只被它周围一定范围内的样本影响,且影响强度随距离衰减。这个“一定范围”就是带宽(bandwidth),它决定了模型是过度拟合(带宽太小,每个点只看自己)还是欠拟合(带宽太大,退化成全局回归)。选错带宽,后面所有步骤都是白忙。
2.1 坐标系统一与空间邻接关系构建
GWR对坐标系极其敏感。如果你的原始数据用的是WGS84经纬度(EPSG:4326),直接计算欧氏距离会严重失真——赤道1度≈111km,北纬40°时1度≈85km,高纬度地区会被系统性低估空间影响半径。必须先投影到等距坐标系:
import geopandas as gpd from pyproj import CRS # 假设原始数据是GeoDataFrame,含geometry列(WGS84) gdf = gpd.read_file("aqi_stations.shp") # 投影到UTM Zone 50N(适用于中国东部,如北京、上海) gdf_proj = gdf.to_crs(CRS.from_epsg(32650)) # EPSG:32650 = UTM 50N # 提取x, y坐标用于GWR coords = np.array(list(gdf_proj.geometry.centroid.apply(lambda p: (p.x, p.y))))提示:UTM带号必须按实际经度选择(东经114°~120°用50N,120°~126°用51N),不能硬套。用
pyproj.CRS.from_string("+proj=utm +zone=50 +north +ellps=WGS84")比硬写EPSG更可控。
2.2 带宽选择:AICc最小化不是终点,要叠加残差空间自相关检验
mgwr库提供Sel_BW自动选带宽,但默认只优化AICc(校正赤池信息量),容易忽略残差的空间结构。正确做法是:先用AICc初筛,再用Moran's I检验残差是否还存在显著空间自相关(p<0.05说明带宽不够,局部模式没挖干净):
from mgwr.sel_bw import Sel_BW from mgwr.gwr import GWR from esda.moran import Moran # 协变量X(如:车流量、绿地率、工业用地占比),因变量y(AQI) X = df[['traffic', 'green_ratio', 'industrial_ratio']].values y = df['aqi'].values # 初筛带宽(AICc最小) sel_bw = Sel_BW(coords, y, X) bw_init = sel_bw.search(bw_min=1000, bw_max=50000, multi_bw=True) # 单位:米(因已投影) # 拟合GWR模型 gwr_results = GWR(coords, y, X, bw_init, fixed=False, kernel='bisquare').fit() # 检验残差空间自相关 moran_res = Moran(gwr_results.resid, gwr_results.w) print(f"Residual Moran's I: {moran_res.I:.4f}, p-value: {moran_res.p_sim:.4f}") # 若p<0.05,手动收紧带宽重试(例如bw_init * 0.8),直到p>0.05 if moran_res.p_sim < 0.05: bw_tight = bw_init * 0.8 gwr_tight = GWR(coords, y, X, bw_tight, fixed=False, kernel='bisquare').fit() moran_tight = Moran(gwr_tight.resid, gwr_tight.w) print(f"Tightened bandwidth {bw_tight:.0f}m -> Moran's I p={moran_tight.p_sim:.4f}")参数说明:
bw_min/bw_max:单位必须是投影坐标系的长度单位(米),不是度;kernel='bisquare':比高斯核更鲁棒,能抑制远距离异常点干扰;fixed=False:使用自适应带宽(每个点邻居数固定),比固定距离带宽更适合站点分布不均的场景(如城区密、郊区疏)。
2.3 局部系数解读与可视化:别只看R²,要看系数符号翻转区
GWR输出的gwr_results.params是n×k矩阵(n个站点,k个协变量),每个站点对应一套系数。重点看两点:
- 系数符号一致性:比如“绿地率”系数在90%站点为负(绿地越多AQI越低),但在某工业区却为正——这提示该区域绿地可能被高污染源包围,形成“绿岛效应”假象,需实地核查;
- 系数变异程度:计算每个协变量系数的标准差/均值比(CV),CV>0.5说明该变量影响高度局地化,不能用全局值替代。
import matplotlib.pyplot as plt import seaborn as sns # 可视化“车流量”系数空间分布 coeff_traffic = gwr_results.params[:, 0] # 假设车流量是第0列 gdf_proj['coeff_traffic'] = coeff_traffic fig, ax = plt.subplots(1, 1, figsize=(10, 8)) gdf_proj.plot(column='coeff_traffic', cmap='RdBu_r', legend=True, ax=ax, scheme='quantiles', k=5, legend_kwds={'loc':'lower left'}) ax.set_title('Local Coefficient of Traffic Volume on AQI') plt.show() # 计算变异系数 cv_traffic = np.std(coeff_traffic) / np.abs(np.mean(coeff_traffic)) print(f"Traffic coefficient CV: {cv_traffic:.3f}")3. 克里金残差建模:变异函数拟合不是调参游戏,是地质统计学基本功
GWR残差不是白噪声,它携带了未被协变量解释的纯空间结构——比如某个山谷常年积聚污染物,或某条河岸因湿度高导致二次气溶胶生成增强。克里金正是用来建模这种结构。但很多人卡在变异函数(semivariogram)拟合上:软件自动拟合的球状模型参数常不合理,导致插值结果发散。
3.1 残差提取与空间结构初判
先确认GWR残差确实存在空间自相关(前文Moran's I已做),再画实验变异函数图,观察三个关键参数雏形:
from skgstat import Variogram # 提取GWR残差(注意:用gwr_tight.resid,即最终选定带宽的残差) residuals = gwr_tight.resid # 计算实验变异函数(最大距离设为站点间最大距离的1/3,避免远距离噪声) max_dist = np.max(cdist(coords, coords)) * 0.33 V = Variogram(coordinates=coords, values=residuals, maxlag=max_dist, n_lags=15, estimator='matheron') # 绘制实验变异函数 V.plot() plt.title('Experimental Semivariogram of GWR Residuals') plt.show() # 打印关键特征值(供后续拟合参考) print(f"Empirical sill: {V.sill:.4f}") # 总方差,决定插值波动幅度 print(f"Empirical range: {V.range:.0f}m") # 空间自相关有效距离(单位:米!) print(f"Empirical nugget: {V.nugget:.4f}") # 纯随机噪声,应< sill的20%注意:
maxlag必须用投影坐标系下的米制距离,否则变异函数形状会畸变。若nugget/sill > 0.2,说明残差中仪器误差或瞬时气象扰动占比过高,需检查GWR是否漏掉关键协变量(如当日温度、相对湿度)。
3.2 变异函数模型选择与参数约束拟合
skgstat支持球状、指数、高斯三种模型,但球状模型(Spherical)在环境变量中适用性最强——它有明确的变程(range)和基台值(sill),物理意义清晰。关键是要人工约束参数范围,避免自动拟合陷入局部最优:
from skgstat import OrdinaryKriging # 手动设定初始参数(基于实验变异函数观察) initial_params = { 'sill': V.sill * 0.9, # 基台值略低于实验值,留出拟合余量 'range': V.range * 1.2, # 变程略放大,防止过拟合短距离结构 'nugget': V.nugget * 1.1 # 微调块金值 } # 使用球状模型拟合(指定model='spherical') V_fitted = Variogram( coordinates=coords, values=residuals, model='spherical', maxlag=max_dist, n_lags=15, # 强制参数在合理区间内搜索 fit_method='trf', # trust-region reflective algorithm,比默认leastsq更稳 fit_sigma=np.ones(len(V.bins)) * 0.05, # 给每个lag bin赋相同权重 **initial_params ) # 验证拟合质量:R² > 0.85才可信 print(f"Fitted model R²: {V_fitted.R2:.4f}") # 可视化拟合效果 V_fitted.plot() plt.title('Fitted Spherical Semivariogram') plt.show()参数说明:
fit_method='trf':比默认leastsq更擅长处理带约束的非线性拟合;fit_sigma:避免远距离lag bin因样本少而主导拟合,统一权重更公平;- 若
R² < 0.8,退回检查残差——可能是GWR没建好(带宽不对/协变量缺失),而非克里金问题。
3.3 残差克里金插值:网格分辨率与边界效应控制
插值不是分辨率越高越好。城市尺度AQI预测,1km×1km网格已足够(对应约100m精度),再细会放大噪声。同时必须处理边界效应——预测区域外的残差无法观测,但克里金需要邻域信息:
# 定义预测网格(覆盖整个研究区,缓冲区+10km防边界截断) bounds = gdf_proj.total_bounds # [minx, miny, maxx, maxy] x_grid = np.arange(bounds[0]-10000, bounds[2]+10000, 1000) # 1km步长 y_grid = np.arange(bounds[1]-10000, bounds[3]+10000, 1000) xx, yy = np.meshgrid(x_grid, y_grid) grid_points = np.column_stack([xx.ravel(), yy.ravel()]) # 执行普通克里金(Ordinary Kriging),因残差均值已接近0 OK = OrdinaryKriging( coords, residuals, variogram_model='spherical', variogram_parameters={ 'sill': V_fitted.sill, 'range': V_fitted.range, 'nugget': V_fitted.nugget }, nlags=15, weight=True, verbose=False ) # 预测残差 residual_pred, residual_var = OK.execute('grid', x_grid, y_grid) # 转为GeoDataFrame便于后续叠加 grid_gdf = gpd.GeoDataFrame( {'residual_pred': residual_pred.ravel()}, geometry=gpd.points_from_xy(xx.ravel(), yy.ravel()), crs=gdf_proj.crs )提示:
weight=True启用距离加权,比默认False(等权重)更能抑制远距离点干扰;nlags=15确保变异函数拟合充分,少于10易失真。
4. 预测集成与不确定性量化:GWR预测值 + 克里金残差 = 完整AQI场
GWR给出每个预测点的期望AQI(基于协变量),克里金给出该点残差的期望值和方差。二者相加得到最终预测,而残差方差则转化为AQI预测的不确定性带——这才是决策者真正需要的。
4.1 GWR预测新位置:必须用相同带宽与坐标系
预测新点(如未布点区域)时,GWR需要重新计算该点的局部权重矩阵。关键:必须用训练阶段确定的最优带宽(bw_tight),且新点坐标必须与训练坐标同投影系:
# 新预测点坐标(已投影到EPSG:32650) new_coords = np.array([[395000, 4420000], [396000, 4421000]]) # 示例两个点 # 提取新点的协变量(需与训练数据同源、同处理逻辑) X_new = np.array([ [1200, 0.15, 0.08], # traffic, green_ratio, industrial_ratio [800, 0.35, 0.02] ]) # GWR预测(使用训练好的bw_tight) gwr_pred = gwr_tight.predict(new_coords, X_new) print(f"GWR-predicted AQI: {gwr_pred.prediction}") # 注意:gwr_tight.predict()返回对象含.prediction(均值)、.se_prediction(标准误)4.2 克里金残差预测与不确定性合成
克里金不仅给出残差均值,还给出预测方差(residual_var)。将GWR预测均值与克里金残差均值相加,GWR标准误与克里金方差开方后按误差传播律合成总标准误:
# 对新点执行克里金残差预测 residual_pred_new, residual_var_new = OK.execute('points', new_coords[:, 0], new_coords[:, 1]) # 合成最终AQI预测 aqi_pred = gwr_pred.prediction + residual_pred_new # 合成总标准误(假设GWR残差与克里金残差独立) aqi_std = np.sqrt(gwr_pred.se_prediction**2 + residual_var_new) # 构建95%置信区间 aqi_lower = aqi_pred - 1.96 * aqi_std aqi_upper = aqi_pred + 1.96 * aqi_std # 输出结果表 results_df = pd.DataFrame({ 'x': new_coords[:, 0], 'y': new_coords[:, 1], 'aqi_pred': aqi_pred, 'aqi_lower': aqi_lower, 'aqi_upper': aqi_upper, 'uncertainty_width': aqi_upper - aqi_lower }) print(results_df.round(2))关键逻辑:
residual_var_new是克里金预测的方差,直接开方即标准误;- GWR的
se_prediction是模型参数不确定性+抽样不确定性,已包含在gwr_pred.se_prediction中; - 二者相加时,总方差 = GWR方差 + 克里金方差(因残差建模独立于GWR拟合)。
4.3 空间AQI场可视化:用不确定性带指导监测布点
最终输出不仅是点预测,更是连续空间场。用matplotlib叠加不确定性带,能直观发现“高预测值+高不确定性”区域——这些正是急需补充监测的盲区:
# 将预测网格转为GeoDataFrame并加入不确定性 grid_gdf['aqi_pred'] = gwr_pred_grid + residual_pred # gwr_pred_grid需提前用相同方法计算 grid_gdf['uncertainty'] = np.sqrt(gwr_pred_var_grid + residual_var) # 同理 # 绘制AQI预测热力图 + 不确定性轮廓线 fig, ax = plt.subplots(1, 2, figsize=(16, 6)) # 左图:AQI预测值 grid_gdf.plot(column='aqi_pred', cmap='YlOrRd', legend=True, ax=ax[0], scheme='quantiles', k=5, legend_kwds={'title': 'Predicted AQI'}) ax[0].set_title('GWR-Kriging Predicted AQI') # 右图:不确定性(标准误)等值线 grid_gdf.plot(column='uncertainty', cmap='Blues', ax=ax[1], legend=True, scheme='quantiles', k=5, legend_kwds={'title': 'Prediction Uncertainty (Std)'}) # 添加等值线(突出高不确定性区) contours = ax[1].contour(xx, yy, residual_var.reshape(xx.shape), levels=[np.percentile(residual_var, 75)], colors='red', linestyles='dashed') ax[1].clabel(contours, inline=True, fontsize=10, fmt='High Uncertainty') plt.tight_layout() plt.show()提示:图中红色虚线圈出的区域,就是模型“最没把握”的地方——可能因缺乏该类下垫面(如新建湿地公园)的训练样本,或受未建模因素(如突发扬尘)影响。把这些区域列为下一轮监测点优先级最高项,比盲目加密站点更高效。
5. 避坑指南:GWR-克里金组合落地中最常踩的5个坑
GWR和克里金单独用都有成熟教程,但二者组合时,很多坑是单模型文档里根本不会提的。以下是我在京津冀、长三角、成渝三大城市群项目中血泪总结的5个高频翻车点,每个都附带现场诊断命令和修复动作。
5.1 坑1:坐标系混用导致带宽单位错乱,模型完全失效
现象:Sel_BW返回的带宽数值巨大(如1e6),拟合后残差Moran's I极显著(p<0.001),但地图上系数分布毫无规律。
原因:训练数据用WGS84经纬度,但Sel_BW默认按欧氏距离计算,把0.01度当0.01米用,带宽被放大10万倍。
解决:
- 用
gdf.crs确认原始CRS; - 用
gdf.to_crs(epsg=326XX)强制投影(XX按经度查UTM带号); - 用
np.linalg.norm(coords[0] - coords[1])验证两点间距离是否符合常识(如城区站点应≈500~5000米)。
5.2 坑2:克里金变异函数拟合R²低,但强行用导致插值发散
现象:V_fitted.R2 < 0.7,插值结果出现大片负值(AQI不可能为负)或剧烈震荡斑块。
原因:残差本身空间结构弱(如GWR已解释95%方差),或存在方向性各向异性(如沿主干道呈带状相关),球状模型无法捕捉。
解决:
- 先做
V.plot()看实验变异函数是否平缓上升——若前3个lag bin就达基台,说明残差几乎无空间结构,直接放弃克里金,用GWR残差均值填充; - 若存在明显方向性,改用
DirectionalVariogram(skgstat支持),指定方位角(如主干道走向)。
5.3 坑3:预测新点时GWR报错“singular matrix”,无法计算局部权重
现象:gwr_tight.predict(new_coords, X_new)抛出LinAlgError: Singular matrix。
原因:新点周围没有足够训练样本(尤其在郊区),导致权重矩阵秩亏。
解决:
- 检查
new_coords是否落在训练站点凸包内——用from scipy.spatial import ConvexHull; hull = ConvexHull(coords); new_in_hull = ...; - 若在外,改用
fixed=True(固定距离带宽)并增大bw_tight(如×1.5),或添加最近K个站点(K≥3)的GWR预测加权平均。
5.4 坑4:克里金插值结果整体偏移,与GWR预测值不匹配
现象:residual_pred均值不为0(如均值=+5.2),导致最终AQI系统性偏高。
原因:GWR残差未中心化(理论上均值应≈0),但实际因带宽选择或协变量缺失有偏移。
解决:
- 强制中心化:
residuals_centered = residuals - np.mean(residuals); - 用
residuals_centered重拟合变异函数; - 克里金预测后,再加回
np.mean(residuals)作为系统偏移修正项。
5.5 坑5:不确定性带宽度远超AQI合理范围,失去决策价值
现象:aqi_upper - aqi_lower > 100(AQI满分为500,但日常波动<50),置信区间宽到无法区分优/良/轻度污染。
原因:GWR标准误gwr_pred.se_prediction被低估(因未考虑协变量测量误差),或克里金方差residual_var被高估(因变异函数拟合过松)。
解决:
- 用
bootstrap法重估GWR标准误:对训练样本重采样100次,每次拟合GWR,计算新点预测值的标准差; - 克里金方差用
V_fitted.conf_int()获取95%置信区间,取上限值作为保守方差; - 最终不确定性 =
sqrt(bootstrap_se² + V_fitted.conf_int()[1]²)。
6. 进阶技巧:用交叉验证量化GWR-克里金的真实泛化能力
教科书常教“留出法”验证,但对空间数据无效——留出的测试点若集中在某片区,会高估模型在该区性能,掩盖其他区域缺陷。必须用空间块交叉验证(Spatial Block CV),这是近年《Environmental Modelling & Software》推荐的黄金标准。
6.1 构建空间块:用Voronoi图划分互斥训练/测试区
核心思想:把研究区划分为若干空间上不相邻的块,每次留一块为测试集,其余为训练集。Voronoi图天然满足“块间无重叠、覆盖全区域”:
from scipy.spatial import Voronoi, voronoi_plot_2d from sklearn.model_selection import PredefinedSplit # 用站点坐标生成Voronoi图 vor = Voronoi(coords) # 获取每个Voronoi多边形(即每个站点的影响域) regions, vertices = vor.regions, vor.vertices # 将Voronoi多边形转为GeoDataFrame,用于后续空间连接 polygons = [] for region in regions: if not -1 in region and len(region) > 0: polygon = Polygon([vertices[i] for i in region]) polygons.append(polygon) vor_gdf = gpd.GeoDataFrame({'geometry': polygons}, crs=gdf_proj.crs) # 随机分配块标签(0为测试,1为训练),确保块间不邻接 vor_gdf['block_id'] = np.random.choice([0, 1], size=len(vor_gdf), p=[0.2, 0.8]) # 关键:将每个监测站点归属到其Voronoi多边形对应的块 gdf_proj['block_id'] = gpd.sjoin(gdf_proj, vor_gdf, how='left', predicate='within')['block_id'].fillna(0).astype(int) # 构建PredefinedSplit对象(sklearn兼容) ps = PredefinedSplit(gdf_proj['block_id'])6.2 块CV循环:记录每次的RMSE与空间残差分布
from sklearn.model_selection import cross_val_score from sklearn.metrics import mean_squared_error def gwr_kriging_scorer(estimator, X, y): """自定义评分函数:返回RMSE""" # estimator是封装好的GWR-Kriging pipeline y_pred = estimator.predict(X) return -np.sqrt(mean_squared_error(y, y_pred)) # sklearn要求越大越好,故取负 # 封装训练流程为可调用对象 class GWRKrigingPipeline: def __init__(self, bw_opt='aic'): self.bw_opt = bw_opt def fit(self, coords, y, X): # 此处放入前述GWR带宽选择、克里金拟合全流程 # ... return self def predict(self, coords_new, X_new): # 此处放入GWR预测+克里金残差预测 # ... return aqi_pred pipeline = GWRKrigingPipeline() scores = cross_val_score(pipeline, coords, y, cv=ps, scoring=gwr_kriging_scorer, n_jobs=-1) print(f"Spatial Block CV RMSE: {np.abs(scores).mean():.2f} ± {np.abs(scores).std():.2f}")6.3 残差空间模式诊断:用Moran's I散点图定位模型失效区
块CV不仅给一个RMSE数字,更要分析每次验证中残差的空间模式。绘制Moran's I散点图(横轴:预测值,纵轴:残差),能一眼看出模型在哪类AQI水平下最不稳定:
# 收集所有块CV的预测值与残差 all_y_pred, all_y_true, all_residuals = [], [], [] for train_idx, test_idx in ps.split(): # 在train_idx上训练,在test_idx上预测 # ... y_pred_block = pipeline.predict(coords[test_idx], X[test_idx]) y_true_block = y[test_idx] residual_block = y_true_block - y_pred_block all_y_pred.extend(y_pred_block) all_y_true.extend(y_true_block) all_residuals.extend(residual_block) # 绘制Moran's I散点图 plt.figure(figsize=(8, 6)) plt.scatter(all_y_pred, all_residuals, alpha=0.6, s=10) plt.axhline(y=0, color='r', linestyle='--') plt.xlabel('Predicted AQI') plt.ylabel('Residual (True - Predicted)') plt.title('Residual vs Prediction: Spatial Model Diagnostics') plt.grid(True, alpha=0.3) plt.show() # 关键洞察:若残差在AQI>150时系统性为正(点群在横轴上方),说明模型高估重污染日的缓解能力——需加入逆温层厚度、边界层高度等气象协变量。我带过的团队里,凡是跳过块CV直接上线的项目,6个月内必返工——因为真实业务场景中,模型总在“你以为不会出问题”的区域翻车。现在我的习惯是:跑通流程后,第一件事就是画这张残差散点图;第二件事,是把图里最离群的5个点坐标导出,带着平板去现场拍照片、问居民、查当天新闻——模型的缺陷,永远藏在数据之外。希望帮到你。
本文还有配套的精品资源,点击获取