
本系列第二篇。上篇讲了全局 Moran’s I(“有没有聚集”),本篇讲 LISA(“谁跟谁聚集”)。建议先读 Part 1[1]。
上篇的结论:波动率的全局 Moran’s I = 0.30(p = 0.001),说明"波动大的城市倾向于挨在一起"。但 Moran’s I 是一个全局指标——它只告诉你整体上有聚集,不告诉你哪些城市在聚集。
这就好比说"班上有小团体",但没说谁跟谁是一伙的。LISA(Local Indicators of Spatial Association)就是用来回答这个问题的。
LISA 把 Moran’s I 拆到每个城市。每个城市得到一个局部 Moran’s I 值和对应的 p 值,然后按四象限分类:
类型 | 含义 | 例子 |
|---|---|---|
HH(高-高) | 该城市值高,邻居也高 | 三亚波动率高,周围海口、北海也高 |
LL(低-低) | 该城市值低,邻居也低 | 秦皇岛波动率低,周围也低 |
HL(高-低) | 该城市值高,但邻居低 | 重庆波动率高,但周围四川城市低 |
LH(低-高) | 该城市值低,但邻居高 | 南宁波动率低,但旁边北海高 |
不显著 | p > 0.05,不能排除随机 | 大部分城市 |
HH 和 LL 是聚类(同类扎堆),HL 和 LH 是异常值(跟邻居反着来)。
from esda.moran import Moran_Local
# 假设 w 是上篇构建的 KNN 权重矩阵,gdf 是 GeoDataFrame
lisa = Moran_Local(gdf['volatility'].values, w)
# 提取结果
gdf['lisa_I'] = lisa.Is # 每个城市的局部 Moran's I
gdf['lisa_p'] = lisa.p_sim # p 值(排列检验)
gdf['lisa_q'] = lisa.q # 象限(1=HH, 2=LH, 3=LL, 4=HL)
# 分类
gdf['cluster'] = '不显著'
gdf.loc[(gdf['lisa_p'] < 0.05) & (gdf['lisa_q'] == 1), 'cluster'] = 'HH'
gdf.loc[(gdf['lisa_p'] < 0.05) & (gdf['lisa_q'] == 2), 'cluster'] = 'LH'
gdf.loc[(gdf['lisa_p'] < 0.05) & (gdf['lisa_q'] == 3), 'cluster'] = 'LL'
gdf.loc[(gdf['lisa_p'] < 0.05) & (gdf['lisa_q'] == 4), 'cluster'] = 'HL'
# 统计
print(gdf['cluster'].value_counts())
不显著 62
HH 4
HL 2
LH 1
LL 1
70 城里只有 8 个通过了 p < 0.05 的显著性检验,其余 62 个在统计上跟邻居没有显著的联动关系。
sig = gdf[gdf['lisa_p'] < 0.05][['CITY', 'volatility', 'cum_return', 'cluster', 'lisa_p']]
print(sig.sort_values('cluster').to_string(index=False))
CITY volatility cum_return cluster lisa_p
秦皇岛 0.70% 51.4% LL 0.027
南宁 0.79% 61.2% LH 0.020
丹东 0.87% 43.7% HL 0.045
重庆 0.82% 104.9% HL 0.045
三亚 2.04% 186.8% HH 0.002
北海 1.30% 84.8% HH 0.004
海口 1.82% 184.6% HH 0.002
湛江 0.87% 69.1% HH 0.002
HH 簇:三亚、海口、北海、湛江——全部在海南+北部湾沿海地带。这四个城市的波动率远超全国均值,且彼此空间相连。
HL 异常:丹东、重庆——波动率高,但周围城市波动率低。丹东靠近朝鲜边境,房地产市场特殊;重庆是直辖市,经济结构跟周边四川城市差异大。
LH 异常:南宁——波动率低,但旁边的北海波动率高。南宁是省会,房价相对稳定;北海是旅游城市,投机氛围浓。
LL 簇:秦皇岛——波动率低,周围的京津冀城市也低。京津冀是政策高度管控区域,市场波动被压制。
LISA 的最终呈现是一张地图,比表格直观 10 倍:
import geopandas as gpd
import matplotlib.pyplot as plt
# 加载行政区划底图(需要一个中国地级市 shapefile)
admin_gdf = gpd.read_file('2023年地级.shp')
fig, ax = plt.subplots(1, 1, figsize=(16, 12))
# 画底图
admin_gdf.plot(ax=ax, color='#fafafa', edgecolor='#dddddd', linewidth=0.3)
# 颜色方案
LISA_COLORS = {'不显著': '#cccccc', 'HH': '#e15759', 'LH': '#76b7b2', 'LL': '#4e79a7', 'HL': '#f28e2b'}
for cluster, color in LISA_COLORS.items():
mask = gdf['cluster'] == cluster
if mask.any():
subset = gdf[mask]
subset.plot(ax=ax, color=color, markersize=100, alpha=0.85,
label=cluster, edgecolor='black' if cluster != '不显著' else None,
linewidth=0.5 if cluster != '不显著' else 0)
# 标注显著城市
for _, row in gdf[gdf['lisa_p'] < 0.05].iterrows():
ax.annotate(row['CITY'], (row.geometry.x, row.geometry.y),
fontsize=10, fontweight='bold', xytext=(10, 10),
textcoords='offset points',
bbox=dict(boxstyle='round,pad=0.2', facecolor='white', alpha=0.8, edgecolor='gray'))
ax.set_xlim(73, 136)
ax.set_ylim(17, 54)
ax.set_title('波动率 LISA 聚类图', fontsize=16, fontweight='bold')
ax.legend(loc='lower left', fontsize=10, framealpha=0.9)
ax.grid(True, alpha=0.2)
plt.savefig('lisa_volatility.png', dpi=150, bbox_inches='tight')
波动率 LISA 聚类图
地图上绝大多数是灰色(不显著),只有 8 个点有颜色。HH 红色点集中在右下角的海南+北部湾一带,形成一个明显的空间簇。
除了 LISA 聚类,还可以用气泡大小+颜色直接展示波动率的空间分布:
fig, ax = plt.subplots(1, 1, figsize=(16, 12))
admin_gdf.plot(ax=ax, color='#fafafa', edgecolor='#dddddd', linewidth=0.3)
# 气泡大小 = 波动率,颜色 = 波动率
gdf.plot(ax=ax, column='volatility', cmap='YlOrRd',
markersize=gdf['volatility'] * 40, alpha=0.8,
legend=True, edgecolor='black', linewidth=0.3,
legend_kwds={'label': '波动率', 'orientation': 'horizontal', 'shrink': 0.6})
# 标注 Top 5
for _, row in gdf.nlargest(5, 'volatility').iterrows():
ax.annotate(f\"{row['CITY']}\\n{row['volatility']*100:.0f}%\",
(row.geometry.x, row.geometry.y), fontsize=9, fontweight='bold',
xytext=(10, 10), textcoords='offset points',
bbox=dict(boxstyle='round,pad=0.2', facecolor='white', alpha=0.8))
ax.set_xlim(73, 136)
ax.set_ylim(17, 54)
ax.set_title('70城房价波动率空间分布', fontsize=16, fontweight='bold')
plt.savefig('volatility_map.png', dpi=150, bbox_inches='tight')
70城波动率空间分布
气泡图比 LISA 聚类图信息更丰富——不只看到谁跟谁扎堆,还能看到每个城市的具体波动率。三亚的气泡(204%)几乎是平顶山(49%)的 4 倍大。
上篇验证了全局 Moran’s I 对 k 值稳健。LISA 也需要验证:
for k in [3, 5, 7]:
w_k = lps.weights.KNN.from_dataframe(gdf, k=k)
w_k.transform = 'r'
lisa_k = Moran_Local(gdf['volatility'].values, w_k)
sig_mask = lisa_k.p_sim < 0.05
hh_mask = sig_mask & (lisa_k.q == 1)
hh_cities = gdf[hh_mask]['CITY'].tolist()
print(f'k={k}: {sig_mask.sum()} 个显著, HH={hh_cities}')
k=3: 6 个显著, HH=['三亚', '海口', '湛江']
k=5: 8 个显著, HH=['三亚', '北海', '海口', '湛江']
k=7: 11 个显著, HH=['三亚', '北海', '广州', '海口', '深圳', '湛江']
核心 HH 簇(三亚、海口、湛江)在所有 k 值下都稳定存在。北海在 k=5/7 时加入,k=3 时退出——说明它跟三亚/海口的空间连接是中等距离的,不是最近的 3 个邻居之一。广州和深圳在 k=7 时才进入聚类,属于更大范围的南方沿海波动带的边缘。
类型 | 城市 | 含义 |
|---|---|---|
HH(高-高) | 三亚、海口、北海、湛江 | 海南+北部湾波动率热点 |
HL(高-低) | 丹东、重庆 | 波动大但跟邻居不同步 |
LH(低-高) | 南宁 | 稳定但被高波动邻居包围 |
LL(低-低) | 秦皇岛 | 京津冀低波动区 |
一句话:全国只有海南+北部湾这一片形成了显著的波动率聚集。买房选城市时,如果你的目标城市在这个区域,要意识到整个片区的风险是联动的。
区域对比 + 时间序列 + 总结——7 个区域的涨幅和波动率排名、10 城 20 年走势、以及最终结论。
参考链接
[1] /blog/geospatial-data-analysis/python-spatial-autocorrelation-housing-70cities