简介:面向城市规划与数据分析场景,这套基于K均值聚类的机器学习项目源码及配套数据文件,用于对城市POI数据进行功能复合类型识别与复合程度评估。资源共7个文件,压缩包大小约19KB,包含3个Python脚本(分别负责数据预处理、聚类建模与结果分析)、1个CSV地块数据、1个说明文件、1张聚类可视化图及1份Markdown文档,可支撑从原始数据清洗到地块功能评分的完整流程。目前已有46人学习下载,适合机器学习初学者、城市规划专业学生及相关从业者参考。借助该项目,读者能够掌握K均值算法在POI多维度特征聚类中的实际应用思路,理解商业-科研机构复合类型、商务区、待开发区、住宅区等不同功能地块的判别方法,并可基于附带数据和脚本快速复现实验、调整聚类参数,进一步迁移到其他城市数据的功能评估与规划决策中。
1. 地址点密度背后,藏着城市功能的拼图游戏
城市POI数据看起来只是一堆带名字、类别和经纬度的点位,但当你把几万个餐饮、公司、小区门口、科研楼宇的坐标叠加到同一张地块上,它立刻就变成了城市功能的“体检单”。K均值聚类算法在POI功能复合类型识别这件事上,几乎是入门机器学习项目里最实用的一把刀:它不需要标注数据,只需要特征矩阵就能把地块分成“纯住宅”“商业-科研复合”“待开发区”等隐含类型,再配合简单的熵指标,就能给每个地块打一个功能复合程度评分。适合刚接触机器学习、手里有一份城市POI数据但不知道从哪下手的从业者,也适合想把“地块功能评分”这种模糊概念落成可量化指标的数据分析项目。
这篇文章不讲花架子,直接告诉你我一般怎么把POI数据变成K均值聚类能吃的样本、K值怎么选不翻车、复合程度评估用什么公式,以及真实数据里那些让人头大的坑。核心思路和代码可以照抄,但你跑完第一轮结果后,大概率还是会回来读一遍第五部分。
2. 把经纬度点变成地块画像:面向K均值聚类的POI特征工程
2.1 从点数据到地块单元的映射逻辑
原始POI数据往往是“点”的集合:某坐标上有一个“星巴克咖啡”,另一个坐标上是“某某大学”。要让K均值聚类起作用,得先把这些点归到某个“地块”上。这里的地块不是宗地意义上的规划地块,而是一个分析单元。常见做法是用固定边长的网格——比如500米×500米——或者用路网切割生成的街区。我实际项目里更推荐网格,因为路网数据不容易拿到干净的。
映射逻辑很简单:给每个网格一个唯一ID,然后把落在网格内的POI按照大类累加计数。为什么不直接把经纬度特征扔进聚类?因为K均值对坐标点做聚类得到的是一堆“空间簇”,不是功能簇。你要的是“这个地块里有什么业态”,不是“哪个区域POI密度高”。所以特征必须是每个地块的业态频数。
下面是一个最小可跑的网格映射脚本,假设手里有一份POI CSV,字段至少包括lng、lat、category三类。
import pandas as pd import numpy as np # 读取原始POI数据,category请替换为你自己的大类字段 poi = pd.read_csv("poi.csv", dtype={"category": str}) poi = poi.dropna(subset=["lng", "lat", "category"]) # 定义网格大小(米),这里用0.005度近似500米 grid_size = 0.005 poi["grid_x"] = np.floor(poi["lng"] / grid_size).astype(int) poi["grid_y"] = np.floor(poi["lat"] / grid_size).astype(int) poi["grid_id"] = poi["grid_x"].astype(str) + "_" + poi["grid_y"].astype(str) # 每个网格内统计各类别POI数量,NaN填0 pivot = poi.pivot_table( index="grid_id", columns="category", values="lng", aggfunc="count", fill_value=0 ) pivot = pivot.loc[:, pivot.sum(axis=0).sort_values(ascending=False).index]逻辑说明:pivot_table是这一步的核心,values="lng"只是占位,真正用的是aggfunc="count"。网格ID用格坐标拼接而成,保证经纬度相近的点归到同一个ID。输出矩阵的行是一个地块,列是各类POI数量。参数说明:grid_size=0.005在赤道附近约合550米,如果你在北方城市,用0.005会略微偏大,可以按0.005 * cos(lat)换算,但K均值对网格大小不敏感,只有特性敏感,所以先用固定值跑通再说。
2.2 类别粗分与频数标准化:决定聚类上限的先手棋
拿到POI原始类别后,不要直接进入聚类。原始POI类别往往有上百种,比如“餐饮服务>中餐厅”“餐饮服务>西餐厅”,这对K均值来说全是稀疏矩阵里的孤立点。必须先将类别粗分为一级大类:住宅、商业、餐饮、公司企业、科教文化、医疗、交通、政府、绿地休闲等。粗分后的类别数量控制在8~15个,否则K均值在高维稀疏矩阵上表现极不稳定。
我一般按一级类别字段做映射,如果没有就用原始分类名的第一段。接下来是频数标准化,这一步经常被忽略:K均值用欧氏距离计算相似度,如果“餐饮”频数普遍几百,“科研机构”只有十以内,聚类结果会被高频类别完全主导。标准做法是取对数后做Z-Score,或者至少做Min-Max归一化。取对数是因为POI数量满足长尾分布,直接归一化会被几栋写字楼拉爆。
from sklearn.preprocessing import StandardScaler # 取对数,避免极端大数支配距离 log_data = np.log1p(pivot.values) # log1p处理0值 # Z-Score标准化,让每个业态维度的均值0方差1 scaler = StandardScaler() scaled_data = scaler.fit_transform(log_data) # 保留行列索引,方便后续回看 feature_names = pivot.columns.tolist() index_names = pivot.index.tolist()参数说明:np.log1p对0值友好,加1后再取对数,再小的频数也不会变成负无穷。StandardScaler让每个特征维度在距离计算里等权重。如果你希望住宅类属性权重更大,可以在标准化后乘以权重系数,但前提是你对这块土地有业务先验。我一般不会一开始就加权重,先跑一版无权重聚类,再用轮廓系数验证,权重只作为后手。
2.3 剔除空网格与POI密度极端值:给聚类减负
网格化之后一定会出现大量“无POI”的空网格,它们对应的全零行会让K均值聚出一个“无人区”类别,看似合理但没有分析价值。常见处理是直接删除全零行,只保留POI有效覆盖的区域。同时还要处理那些POI数量异常大的网格——比如市中心某路口可能有几千个POI,它们是同一栋综合体的多个入口,会让频数列的方差过大。
我的做法是:删除行和小于等于0的网格,再按每行总量做分位数截断。这块没有标准公式,血泪经验是先把每类数量在95百分位以上的值截断,防止单个异常点带偏簇中心。注意这只会影响数值,不会改变网格编号顺序。
3. K均值聚类识别功能复合类型:从K值选定到类型命名
3.1 使用肘部法则与轮廓系数确定最佳K值
K均值必须提前指定K,这个K就是“功能复合类型”的数量。K并不是越大越好,城市规划里常见的地块功能类型也就6~10种。确定K我会同时看两个指标:簇内误差平方和(SSE)肘部法和轮廓系数。SSE拐点不明显时,以轮廓系数为准。
先写一个辅助函数,跑1到12的K,输出评估指标:
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse_list = [] sil_list = [] k_range = range(2, 13) for k in k_range: model = KMeans(n_clusters=k, random_state=42, n_init=10) labels = model.fit_predict(scaled_data) sse_list.append(model.inertia_) sil_list.append(silhouette_score(scaled_data, labels)) # 打印结果,人工判断肘部位置 for k, sse, sil in zip(k_range, sse_list, sil_list): print(f"K={k:2d} SSE={sse:8.1f} Silhouette={sil:.4f}")参数说明:n_init=10是K均值里最容易忽略的参数,它表示用不同初始化中心跑10次取最优,开始时用默认的10没问题,但如果数据量超过10万行,建议把n_init降到5节省时间。random_state=42保证结果可复现。这里的inertia_就是SSE,越小说明簇内越紧凑,但K大了必然会小,所以要结合轮廓系数的峰值选K。实际项目中,SSE曲线经常是平滑下降没有明显肘点,这时候我优先选轮廓系数最大的K,但别超过10。
如果出现过拟合式的结果——某个簇只包含一两个网格,通常是把K定得太大。K均值本质是硬聚类,K太大就会捕捉噪声点。一个判断技巧:如果最大簇的样本数占总样本数的70%以上,说明K太小;如果最小簇样本数少于样本总数的2%,说明K太大。
3.2 用簇中心反向解读功能复合类型的业务含义
K值定好后,重新训练并得到每个簇的中心向量,也就是model.cluster_centers_在标准化前的真实频数参考。这一步必须把数据反标准化回来,否则看到的是Z-Score值,很难和业务对应。反标准化的代码很简单:
# 用训练时的scaler做逆变换 center_log = scaler.inverse_transform(model.cluster_centers_) center_freq = np.expm1(center_log) # 指数还原 # 转成DataFrame方便比较 center_df = pd.DataFrame(center_freq, columns=feature_names)逻辑说明:inverse_transform把Z-Score还原成log1p后的值,再用np.expm1还原成原始频数。每个簇中心一行,每一列表示该簇平均每个网格里对应类别POI的数量。这时去看哪一类占比最高,就给这个簇命名。
举一个典型结果:假设K=5,第0簇的“住宅小区”中心值为120、“餐饮”为45、“便利店”为20,其他都小于5,那这个簇就是“纯住宅型”。第1簇“公司企业”中心值为90、“科研机构”为60、“教育”为30,那它就是“商业-科研机构复合类型”。第2簇所有特征都接近0,它就是“待开发区”。命名这件事机器干不了,必须结合中心值和城市规划的常识。
我还会做一个交叉验证:把每个网格的标签拉出来,随机抽20个网格,去地图上看真实业态是否和簇名吻合。这一步虽然不能写进自动化流程,但能帮你发现是不是因为类别粗分时把“事业单位”并错了导致聚类中心混淆。
3.3 复合类型识别结果的可视化与落图
识别出复合类型后,要落到空间上才直观。最常见的方式是把网格ID经纬度坐标还原成GIS点或直接画散点图。下面是用Matplotlib画彩色散点图的示例:
import matplotlib.pyplot as plt # 从grid_id拆回网格中心坐标 grid_info = pivot.reset_index()[["grid_id"]] grid_info["grid_x"] = grid_info["grid_id"].apply(lambda x: int(x.split("_")[0])) grid_info["grid_y"] = grid_info["grid_id"].apply(lambda x: int(x.split("_")[1])) grid_info["lng_center"] = (grid_info["grid_x"] + 0.5) * grid_size grid_info["lat_center"] = (grid_info["grid_y"] + 0.5) * grid_size # 把聚类标签添加进来 grid_info["cluster_label"] = labels # 绘制散点图,按簇标签着色 plt.figure(figsize=(10, 10)) scatter = plt.scatter( grid_info["lng_center"], grid_info["lat_center"], c=grid_info["cluster_label"], cmap="tab10", s=8, alpha=0.7 ) plt.colorbar(scatter) plt.xlabel("经度") plt.ylabel("纬度") plt.title("地块功能复合类型聚类结果") plt.show()说明:这里的grid_x和grid_y是网格索引,加0.5后乘grid_size得到中心点度数,再乘所在纬度得到真实坐标的估算。注意如果你的网格是按米而非度数划分,这里要换成投影坐标转换。散点图只看个大概,真正做规划汇报建议导出GeoJSON或Shapefile。
从这一步开始,你已经有了“每个地块属于什么功能复合类型”的分类结果,下一步才是区分“复合程度”的评分。
4. 功能复合程度评估:用熵和占比构造地块混合度评分
4.1 为什么单独做复合程度评估,而不直接用聚类簇判断
聚类结果告诉了你“这个地块整体上属于哪一类”,但同一个“商业-科研复合类型”里,有的地块是商业占80%、科研占20%,有的地块是各占50%。前者只是“功能复合的名义分类”,后者才是真正的功能混合。如果要给政府报告或规划提供量化评分,单靠聚类标签是不够的,必须给每个地块算一个连续分数。
这里最直观的指标是香农熵。把地块内各类POI的占比看作概率分布,熵越大说明分布越均匀,功能复合程度越高。熵除以类别总数的对数可以得到0~1之间的归一化指数——我一般叫它混合度指数。另外还需要一个主功能占比指标,避免“所有类别都很少”的低密度地块被误判为高复合。
4.2 用香农熵计算混合度指数:代码与公式拆解
香农熵的计算公式为:
H = -Σ(p_i * log(p_i))其中p_i是第i类POI在该网格中的占比。归一化后H_norm = H / log(n_classes)。取对数时必须处理0值,占比为0的项直接跳过。这里给出完整的Python实现:
import numpy as np # pivot是原始频数矩阵(未经标准化),行是网格,列是类别 freq_matrix = pivot.values.astype(float) def mixture_entropy(freq_row): """输入一行频数,返回归一化混合度指数""" total = freq_row.sum() if total == 0: return 0.0 prob = freq_row / total # 只保留大于0的项,否则log(0)会报错 prob = prob[prob > 0] entropy = -np.sum(prob * np.log(prob)) # 用类别数做归一化,让值落在0-1之间 n_classes = freq_row.size if n_classes <= 1: return 0.0 return entropy / np.log(n_classes) # 应用到每个网格 mixture_scores = np.apply_along_axis(mixture_entropy, axis=1, arr=freq_matrix) pivot["mixture_score"] = mixture_scores参数说明:total是网格内所有POI数量之和,如果某网格只有一条POI,那它的熵为0,混合度最低,这是合理的。n_classes用的是原始类别数而不是“有效类别数”,归一化后同样是9类POI的地块,富集度会不同。这个指标并没有考虑空间布局,只要类别均衡就算高混合度,你可以在后续加入“不同类别POI是否在空间上交错”的邻域指标,但第一版用熵就足够了。
4.3 结合主功能占比构造复合程度评分卡
光有熵还不够,因为一个只有3个POI、类型刚好各占1/3的小空地,熵会接近最高值,但这种地块显然不是功能复合区。所以要加一个约束:地块POI密度或总量至少达到某个阈值,才参与评分。实际项目中我会再加一个主功能占比,计算公式为max(p_i),最后组成一个二维评分。
常见的做法是做一个评分卡:
| 地块特征 | 判定方向 | 建议权重 |
|---|---|---|
| 归一化熵(混合度) | 越高越好 | 0.6 |
| POI总量(对数) | 越高越好 | 0.2 |
| 主功能占比倒数 | 越高越好 | 0.2 |
具体分数可以按百分位截断后线性映射到0~100。这样每个地块得到一个混合度得分,再结合聚类标签做交叉分析。比如“商业-科研复合类型”里,混合度得分高于80的网格就是真正的产城融合高地;低于40的网格虽然同簇,但实际还是单一功能占主导。
我在写评分卡时有一个习惯:先不给权重,直接把三个指标算出来分别画地图,用眼睛看哪个指标和城市认知更吻合,再回到权重。比如某城市的老城区,熵很高但POI总量也高,主功能占比倒数稳定,那权重就不用调;但如果熵高是因为小餐馆类别细分太碎,就要先合并类别再重算。
5. 从数据到结论的5个踩坑点:这些坑我都替你踩过
5.1 网格大小不同导致聚类结果天差地别
现象:用三百米网格聚类出5类,换成八百米网格变成3类,且住宅和商业被混在一起。原因:网格边界影响POI归属,小网格更碎,大网格更平滑,但面积过大的网格会把功能混合度人为抬高。解决:不要追求“唯一正确”的网格大小,而是确定业务口径。城市功能区评估普遍采用500米到1公里网格,我用500米跑主模型,再用250米做敏感性测试。如果两种网格下,主要簇中心业态占比差异小于15%,说明结果稳定;否则就要回头检查类别粗分是否合理。
5.2 K均值对“待开发区”这类零值样本产生误判
现象:没有任何POI的已剔除,但只有零星一两个POI的待开发区(比如一个加油站在荒地里)会被分到“商业型”簇。原因:K均值基于欧氏距离,零星样本与某个簇中心的距离可能恰好最近,但业务上它们是低密度未开发区。解决:先按POI总量做密度分档,总量小于某个四分位阈值的网格单独标记为“待开发/低密度区”,不参与聚类。这也是我在地块功能识别里最常用的一招:用密度先做排除法,再用K均值做类型划分。
5.3 类别分布不均衡导致的“餐饮独大”问题
现象:某个K代表的地块簇,所有类别中心里只有“餐饮”频数突出,簇内所有地块都被打上“餐饮区”标签。原因:餐饮POI本身数量占比过高,且种类多、密度高,即使做了Z-Score,它内部方差也远大于其他类别。解决:在特征工程阶段把餐饮细类合并成“餐饮”一类,并在标准化后对餐饮列做额外的降权,比如乘以0.7。另外一个更彻底的办法是改用min–max归一化而不是Z-Score,但整体效果不如精细粗分。
5.4 聚类结果不稳定,换random_state就变样
现象:K均值运行两次,轮廓系数相同,但某个边缘地块的标签从“住宅”变成“商务”。原因:初始中心点随机,数据量大或者簇间边界模糊时,结果可能落在不同局部最优。解决:固定random_state是第一步,但不能只固定一个。我一般跑20个不同的随机种子,统计每个地块的标签众数作为最终标签,只有超过70%次数稳定的地块才认定标签可信。这一步代码量不大,却能让后续评分不再被人质疑“是玄学吗”。
5.5 将“识别类型”和“复合评分”混为一谈
现象:有人直接拿聚类标签当作复合程度高低的排序,比如认为“商业-科研复合”就是比“商业”更厉害,这是业务逻辑错误。原因:聚类簇是按特征相似度分组的,不代表某个簇天生比另一簇混合度更高。解决:始终遵循“标签给类型,熵给程度”的原则。同样一个“商业-科研复合”簇内,混合度得分排序才是规划部门关心的,标签本身只用于地图专题。
6. 进阶:把聚类结果做成一个可评估的地块功能体检工具
到这里你已经完成了从POI到网格画像、从K均值聚类到复合程度评分的全过程。接下来值得做的不是再调参,而是把结果固化成一个可重复执行的流程。我一般的做法是把上面所有代码封装成三个函数:poi_to_grid、cluster_type、mixture_scoring,输入是POI原始表和参数,输出是一张含网格ID、类型标签、混合度得分的数据表。封装的好处是换城市或换年份数据时,只要重新跑一遍脚本就能得到纵向对比底表。
验证这个工具的方法很简单:随机抽取当年新增的商业用地或产业园区名录,查看它们在聚类结果中是不是被识别为商业或商业-科研复合,混合度得分是否高于全市中位数。我曾在一次项目中拿城市当年的招商落地项目名单做验证,命中率大约75%,剩下25%主要是新开发的待建区,POI还没长出来。这提醒我,在“待开发区”类型里,混合度评分应该默认偏低,否则会干扰验证。
还有一个进阶方向是引入K均值以外的参照算法。K均值擅长球状簇,但城市功能边界是复杂形状,所以可以用DBSCAN对网格中心坐标做一次空间聚类,把空间上连片的同类地块合并成“功能片区”,再做一次复合程度聚合。注意DBSCAN的eps参数要跟网格大小挂钩,比如网格是500米,eps设为2(约1公里)比较合适。不过这只是一种补充,K均值在这个项目里的核心地位不会动摇:它给你一个稳定、可解释、能落表的离散分类,而复合程度评估给了连续分数。
最后说一个我自己的习惯:每次跑完聚类,我会把K值、随机种子、网格大小、类别粗分映射表全部写进配置清单,然后截个图放在项目笔记里。这不是强迫症,而是三个月后有人问你“这个住宅评分怎么来的”时,你能把每一步的后悔药都翻出来。功能复合类型识别是城市数据机器学习项目里少有的“投入小、见效快、好解释”的方向,希望这次的拆解能帮你在拿到一包城市POI数据时,少走几段我走过的弯路。
本文还有配套的精品资源,点击获取