ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python数据挖掘实践:k-means聚类与二手房数据分析全流程

Python数据挖掘实践:k-means聚类与二手房数据分析全流程 简介面向Python数据分析初学者与高校教学场景的实践型配套资源包内容覆盖从数据加载、清洗、特征工程到建模评估、结果可视化的完整流程。压缩包内含约两千个文件总大小约120MB以代码脚本、数据集、可视化图表、说明文档及交互式演示页面等为主便于读者对照代码逐步理解思路并复现实验结果。目前已有两百二十四人学习使用。资源中提供电商用户行为、区域房价分布、聚类结果分组等多场景完整项目读者可结合真实数据与配套脚本熟悉Pandas、Numpy、Scikit-learn等常用库的操作方法掌握数据预处理、特征构造、模型选择与调优、模型评估等关键技能系统提升数据挖掘实战能力。可作为课程设计、毕业设计或竞赛准备时的参考实现缩短从理论到落地的距离。1. Python 数据挖掘实践配套代码及数据不是给你看是给你跑的这份压缩包拆开后里面全是一堆 HTML 文件南京二手房总价小于 200 万的分布图、k-means 聚类结果分组 04 的六张图外加一份“原始代码.html”。第一次解压的人很容易误判它是一个看图包实际它是《Python 数据挖掘实践》教学的配套产物——HTML 里嵌着原始代码、交互图表和分组明细聚类跑出来的结论已经被固化成了文件。我拆完后的判断是它适合正在学 Python 数据挖掘、想拿真实城市数据走一遍全流程的人。你能从它学到的不是单个函数而是「pandas 清洗 → sklearn 聚类 → pyecharts 出图 → 分组落地」的完整闭环。想用 k-means 练手的直接照着 HTML 里的代码重跑一遍比看任何教程都管用。2. 搭建 Python 环境并把 zip 包验证干净从 python 安装到依赖对齐资源里的代码不会自己跑起来第一步永远是先把环境对齐。数据挖掘项目对环境敏感pandas、scikit-learn 的版本差异经常导致结果和图表的细微不同所以我不建议你直接在系统 Python 里裸装而是单独建一个虚拟环境让这个资源拥有一套独立依赖。2.1 为什么先对齐 Python 和依赖版本数据挖掘脚本最容易挂在环境上我拆过很多配套资源发现一个规律代码本身没问题翻车基本都翻在环境上。比如这段资源里如果用到 pyecharts 1.x而你装的是 pyecharts 0.5.xAPI 完全不同HTML 图表就生成不出来。再比如 pandas 2.0 里部分字符串方法的行为和 1.x 有差异处理“万”这种单位后缀时结果可能不一样。所以拿到 zip 包别急着解压先确定三件事你的 Python 版本、pip 版本、以及项目依赖的库版本。这个资源包定位是教学配套依赖不会很激进用 Python 3.93.10 基本都能跑通。下面是这个场景下最常见的一组依赖库用途建议版本区间pandasDataFrame 数据清洗与聚合1.5.x ~ 2.0.xnumpy数值计算与数组操作1.23.x ~ 1.26.xscikit-learnKMeans、StandardScaler、评估指标1.2.x ~ 1.3.xmatplotlib静态图表绘制3.7.x ~ 3.8.xpyecharts生成可交互 HTML 图表1.x注意不是 0.5.xplotly备用的交互可视化方案5.xjupyter运行 .ipynb 教学代码任意较新版本版本区间是经验值不是硬性要求。关键是别混装老版本 pyecharts否则资源里那些带 dark 后缀的暗色主题页面你复现不出来。2.2 python 安装与虚拟环境创建一条命令建出干净运行空间如果你还没装 Python直接去官网下载 3.10 版本的安装包安装时记得勾选“Add Python to PATH”。这一步经常被跳过导致后面命令行里找不到 python。装完之后我习惯用下面的方式创建虚拟环境py -3.10 -m venv .venv # windows 激活 .venv\Scripts\activate # mac / linux 激活 source .venv/bin/activate # 安装依赖 pip install pandas numpy scikit-learn matplotlib pyecharts plotly jupyter激活成功后命令行前缀会变成(.venv)这时 pip 装的包都隔离在这个环境里不会污染全局 Python。Windows 上如果执行 pip install 时报“由于找不到 msvcp140.dll 无法继续执行代码”说明系统缺少 VC 运行库去下载微软官方 Visual C Redistributable 装一遍就能解决和 Python 本身没关系。装完依赖后强烈建议先跑一次导入校验确认所有库的版本是兼容的import pandas as pd import numpy as np import sklearn import pyecharts print(pandas:, pd.__version__) print(numpy:, np.__version__) print(sklearn:, sklearn.__version__) print(pyecharts:, pyecharts.__version__)如果 pyecharts 打印出来是 0.x说明装错了版本立刻pip uninstall pyecharts后重装。这一步只要十分钟但能省掉后面无数个“为什么我的图表是空白”的排查时间。2.3 解压 zip 并验证内容完整先把文件结构摸清楚环境就绪后再回来处理 zip 包。很多人直接用鼠标右键解压解压完发现 HTML 文件散落一地。我一般会先用脚本看一眼压缩包内部结构再决定解压到哪个目录import zipfile from pathlib import Path zip_path Path(Python 数据挖掘实践配套代码及数据.zip) out_dir Path(D:/dataming_practice) out_dir.mkdir(exist_okTrue) with zipfile.ZipFile(zip_path) as zf: # 先预览文件清单确认目录结构避免解压后文件满天飞 for info in zf.infolist(): print(info.filename, info.file_size) # 解压全部文件 zf.extractall(out_dir, pwdNone)代码里的infolist()会打印每个文件的名称和大小方便你核对是否有损坏条目。extractall(out_dir)把全部文件解压到指定目录out_dir 不存在时会自动创建。注意pwdNone表示压缩包没有密码如果你的 zip 有密码需要改为pwdbyour_password。解压后逐个用浏览器打开 HTML 文件验证。带dark后缀的通常是暗色主题页面不带的是常规主题内容应该一一对应。如果某个 HTML 打开是空白或者显示乱码先别怀疑资源损坏很可能是文件路径带中文导致浏览器编码识别错误稍后再讲怎么处理。这一步走通环境问题就算清零了。3. 从 HTML 反推数据处理流水线缺失值、量纲和特征工程的一条完整流水线资源里核心主题是南京二手房文件列表里出现了“总价小于 200 万的分布图”和 k-means 分组结果说明原始数据处理流程大概率是读取二手房数据 → 清洗缺失值和异常值 → 构造特征 → 交给聚类模型。原始代码.html 里应该能看到这段流程的源码我按这个教学场景最常见的做法把整条流水线拆给你看。3.1 从 HTML 里的原始代码还原数据加载与字段字典如果是网页版代码你可以直接在浏览器里按 CtrlU 查看网页源码把代码段复制出来另存为.py文件。以下是我在类似教学资源里最常见的加载方式import pandas as pd df pd.read_csv(nanjing_ershoufang.csv, encodingutf-8) print(df.shape) print(df.columns.tolist()) print(df.head())encodingutf-8是必须显式指定的。很多爬虫导出的二手房 CSV 实际是 GBK 编码如果你不指定pandas 会用系统默认编码去读大概率直接报 UnicodeDecodeError。如果读取时报错试着把编码改成gbk或gb2312。在真实场景里这份数据的字段一般长这样字段名含义清洗注意事项total_price总价单位万元可能带“万”字需转数值area建筑面积平方米注意 0 和极端大值unit_price单价元/平方米可用 total_price*10000/area 交叉验证district区属比如鼓楼、江宁做编码或 one-hotlayout户型比如“3室2厅1卫”正则拆出室、厅数量toward朝向做类别映射南北通透单独标记floor_level楼层/总层数可能缺省值缺失时考虑填充策略字段名不一定要和资源里的完全一致但清洗思路是一致的先把文本字段转数值再处理缺失最后再谈建模。3.2 缺失值、重复值与总价字段清洗直接跑一遍给你看二手房数据最常见的三个坑是缺失值、重复记录和单位后缀。下面是我常用的清洗套路可以直接套用# 1. 去除完全重复的记录 df df.drop_duplicates() # 2. 处理总价字段去掉“万”字并转成 float df[total_price] ( df[total_price] .astype(str) .str.replace(万, , regexFalse) .str.replace(,, , regexFalse) .astype(float) ) # 3. 删除总价或面积缺失的行 df df.dropna(subset[total_price, area]) # 4. 剔除明显异常值总价小于10万或面积小于5平米 df df[(df[total_price] 10) (df[area] 5)] print(df.info())这段代码的逻辑是先去掉完全重复的行避免同一条房源被爬两次再把总价从字符串转成数值。.str.replace(万, , regexFalse)里的regexFalse表示按字面量替换不用正则性能更好且不会误伤其他字符。最后用布尔索引过滤掉明显不符合市场的异常记录。注意第 2 步必须放在dropna之前因为总价如果是 NaN.astype(str)之后会变成字符串 nan后续操作会引入脏数据。正确顺序是先转数值再删缺失。3.3 特征工程面积单价交互项、朝向编码与价格带切分数据清洗完之后下一步是让特征更贴合聚类任务。这里重点做三件事构造面积与总价的交叉特征、把户型文本拆成数值、把朝向映射成有序类别。# 构造交叉特征均价单位万元/平方米后续聚类时按需选用 df[avg_price] df[total_price] / df[area] # 从“3室2厅1卫”里拆出室和厅 df[room_num] df[layout].str.extract(r(\d)室).astype(float) df[hall_num] df[layout].str.extract(r(\d)厅).astype(float) # 朝向编码南北通透给最高分单朝向其次 toward_map { 南北: 2, 南: 1, 东: 1, 东南: 1, 北: 0, 西: 0, 西北: 0, } df[toward_score] df[toward].map(toward_map).fillna(0) # 构造价格带标签用于后续验证聚类结果是否符合业务直觉 df[price_band] pd.cut( df[total_price], bins[0, 100, 200, 300, 500, 10000], labels[100, 100-200, 200-300, 300-500, 500], )str.extract(r(\d)室)用正则从户型文本里抓第一个数字得到室的数量抓不到就变成 NaN。fillna(0)处理未匹配到的朝向。pd.cut把连续的总价切成价格带这一步不是为了建模而是给后面解读 k-means 分组提供一个业务对照基准。做特征工程时我习惯把每一步的结果打印出来验证一眼别一口气写完再回头查。你会发现“为什么分组里出现单价 2 万但总价才 80 万”这种问题十有八九是某个字符串没转干净。4. k-means 聚类完整落地从肘部法选 K 到分组 04 的业务解读资源里的 HTML 文件名已经把结论告诉我们了k-means 聚类结果分成了 5 组编号 0 到 4。这意味着 K5。但如果你自己动手复现不能直接拍脑袋定 5得有一个选 K 的过程。这一章讲清楚选型理由、参数含义和分组解读。4.1 为什么用 k-means 而不是层次聚类或 DBSCAN和业务诉求对齐这个场景数据量在几千到几万条量级特征以连续的数值型为主业务诉求是“把南京二手房分成几个有解释力的价格段”k-means 是最合适的选择。相比层次聚类k-means 在大样本下计算快、内存占用可控相比 DBSCANk-means 的结构更稳定不会因为局部密度波动产生大量噪声点。当然 k-means 也有边界它假设簇是凸的对量纲敏感。所以用之前必须做标准化否则面积和总价不在一个数量级上聚类结果会被单价或总价一个特征牵着走。这也是资源里为什么会有多个“分组”HTML 文件——同一个模型输出被固化成多张页面方便逐组观察。特性k-means层次聚类DBSCAN适合规模万级以上几千条以内任意规模簇形状偏凸形任意但计算重任意含噪声参数敏感性主要调 K连接准则eps 和 min_samples结果可解释性较好好有树状图一般噪声需单独处理本资源核心用 k-means 是合理的教学场景也最容易理解。4.2 肘部法与轮廓系数选 K别把 K 定成拍脑袋的数字选 K 的常见做法是肘部法遍历候选 K计算每个 K 下的簇内误差平方和inertia找到曲线拐点。但惯性是随着 K 增大单调递减的拐点经常不清晰所以我习惯搭配轮廓系数一起看。以下是完整代码from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score from sklearn.preprocessing import StandardScaler # 这里只取三个核心连续特征做聚类 features [total_price, area, avg_price] X df[features].dropna().copy() # 标准化均值0方差1避免量纲主导距离 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 候选K从2到8分别记录惯性和轮廓系数 k_range range(2, 9) inertia_list [] silhouette_list [] for k in k_range: km KMeans(n_clustersk, n_init10, random_state42) labels km.fit_predict(X_scaled) inertia_list.append(km.inertia_) sil silhouette_score(X_scaled, labels) silhouette_list.append(sil) print(fK{k}, inertia{km.inertia_:.2f}, silhouette{sil:.4f})n_init10表示用 10 组不同的初始质心跑 10 次取最优结果避免落入局部最优。random_state42固定随机种子保证你重跑和第一次跑得到完全一致的标签。sklearn 新版本里n_init默认值已经变大但显式写上能把意图表达清楚。实际判断时看 inertia 曲线K3 到 5 之间如果是明显的折线拐点那个 K 优先再看 silhouette一般 0.25 到 0.5 之间说明聚类结构比较清晰。如果 silhouette 低于 0.2这个特征组合本身可能不适合 k-means要回退调整特征。4.3 训练、分组与画像输出从聚类结果到分组 04 业务解释选好 K 之后用 K5 重训一次并输出每个分组的特征均值这是我在项目里必做的一步。只给标签不给画像等于白做了聚类import numpy as np # 固定K5和资源里的分组0~4对齐 km KMeans(n_clusters5, n_init20, random_state42) km.fit(X_scaled) # 把标签回填到原数据 df[cluster] km.labels_ # 生成分组画像 profile ( df.groupby(cluster)[[total_price, area, avg_price]] .mean() .round(2) ) print(profile)跑完后你会看到一个类似下面的画像表数值因数据而异clustertotal_price 均值area 均值avg_price 均值业务定性0180 万62 ㎡2.9 万/㎡刚需上车盘1350 万105 ㎡3.3 万/㎡改善型295 万40 ㎡2.4 万/㎡老破小/小户型3680 万160 ㎡4.2 万/㎡高端改善/学区4130 万55 ㎡2.3 万/㎡低价刚需注意k-means 的分组编号是随机的编号 0 不代表总价最低只代表算法迭代时最先碰到的质心。如果你看到自己的分组 2 和其他人分组 2 含义完全不同这是正常的后面避坑章会讲如何重映射标签让分组编号按业务口径统一。5. 避坑与常见问题排查聚类现场的五个坑还原这部分是我在实际复现教学资源时踩过或看别人踩过的坑每个都按现象、原因、解决的顺序讲清楚。没有玄学全是真实操作细节。5.1 HTML 图表打不开双击变成空白页现象解压后双击“南京二手房总价小于200万的分布图.html”浏览器打开是一片空白控制台报错。原因这类 HTML 图表是 pyecharts 或 plotly 生成的JavaScript 脚本需要从本地或其他路径加载资源。如果你的文件被移动过相对路径失效图表自然渲染不出来另一种情况是浏览器插件拦截了本地脚本。解决把整个目录保持原样不要单独把 HTML 抽出来放在别处。用浏览器打开时优先用 Chrome 或 Edge地址栏输入file:///开头的完整路径避免双击默认浏览器造成编码识别问题。如果还是空白右键查看网页源码看div容器里是否有图表初始化代码确认 HTML 没有损坏。5.2 聚类结果每次跑都不一样同一份代码两次结果不同现象你照着原始代码重跑了一次 k-means发现分组结果和资源里保存的完全对不上甚至自己连跑两次出来的分组归属也不同。原因KMeans 的初始质心是随机选择的sklearn 默认n_init较小时不同批次的结果会漂移。教学资源里的 HTML 是用固定随机种子生成的你复现时没设置random_state就复现不出完全一致的分组。解决建模时显式传入random_state42和n_init10以上。Python 运行前先执行import numpy as np; np.random.seed(42)统一随机源。如果仍然不一致检查 pandas 读入数据的行序是否一致df.sample()或洗牌操作会打乱行序直接影响聚类结果。5.3 总价字符串转数值后整列全是 NaN现象执行df[total_price].astype(float)后总价列变成大量 NaN后续聚类直接报错。原因总价字段里混入了逗号分隔符比如1,050,000或1050万这种混合格式astype(float)遇到非法字符串会直接转为 NaN而且会在整列生效。解决先清理再转换按顺序执行去逗号、去“万”字、统一转 float。推荐用pd.to_numeric(df[total_price], errorscoerce)errorscoerce会把无法解析的值转成 NaN 而不是抛异常这样你能看到哪些脏数据需要单独处理。df[total_price_clean] pd.to_numeric( df[total_price].astype(str).str.replace(,, , regexFalse), errorscoerce, )5.4 忘了标准化聚类结果只按总价一个维度切分现象分组画像显示 total_price 差异很大但 area 和 avg_price 在各组几乎没有区分度整份结果像是“手动设阈值切总价”。原因总价的数值范围几十到上千远大于面积和均价欧氏距离计算时总价主导了样本距离。不标准化的情况下k-means 等价于只对总价做一维分箱。解决对参与聚类的所有连续特征做StandardScaler在训练前用scaler.fit_transform(X)而不是只用原始数值。注意标准化和反标准化要成对使用预测时用训练时的scaler.transform描述分组时用scaler.inverse_transform还原成真实价格否则你打印出来的分组中心是 0 均值标准化后的数值没法直读。5.5 标签编号不对齐你的分组 0 不是资源里的分组 0现象你复现出 5 个分组但和资源里分组 04 的画像对应不上你的分组 0 总价均值最高资源里分组 0 却是最低价组。原因k-means 的标签编号由初始化位置决定同一个模型跑 10 次种子分组编号顺序都不同。编号本身没有业务含义说“分组 0 是刚需盘”是错误理解。解决训练完成后用聚类中心的总价均值给每组分排序把标签统一重映射到业务顺序# 按聚类中心的 avg_price 从小到大排序重映射标签编号 order np.argsort(km.cluster_centers_[:, 2]) # 第3列是avg_price label_map {old: new for new, old in enumerate(order)} df[cluster_ordered] df[cluster].map(label_map)这里的order是聚类中心按均价排序后的旧标签索引label_map把旧标签映射成 04 的新编号0 表示均价最低的一组。这样做之后你的分组 0 就和资源里按业务口径定义的“低价刚需组”对齐了。6. 把分组结果做成可交付结论稳定性验证、业务标签与一键出图聚类做完不等于项目交付业务方要的是“哪些房源归到同一类、为什么”。我最后一步习惯做两件事稳定性验证和业务标签落地。先跑一遍稳定性验证用多个随机种子重复聚类计算两两之间标签的相似度确认分组不是随机种子碰巧碰出来的from sklearn.metrics import adjusted_rand_score X_scaled scaler.fit_transform(df[features]) labels_list [] for seed in [0, 1, 42, 2024]: km KMeans(n_clusters5, n_init20, random_stateseed).fit(X_scaled) labels_list.append(km.labels_) # 用ARI衡量不同种子下聚类结果的一致性1表示完全一致 ari adjusted_rand_score(labels_list[0], labels_list[2]) print(fseed 0 vs seed 42 ARI: {ari:.4f})ARI 在 0.9 以上说明聚类结构稳定可以作为可靠结论交付低于 0.7 建议重新选特征或调 K。这一步做完再把分组标签映射成业务口径输出最终 CSVdf[business_label] df[cluster_ordered].map({ 0: 低价小户, 1: 刚需上车, 2: 改善型, 3: 高端改善, 4: 学区/稀缺 }) df[[district, layout, total_price, area, avg_price, cluster_ordered, business_label]].to_csv(nanjing_clusters.csv, indexFalse)最后用 pyecharts 画一张按分组着色的散点图X 轴面积、Y 轴总价直接输出 HTML和资源里那些页面保持同一种交付形态from pyecharts.charts import Scatter from pyecharts import options as opts scatter ( Scatter() .add_xaxis(df[area].tolist()) .add_yaxis( 南京二手房分组, df[[total_price, cluster_ordered]].values.tolist(), is_selectedTrue, ) .set_global_opts(title_optsopts.TitleOpts(title南京二手房 k-means 分组)) ) scatter.render(nanjing_clusters.html)从那以后我每次做聚类交付都强制走一遍这套流程先标准化、再固定随机种子、输出分组画像、重映射标签、验证稳定性最后才生成图表。这几个动作缺一个结果就有可能在交付时被人问倒。希望帮到你也祝你能把这套配套资源吃透跑出自己的分组结论。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进