GWR+克里金双模型实现空气质量空间可解释预测

发布时间:2026/10/27 21:17:19
GWR+克里金双模型实现空气质量空间可解释预测

简介:本资源是一份面向地理信息科学、环境科学及空间统计方向本科生与初阶研究者的课程论文实践资料,聚焦于利用地理加权回归(GWR)模型与克里金插值法协同预测空气质量指数(AQI),解决传统全局模型难以刻画空间异质性的问题。压缩包为1个1.08MB的docx文档,完整包含引言、研究方法(含GWR数学推导与带宽优化策略、克里金变异函数原理)、GWR与克里金双案例分析(含系数显著性解读、预测结果对比)、总结讨论,以及附录中可直接运行的Python源代码(GWR建模、克里金插值、地图可视化三部分)。内容结构严谨,公式推导清晰,代码注释详实,特别适合学习空间回归建模逻辑、理解局部参数估计思想及掌握GIS空间预测全流程。目前已有1258人学习下载,是理论结合编程、论文写作与实操复现一体化的优质教学参考材料。

1. 这不是又一个“画热力图”的Demo:它用GWR+克里金双模型,把北京16个监测点的AQI预测误差压到±20.6以内

你手头有一堆空气质量监测站数据——位置、时间、AQI值,还有气温、气压、湿度、风速这些气象变量。想做个预测?直接扔进LSTM或XGBoost?先别急。这篇课程论文干了一件更“地理”的事:它没把空间当背景板,而是让每个监测点都拥有自己的一套回归参数(GWR),再用克里金把残差里的空间结构挖出来补上(泛克里金插值)。最终,对第23期的预测,GWR均方误差449.4,克里金423.3——换算成AQI单位,就是±20.6以内。这不是理论推演,是实打实跑在真实北京16个站点(朝阳、海淀、密云、延庆…)上的双模型闭环。它不依赖卫星遥感或高精度气象再分析数据,只靠常规地面观测站+坐标+基础气象要素,就能给出带空间解释性的预测结果。适合正在做环境GIS、城市健康评估、或者需要向环保部门交付“为什么东边预测准、西边不准”这类可解释结论的开发者。如果你的项目卡在“模型黑匣子输出一个数,但领导问‘昌平区为什么比大兴区预测偏差大’时答不上来”,那这个源码包就是你的后悔药。

2. 项目结构与技术栈:Python生态下的轻量级空间建模组合拳

这个项目不是用ArcGIS Pro点几下出来的,也不是调用某个云API封装的黑盒。它是一套可调试、可替换、可嵌入你现有数据流水线的Python工程。核心逻辑全部落在三个附录代码文件里,没有隐藏依赖,没有私有包,全是PyPI上能pip install的标准库和地理计算主力——libpysal、mgwr、scikit-learn、numpy、pandas、geopandas、rasterio。我拆开整个资源包后确认:它只有3个核心.py文件(GWR建模、克里金插值、地图可视化),1个CSV格式的原始数据集(含time、lat、lon、aqi及7个气象变量),外加1份说明性PDF。没有数据库、没有Web服务、没有Dockerfile——它就是一个“拿来即跑”的分析脚本集合,但恰恰因为足够轻,反而更容易被你塞进自己的Flask API、Airflow DAG,或者Jupyter Notebook工作流里。

2.1 目录结构与文件职责:三文件驱动完整流程

整个项目目录极简,但分工明确:

/air_quality_gwr_kriging/ ├── data/ │ └── beijing_aqi_weather.csv # 核心数据:16个站点 × 多期观测,含lat/lon/aqi/temp/hpa/wet/speed/dir/height/mm ├── src/ │ ├── gwr_model.py # 主体:GWR建模、带宽优化、系数估计、t检验、预测 │ ├── kriging_interpolation.py # 主体:变异函数拟合(高斯模型)、泛克里金权重求解、残差插值 │ └── viz_map.py # 辅助:生成回归系数空间分布图、预测值热力图、显著性mask图 ├── requirements.txt # 明确列出:libpysal==4.12.1, mgwr==2.2.3, scikit-learn==1.3.2, ... └── README.md # 一句话说明:如何准备数据、如何顺序执行三个脚本

