简介面向计算机相关专业在校学生与毕业设计人员的Python实战项目基于链家真实房源数据完成深圳各区二手房房价的爬取、清洗、分析与预测并生成可视化图表。作者为获得导师认可的高分毕业设计评审96.5分代码经测试运行成功适合作为毕设、课程设计或期末大作业的参考底稿也便于入门者学习数据采集与房价预测的完整流程。压缩包共13个文件包括Python源代码、9个分区房源xls数据文件福田、南山、龙岗、宝安等、README说明文档与效果预览图整体大小654KB结构清晰便于检索。已有236人下载学习。资源提供完整可运行的项目方案既有房价数据爬取脚本也有预测分析与可视化实现读者可对照代码理解数据预处理、特征分析与模型预测的完整链路也可在此基础上替换数据或扩展功能用于自己的课题。1. 基于Python实现深圳二手房房价预测分析这套源码包能让你少走三个月弯路拿到「基于Python实现深圳二手房房价预测分析及可视化源代码文档说明效果图(数据来源链家).zip」这个标题时我第一反应是这不就是典型的数据分析入门到实战的完整闭环吗用链家的公开二手房数据做房价预测再配上一套可视化大屏既能练爬虫、又能练特征工程、还能练模型调参与前端展示对一个想往数据方向转的Python工程师来说是性价比很高的一份练手工程。但正因为这类项目遍地都是反而更需要说清楚里面的源代码到底怎么组织、文档说明覆盖到什么程度、效果图对应的图表能不能复现以及最关键的是——链家数据在实际抓取和建模时有哪些坑是教程里不会写的。我拆开看过不少类似的项目包结构大同小异一个爬虫脚本、一个清洗脚本、几个模型训练文件、一堆HTML或者Flask模板最后配一张看起来很唬人的可视化大屏截图。真正决定这个项目值不值得跑一遍的不是代码多不多而是它的数据管线是否完整、预测模型是否有合理的评估逻辑、可视化是否真的能回答哪些因素在影响深圳房价这个问题。这篇文章我就按自己平时做这类项目的路子把从链家源数据到预测结果可视化的每一步拆开讲包括命令行怎么跑、参数怎么调、哪些地方最容易翻车以及最后的SHAP解释性分析——这是让模型从黑匣子变成业务方可信结论的关键一步也是新手最容易忽略的地方。2. 从链家到DataFrame深圳二手房数据抓取与清洗的完整姿势2.1 为什么先要搞清链家页面的结构URL规律与反爬压力测试做链家二手房数据抓取第一步不是写爬虫而是打开浏览器手动翻几页列表页把URL规律摸清楚。深圳链家二手房列表页的地址形如https://sz.lianjia.com/ershoufang/pg2/其中pg2就是页码sz是城市缩写。这个规律简单到让人掉以轻心但实际抓取时你会发现列表页能拿到的是房源标题、总价、单价、小区名、几室几厅、面积和关注度而真正对房价预测更有用的房屋年限电梯配比朝向装修情况这些细节藏在详情页里。详情页URL通常是/ershoufang/房源编号.html房源编号是10位数字在列表页的链接里可以正则提取。反爬方面链家对高频请求的封禁策略比较明显。我实测过用普通requests库无脑循环抓列表页每页睡2秒抓30多页就会出现验证码跳转响应里的页面标题从深圳二手房变成访问验证。所以我的常用做法是先抓少量样本做压力测试看请求频率和封禁阈值之间的关系再决定是加随机延时继续扛还是直接用Selenium模拟浏览器操作。请注意爬虫合规性需要自己把握这里只讲技术路径。2.2 最小可复现的爬虫代码从列表页到详情页的字段补齐下面这段代码是我在类似项目里常用的一个精简版爬虫核心思路是先抓列表页拿到基础字段与房源编号再对详情页做二次请求补齐“梯户比”“产权年限”这类字段。为了降低被封概率用了requests.Session保持会话并且设置了随机User-Agent。import requests import re import time import random import pandas as pd from bs4 import BeautifulSoup SESSION requests.Session() SESSION.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Referer: https://sz.lianjia.com/ }) def parse_list_page(html, district): 解析列表页返回基础字段DataFrame soup BeautifulSoup(html, lxml) items [] for li in soup.select(li.clear): title_tag li.select_one(.title a) if not title_tag: continue link title_tag[href] house_id re.search(r/(\d{10})\.html, link).group(1) total_price li.select_one(.totalPrice span) # 总价单位万元 unit_price li.select_one(.unitPrice span) # 单价单位元/平 info li.select_one(.houseInfo).get_text().split(|) # info 形如 [2室1厅, 57.5平米, 南 北, 简装, 高楼层/共28层, 板楼] items.append({ house_id: house_id, district: district, title: title_tag.get_text(stripTrue), total_price: float(total_price.get_text()) if total_price else None, unit_price: float(unit_price.get_text().replace(,, )) if unit_price else None, house_info: |.join([x.strip() for x in info]), link: link }) return pd.DataFrame(items) def fetch_detail(house_id): 抓取详情页补齐预测所需的额外特征 url fhttps://sz.lianjia.com/ershoufang/{house_id}.html try: resp SESSION.get(url, timeout10) if resp.status_code 200 and 访问验证 not in resp.text: soup BeautifulSoup(resp.text, lxml) intro_list soup.select(.introContent .base li) attrs {} for li in intro_list: label li.select_one(.label) value li.select_one(.value) if label and value: attrs[label.get_text(stripTrue)] value.get_text(stripTrue) return attrs else: return None except Exception as e: print(ffetch {house_id} failed: {e}) return None逻辑说明parse_list_page里用BeautifulSoup抓取列表页的每一个li.clear模块从中提取房源编号、总价、单价和户型面积信息。这里的house_info字段用竖线拼接方便后面在清洗阶段做拆分。fetch_detail是单独对详情页发起请求将基础属性如房屋年限、电梯配比存成字典返回。需要注意详情页一次请求只能补一个房源属性如果抓取量大建议只对训练集样本抓取详情避免请求量过高。2.3 数据清洗的细节朝向、楼层与面积字段怎么拆才不丢信息链家返回的house_info是一个混合字符串例如2室1厅 | 57.5平米 | 南 北 | 简装 | 高楼层/共28层 | 板楼 | 有电梯。直接把这个字符串丢给模型是不行的必须先拆成结构化字段。我一般会写一个独立的清洗函数用固定的分隔符去切分并做异常值处理def clean_house_info(df): 把house_info切分为多个特征列并处理脏数据 split_df df[house_info].str.split(|, expandTrue) # 链家字段通常按顺序出现但某些房源字段缺失所以先重命名 split_df.columns [layout, area, orientation, decoration, floor, structure, elevator] \ [extra] * (split_df.shape[1] - 7) # 面积列可能带平米后缀需要提取数值 split_df[area] split_df[area].str.replace(平米, ).astype(float) # 朝向可能是 南 北拆分出主朝向并处理空值 split_df[orientation_main] split_df[orientation].str.split( ).str[0] # 楼层信息解析高楼层/共28层 - 楼层数、楼层位置 floor_info split_df[floor].str.extract(r(.*?)楼层?/共(\d)层) split_df[floor_position] floor_info[0] split_df[total_floors] floor_info[1].astype(float) # 删除原混合字段 df df.drop(columns[house_info]).join(split_df) return df参数说明str.extract用正则(.*?)楼层?/共(\d)层匹配楼层位置和总层数。这里有个坑链家有的房源写的是地下室/共1层有的写低楼层/共6层正则里的楼层?可以兼容楼层和楼层两种写法但如果你遇到暂无数据这类占位符astype(float)会直接报错。所以实际清洗前我通常会先对floor_info做一次replace(暂无数据, np.nan)再转数值类型。另外layout字段里还包含2室1厅里的数字可以进一步提取room_num和hall_num这两个特征对房价影响显著。2.4 坐标与行政区特征链家数据里没有的得靠外部补充链家列表页和详情页都没有经纬度只有行政区、商圈和小区名。如果只做纯表格特征建模行政区就够用了但如果想在地图上做可视化就需要地理编码。我一般不引入太重的外部依赖直接用高德或百度开放平台的逆地理编码API按小区名批量查询经纬度。查询时需要控制并发因为免费配额有限而且返回的JSON里geocodes可能为空这时我会把该小区名加入待人工核对列表而不是直接丢掉。补充坐标这一步不是必须的但如果你想让可视化效果图更丰富比如在ECharts上画散点图按总价着色经纬度就是必需品。另外行政区本身是一个强特征深圳的南山、福田、罗湖、宝安、龙岗、龙华、光明、坪山、盐田、大鹏新区这些区域的房价差异很大建模时建议对行政区做OneHot或者目标编码而不是简单换成数字0、1、2因为后者的排序逻辑会误导模型。我自己的经验是把行政区做pd.Categorical编码再用XGBoost的enable_categorical参数直接吃类别特征效果比OneHot更稳定。3. 房价预测模型怎么选线性回归、随机森林与XGBoost的实测对比3.1 特征工程哪些字段该进模型哪些字段是噪音模型训练前特征工程决定了预测上限。我按项目打包里的常见字段整理了一份推荐特征清单特征名来源类型理由total_price列表页float目标变量单位万元unit_price列表页float一般不直接作为特征避免数据泄露area详情页float面积单位平米room_num / hall_numhouse_info拆分int户型结构floor_positionhouse_info拆分category高/中/低楼层影响采光与噪音total_floorshouse_info拆分int高层与小高层的价格逻辑不同elevatorhouse_info拆分category无电梯的老破小单价偏低decorationhouse_info拆分category精装/简装对价格影响明显orientation_mainhouse_info拆分category朝南比朝北贵是共识district列表页category行政区是房价最强区域特征building_year详情页int房龄注意缺失率排除项title房源标题、linkURL、house_id唯一ID这些字段和房价没有稳定的因果逻辑放进去会让模型学习到ID记忆泛化能力变差。另外unit_price更不能进特征因为total_price unit_price * area它一进去就是作弊。我一般会在建模前做一次缺失值统计用df.isnull().mean()看每列的缺失比例。链家详情页里building_year的缺失率有时会超过30%对这种情况我倾向于保留缺失在XGBoost里让缺失值走向最优方向分支而不是强行用中位数填充。3.2 三个模型的训练与对比不要只看R²还要看误差分布训练集和测试集的划分要用时间切分而不是随机切分。链家数据样本是一个月内的挂牌房源如果按随机切分同一个小区的相似房源会同时出现在训练集和测试集导致R²虚高。我习惯按房源发布时间排序取前80%做训练后20%做测试这样能贴近真实调用场景——用历史数据预测最新挂牌价。下面是一段用sklearn和xgboost分别建模的代码输出三个模型的评估指标import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import LinearRegression from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import xgboost as xgb import warnings warnings.filterwarnings(ignore) def build_model_data(df): 构造模型输入特征X和目标y feature_cols [area, room_num, hall_num, total_floors, floor_position, elevator, decoration, orientation_main, district] # 对类别列做字符串化方便统一编码 cat_cols [floor_position, elevator, decoration, orientation_main, district] for col in cat_cols: df[col] df[col].astype(category).astype(str) X df[feature_cols] y df[total_price] return X, y def train_and_evaluate(X, y): 按时间切分训练集/测试集训练三个模型并比较 # 这里假设 df 已按发布时间排序直接用位置切分 split_idx int(len(X) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] # 管道1线性回归类别变量做OneHot preprocess ColumnTransformer([ (num, passthrough, [area, room_num, hall_num, total_floors]), (cat, OneHotEncoder(handle_unknownignore), [floor_position, elevator, decoration, orientation_main, district]) ]) lr_pipe Pipeline([ (prep, preprocess), (lr, LinearRegression()) ]) # 管道2随机森林同样用OneHot rf_pipe Pipeline([ (prep, preprocess), (rf, RandomForestRegressor(n_estimators300, random_state42, n_jobs-1)) ]) # 模型3XGBoost直接喂类别字符串xgb会自动处理 xgb_model xgb.XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, enable_categoricalTrue, random_state42 ) models { linear: lr_pipe, rf: rf_pipe, xgb: xgb_model } results {} for name, model in models.items(): model.fit(X_train, y_train) pred model.predict(X_test) mae mean_absolute_error(y_test, pred) mse mean_squared_error(y_test, pred) rmse np.sqrt(mse) r2 r2_score(y_test, pred) results[name] {MAE: mae, RMSE: rmse, R2: r2} print(f{name}: MAE{mae:.2f}万元, RMSE{rmse:.2f}万元, R2{r2:.3f}) return results, y_test, models逻辑说明ColumnTransformer把数值列原样透传类别列做OneHot编码线性回归要求所有输入必须是数值所以这一步不能省。随机森林同样吃OneHot后的矩阵。XGBoost在enable_categoricalTrue时会自动识别类别列但要注意XGBoost的类别列必须是categorydtype传字符串不会自动转换所以我在前面的build_model_data里已经强制astype(category)。实测结果通常符合预期XGBoost的R²最高随机森林次之线性回归最低。但重点看MAE——深圳二手房总价中位数大概在400万左右MAE如果在30万以内说明模型对大多数房源的预测误差控制在7.5%以内这在业务上勉强能用如果MAE超过50万那就得回去补特征比如缺少房龄和电梯字段或者行政区编码不合理。3.3 目标变量变换为什么我用对数价格建模直接预测总价容易让模型在高端楼盘上产生极大偏差。深圳豪宅和普通住宅的价格差可以达到10倍如果直接用MSE做损失模型会把全部注意力放在拟合贵房子上普通房源的预测精度反而下降。常见做法是对total_price做log1p变换让目标变量接近正态分布损失函数在高价区和低价区的权重相对均衡。from sklearn.model_selection import cross_val_score def train_with_log_target(X, y): 对目标变量做log变换后训练预测完再还原 y_log np.log1p(y) split_idx int(len(X) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_log_train, y_log_test y_log.iloc[:split_idx], y_log.iloc[split_idx:] model xgb.XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, enable_categoricalTrue, random_state42 ) model.fit(X_train, y_log_train) pred_log model.predict(X_test) pred np.expm1(pred_log) # 还原真实房价 y_test np.expm1(y_log_test) mae mean_absolute_error(y_test, pred) r2 r2_score(y_test, pred) print(flog-target XGBoost: MAE{mae:.2f}万元, R2{r2:.3f}) return pred这里用np.log1p和np.expm1互为反函数。要注意预测值还原后的误差是相对均等的而不是对高价房一边倒。我在多个城市的数据上试过对数变换后XGBoost的MAE能比直接预测降低5%10%尤其当测试集里含有800万以上房源时效果更明显。如果你的项目文档说明里没有这一步你可能觉得模型效果不错但你看误差分布图就会发现所有低价房都被高估了——这就是没做目标变换的典型症状。3.4 调参经验网格搜索很慢先用手动观察学习曲线XGBoost参数多全部交给GridSearchCV会非常慢数据量上万条时单个参数组合可能跑十几分钟。我一般先固定n_estimators500然后手动调整learning_rate0.01到0.1之间观测训练集和测试集的损失曲线。如果训练集R²高但测试集R²低说明过拟合优先调低max_depth从6降到4同时增加subsample到0.7。如果两边都低说明欠拟合先调大n_estimators并调低learning_rate。随机森林的调参相对粗暴n_estimators从100加到500R²基本不变时就不用再加了。max_depth默认None无限深在特征少的情况下容易过拟合建议设成1020之间。另一个容易被忽略的参数是min_samples_leaf设成5可以减少模型对个别房源的记忆。4. 把预测结果画成图Matplotlib与Flask可视化的落地配置4.1 静态图表哪些图最能说明问题代码怎么组织效果图里最常见的三类图房价分布直方图、行政区均价条形图、实际价格与预测价格散点图。这三张图能回答数据长什么样、模型准不准两个核心问题。下面这段代码生成这三张图也是我放在源码包里的标准示例import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] # 防止中文乱码 plt.rcParams[axes.unicode_minus] False def make_static_plots(df, pred, y_test): 生成三张效果图并保存为PNG # 图1房价分布直方图 plt.figure(figsize(10, 5)) sns.histplot(df[total_price], bins50, kdeTrue) plt.xlabel(总价万元) plt.ylabel(房源数量) plt.title(深圳二手房总价分布) plt.savefig(price_distribution.png, dpi150) # 图2行政区均价条形图 district_mean df.groupby(district)[total_price].mean().sort_values() plt.figure(figsize(10, 6)) district_mean.plot(kindbarh) plt.xlabel(平均总价万元) plt.title(深圳各行政区二手房平均总价) plt.tight_layout() plt.savefig(district_mean.png, dpi150) # 图3测试集实际价格与预测价格散点图 plt.figure(figsize(8, 8)) plt.scatter(y_test, pred, alpha0.4, s10) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--) plt.xlabel(实际总价万元) plt.ylabel(预测总价万元) plt.title(实际价格 vs 预测价格) plt.tight_layout() plt.savefig(pred_vs_actual.png, dpi150)注意plt.rcParams[font.sans-serif] [SimHei]是必须的否则图上的中文全部变成方块。如果你运行环境里没有SimHei字体也可以用[WenQuanYi Zen Hei]或[Noto Sans CJK SC]但要在脚本开头检测系统字体列表避免报错。散点图里那条红色虚线是完美预测线点越靠近它说明模型越准。如果点在线的上方说明模型高估了房价下方则低估。从这张图能直观看出模型系统性的偏差。4.2 Flask可视化从静态图到可交互大屏的最小实现静态图只能看不能筛选。项目标题里的可视化如果只输出PNG说服力不够。常见的做法是把预测结果做成一个Flask应用页面里通过ECharts展示交互式图表用户可以通过下拉框选择行政区查看该区的均价趋势和房源分布。我不建议使用太重的前端框架一个templates/index.html加一个app.py就够了。from flask import Flask, render_template, jsonify import pandas as pd import json app Flask(__name__) # 假设 df_result 已经包含预测结果列 pred_price df_result pd.read_csv(result.csv) app.route(/) def index(): return render_template(index.html) app.route(/api/district/district) def district_data(district): 返回指定行政区的房源基础数据和预测价格 sub df_result[df_result[district] district] data { x: sub[area].tolist(), y: sub[total_price].tolist(), pred: sub[pred_price].tolist(), unit_price: sub[unit_price].tolist() } return jsonify(data) if __name__ __main__: app.run(host0.0.0.0, port8080, debugFalse)前端index.html里用ECharts的散点图接收fetch的响应把横轴设为面积纵轴设为总价每个点的颜色按单价映射。这个逻辑不复杂但有几个细节需要注意df_result在服务启动时一次性加载进内存如果你更新了数据需要重启Flask才能看到新结果所以我会在接口里加一个时间戳参数用?ts绕过浏览器缓存前后端交互时total_price和pred_price要保留两位小数否则图上数值会显示一长串小数。4.3 效果图的还原要点颜色映射与坐标轴尺度很多源码包里的效果图看起来很专业但你自己跑出来却是灰蒙蒙一片。原因通常是色带选择和数据尺度没处理好。比如在ECharts里用visualMap给散点着色如果min和max没有按真实数据设置颜色会全部偏到某一端。我一般会先跑一次df.describe()看total_price的分位数然后设置visualMap: { min: 200, max: 1000 }让色带集中在数据密集区。另一个坑是坐标轴的范围面积在30到200平米之间总价在200到1500万之间如果X轴和Y轴不设min/maxECharts会自动扩展导致大多数点挤在左下角效果图很难看。正确做法是取area的1%到99%分位数作为坐标轴边界。5. 避坑指南链家反爬、数据漂移与模型过拟合的5个常见问题5.1 链家页面结构变动导致解析失败现象昨天还能正常抓取的列表页今天突然一个房源都解析不出来select_one(.title a)返回None。原因链家前端改版CSS类名从.title换成了.tt或者干脆把链接嵌到了JavaScript渲染后的动态节点里。解决先手动用浏览器的查看网页源代码确认当前页面是否存在li.clear和.title这两个选择器如果不存在用requests.get的返回值打印前2000个字符检查是否有window.__NUXT__这类内嵌数据。链家部分页面已经开始用Vue服务端渲染数据可能在window.__NUXT__里这时解析方式完全不同应该改用正则抽取__INITIAL_STATE__里的JSON。5.2 时间上都手抓的房源价格中位数突然跳变现象训练出来的模型在测试集上MAE突然从30万涨到60万。原因链家数据是挂牌价不是成交价而且每个月挂牌结构会变。比如某一季度大量新增了宝安中心区的新盘挂牌均价被拉高而模型没有见过这个区域该时间段的样本。解决训练集里引入发布时间作为特征或者干脆按行政区月份做分组交叉验证保证每一个测试区域都有自己的训练对应时段。另外深圳的二手房指导价在2021年后对挂牌价产生明显影响链家页面上显示的参考价和挂牌价可能不一致这部分数据源里没有单独字段只能靠人工规则处理比如对单价超过某阈值的房源做缩尾处理。5.3 模型在训练集R²高达0.95测试集却只有0.75现象典型的过拟合信号。原因特征中包含了太多与目标变量直接相关的派生字段比如把unit_price放进特征或者total_floors和floor_position同时存在时模型记住了每个楼层的平均价而非真实规律。解决先删除unit_price字段再看district是否被OneHot后产生了高维稀疏列如果行政区有10个以上且某些区样本量极少模型会把这些区学成完全独立的截距无法泛化。我通常把样本量小于50的行政区合并为其他或者用目标编码让稀有类别向整体均值收缩。5.4 抓取数据量大时IP被封后前功尽弃现象爬虫跑了半小时后所有请求都返回验证码页面即使加延时也没用。原因链家的反爬策略不止看频率还看cookie和会话行为。如果同一个IP在短时间内访问了大量详情页容易被临时封禁。解决我的做法是把列表页和详情页分别控制速率列表页每页睡眠510秒详情页每请求睡眠38秒并且提前用session.cookies访问首页两次模拟普通用户进入网站的流程。如果项目要求抓取上万条数据最好还是准备多代理IP池但价格不低对学习项目来说没必要把样本控制在30005000条就够模型训练了。5.5 爬虫与后续处理脚本脱节DataFrame列名对不上现象清洗脚本跑完报KeyError: house_info但是爬虫明明抓到了这个字段。原因爬虫脚本输出的列名是英文但清洗脚本里写的是中文或者house_info在部分房源中不存在导致str.split之后列数少于预期。解决在爬虫代码里统一用英文列名比如house_info就叫house_info清洗函数开头对列名做一次assert并打印缺失率。另一个常见的脱节点是爬虫脚本生成的CSV用了utf-8-with-bom编码而ECharts读取时默认按UTF-8解析BOM带进来导致第一个字段名出现\ufeff前缀前端过滤条件永远匹配不上。解决方案是在写CSV时指定encodingutf-8不写默认的utf-8-sig。6. 最后一个技巧用SHAP解释房价预测说服业务方的不二法门我们花了一整篇文章讲爬虫、建模和可视化但如果你只是把预测模型的结果堆到页面上业务方还是会问为什么这套房你预测800万旁边的房却预测750万这时你需要模型解释性工具。我在项目里最后加的通常是一个SHAP分析脚本它能告诉你每个特征对单套房源价格的影响方向与大小。用XGBoost模型计算SHAP值非常方便一段简单的代码就能输出特征重要性排序和单个样本的力引导图import shap def explain_model(model, X_test): 用SHAP解释XGBoost模型的预测 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test) # 特征重要性条形图 shap.summary_plot(shap_values, X_test, plot_typebar, max_display10, showFalse) plt.savefig(shap_importance.png, dpi150) # 单个样本的力引导图选测试集第一个样本 shap.force_plot(explainer.expected_value, shap_values[0, :], X_test.iloc[0, :], matplotlibTrue, showFalse) plt.savefig(shap_force_sample0.png, dpi150) return shap_values参数说明shap.TreeExplainer专门用于树模型比KernelExplainer快几个数量级而且能处理缺失值。summary_plot有plot_typebar和默认的点图两种形式点图能看出每个特征是推高还是拉低房价。force_plot的力引导图是说服业务方的好东西它把基准预测值和每个特征贡献值画成一条推进线红色为正贡献蓝色为负贡献一眼就能看到面积大贡献了80万楼层低减了15万这种结论。在多个项目里我最后都是用SHAP图收尾并把它和效果图一起放进交付文档。说实话SHAP不是银弹它只对树模型稳定线性回归的SHAP计算需要换LinearExplainer否则结果可能不正确。但作为一份从链家数据到房价预测的完整源码包有没有SHAP解释决定了你交付出去的是一个公式还是一个可沟通的决策工具。这个步骤我建议每个复现的人都加上哪怕只是画一张全局特征重要性图也会让你对模型的理解比单纯看R²深刻得多。最后说个我自己的习惯每次跑完一个项目我会把result.csv和所有PNG图保存在带时间戳的目录里比如output/20240601/这样重新跑模型时不会覆盖旧结果对比调参前后效果也方便。模型文件也要导出为model.json而不是model.pkl因为JSON格式能跨版本使用换环境时不用重新训练。希望这份笔记能帮你把标题里那套zip跑通也祝你顺手把SHAP解释加进去——那才是整个项目最值得对外的亮点。本文还有配套的精品资源点击获取