多尺度地理加权回归(MGWR)在Python里并不算一个“装好就能用”的库,更多时候,它需要你对数据、坐标、带宽选择和结果解读都有完整的把握。这篇文章不是把文档翻译一遍,而是把我在真实项目中跑MGWR的全流程拆开讲,包括数据准备、模型拟合、带宽校准、结果可视化和最常见的报错处理,你会拿到一套可以直接迁移到自己的空间数据上的完整思路。
MGWR解决的问题很具体:传统的全局回归(比如OLS)把自变量对因变量的影响理解为“一个统一系数”,但在房价、环境质量、公共服务可达性这类随空间变化的场景里,同一个因素在不同街道的影响强度往往差别很大。地理加权回归(GWR)允许系数随位置变化,但它要求所有变量共用同一个带宽,也就是默认了“所有变量的影响尺度一样”。显然这不现实,某些变量可能只影响几百米范围,另一些变量则可能在全市尺度上都稳定。MGWR的“多尺度”三个字,就是想打破这个限制,给每个自变量一个独立的带宽,让模型自己去数据里学出来,多大尺度、多强的空间变化。
这篇文章适合刚接触空间回归的研究生、做数据分析的从业者,以及想把GWR方法进一步升级的GIS爱好者。你不需要已经精通空间计量,但如果你跑过OLS或至少熟悉pandas和GeoPandas,上手会顺利很多。我会用Python的主流MGWR实现库,也就是PySAL生态下的mgwr包,从零开始带你走完整条链路。
1. 为什么选MGWR:先理解“多尺度”到底改变了什么
1.1 从OLS到GWR再到MGWR
要真正理解MGWR,我建议先把OLS、GWR、MGWR这三层放在一起对比。OLS拟合的是一个全局线性方程:
y_i = β0 + β1·x1_i + β2·x2_i + ... + ε_i
这个模型假定β是固定的,对每个样本都一样。如果是在一个同质性很强的区域,这样没问题;但只要数据里有明显的空间异质性,比如城市中心房价受地铁距离的影响很大,而郊区更多受学区影响,一个全局β就会把两种效应强行折中,导致两头的解释力都不好。
GWR做了一个聪明的改动,它让系数随地理位置变化:
y_i = β0(u_i, v_i) + β1(u_i, v_i)·x1_i + β2(u_i, v_i)·x2_i + ... + ε_i
其中(u_i, v_i)是第i个样本的坐标。系数不再是全局常数,而是通过空间加权回归,在每个样本点上用它周围的邻居样本估计出来的。这背后的直觉很像查天气:你要知道你所在位置的温度,最简单的办法不是看全国平均值,而是看周围几个城市的温度,然后加权平均。GWR里的带宽就是“看周围多少个城市”的范围,权重按距离衰减,越近的样本对当前点系数估计影响越大。
但GWR暗含了一个很硬的前提:所有解释变量在空间上变化的速度是相同的。带宽是一个数,它同时约束了x1影响y和x2影响y时的空间尺度。我在实际项目里经常遇到这样的情况,比如在研究共享单车需求时,天气因素可能在整个城市范围内都稳定影响骑行量,而站点周边POI数量的影响只在小范围内有意义。用GWR跑,它会为所有变量选一个折中带宽,结果很尴尬:天气变量的系数被过度局部化,波动得很噪;而POI变量则被过于平滑,区域差异被抹掉了。
MGWR的改进就在这一点,它不再是全局一个带宽,而是每个变量都有自己的带宽βj(u_i, v_i)。数学上它拟合的是:
y_i = β0(u_i, v_i) + Σ_j βj(u_i, v_i)·xj_i + ε_i
其中每个βj的带宽向量都不同。MGWR在估计过程中会交替优化每个变量的带宽,直到所有带宽稳定收敛。这样,模型会自动判断:这个变量的影响是“本地尺度”还是“全局尺度”。如果某个变量最后的带宽很大,达到样本规模的80%以上,你甚至可以视它为全局变量;如果带宽很小,说明它对邻域的局部差异极度敏感。
用生活类比来说,GWR像是给所有人定做了一套固定尺码的衣服,每个变量都是同一个号;MGWR则是量体裁衣,每个变量的尺度都是独立的。而这个“自行判断尺度”的能力,才是MGWR最大的价值。
1.2 MGWR在Python生态中的位置与工具选型
Python里能跑GWR的工具其实不少,但真正成熟支持MGWR的,我目前最常用的还是PySAL下的mgwr包。这个包由亚利桑那州立大学GeoDa团队参与维护,和libpysal、geopandas等空间分析库无缝衔接。
除了mgwr包,另一个常见的库是spgwr,它只提供经典GWR,不支持多尺度版本,如果你只是做GWR基准模型,它也可以用,但无论是接口友好度、文档完整度还是模型诊断输出,我都更推荐基于PySAL家族。我见过有些同学为了画GWR系数图,自己写距离加权矩阵、循环回归、带宽搜索,费了半天劲,结果性能还不稳定,其实现在完全没有必要重复造轮子。
安装mgwr很简单,直接用pip:
pip install mgwr它会自动带上libpysal、esda等依赖。建议顺便把GeoPandas、Matplotlib也装好,因为后面可视化会用到:
pip install geopandas matplotlib scikit-learn要注意mgwr包对Python版本有一定要求,我通常在Python 3.9到3.11环境下跑,都比较稳定。如果你用的是3.12或更新版本,偶尔会遇到依赖编译问题,建议准备一个虚拟环境,别把系统Python环境搞乱了。
mgwr包的核心接口分为两部分:一个是GWR类,负责经典地理加权回归;另一个是MGWR类,负责多尺度版本。两者的输入形式很接近,都是从样本数据里准备好因变量、自变量和坐标,这让从GWR切换到MGWR的成本非常低,你只需要改类名和几个参数。我一般会在项目中先跑一个GWR作基准,再跑MGWR,用它们输出的AICc和调整R²做对比,看多尺度是不是真的带来了提升。
2. 数据准备:跑MGWR前最容易踩坑的环节
2.1 坐标到底怎么传:几何列、投影与坐标数组
我在无数个答疑场合里看到有人把MGWR报错归咎于模型不稳定,到最后排查发现是数据结构就没搞对。MGWR是个逐点回归模型,它在计算“邻居”和“带宽”时,依赖的是样本点的平面坐标。这个问题用数学的语言简单表述就是:你给它多少对坐标,它就认为这些点是立在世界里的一个个小王国的中心,然后计算这些王国之间的物理距离。
因此,数据准备的第一步,是确保你有一个真正的空间数据框。如果你是拿普通pandas DataFrame,里面有两列经纬度,那也不是不能用,但最正规的做法是转成GeoDataFrame,并且把坐标系投影到平面坐标系。千万别把经度纬度直接丢进去跑,因为经纬度是球面坐标,没有经过投影变换,纬度高的地方单位经度对应的实际地面距离会明显回缩,这样计算出来的距离就失真了。我实测过,在北方城市做研究时,直接使用经纬度与使用投影坐标的结果差距随带宽变小而增大,那种几百米范围的小带宽尤其敏感。
具体的处理流程如下:
import geopandas as gpd import pandas as pd # 如果原始数据是普通表格 df = pd.read_csv("your_spatial_data.csv") # 把经纬度转成GeoDataFrame,并指定原始的坐标参考系(一般GPS数据是WGS84,即EPSG:4326) gdf = gpd.GeoDataFrame( df, geometry=gpd.points_from_xy(df["lng"], df["lat"]), crs="EPSG:4326" ) # 投影到适合自己城市的米制坐标系 # 例如UTM分区,或者根据城市所在地区选一个合适的投影 gdf = gdf.to_crs(epsg=32650) # 这里以UTM 50N为例 # 提取投影后的坐标 coords = list(zip(gdf.geometry.x.values, gdf.geometry.y.values))如果你已经有Shapefile、GeoPackage这样的空间文件,可以直接:
gdf = gpd.read_file("your_data.gpkg") gdf = gdf.to_crs(epsg=32650) coords = list(zip(gdf.geometry.x.values, gdf.geometry.y.values))注意EPSG代码要根据你的研究区域选。中国的城市大多分布在UTM 46N到51N之间,也可以使用CGCS2000或Albers等面积投影。重要原则只有一个:投影后坐标的单位是米,别让经纬度进场。
如果你不太确定选什么坐标参考系,有一个实用技巧:gdf.estimate_utm_crs()能根据数据范围自动推荐UTM分区,大多数情况下用它就够。
2.2 变量筛选、标准化与多重共线性控制
坐标处理完之后,下一步是准备因变量y和自变量X,这一步直接影响模型的收敛性和结果的可解释性。
因变量y一般是一维数组,自变量X是二维数组,每一列对应一个解释变量。这里有个关键点:mgwr包会自动在模型中加入截距项,你不要人为地在X矩阵里塞一列全是1的常量,否则会导致完全共线性,模型结果会变得非常离谱。这也是新手特别容易犯的错误。
变量筛选方面,我的经验是先跑一下全局OLS或随机森林,初步判断哪些变量有价值。MGWR虽然允许每个变量有独立的带宽,但它并不擅长同时处理几十个高度相关的变量,输入太多强相关的变量会让局部系数估计产生剧烈波动,带宽校准也会变得不稳定。常规做法是把相关系数删除或合并之后再进入模型。可以使用variance inflation factor(VIF)做个快速筛查,VIF超过10的变量,除非有极强的理论依据,不然建议剔掉。
关于变量的标准化,我一直是建议做的。虽然MGWR的系数最终会画成空间分布图,标准化之后系数的绝对数值不再代表原始单位的影响量,但在模型层面,标准化能显著改善数值稳定性。因为带宽搜索和迭代拟合过程中会大量计算加权矩阵、矩阵求逆,如果变量量纲差异过大,比如一个变量范围是0到1,另一个是100到10000,数值小的变量很容易被矩阵运算中的舍入误差盖掉,造成收敛困难或结果不可复现。我通常在进入模型之前用StandardScaler处理所有自变量。
from sklearn.preprocessing import StandardScaler feature_cols = ["poi_density", "transit_distance", "avg_price", "green_area_ratio"] scaler = StandardScaler() X = scaler.fit_transform(gdf[feature_cols]) y = gdf["target_value"].values # 因变量一般不用标准化,想标准化也可以,但解读时要注意有一点要说明:标准化不影响MGWR的局部估计结构,因为每个变量对应的带宽是独立的,标准化只会让数值计算更稳定,不会改变“哪个变量局部化、哪个变量全局化”的结论。但如果你最终想把系数还原回原始变量的单位,就得自己记录下均值、方差再做反向变换。
另外,还有一个常见问题:研究区内样本点分布极度不均匀时,比如市中心密、郊区疏,我建议优先考虑自适应带宽(adaptive bandwidth)。自适应带宽不按固定距离圈邻居,而是按最近邻数,比如“每个局部回归用最近的100个点”。这种带宽在数据稀疏区域自动变大,在数据密集区域自动变小,能有效避免局部样本量不足的问题。参数设置上就是fixed=False,后面我会展开讲。
3. 核心实操:用MGWR完成一次完整建模
3.1 最简单但完整的建模流程
数据准备好了,接下来直接进入建模环节。我用一个实际的城市房价案例来做演示,假设我们的因变量是每平方米均价,自变量包括地铁可达性、POI密度、绿化率、周边学校数量等。以下代码可以直接跑通。
import numpy as np import geopandas as gpd from sklearn.preprocessing import StandardScaler from mgwr.multiscale import MGWR from mgwr.gwr import GWR # 1. 读取空间数据 gdf = gpd.read_file("house_price.gpkg") gdf = gdf.to_crs(epsg=32650) # 2. 准备坐标 coords = list(zip(gdf.geometry.x.values, gdf.geometry.y.values)) # 3. 准备自变量和因变量 feature_cols = ["metro_distance", "poi_density", "green_area", "school_count"] scaler = StandardScaler() X = scaler.fit_transform(gdf[feature_cols]) y = gdf["price_per_sqm"].values # 4. 先跑一个经典GWR做基准 gwr_model = GWR(y, X, coords, kernel="bisquare", fixed=False) gwr_res = gwr_model.fit() print(gwr_res.summary()) # 5. 再跑MGWR mgwr_model = MGWR(y, X, coords, kernel="bisquare", fixed=False) mgwr_res = mgwr_model.fit() print(mgwr_res.summary())这段代码里,GWR和MGWR的输入完全一致,都是从因变量、自变量和坐标出发。GWR拟合时会在全特征维度上搜索一个最小AICc的带宽,MGWR则会为每个变量单独搜索带宽。如果样本量比较大,MGWR的拟合时间会比GWR长很多,这是正常的,因为它要做多轮迭代。
跑完之后的summary()输出会给出关键信息:AICc、BIC、R²、调整R²、每个变量的有效带宽。我记得第一次跑MGWR时的感受是,它选出来的带宽向量差异真的很大,有的变量带宽接近全局,有的变量带宽只有全局的十分之一。这个结果本身就非常值得写进论文或报告里,它定量地回答了“哪些因素在空间上是全局的,哪些是局部的”。
想获取MGWR结果里的具体系数,可以这样:
# mgwr_res.params 是样本数 × (变量数+1) 的矩阵,包含截距项 coef_names = ["intercept"] + feature_cols coef_df = pd.DataFrame(mgwr_res.params, columns=coef_names) coef_df["coords_x"] = gdf.geometry.x.values coef_df["coords_y"] = gdf.geometry.y.values # 保存结果,方便在地图软件里查看 coef_df.to_csv("mgwr_coefficients.csv", index=False)同时还可以拿到拟合值和残差:
fitted = mgwr_res.fittedvalues resid = mgwr_res.resid_response残差的空间分布很值得画,如果残差还呈现出明显的聚簇结构,说明你的模型可能遗漏了某个关键空间变量,或者存在空间自相关的遗漏变量。
3.2 带宽校准与核函数取舍
很多初学者面对内核函数和带宽参数一脸懵,这里我给出一个基于经验的完整判断框架。先说核函数,mgwr包支持多种核,最常用的是bi-square、Gaussian和tricube。
bi-square核的定义是,距离小于带宽的样本点按二次衰减公式加权,距离超过带宽的直接给零权重,相当于画了一个硬边界圈邻域。Gaussian核则没有硬边界,所有点都有非零权重,但权重随距离按高斯曲线衰减。我个人的习惯是优先选bi-square,因为硬边界让局部估计更稳定,计算效率也更高,而且在大样本场景下,Gaussian核的远距离小权重点其实贡献很小,可以忽略,bi-square本质上是用更直白的方式做了这件事。如果你发现带宽附近样本点很少,担心硬切割导致系数不连续,可以试试tricube,它介于两者之间。
带宽方面,决定你要用固定距离带宽还是自适应最近邻带宽。固定带宽适合样本点分布均匀的研究区域,比如规则格网上的土壤采样数据。自适应带宽适合样本点分布差异大的情况,比如POI、房价、犯罪事件这类城市数据,市中心点密密麻麻,郊区稀稀拉拉。使用fixed=False时,模型不是按固定半径圈点,而是按“最近的K个邻居”来做局部回归,每个局部回归的样本量几乎一致,有效避免因点密度不均导致的估计方差剧烈变化。
带宽的选择标准,mgwr包里一般通过AICc或AIC来搜索最优带宽。AICc是小样本修正后的Akaike信息准则,它平衡了模型拟合优度和复杂度,带宽越小,模型越灵活,但容易过拟合;带宽越大,模型越平滑,但可能丢失局部细节。MGWR的带宽搜索会在每个变量上进行,可以设置初始带宽范围或带宽列表,也可以让它自动搜索。自动搜索在样本量几千以内基本够用,但要给足迭代时间。
MGWR拟合时还有一个参数叫max_iter,它控制交替优化的最大迭代次数。每轮迭代,模型固定其他变量的带宽,单独优化某一个变量的带宽,然后换下一个变量,循环下去,直到所有带宽的变化幅度小于预设阈值。理论上,max_iter默认值对大部分场景够用,但如果你的变量多、样本量大,或者结果里出现带宽反复跳变,可以把max_iter调大,同时观察每次迭代的AICc下降情况。AICc应该在早期迭代快速下降,后期趋于平稳;如果一直震荡不收敛,更可能的问题不是迭代次数,而是变量共线性或数据噪声太大。
4. 结果解读:别只盯着R²,系数空间模式才是重心
4.1 系数与t值的空间可视化
MGWR拟合完成后,最重要的产出不是那一个R²,而是每个变量系数在全空间上的变化模式。因为MGWR给出的是每个样本点上的局部系数,你必须把它们画在地图上才看得懂。
我做可视化时的标准流程是:把系数、t值、拟合值、残差都合并回GeoDataFrame,然后用Matplotlib逐张出图。核心是画系数地图,但如果没有t值把关,很容易误读,因为系数高低并不等于影响显著,还要看局部t值的绝对值是否大于1.96。我建议你每次画系数图时都把t值图放在旁边,形成对照。
import matplotlib.pyplot as plt # 假设coef_df里存了系数和坐标 gdf_coef = gdf.copy() gdf_coef[coef_names] = coef_df[coef_names] fig, axes = plt.subplots(1, 2, figsize=(12, 5)) ax1 = axes[0] sc1 = ax1.scatter( gdf_coef.geometry.x, gdf_coef.geometry.y, c=gdf_coef["metro_distance"], cmap="RdYlBu_r", s=20 ) ax1.set_title("metro_distance Coef") plt.colorbar(sc1, ax=ax1, fraction=0.036) ax2 = axes[1] sc2 = ax2.scatter( gdf_coef.geometry.x, gdf_coef.geometry.y, c=..其他变量.., cmap="RdYlBu_r", s=20 ) ax2.set_title("另一变量 Coef") plt.colorbar(sc2, ax=ax2, fraction=0.036) plt.tight_layout() plt.show()系数地图的解读有几个层次。第一步,看系数的空间梯度,如果系数从区域南端到北端颜色由深变浅,说明这个变量对因变量的影响方向或强度有明显的空间分层。第二步,联系实际背景,比如“地铁可达性”在中心区不显著,因为中心区本身交通极度便利,地铁的边际贡献被其他因素掩盖;但在郊区站点附近,交通可达性的边际贡献非常大,这样的结果非常有解释力。
但要注意,MGWR的局部系数对样本量、带宽选择和数据噪声比较敏感。我不建议看到一张图上几个点的系数特别大就急着下结论,稳定的做法是结合t值图、系数置信区间图和多次不同核函数或带宽下的结果,交叉验证。如果某个区域在不同设置下都呈现相同的系数模式,才算一个稳健发现。
另一个实用技巧是计算“有效参数数量”,也就是enp数值。MGWR输出结果会有每个变量对应的有效参数数,它反映的是局部模型实际使用的复杂度。如果某个变量的有效参数数接近全局回归的参数数,说明它确实不需要空间变化,接近全局;如果有效参数数很大,说明它高度局部化。这个指标和带宽判断相辅相成。
4.2 模型对比与诊断指标
跑模型不能只输出一张summary就结束。我一般会做一个“OLS vs GWR vs MGWR”三模型对照表,放在结果部分的第一张表。重点关注的指标有:
| 指标 | OLS | GWR | MGWR |
|---|---|---|---|
| R² | 全局拟合度 | 上升明显 | 在GWR基础上继续提升 |
| 调整R² | 考虑变量数 | 局部模型的复杂度更高 | 多尺度更精细 |
| AICc | 越小越好 | 通常远小于OLS | 通常最小 |
| 带宽个数 | 无 | 1个 | p个 |
| 有效参数数 | p | 受全局带宽影响 | 每变量独立 |
在实际项目里,MGWR的AICc通常会显著低于GWR,这表明数据确实存在多尺度结构。但也要小心一种情况:如果研究区域本身空间异质性很弱,GWR和MGWR的差距就会很小,这时候强行用MGWR反而会引入过拟合。我的判断标准是,MGWR比GWR的AICc低2以上才值得用,如果只低0.3、0.5,说明多尺度增益有限。
模型的残差空间自相关测试也很关键。你可以用莫兰指数I(Moran‘s I)检验残差是否仍有空间聚集。理论上,一个好的MGWR模型应该把残差里的空间结构吸收掉大部分,使残差趋近随机分布。如果残差的Moran’s I显著为正,说明模型里还有解释不了的局部结构,可能需要补充变量或调整带宽。
需要注意的是,MGWR的R²和AICc是基于有效参数数量调整过的,它不是把模型里有空间关联的样本当作完全独立样本,而是用本地回归的复杂度进行了惩罚。所以不同模型的AICc可以直接比较,这也是它能做GWR和MGWR对比的前提。
5. 常见问题与排错实录
5.1 拟合缓慢或内存压力大
MGWR的拟合过程涉及在每个样本点构建局部加权回归,计算距离矩阵,并对每个变量的带宽做迭代搜索。样本量上了五千、一万以后,计算量会显著增加,内存也很容易吃紧。
我在处理两三万条样本时经常遇到内存饱和。一个实用优化是减少迭代中的冗余计算,比如将坐标用float32而不是float64存储,使用更紧凑的数据类型能省掉一半内存占用。另外就是可以考虑减少候选带宽的数量,比如自适应的最近邻取值范围,不必从5到N全部搜索,先按十分位点生成候选带宽列表,减少带宽搜索的档位数。
如果数据规模实在太大,我的建议是先按区域随机抽样出一个子集做模型调试,比如抽2000个点,把变量、核函数、带宽策略都调试好,再全量跑一次最终模型。这样既能快速试错,也能给最终结果一个大致的预期范围。
5.2 收敛异常或结果不稳定
MGWR在交替优化带宽时,偶尔会出现带宽在相邻几轮迭代里来回振荡不收敛。这个问题多半不是算法本身的错,而是数据层面的问题。变量多重共线性是最重要的诱因,尤其是局部样本量本来就小,如果两个自变量高度相关,局部回归的矩阵求逆就会变得病态,带宽搜索也会跟着失灵。
另外,异常值也是隐藏的凶手。有些样本点的因变量或者自变量存在极端值,局部回归中它的权重很大,会强烈干扰系数估计。我建议在建模前对关键变量做分位缩尾或者对数变换,比如房价数据明显右偏,先取log再进模型,稳定性会提升不少。如果某个变量的原始分布跨度超过几个数量级,强烈建议转换后再说。
MGWR的summary输出里通常会附收敛信息,如果发现AICc在迭代中不降,除了检查数据,还可以换一个核函数试试。bi-square不收敛时,可以换Gaussian或tricube,因为Gaussian核给远处样本保留了一定的但很小的权重,有时能够使优化曲面更平滑,帮助收敛。
5.3 结果不可复现
MGWR里有随机初始化的成分吗?严格说,带宽搜索并非随机,但如果你设置了多进程并行或依赖系统资源,不同环境下可能出现微小数值差异。如果你希望结果严格可复现,有两个要点:一是固定随机种子,但它只影响库内部可能调用的随机组件,不影响CPU浮点运算;二是固定带宽初值,也就是在MGWR里传入multi_bw参数,以一组确定的带宽值启动迭代,这样至少能保证同环境下每次运行一致。
在发布报告或论文时,我强烈建议把关键参数记录成配置字典,包括核函数、fixed设置、带宽初值、变量列表、坐标参考系,以及标准化参数。这些信息看似繁琐,但对结果复现和同行评审至关重要。我在项目里通常会在脚本开头定义一个config dict,把这一切都存好,既方便自己回头看,也方便别人复核。
最后再分享一个小技巧,我最开始跑MGWR时总是把注意力放在R²和AICc上,忽视了系数空间模式的可解释性,后来慢慢意识到,MGWR真正的价值在于它帮你发现“哪个变量在什么尺度上发挥作用”。当你把那些带宽差异巨大的变量摊在地图上,再结合业务背景解释时,那种“原来这个因素只在局部起作用”的发现,才是MGWR最让人上瘾的地方。所以建议你跑完模型后,先别急着删掉旧版本,试着把每个变量的系数地图和t值地图都存下来,多花点时间对比观察。数据里的空间故事,往往比模型指标本身更值钱。