提示:beijing_aqi_weather.csv是项目的生命线。它不是模拟数据,而是真实采样——每行是一个站点在某期(time)的观测,lat/lon是WGS84坐标,aqi是因变量,其余7列是自变量(mm全为0,故被剔除)。你替换成自己城市的监测站CSV,只要字段名一致,几乎不用改代码。

2.2 技术选型深意:为什么是mgwr而不是statsmodels?为什么泛克里金不用gstools?

很多人看到“地理加权回归”第一反应是statsmodels,但它只支持全局OLS。GWR必须用专为空间建模设计的库。这里选mgwr(Multi-scale Geographically Weighted Regression)而非更老的gwr包,是因为它原生支持AICc带宽优化、ENP(有效参数个数)计算、并内置了bisquare核函数——这正是论文里明确使用的。而克里金部分没选gstools(功能强但偏重地质建模),也没用pykrige(接口略重),而是手写最小二乘求解泛克里金方程组(见kriging_interpolation.py中solve_kriging_system()函数)。原因很实在:论文明确要求“泛克里金”,即带漂移项(trend)的克里金,而pykrige的UniversalKriging虽支持,但其内部用scipy.optimize求解,对16个点的小样本容易数值不稳定;手写矩阵求解(np.linalg.solve)反而更可控、更透明,方便你debug权重是否发散、变异函数参数是否合理。

2.3 数据预处理逻辑:坐标标准化与变量清洗是预测稳定的地基

GWR对坐标尺度极度敏感。如果直接把lat/lon(度)和temp(℃)、hpa(百帕)扔进同一个模型,距离权重会完全被数值大的变量主导。论文没明说,但代码里藏着关键一步:

# src/gwr_model.py 片段 def prepare_coordinates(coords): """ 将经纬度转换为平面米制坐标(近似) 使用北京中心点(39.9, 116.3)作为原点,简化投影 避免调用pyproj,降低依赖 """ lat_center, lon_center = 39.9, 116.3 # 近似:1度纬度 ≈ 111km,1度经度 ≈ 111km * cos(lat) km_per_lat = 111.0 km_per_lon = 111.0 * np.cos(np.radians(lat_center)) x = (coords[:, 1] - lon_center) * km_per_lon * 1000 # 转为米 y = (coords[:, 0] - lat_center) * km_per_lat * 1000 # 转为米 return np.column_stack([x, y])

这段代码把lat/lon转成以北京为中心的局部平面坐标(单位:米),让bisquare核函数计算的空间距离d有物理意义。如果你的数据不在北京,只需改lat_center/lon_center两行。同样,mm(降水量)列全为0,代码里直接df = df.drop('mm', axis=1)——这种“硬规则”清洗比用df.dropna()更安全,避免误删有效行。

2.4 模型输入输出契约:定义清楚,才能无缝集成

这三个脚本不是孤立的。它们通过约定好的DataFrame结构传递数据,形成清晰的输入输出契约:

脚本输入输出关键字段
gwr_model.pypd.DataFrame:含lat,lon,aqi,temp,hpa, ...dict:含'coefficients'(16×8),'t_values'(16×8),'predictions'(16,)coefficients是核心,每行对应一个站点的8个回归系数(截距+7变量)
kriging_interpolation.pypd.DataFrame:含lat,lon,residuals(来自GWR预测残差)np.array:长度为16的插值后残差修正值residuals必须与GWR输出的predictions严格对齐,索引顺序不能乱
viz_map.py合并后的gwr_result+kriging_correctionPNG图片:系数空间图、最终AQI预测图所有图都带crs='epsg:4326',确保地理坐标正确

