简介:针对广州市二手房价预测这一典型数据分析场景,该压缩包提供了一套由数据、代码与图表构成的完整示例项目。压缩包共十九个文件,大小约1.05MB,以一份Python预测脚本、一份广州二手房数据集和十七张可视化图片为主要内容,结构清晰,便于对照学习。代码部分涉及数据读取、缺失值处理、特征分析、模型训练与效果评估等关键流程,调用Pandas、Scikit-learn等常用库完成;十七张图表则从朝向、所在区域、面积、装修、楼层、建成时间等维度展示了数据分布与总价关系,有助于直观理解特征对房价的影响。资源目前已有984人学习下载,适合希望借助真实数据掌握房价预测全流程的Python数据分析初学者。解压后可直接运行脚本,复用数据清洗与建模思路,并进一步尝试特征筛选和参数优化。
1. 广州二手房价预测:用一份 rar 把「玄学估价」变成可复现的回归模型
做二手房估价这件事,中介靠经验和话术,买家靠感觉和砍价,其实本质上都是同一个需求——给一套房定一个「合理的价」。我拿到「广州市二手房价预测——数据+python代码.rar」这个包,第一反应是这题值得认真做:它有明确的业务场景(广州真实的挂牌房价)、现成的数据文件、和一套可以改改就跑起来的 Python 代码,正好把「预测房价」从拍脑袋变成可复现的回归任务。这个方向适合三类人:想找笋盘的自住买家,可以用模型把挂牌价和预测价做差筛漏;房产相关从业者,可以用它批量估价做市场复盘;还有正在做数据挖掘项目或毕设的同学,它给你一个完整的数据清洗、特征工程、建模评估链路,不用从零攒数据和造轮子。这篇文章我就顺着这条线,把数据怎么盘、特征怎么造、模型怎么调、哪些坑必踩,完整过一遍。
2. 数据怎么盘:把挂牌数据从 Excel 洗成能喂给模型的特征表
2.1 数据结构长什么样:先看清楚每一列的含义再动手
任何一份二手房价数据,拿到的第一时间别急着跑模型,先搞清楚这张表到底长什么样。常见的 rar 解压后是一个 CSV 或 Excel 文件加一个 ipynb 或 py 脚本,数据列一般覆盖:小区名称、所在行政区(天河、海珠、越秀、番禺、黄埔、白云、荔湾、花都、南沙、增城、从化)、户型(几室几厅)、建筑面积、单价、总价、朝向、楼层、楼龄、装修情况、挂牌时间、关注人数、带看次数。有些数据集还会带经纬度、到最近地铁站距离、周边学校医院数量这些衍生字段,但这通常要自己做地理编码补齐,原始文件里不一定有。
先写一段代码把基础信息打印出来,这在任何数据处理项目里都是第一步:
import pandas as pd # 如果解压出来是 xlsx 就用 pd.read_excel,是 csv 就编码选 utf-8-sig df = pd.read_excel("guangzhou_house_data.xlsx") # 核心摸底三件套 print(df.shape) # 行数、列数 print(df.columns.tolist())# 完整字段名 print(df.head(10).to_string()) # 前 10 行,肉眼看数据质量 print(df.dtypes) # 看每列的类型是否被 Excel 的格式带偏这段代码没做任何加工,只是把「数据长什么样」这个问题可视化。dtypes 这一步非常关键:Excel 里常见的坑是数字列被读成文本、日期列变成字符串、金额里有逗号或单位,这些都会在后面计算时报莫名其妙的错。接着统计每列的缺失率:
missing_df = df.isnull().sum() missing_df = missing_df[missing_df > 0] / len(df) * 100 missing_df.sort_values(ascending=False)缺失率超过三成的列要慎重——要么放弃,要么用「存在与否」做二值特征,而不是硬把缺失值补成一个常数。比如「带看次数」这一列在热度高的区域里缺失可能代表少人看,补 0 和补均值含义完全不同,这个业务判断只能由你来定。
2.2 缺失值、重复记录和「-1 层」:三类脏数据的标准清理姿势
二手房价数据是典型的多源整合结果,脏得很有规律。最常见的有三类。
第一类是重复记录。同一套房子在不同中介网站上挂牌会出现多次,总价、户型、面积完全相同,但挂牌时间可能有细微差异。判断重复不能用「小区名+总面积」,因为同一楼栋不同楼层的定价逻辑不同,要用「小区+户型+面积+朝向」四要素加总价,重复且总价相同才值得怀疑:
subset = ["小区名称", "户型结构", "建筑面积", "朝向", "总价"] df = df.drop_duplicates(subset=subset, keep="last") # 去重后顺手检查分布 print(df.shape) print(df["总价"].describe())参数说明:keep="last" 表示保留同组中最后一条记录,因为最后一条往往是最新挂牌、价格更接近业主真实预期;子集列的粒度按你的数据字段调整,但总价不能放进 subset,因为同户型同面积也可能因楼层出现合理价差,放进 total_price 会误删有效样本。
第二类是「-1 层」「低层、中层、高层」这类文本楼层。先说地下室、车库改住房这种-1 层,房价预测里我一般不直接剔除,而是把楼层做一次映射:
floor_map = {"-1": -1, "低层": 1, "中层": 2, "高层": 3, "顶层": 4, "别墅": 0} df["floor_level"] = df["楼层"].map(floor_map) # map 没匹配上的会变成 NaN,打印出来看还剩哪些奇葩值 print(df["楼层"].value_counts().tail(20)) print(df["floor_level"].isnull().sum())逻辑说明:低层中层高层本来就是模糊语义,直接转数字后会丢失「相对位置」信息,但作为回归特征已经够用。最关键的是 NaN 的检查——如果某些房源写着「共 30 层, 所在 25 层」,就必须从这句话里拆出两个特征,这种信息密度比「高层」高,值得单独解析。
第三类是总价和单价之间的常识矛盾。广州的房价水平下,单价在 5 万与楼龄二十年的房子同时存在是合理的,但「建筑面积 70 平、总价 9999 万」这种明显录入错误必须先过滤,否则一个错误样本就能把模型训歪:
# 通过单价与总价的乘积关系反推建筑面积,再和原始建筑面积对撞 df["calc_area"] = df["总价"] / df["单价"] df["area_error_ratio"] = (df["calc_area"] - df["建筑面积"]).abs() / df["建筑面积"] # 误差超过 15% 的样本多半是字段填错 df = df[df["area_error_ratio"] < 0.15]参数说明:15% 这个阈值是经验值,广州房产证面积和挂牌面积极少差超过 15%,超过就说明其中一个字段录错了。做完这步再检查area_error_ratio的分布,如果大量样本落在 10%-15% 区间,说明原始数据里单价和总价本身就不互洽,可能要回到来源端重抓。
2.3 数据质量自查:哪些房源该过滤,哪些字段该留着做特征
清理完显性脏数据,还得做一遍业务层面的过滤。广州的二手房市场有明显「结构性异常」房源:法拍房(产权风险高、定价逻辑不同),车位、商铺改住宅,首层带花园的特殊户型,以及楼龄超过 40 年没有电梯的老破小。它们不是数据错误,但会严重干扰回归模型的稳定性,因为它们的基本面价值逻辑完全不同。我的处理原则是——先看样本量,如果异常房源占比小于 5%,直接剔除;如果超过 10%,就加一个is_special二值特征,而不是硬删。
数据质量要可视化自查:
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 3, figsize=(15, 4)) df["单价"].hist(bins=50, ax=axes[0]) axes[0].set_title("单价分布: 单峰才正常, 双峰说明混入了非住宅房源") df["建筑面积"].hist(bins=50, ax=axes[1]) axes[1].set_title("面积分布: 200 平以上样本极少, 需要观察") df.groupby("行政区")["总价"].median().plot.bar(ax=axes[2]) axes[2].set_title("各区总价中位数: 异常高或异常低都要查明原因") plt.tight_layout() plt.savefig("eda_qc.png", dpi=150)这段代码输出的三张图基本能看出数据能不能继续用。单价分布出现双峰,常见原因是把车位和住宅混在了一张表里——车位单价低但总价高,拉出的形态完全不一样。面积分布右侧长尾太长,说明有些「超大户型」其实是别墅或者办公产权,这类样本要么剔除,要么单独标注。各区中位数如果出现前面前 3 名的区不如后面几个区,要查是不是行政区字段有别名——「天河」和「天河区」在原始数据里常被写成了两个值,这必须做一次字段归并。
提示:这一步做完,数据的行数和列数你自己要心里有数。后面所有特征工程都建立在这张「干净表」上,如果这时候就发现原始数据只有两三百行,我的建议是别硬做回归了,直接用统计描述出报告,样本量撑不起机器学习模型。
3. 特征工程与模型选型:为什么广州房价不适合跑线性回归
3.1 从地址解析到位置特征:用 haversine 计算地铁站距离
原始数据里如果有「地址」或「小区名」,位置信息可以说是房价预测里信息量最大的一层。广州的房价跟交通结构强相关,同样的面积和楼龄,珠江新城地铁站旁和从化城区郊,单价能差 3 到 5 倍,只看面积和户型根本解释不了。如果原始数据自带经纬度,直接用它做距离计算;如果只有小区名,就需要地理编码的步骤。
常见做法是调用高德或百度地图 API,把小区名转成经纬度再算到地铁站的距离。依赖外部 API 的和已经固定经纬度的情况分开处理,没有坐标就用不了直接方案。我自己会先看数据集里有没有线经度列,没有就写一个简单的补全逻辑:
import math # 广州主要地铁站点经纬度,时效性检查后再补全,这里只做示范 subway_stations = [ {"name": "体育西路", "lat": 23.1357, "lon": 113.3212}, {"name": "珠江新城", "lat": 23.1200, "lon": 113.3185}, {"name": "公园前", "lat": 23.1260, "lon": 113.2660}, ] def haversine(lat1, lon1, lat2, lon2): R = 6371.0 phi1, phi2 = math.radians(lat1), math.radians(lat2) dphi = math.radians(lat2 - lat1) dlambda = math.radians(lon2 - lon1) a = math.sin(dphi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2)**2 return 2 * R * math.asin(math.sqrt(a)) def nearest_subway_distance(lat, lon): if pd.isna(lat) or pd.isna(lon): return None return min(haversine(lat, lon, s["lat"], s["lon"]) for s in subway_stations) df["地铁距离"] = df.apply(lambda r: nearest_subway_distance(r["纬度"], r["经度"]), axis=1)逻辑说明:haversine 公式用来算球面两点距离,单位是公里,广州主城区范围不大,用这个公式已经够精确,不需要引入地理库加重依赖。地铁站点列表是静态的,只列出几个核心站做演示——实际做的时候要把广州所有已运营站点加进列表,数据量大概在两百个级别,不会对性能造成压力。参数说明:距离小于 0.3 公里和距离 3 公里以上的房源,预测误差表现完全不同,这个数字可直接作为后续特征交叉的依据——「地铁距离 × 行政区」的组合特征常常比单独距离列对模型提升更明显。
3.2 房价不是只跟面积有关:构造小区梯度特征和楼龄分段
广州房产市场有一个明显特征:小区与小区之间的价格梯度,往往比同小区内不同房源之间的梯度大得多。同一地段,老牌大型社区的均价和旁边新建高端盘的均价能差 40%。这提示我们,小区级别的聚合统计特征非常有用。
# 按小区聚合, 计算该小区已成交房源的均价中位数 community_stats = df.groupby("小区名称")["单价"].agg(["median", "count", "std"]).rename( columns={"median": "小区单价中位数", "count": "小区样本数", "std": "小区单价标准差"} ) # 样本数少于 3 的小区, 中位数不可靠, 用全区中位数填充 community_stats.loc[community_stats["小区样本数"] < 3, "小区单价中位数"] = \ df.groupby("行政区")["单价"].transform("median").median() df = df.merge(community_stats, left_on="小区名称", right_index=True, how="left")逻辑说明:建模时给新购入二手房估价,它的「小区中位数」不是已知量,只有历史成交样本存在时这个特征才有值。所以这个特征必须限制在「训练集内有出现过的小区」,测试集里出现全新小区时该特征缺失,要再设计一个 fallback 值。参数说明:聚合至少 3 个样本的门槛是我给的底线,少于 3 个样本的小区,中位数的波动极大,直接用均值会造成特征泄漏的错觉。
楼龄的处理也是一样。广州的二手房楼龄跨度从 1980 年代的老破小到 2023 年交付的次新房,楼龄与房价不完全是线性关系——楼龄 10 年以内价格递减不明显,超过 20 年跌幅趋缓,30 年以上反而有一些「待拆迁或学位价值」支撑的价格反弹:
def age_segment(age): if age <= 5: return "次新" elif age <= 15: return "中年" elif age <= 25: return "老旧" else: return "老破小" df["楼龄段"] = df["楼龄"].map(age_segment)分段编码的解释能力远强于直接把楼龄做数字回归,根本原因是非线性且分段的逻辑在不同价位段不同——珠江新城的老房子和番禺的老房子,年龄衰减速率差异巨大。同样的思路可以考虑用于房型层数、视野朝向保持为分类特征不做硬编码。
3.3 模型选型与参数:为什么 LightGBM 是这类表格回归的默认项
广州房价这种规模的表格数据,我一般不会拿深度学习出来跑,两个原因:一是有几千行的话神经网络没有优势,调起来还慢;二是特征里大量离散分类(行政区、朝向、装修)和缺失值,树模型天生能处理。xgboost 和 LightGBM 都是首选,但我更倾向 LightGBM,因为样本量几千时它的直方图算法已经足够快,对缺失值有原生支持,支持类别特征直接 doi。
from lightgbm import LGBMRegressor from sklearn.model_selection import train_test_split feature_cols = [ "建筑面积", "户型结构调整为整数", "容积率", "绿化率", "行政区", "朝向", "装修情况", "楼层level", "地铁距离", "小区单价中位数", "楼龄段" ] X = df[feature_cols].copy() y = df["总价"].copy() # 类别型字段显式声明 cat_cols = ["行政区", "朝向", "装修情况", "楼龄段"] for col in cat_cols: X[col] = X[col].astype("category") X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LGBMRegressor( n_estimators=1200, learning_rate=0.05, max_depth=6, num_leaves=31, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=0.5, random_state=42, verbose=-1 ) model.fit(X_train, y_train) y_pred = model.predict(X_val)参数选择依据:n_estimators=1200 配 learning_rate=0.05 的组合适合几千行的中小数据集,默认的学习率要同步调大树的棵数,只动其中一个没有意义。max_depth=6 加 num_leaves=31 是 LightGBM 的保守默认值,广州房价数据没有复杂到需要几十万叶子才能拟合,控制叶结点数量就是控制过拟合。subsample 和 colsample 都是 0.8 属于轻量防过拟合,如果验证集表现比训练集差 5% 以上,把这两个降到 0.7 同时加大 reg_alpha 和 reg_lambda。reg_alpha 是 L1 正则,帮助稀疏类别特征不单独分叉过深;reg_lambda 是 L2 正则,主要防数值型特征的大梯度异动。
提示:如果你调整了上面参数后训练时间翻了两倍但精度只涨千分之几,立刻回滚。这类中小数据集上 80% 的提升来自特征,而不是模型复杂度和调参。省钱省力才是参数选择的判断标准。
4. 广州房价预测常见问题与避坑:连续模型翻车的那几个瞬间
4.1 现象:训练集 R² 0.98,验证集 R² 0.72
第一次跑出来 R² 高得离奇的那几天,我一度以为找到房价密码了,直到把验证集的残差打出来才意识到是特征用错了。
原因:「小区单价中位数」这个特征里有它的同期总价信息。理论上更常见也更阴险的是,原始数据里「单价」列被复制到了特征列,或者「小区均价」列直接包含了待预测样本本身。模型中包含与预测目标同源的列,是典型的特征泄漏。另一个隐蔽来源是groupby transform做小区聚合时,把当前行的目标值也纳入了均值计算,变形实现就成了泄漏。
解决:用train_test_split划分之后,必须在训练集内部计算聚合特征,再 merge 回验证集;如果这份预测是针对「某小区首个待估价房」这种全新样本,小区聚合特征不复存在,要用二值缺失标记 + 区级均值做兜底:
# 先划分再建小区聚合特征, 而不是先整体聚合再划分 df_train, df_val = train_test_split(清理完成的df, test_size=0.2, random_state=42) comm_train = df_train.groupby("小区名称")["总价"].agg(["median", "count"]) df_train = df_train.merge(comm_train, left_on="小区名称", right_index=True, how="left") # 验证集按训练集的聚合结果做映射, 测试集中新小区的小区特征是空 df_val = df_val.merge(comm_train, left_on="小区名称", right_index=True, how="left")这段代码的关键在于:验证集不参与聚合计算,只做一个查询动作。线下验证指标的可靠性,严重依赖这类「会不会用到未来信息」的顺序是否正确。
4.2 现象:房价预测出负值或低于 1 万元的怪值
二手房成交量在某几个远郊板块本来就少,负预测值和极端离谱的预测值,几乎是每个做回归的人都会碰到的坑。
原因:一是训练集本身就含有车位、楼梯房、半产权房这些单价极低的样本,模型把这类样本学成了一个数量级;二是模型在特征空间外推时没有物理约束,总价 30 万的车位和总价 800 万的住宅在面积特征上可能只差 20 平,树模型这种非线性拟合很容易在样本稀疏区域拉出负区间。
解决:最简单直接的方案是给 y 取对数让它满足正态分布假设,让预测输出限制在合理区间:
import numpy as np df["log_total_price"] = np.log1p(df["总价"]) # 训练用 log_total_price 做目标 # 预测结果出来后再 expm1 还原 pred_total_price = np.expm1(model.predict(X_val))log1p是针对总价整体偏移 1 的对数变换,把 100 万和 800 万的量级差距压缩到可学习的范围。预测完成后用expm1还原真实总价。如果还原后仍然出现低于同区最低价的预测,直接用后处理裁剪:预测总价低于 30 万或者高于 1 亿,就按该行政区的 1% 和 99% 分位数做截断。物理常识是模型的兜底机制,这不是玄学。
4.3 现象:单价和总价同时进特征,模型学到的是「乘法」
单价、总价、建筑面积三个字段,很多新手会一起塞进特征表,因为看起来它们都是「不同」的列。但是废掉。
原因:单价 = 总价 / 建筑面积,三者两两相乘自然构成一个周期性变形,树模型不需要聪明到这个程度,它只是从中找到了一个近乎线性的绑定规则,结果就是模型严重依赖单价,一旦某套房没有单价或者单价不准,预测直接崩盘。
解决:这三列里只能选一个作为目标,另外两个最多选一个作特征。我的标准做法是:用「总价」做预测目标,用「建筑面积」和「单价」中选建筑面积——因为单价本身是目标除以面积的商,等于在特征里已经泄露了一半的答案。单价这个列可以在 EDA 阶段用来看数据分布,不进建模特征。
4.4 现象:地铁站距离算出来全是同一个小数
有一次我拿到坐标列后发现所有房源的经纬度完全一样,连小区都区分不出来,才知道是网络抓取时字段错位,所有行都拿到了某个地图 center 的坐标,距离自然都是一个常数。
原因:原始数据的经纬度批量抓取时,如果爬虫代码里的坐标解析出了问题,经常整断落到同一个网格中心或者同一个值。这个不通过分布检查完全看不出来。
解决:写一个极简单例检查建筑面积和经纬度是否具有足够的方差:
# 坐标方差接近 0 直接判定为抓取异常 print(df[["纬度", "经度"]].std()) # 如果 std 只有 0.0001 这个量级, 说明所有点在差不多同一个位置 # 此时放弃经纬度, 用地名文本匹配 + 行政区级特征做位置信息参数说明:0.0001 度大约对应 11 米,广州主城区正常房源间的经纬度标准差应该是这个的十倍以上。如果数据真的损坏成一条线,与其硬用距离特征,不如退回到「行政区 + 小区」的组合作为位置代理,损失的信息量从实践来看可控。
4.5 现象:模型在珠江新城区域整体便宜了 15%
有一次模型的 SHAP 图上「珠江新城」区域的预测整体偏低,误差高度集中在高价区,而几千万的豪宅却预测得像刚需盘。树模型对样本量充足的中间价位拟合好,对头顶上的长尾极值几乎无感,因为损失函数被多数样本主导——这是回归模型对「量级不均」的天然偏置,没法靠调参完全扭转。
原因:训练样本中 80% 是 200 万到 1000 万之间的房子,太贵的豪宅挂牌量本来就少,数据量不够,模型学不到那类样本的特殊规律。
解决:广州只有天河和越秀的头部分区有这个情况,此时不是删掉高价样本,而是对训练样本加指数权重——把总价超过 1500 万的样本权重设成常规样本的 2 到 3 倍,强制损失函数去关注高价样本:
# 按总价区间设置样本权重, 让模型不能无视豪宅 def price_weight(total_price): if total_price > 1500: return 3.0 elif total_price > 800: return 1.8 else: return 1.0 sample_weight = np.array([price_weight(p) for p in y_train]) model.fit(X_train, y_train, sample_weight=sample_weight)这只是把问题从「整体平均」往「局部也合理」拉一步。更好的方案是单独建一个高价房专用模型,但要靠额外的高价挂牌源数据,在只有这份 rar 数据的前提下,权重很难说是最优雅的,但至少管用且容易落地。
5. 评估与验证:判断预测值能不能用,先看这三张图
5.1 评估指标怎么选:RMSE 与 MAE 的分歧说明了什么
做回归最常见的误区是只盯着 R² 看。房价这种目标值方差很大,R² 在 0.85 以上都算正常,但 R² 对错误量级的惩罚是非线性的,RMSE、MAE 和 MAPE 三个指标合起来看,才能反映出模型真正的误差面貌。
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score rmse = mean_squared_error(y_val, y_pred, squared=False) mae = mean_absolute_error(y_val, y_pred) r2 = r2_score(y_val, y_pred) mape = (abs(y_val - y_pred) / y_val).mean() * 100 print(f"RMSE: {rmse:.2f} 万元") print(f"MAE: {mae:.2f} 万元") print(f"R²: {r2:.4f}") print(f"MAPE: {mape:.2f}%")用广州的房价量级来感受这几个数字,如果 MAE 是 60 万,RMSE 是 120 万,说明模型在一部分房源上出现了「离群极值」级别的预测错误,拉高了平方项,而大部分房子预测质量还行——RMSE 对这类集中性的偏离尤其敏感。如果 RMSE 和 MAE 差距不大,误差分布均匀,模型整体稳定。MAPE 在 12% 以内,对一个几十万套房的片区做趋势性判断才说得过去。任何单一指标都有盲区,RMSE 和 MAE 的分歧本身就诊断了误差分布到底有多偏。
5.2 残差图:只有一张图能看出模型哪里系统性出错
评估指标是浓缩信息,真正的诊断要靠残差图。横轴是预测总价,纵轴是真实总价减预测总价(正负 100 万之内为合理),这张图暴露的问题种类比任何指标说明都多:
import matplotlib.pyplot as plt resid = y_val - y_pred plt.figure(figsize=(8, 6)) plt.scatter(y_pred, resid, alpha=0.4, s=10) plt.axhline(y=0, color="red", linestyle="--", linewidth=1) plt.xlabel("预测总价(万元)") plt.ylabel("实际总价 - 预测总价(万元)") plt.title("残差散点图:按预算分段看误差方向") plt.savefig("residual_check.png", dpi=150)这张图看三个现象。预测值 300 万以下的位置残差基本在零轴上下对称分散,说明刚需盘的预测是正常的;500 万到 1000 万区间残差开始出现大面积负值,说明真实总价普遍高于预测,也就是 4.5 那个「珠江新城系统性偏低」问题的证据;200 万以下区域出现尾部向上的极端正残差,说明低价盘被高估了。这种分段式的残差规律,R² 指标永远看不出来,但它直接告诉你要不要做样本加权,要不要单独训练区间模型。
5.3 用 Shap 解释关键特征:别把模型当黑匣子
很多人把 Shap 当成一种「高级可视化」,实际上它是回归落地的必要环节——没有它就说不清模型是在靠逻辑判断还是在抄数据作弊。
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val, plot_type="bar", max_display=12)运行这个对小数据集几乎不耗时。我重点看的是训练过的模型对哪个特征的依赖最大。广州房价如果依赖度前三名是「小区单价中位数」「建筑面积」「行政区编码」,说明模型学的是市场聚群效应,符合业务常理;如果「地铁距离」排到十名开外,「朝向」反而排到前三,那就要怀疑原始数据里朝向字段是不是变成了某种错乱的编号,在跟具体地段高度相关。Shap 这层检验做扎实了,模型放进业务场景才敢让人信。
提示:Shap 值最大的用途是给非技术方做决策解释。有老板过来问「为什么这套 1000 万的老房子你预测 850 万?」,拿 shap.force_plot 一拉,楼龄、学qu划分、地铁距离、小区梯度这四个因素的影响全部列出来,比任何空口解释都有说服力。
6. 把模型用起来:批量估价、笋盘筛选与工作流落地
模型训练好了,评估也过了,最后要让它真正为买房决策服务。我平时会做一个小的批处理脚本,把目标区域的在售房源从 Excel 读进来,跑一遍已经训练好的模型,输出一张带「预测价/挂牌价偏离率」的表,然后按偏离率排序。这里有一个很重要的工程细节——模型要序列化保存,不然每次预测都要重新训练:
import joblib # 保存模型 joblib.dump(model, "gz_house_price_lgb.pkl") # 新数据到来时直接加载,不做训练 loaded_model = joblib.load("gz_house_price_lgb.pkl") # 生成批量估价报告 df_new["预测总价"] = np.expm1(loaded_model.predict(X_new)) df_new["偏离率"] = (df_new["总价"] - df_new["预测总价"]) / df_new["预测总价"] result = df_new.sort_values("偏离率", ascending=False) result.to_excel("笋盘筛选结果.xlsx", index=False)做完这套脚本,再把每天的挂牌数据更新进来跑一遍,偏离率超过 8% 的房源就值得人工核实一下:是业主急售的笋盘,还是房源信息本身有水分。我会把这些候选小区再放回 EDA 阶段的单价分布图里看一眼,确认它们不是数据异常导致的虚假信号,才进入线下看房环节。
说实话,用模型估价这件事我栽过最大的跟头,就是对低价上榜的房源太兴奋,忘了一张图——残差图。后来每次批量预测跑完都先看残差趋势,再谈「笋盘」。这套流程从数据清洗到最后的筛选脚本,每一步都是踩过坑才补上的,希望帮到你。
本文还有配套的精品资源,点击获取