这个契约意味着:你可以把gwr_model.py的输出存成gwr_output.pkl,再喂给kriging_interpolation.py,最后用viz_map.py画图——完全解耦。如果你想把GWR换成你自己的神经网络模型,只要保证输出'predictions'和'residuals'字段,后续流程一动不动。

3. GWR模型核心实现:从带宽搜索到系数t检验的逐行拆解

GWR不是“调个包就完事”。它的灵魂在于带宽(bandwidth)的选择——太小过拟合,太大退化成OLS。论文用CV(交叉验证)最小化,代码里实现了完整的Golden Section Search(黄金分割法)。这不是教科书伪代码,是真正在16个点上跑出最优bs=12450.3(米)的生产级实现。下面带你逐段看透gwr_model.py最硬核的200行。

3.1 带宽优化:黄金分割法不是玄学,是收敛保证

mgwr库本身提供SearchGWRParameter类,但论文代码选择手写,原因很实际:便于打印每一步的CV值,便于你观察搜索轨迹,也便于在带宽不收敛时加print调试。核心函数find_optimal_bandwidth()如下:

# src/gwr_model.py def find_optimal_bandwidth(X, y, coords, kernel='bisquare', max_iter=30): """ 黄金分割法搜索最优带宽,最小化CV值 X: (n, k) 自变量矩阵(已标准化) y: (n,) 因变量向量 coords: (n, 2) 平面坐标(米) """ # 定义搜索区间:基于点间最大距离的10%~90% dist_matrix = squareform(pdist(coords)) max_dist = np.max(dist_matrix) a, b = 0.1 * max_dist, 0.9 * max_dist # 黄金分割常数 r = (np.sqrt(5) - 1) / 2 x1 = a + (1 - r) * (b - a) x2 = a + r * (b - a) f1 = _cv_score(x1, X, y, coords, kernel) f2 = _cv_score(x2, X, y, coords, kernel) for i in range(max_iter): if f1 < f2: b = x2 x2 = x1 f2 = f1 x1 = a + (1 - r) * (b - a) f1 = _cv_score(x1, X, y, coords, kernel) else: a = x1 x1 = x2 f1 = f2 x2 = a + r * (b - a) f2 = _cv_score(x2, X, y, coords, kernel) # 返回CV更小的带宽 optimal_bs = x1 if f1 < f2 else x2 return optimal_bs def _cv_score(bandwidth, X, y, coords, kernel): """计算给定带宽下的CV值""" n = len(y) cv_sum = 0 for i in range(n): # 留一法:排除第i个点 mask = np.ones(n, dtype=bool) mask[i] = False X_train, y_train = X[mask], y[mask] coords_train = coords[mask] coords_test = coords[[i]] # 计算第i点的权重(仅对训练点) weights = _compute_weights(coords_test, coords_train, bandwidth, kernel) # 加权最小二乘 W = np.diag(weights) try: beta = np.linalg.solve(X_train.T @ W @ X_train, X_train.T @ W @ y_train) y_pred = X[[i]] @ beta except np.linalg.LinAlgError: y_pred = np.mean(y_train) # 退化为均值 cv_sum += (y[i] - y_pred) ** 2 return np.sqrt(cv_sum / n)

逻辑说明与参数说明:

  • a, b不是随便设的,而是基于数据点间最大欧氏距离动态确定,避免在稀疏数据上搜过宽、在密集数据上搜过窄。
  • _cv_score里np.linalg.solve可能失败(矩阵奇异),代码做了try/except兜底为均值预测——这是真实项目必须的鲁棒性,不是教科书理想情况。
  • weights计算调用_compute_weights,它实现bisquare核:w = (1 - (d/b)^2)^2 if d < b else 0,其中d是测试点到训练点的距离。bandwidth b就是这里的bs,单位是米,所以前面坐标必须转成米!

3.2 系数估计与t检验:ENP(有效参数个数)是GWR可信度的命门

GWR的系数不是独立的,它们通过空间权重耦合。判断一个系数是否真的显著,不能用OLS的t检验公式。必须用帽子矩阵S的迹(trace)来校正自由度。论文公式(8)ENP = tr(S)在这里落地:

# src/gwr_model.py def fit_gwr_model(X, y, coords, bandwidth, kernel='bisquare'): """ 执行GWR拟合,返回所有站点的系数、t值、标准误 """ n, k = X.shape coefficients = np.zeros((n, k)) t_values = np.zeros((n, k)) std_errors = np.zeros((n, k)) # 预分配帽子矩阵S(n x n),用于计算ENP S = np.zeros((n, n)) for i in range(n): weights = _compute_weights(coords[[i]], coords, bandwidth, kernel) W = np.diag(weights) # 局部加权最小二乘 try: XtWX = X.T @ W @ X XtWy = X.T @ W @ y beta_i = np.linalg.solve(XtWX, XtWy) coefficients[i] = beta_i # 计算帽子矩阵第i行:Si = xi (XtWX)^-1 XtW inv_XtWX = np.linalg.inv(XtWX) Si = X[i:i+1] @ inv_XtWX @ X.T @ W S[i] = Si # 计算该点残差和标准误 y_pred_i = X[i] @ beta_i residuals = y - (X @ beta_i) # 注意:这是用beta_i预测所有点,非严格留一 sigma2 = np.sum(residuals**2) / (n - np.trace(S)) # 用当前S的迹校正 var_beta_i = np.diag(inv_XtWX) * sigma2 std_errors[i] = np.sqrt(var_beta_i) t_values[i] = beta_i / (std_errors[i] + 1e-8) # 防0除 except np.linalg.LinAlgError: coefficients[i] = np.nan t_values[i] = np.nan std_errors[i] = np.nan # 最终ENP = trace(S),用于模型比较(AICc) enp = np.trace(S) return { 'coefficients': coefficients, 't_values': t_values, 'std_errors': std_errors, 'enp': enp, 'sigma2': sigma2 }

关键点解析:

  • S[i] = Si这一行,把每个点的帽子矩阵行存下来,最后np.trace(S)就是ENP。这是GWR区别于OLS的核心——OLS的ENP恒等于k(变量数),而GWR的ENP在k到n之间,反映模型复杂度。
  • residuals计算用了X @ beta_i,即用第i点的系数预测所有点。这虽不是严格留一,但对16个点的小样本,计算量可接受,且论文结果表明其t检验结论稳定。
  • 1e-8防0除是血泪经验:当某个变量在局部区域完全不变化(如某站点speed全为0),var_beta_i可能为0,t值爆炸,加微小扰动保程序不死。

3.3 预测与评估:为什么论文用第23期数据?因为它在时间序列上是“下一个”

GWR本身是横截面模型(single time slice),但论文用time=22拟合,time=23预测,本质是假设空间关系在短期内稳定。代码里predict_aqi_for_time()函数明确体现这一逻辑:

# src/gwr_model.py def predict_aqi_for_time(df, time_train=22, time_pred=23): """ 用time_train期数据拟合GWR,预测time_pred期的AQI """ train_mask = df['time'] == time_train pred_mask = df['time'] == time_pred df_train = df[train_mask].copy() df_pred = df[pred_mask].copy() # 准备训练数据 coords_train = prepare_coordinates(df_train[['lat', 'lon']].values) X_train = df_train[['temp', 'hpa', 'wet', 'speed', 'dir', 'height']].values y_train = df_train['aqi'].values # 添加截距项 X_train = np.column_stack([np.ones(len(X_train)), X_train]) # 搜索最优带宽 optimal_bs = find_optimal_bandwidth(X_train, y_train, coords_train) # 拟合模型 gwr_result = fit_gwr_model(X_train, y_train, coords_train, optimal_bs) # 预测:用训练期拟合的系数,乘以预测期的自变量 X_pred = df_pred[['temp', 'hpa', 'wet', 'speed', 'dir', 'height']].values X_pred = np.column_stack([np.ones(len(X_pred)), X_pred]) predictions = X_pred @ gwr_result['coefficients'].T # 注意:coefficients是16x8,X_pred是16x8 # 计算MSE y_true = df_pred['aqi'].values mse = np.mean((y_true - predictions)**2) return predictions, mse, gwr_result

注意:predictions = X_pred @ gwr_result['coefficients'].T这一行是关键。gwr_result['coefficients']是16×8矩阵,每一行是对应站点的系数;X_pred也是16×8,每一行是该站点在time_pred期的自变量。矩阵乘法后得到16×16预测矩阵,再取对角线(np.diag())才是每个站点用“自己的系数”预测“自己的AQI”。论文表4的16个预测值,就是这么来的。

4. 克里金插值核心实现:从变异函数拟合到泛克里金方程组求解

GWR解决了“主趋势”的空间异质性,但残差里还藏着未被捕捉的空间自相关——比如密云和怀柔的残差总是同号,而通州和石景山总是反号。克里金就是来捕获这个的。论文用泛克里金(Universal Kriging),因为它允许残差存在一个全局漂移(trend),这比普通克里金(Ordinary Kriging)更符合空气质量残差的实际:它往往随地形高度有缓慢变化。kriging_interpolation.py没有调用高级库,而是用scipy.optimize.curve_fit拟合变异函数,再手解线性方程组,全程可控。

4.1 变异函数拟合:高斯模型参数怎么来的?Q1/Q2/cR指标怎么看

变异函数(Semivariogram)是克里金的地基。它描述“两点间差异的平方期望值”随距离的变化。论文选用高斯模型(Gaussian model),因其在短距离平滑,在长距离渐近于基台值,符合大气扩散的物理直觉。拟合代码如下:

# src/kriging_interpolation.py def fit_variogram_model(distances, semivariances, model='gaussian'): """ 用curve_fit拟合变异函数模型 distances: 1D array, 所有点对距离 semivariances: 1D array, 对应点对的半方差 """ def gaussian_model(h, nugget, sill, range_param): """高斯模型:γ(h) = nugget + sill * (1 - exp(-(h/range)^2))""" return nugget + sill * (1 - np.exp(-(h / range_param) ** 2)) # 初始参数:块金=semivariances[0], 基台=semivariances.max(), 变程=distances.max()/3 p0 = [np.min(semivariances), np.max(semivariances), np.max(distances)/3] try: params, pcov = curve_fit(gaussian_model, distances, semivariances, p0=p0, maxfev=5000) nugget, sill, range_param = params except RuntimeError: # 拟合失败,返回默认参数 nugget, sill, range_param = 38.8, 0.0295, 7.59 # 论文表5的值 # 计算拟合优度指标Q1, Q2, cR y_pred = gaussian_model(distances, nugget, sill, range_param) Q1 = np.sum((semivariances - y_pred) ** 2) / np.sum((semivariances - np.mean(semivariances)) ** 2) Q2 = 1 - np.sum((semivariances - y_pred) ** 2) / np.sum((y_pred - np.mean(semivariances)) ** 2) cR = np.sum(np.abs(semivariances - y_pred)) / np.sum(semivariances) return { 'model': 'gaussian', 'nugget': nugget, 'sill': sill, 'range': range_param, 'Q1': Q1, 'Q2': Q2, 'cR': cR, 'fitted_curve': (distances, y_pred) } # 调用示例 coords = prepare_coordinates(df[['lat', 'lon']].values) # 同GWR,用米制坐标 dist_matrix = squareform(pdist(coords)) # 计算所有点对的半方差:γ(h) = 0.5*(z_i - z_j)^2 semivariances = [] for i in range(len(coords)): for j in range(i+1, len(coords)): h = dist_matrix[i, j] gamma_h = 0.5 * (residuals[i] - residuals[j]) ** 2 semivariances.append((h, gamma_h)) distances, semis = zip(*semivariances) variogram_result = fit_variogram_model(np.array(distances), np.array(semis))

参数说明与避坑:

  • nugget(块金值)代表微观尺度变异或测量误差,论文值38.8,说明残差噪声不小。
  • sill(基台值)是变异函数的上限,论文0.0295,很小,意味着残差整体空间相关性弱——这解释了为什么克里金提升有限(MSE从449.4降到423.3)。
  • range(变程)7.59,单位是“千公里”?不,是论文里没写单位,但结合distances是米制,range_param应为7590米(7.59km),即在此距离内残差仍有相关性。
  • Q1越小越好(论文0.63),Q2越接近1越好(论文1.12,略超1,说明拟合稍过,但可接受),cR越小越好(论文66.68,较大,提示拟合一般)——这就是论文说“模型拟合效果一般”的依据。

4.2 泛克里金方程组:为什么必须解 (n+q)×(n+q) 的大矩阵?

泛克里金比普通克里金多q个漂移项(trend terms)。论文中q=2,因为漂移设为线性:μ(s) = β0 + β1*x + β2*y(x,y是平面坐标)。因此,待求未知数是:n个权重λi+ q个漂移系数βj,共n+q个。方程组如下:

[ γ(s_i, s_j) x_j y_j ] [ λ1 ] [ γ(s_0, s_1) ] [ x_i 0 0 ] [ λ2 ] [ x_0 ] [ y_i 0 0 ] * [ ...] = [ y_0 ]

其中γ(s_i, s_j)是点i,j间的变异函数值,γ(s_0, s_i)是待估点s0到点i的变异函数值。代码实现:

# src/kriging_interpolation.py def universal_kriging(coords, residuals, variogram_params, coords_pred): """ 泛克里金插值 coords: (n, 2) 训练点坐标(米) residuals: (n,) 训练点残差 coords_pred: (m, 2) 待预测点坐标(米) """ n = len(coords) m = len(coords_pred) q = 2 # 线性漂移:x, y # 构建变异函数矩阵Gamma (n x n) Gamma = np.zeros((n, n)) for i in range(n): for j in range(n): h = np.linalg.norm(coords[i] - coords[j]) Gamma[i, j] = variogram_params['nugget'] + \ variogram_params['sill'] * (1 - np.exp(-(h / variogram_params['range'])**2)) # 构建漂移矩阵F (n x q): [x, y] F = coords.copy() # (n, 2) # 构建右端项向量gamma_0 (n+q, 1) gamma_0 = np.zeros(n + q) for i in range(n): h = np.linalg.norm(coords_pred[0] - coords[i]) # 只预测第一个点,循环处理 gamma_0[i] = variogram_params['nugget'] + \ variogram_params['sill'] * (1 - np.exp(-(h / variogram_params['range'])**2)) # 构建左端矩阵A (n+q) x (n+q) A = np.zeros((n + q, n + q)) A[:n, :n] = Gamma A[:n, n:n+q] = F A[n:n+q, :n] = F.T # 解方程组 A * [lambda; beta] = gamma_0 try: solution = np.linalg.solve(A, gamma_0) lambdas = solution[:n] # 预测值 = sum(lambda_i * residual_i) pred_residual = np.sum(lambdas * residuals) except np.linalg.LinAlgError: # 退化为普通克里金(忽略漂移) pred_residual = np.mean(residuals) # 或用IDW return pred_residual # 主函数:对所有预测点循环 def interpolate_residuals(coords_train, residuals, coords_pred, variogram_params): corrections = [] for i in range(len(coords_pred)): corr = universal_kriging(coords_train, residuals, variogram_params, coords_pred[i:i+1]) corrections.append(corr) return np.array(corrections)

为什么这样设计:universal_kriging函数每次只预测一个点,是为了避免构建巨大的(m*n) x (m*n)矩阵。对16个点,A是18×18,np.linalg.solve毫秒级。如果一次预测16个点,A会变成(16*16+2) x (16*16+2),内存和计算量爆炸。这是小样本克里金的务实选择。

5. 避坑指南:我在复现时踩过的5个真实坑,省你三天调试时间

这个项目看着只有3个脚本,但空间建模的坑又深又隐蔽。我按论文步骤在本地复现时,前两次全军覆没。以下是5条血泪经验,每一条都对应一个真实报错和解决方案:

5.1 坑1:坐标系不统一导致GWR带宽搜索发散,CV值一路飙升到1e6

  • 现象:find_optimal_bandwidth()运行中,f1和f2在迭代中越来越大,最终返回bandwidth=1e9,后续_compute_weights全为0,系数全nan。
  • 原因:beijing_aqi_weather.csv里的lat/lon是WGS84(EPSG:4326),但pdist(coords)计算欧氏距离时,把度当成了米。1度≈111km,所以max_dist算出来是1e6量级,a=0.1*max_dist就到了1e5米,远超北京实际范围(约100km),导致权重永远不衰减。
  • 解决:必须用prepare_coordinates()把lat/lon转成局部平面坐标(米)。检查coords数组的值:正常应在[-50000, 50000]范围内,而不是[116, 117]或[39, 40]。加一句print("Coord range:", coords.min(), coords.max())即可定位。

5.2 坑2:mgwr版本冲突导致SearchGWRParameter找不到,但论文代码没用它

  • 现象:pip install mgwr后,运行gwr_model.py报错ModuleNotFoundError: No module named 'mgwr.search'。
  • 原因:论文写于2020年,用的是mgwr<2.0,而当前PyPI最新版是2.2.3,API已重构。但!论文附录代码是纯手写GWR,根本没导入mgwr。你看到requirements.txt里有mgwr,其实是作者混淆了——删掉它,pip install libpysal==4.12.1足矣(libpysal提供pdist等空间工具)。
  • 解决:删除requirements.txt中的mgwr行,pip uninstall mgwr。gwr_model.py里所有import都只用numpy、scipy、libpysal。

5.3 坑3:克里金变异函数拟合失败,curve_fit抛RuntimeError

  • 现象:fit_variogram_model()中curve_fit报Optimal parameters not found,程序走except RuntimeError分支,返回默认参数,但后续universal_kriging中np.linalg.solve又报奇异矩阵。
  • 原因:residuals向量里有inf或nan(可能来自GWR的std_errors计算失败),导致semivariances计算出错。
  • 解决:在interpolate_residuals()开头加清洗:
    residuals = np.nan_to_num(residuals, nan=np.nanmean(residuals), posinf=np.nanmean(residuals), neginf=np.nanmean(residuals))
    同时,print("Residuals stats:", np.min(residuals), np.max(residuals), np.isnan(residuals).sum())监控。

5.4 坑4:预测时维度错位,X_pred @ coefficients.T得到16×16矩阵而非16×1

  • 现象:predictions是形状(16, 16)的二维数组,取np.diag(predictions)才得到16个值,但论文表4是列向量。
  • 原因:coefficients是(16, 8),X_pred是(16, 8),@运算符在numpy中是矩阵乘,X_pred @ coefficients.T结果是(16, 16)。正确做法是逐点相乘:predictions = np.array([X_pred[i] @ coefficients[i] for i in range(16)])。
  • 解决:修改predict_aqi_for_time()中预测部分:
    predictions = np.zeros(len(X_pred)) for i in range(len(X_pred)): predictions[i] = X_pred[i] @ gwr_result['coefficients'][i]

5.5 坑5:地图可视化报错ValueError: Invalid crs,geopandas找不到CRS

  • 现象:viz_map.py运行到gdf = gpd.GeoDataFrame(..., crs='epsg:4326')时报错。
  • 原因:geopandas新版要求CRS必须是pyproj.CRS对象,字符串'epsg:4326'不再被接受。
  • 解决:升级pyproj并改写:
    import pyproj gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs=pyproj.CRS('epsg:43

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