ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

二手房房价预测Python实战:数据清洗到机器学习建模全流程

二手房房价预测Python实战:数据清洗到机器学习建模全流程 简介基于链家网二手房交易数据这套项目源码覆盖从数据爬取、清洗、分析到房价预测的完整流程。项目获导师认可并以98分通过毕业设计答辩适合计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生也适合需要真实项目练手的数据分析初学者。压缩包共17个文件包含12个csv数据文件、3个ipynb分析笔记本、1个py脚本和1个docx说明文档整体大小仅6.23MB轻量但结构清晰。已有336人学习验证了其实用价值。源码经严格调试可直接运行既有链家网数据爬取脚本也有针对房价数据的探索性分析和预测模型实现。通过完整阅读Notebook并运行脚本可理解二手房特征工程、模型评估等关键环节并迁移到其他房屋数据集或类似预测任务中。1. 二手房房价分析与预测一个 Python 实战项目到底在做什么“Python 的二手房房价数据分析与预测项目源码.zip”这个标题几乎是每个 Python 学习者绕不开的 mid-term 难题既要有数据分析的完整链路又要有建模预测的结果最后还得打包成能交差的源码包。我见过太多人第一步就栽在“房价数据从哪来”上——没有真实数据后面的 pandas 操作、可视化、回归模型全是空中楼阁。这个项目的本质是一条标准的数据科学流水线数据采集 → 清洗 → 特征工程 → 建模 → 评估。它适合三类人准备找数据分析岗位的求职者需要一个拿得出手的项目、课程作业选题的学生需要可复现的完整流程、以及刚学完 pandas 和 sklearn、想练手的新手。你不需要分布式计算不需要高深的深度学习一个 Jupyter Notebook 加 scikit-learn 就够跑通全流程。下面我从解压 zip 包开始把每一步的代码、参数和边界都讲清楚。2. 解压 zip 后的项目结构与环境准备拿到手先看这四类文件一个规范的房价分析项目源码包解压后通常是模块化组织的而不是一个堆满 cell 的 notebook。搞清楚目录结构是第一步避免队友交过来一个没法复现的烂摊子。2.1 标准目录长什么样先分清“数据层、处理层、模型层”我经手和复查过的二手房项目里目录结构大体接近下面这样定制化部分我会特别注明second_hand_house/ │ ├── data/ # 数据层 │ ├── raw/ # 原始数据CSV 通常存这里 │ └── processed/ # 清洗后的数据 │ ├── src/ # 源码层 │ ├── data_clean.py # 清洗逻辑 │ ├── feature_engineer.py # 特征工程 │ └── train_model.py # 建模入口 │ ├── notebooks/ │ └── EDA.ipynb # 探索性分析通常先在这里试 │ ├── output/ │ └── model/ # 训练好的模型文件.pkl │ ├── requirements.txt └── README.md先强调一个容易忽略的细节确保src下的模块能被正确导入。以我跑通这类项目的习惯第一步一定是在项目根目录建一个空的__init__.py如果源码是以包形式组织的话然后确认当前工作目录在根目录下否则import src.data_clean这类语句会直接报 ModuleNotFoundError。2.2 环境搭建的三条命令不要用最新版 Python 自找麻烦这个项目最稳妥的 Python 解释器版本是 3.9 或 3.10。3.11、3.12 虽然能跑但你要处理一些非主流包的 wheel 兼容问题——在这些细节上没必要追求前沿版本。cd second_hand_house python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple逻辑说明venv创建独立虚拟环境避免污染全局解释器-i参数指定清华镜像源是为了加速国内下载实测对 pandas、numpy 这种大体积包提速非常明显。如果你的requirements.txt缺失这是源码包常见的坑手动补装这四个核心库就够起步pip install pandas numpy matplotlib scikit-learn jupyter参数说明scikit-learn建议版本在 1.2 到 1.4 之间太新的版本有些 API 会报 DeprecationWarning不太影响运行但新手看了容易慌。装完后跑一句python -c import pandas, sklearn; print(pandas.__version__, sklearn.__version__)验证环境能同时满足“能 import、版本号正常”两个条件下再继续。3. 数据清洗与特征工程决定预测精度上限的三个关键处理房价预测项目的成败八成取决于数据质量而不是模型选得多花哨。很多人在 Kaggle 上用官方清洗好的数据集跑出 0.85 的 R²一换自己爬的数据就崩到 0.3 以下——差的就是这一步。3.1 缺失值处理不要一 dropna 了之先看缺失率和变量含义二手房数据的缺失模式很典型总价、面积几乎不缺失这是挂牌必填项但“户型”“装修程度”“建筑年代”可能缺失率超过 30%。我见过最糟糕的做法是df.dropna()一把删掉超过一半的行。import pandas as pd df pd.read_csv(data/raw/house_data.csv, encodinggb18030) print(df.shape) missing_ratio df.isnull().mean().sort_values(ascendingFalse) print(missing_ratio[missing_ratio 0.1])逻辑说明isnull().mean()计算每列缺失值占比先摸清哪些特征缺失严重再决定策略。编码指定gb18030因为国内房产网站导出的 CSV 常见中文编码是 GBK 系默认的utf-8会直接报 UnicodeDecodeError——这个坑几乎人人会踩。处理原则缺失率低于 5% 的数值列用中位数填充高于 30% 且业务上不重要的直接删列保留“建筑年代”这类关键特征则用小区均价反推或按众数填充。对于总价、面积这类核心列缺失值直接删行更安全不要用均值填充去制造虚拟样本。3.2 异常值筛选把“异形户型”从样本中拿掉否则回归系数全被带偏二手房数据里的异常值比一般数据更隐蔽。一套 10 平米标价 530 万的学区房、一套 300 平米却总价 70 万的郊区房——前者可能真实存在但会让模型学到错误的单价区间后者大概率是录入错误。import numpy as np # 单价 总价 / 面积 df[unit_price] df[total_price] / df[area] # 按单价分位数 逻辑规则双重过滤 q_low, q_high df[unit_price].quantile([0.01, 0.99]) df df[(df[unit_price] q_low) (df[unit_price] q_high)] # 面积逻辑校验正常住宅最小 15 平最大 400 平 df df[(df[area] 15) (df[area] 400)] print(f过滤后样本量: {len(df)})逻辑说明先构造unit_price列再按 1% 和 99% 分位数切掉极值。分位数比 3σ 准则更适合房价数据因为房价分布是严重右偏的——高价位端的“正常值”在 3σ 下会被误杀。面积筛选是硬性业务逻辑写死边界即可不用参考分布。参数说明分位数阈值不要设太紧比如 5% / 95%否则会把真实的豪宅样本切掉导致模型在高端价位的泛化能力明显变差。1% / 99% 是经验值大多数项目不用再调。过滤完记得df.to_csv(data/processed/house_clean.csv, indexFalse)存一份中间结果后面改特征时不用从头跑。3.3 特征工程经纬度不如“到市中心距离”、房龄平方项值得尝试二手房项目的特征工程通常分两部分基础特征加工和派生特征构建。先说一个最常见的误用——把“区域”直接做了 LabelEncoder 变成 0、1、2、3 丢给线性模型。这是错误的因为编码本身带了不存在的顺序关系。正确做法是对区域做 One-Hot注意 pandas 的get_dummies或者按业务口径编码成“区域均价”或“到市中心距离”。我的经验里真正对预测精度贡献明显的是下面几个派生特征特别是最后两个往往绕过所有人的盲区# 到市中心的欧氏距离经纬度映射到平面坐标 import numpy as np # 数据中心点坐标按城市实际调整 center_lat, center_lng 39.9042, 116.4074 # 简化球面距离北京约 1 纬度 111km1 经度 85km随纬度变化 df[dist_center] np.sqrt( ((df[lat] - center_lat) * 111) ** 2 ((df[lng] - center_lng) * 85) ** 2 ) # 房龄与是否带电梯的交叉项 df[house_age] 2025 - df[build_year] df[age_elevator_inter] df[house_age] * df[elevator] # 面积平方项用来捕捉面积对总价的非线性影响 df[area_sq] df[area] ** 2逻辑说明经纬度直接作为特征对线性模型不友好需要转换成有业务含义的物理量——“到城市中心距离”就是最直观的降维方式。不同城市纬度不同公式里的两个缩放系数111 和 85要根据城市代码中的经纬度修正不能照抄。交叉项把“老房子带电梯”这个场景单独建模有过实际案例分析两种房源的使用价值和定价逻辑明显不同。参数说明np.sqrt不加axis参数时默认对整个矩阵做运算但这里只传入一个 Series所以输出是一个 Series不会出问题。注意build_year如果有 0 值或缺失值在减计算前要先处理否则会出现荒谬的负房价。4. 建模与预测从线性回归到 XGBoost 的完整落地路径数据洗完了特征构造好了接下来是房价预测的核心环节。这一步要避免一个方向性错误一上来就调 XGBoost 追求高精度连基线模型都没建过。正确路径是先拿线性回归做基准再上树模型做对比最后才看集成模型的效果。4.1 划分训练集和测试集时间序数据不能随机打乱二手房数据是带时间戳的如果直接train_test_split(random_state42)随机切分你会把 3 月份挂牌的房子和 9 月份的混在一起模型无意中学到了“时间的趋势”而在真实场景中你只能用过去的数据预测未来。这是在二手房预测项目里数据泄漏最容易发生的地方必须兜住。from sklearn.model_selection import train_test_split # 按时间排序后切分前 80% 做训练后 20% 做验证 df[list_date] pd.to_datetime(df[list_date]) df df.sort_values(list_date) X df.drop([total_price, list_date, unit_price], axis1) y df[total_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, shuffleFalse, random_state42 ) print(f训练集{X_train.shape}测试集{X_test.shape})逻辑说明shuffleFalse是关键参数它让切分完全按排序后的顺序进行。random_state在这里不再决定样本分布只留给可能后续做采样时使用。注意list_date列要从特征里去掉否则模型直接用日期破功。参数说明为什么先按list_date排序再切因为只有按时间的向前切分才能模拟出你站在某个历史节点上对未来做预测的真实场景。测试集代表了“未来”训练集代表“过去”这样评估出来的误差才是可信的。4.2 线性回归基线跑完 R² 先别高兴检查残差分布线性回归是房价预测项目里最容易被低估的模型。它的优势在于可解释性极高——每个特征的权重系数直接告诉你是“面积没多 1 平米涨多少万”这个能力后面的 XGBoost 给不了。from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error lr LinearRegression() lr.fit(X_train, y_train) y_pred_lr lr.predict(X_test) print(f线性回归 R²{r2_score(y_test, y_pred_lr):.3f}) print(fMAE{mean_absolute_error(y_test, y_pred_lr):.2f} 万元) print(fRMSE{mean_squared_error(y_test, y_pred_lr, squaredFalse):.2f} 万元) # 残差检查 residuals y_test - y_pred_lr print(f残差均值{residuals.mean():.2f}残差标准差{residuals.std():.2f})逻辑说明线性回归要重点看残差的均值是否接近 0。如果均值明显偏离 0说明模型存在系统性偏差——比如你在高单价区域预测偏低在低单价区域预测偏高这就意味着线性关系假设有问题。结合residuals.std()还可以判断预测误差的波动范围是否可接受。参数说明squaredFalse在 RMSE 计算中返回原始的均方根误差单位是“万元”。如果 sklearn 版本太新这个参数名可能改成root_mean_squared_error函数我一般直接写np.sqrt(mean_squared_error(y_test, y_pred_lr))更保险不依赖版本变更。4.3 随机森林与 XGBoost为什么要用两个以及关键调参参数对比线性回归做完后下一步通常是在随机森林和 XGBoost 之间二选一也可以两个都跑对比结果更有说服力——很多源码包就是把两者同时放进项目里撑篇幅但很少写清楚各自的使用边界。维度随机森林XGBoost对非线性关系的拟合能力强很强对抗过拟合的方法树的数量加样本扰动学习率降 正则化系数主要超参数n_estimators, max_depth, min_samples_splitlearning_rate, n_estimators, max_depth, reg_lambda速度同配置快偏慢需要特征缩放不需要不需要适用场景数据量中等、追求稳定数据量大、需要最后一点提升这是为什么我一般会两个都跑一遍的原因随机森林几乎不挑参数默认状态就能输出一个不错的结果拿来当稳健基线XGBoost 给了你在上限上再抠一个百分点的可能性前提是你肯花时间调参。如果最终精度只差 0.01我倾向于选随机森林——它在部署和维护上更省心黑匣子程度也更低。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth15, min_samples_split10, min_samples_leaf4, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(f随机森林 R²{r2_score(y_test, y_pred_rf):.3f}) print(f随机森林 MAE{mean_absolute_error(y_test, y_pred_rf):.2f} 万元)逻辑说明n_jobs-1把 CPU 所有核心都用满训练速度提升明显但会高负载运行注意散热。max_depth限制在 15 层以内防止过拟合——房价数据的特征不多树太深没有意义min_samples_split10和min_samples_leaf4进一步强制每个叶子至少有 4 个样本这些都是控制方差的手段。参数说明n_estimators300在这个样本量千级或万级下足够了。超过 500 棵树对精度提升几乎为零只增加训练时间和模型体积。如果想快速验证先设 100 跑一遍看分数再决定是否加大。import xgboost as xgb xgb_model xgb.XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, reg_lambda1.0, random_state42 ) xgb_model.fit(X_train, y_train) y_pred_xgb xgb_model.predict(X_test) print(fXGBoost R²{r2_score(y_test, y_pred_xgb):.3f}) print(fXGBoost MAE{mean_absolute_error(y_test, y_pred_xgb):.2f} 万元)逻辑说明learning_rate0.05对比默认的 0.3学习率压低后需要更多棵树来达到同等精度但能有效防止过拟合。subsample0.8表示每轮迭代随机抽 80% 样本训练colsample_bytree0.8表示每棵树只用 80% 的特征这两个参数是 XGBoost 抗过拟合的核心武器比单纯调max_depth更有效。参数说明reg_lambda1.0是 L2 正则这个在数据量小的项目里尤其重要。如果训练集 R² 远高于测试集 R²优先把reg_lambda提高到 2 或 3再配合降低learning_rate试试。4.4 用交叉验证选参数网格搜索的三个必设参数模型能跑出分数只是第一步要防止“恰好在这个测试集上表现好”的假象交叉验证是校准模型泛化能力的常用手段。配合网格搜索选超参时我只推荐把三个参数放进搜索空间先跑——一次全参数组合展开会爆炸尤其是在数据量大的情况下时间和算力都不划算。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300], max_depth: [10, 15], min_samples_split: [5, 10], } rf_grid RandomForestRegressor(random_state42) grid_search GridSearchCV( rf_grid, param_grid, cv5, scoringneg_mean_absolute_error, n_jobs-1, verbose1 ) grid_search.fit(X_train, y_train) print(f最佳参数{grid_search.best_params_}) print(f最佳 MAE{-grid_search.best_score_:.2f} 万元)逻辑说明cv5表示五折交叉验证——把训练集再切成 5 份每轮用 4 份训练、1 份验证轮转 5 次分数取平均。这比单次 train/test split 更可信。scoringneg_mean_absolute_error表示用 MAE 作为评估指标负号是 GridSearchCV 的约定内部最小化所以输出时取负回正。参数说明建议手动把verbose1打开否则长任务跑起来完全没输出你无法判断是正常训练还是卡死了。3 个参数的网格搜索规模已经不小更多参数留给后面手工验证。5. 避坑记录二手房房价预测项目里最容易翻车的五个环节这个项目里我踩过不少雷也替别人排查过各种各样的报错。选五个高频问题写成记录覆盖从数据到建模的完整链路每个都按“现象 → 原因 → 解决”的方式列出可以直接对照排查。踩坑一read_csv 报 UnicodeDecodeError数据一个字都读不进来现象pd.read_csv(house_data.csv)直接报编码错误换成utf-8也报错。原因国内房产网站导出文件的中文编码常用 GBK/GB2312兼容字符集正是gb18030。这一点经常被忽略因为文件在 Excel 里打开一切正常但 pandas 默认utf-8解码就会翻车。解决编码参数从encodingutf-8改成encodinggb18030。加一行df pd.read_csv(path, encodinggb18030)即可。如果文件里混杂了繁体字或生僻字gb18030比gbk更稳两个字库覆盖范围不同。踩坑二总价列读出来是“530万”这样的字符串没法算数现象df[total_price]的 dtype 是 object字符串画图、算回归全部报 TypeError。原因网站数据导出时没有做类型转换总价里带了单位“万”字还有“面议”“随时看房”这类脏数据混在里面。解决df[total_price] df[total_price].astype(str).str.extract(r(\d\.?\d*))[0].astype(float)代码说明先用正则(\d\.?\d*)提取数字部分忽略“万”“元”等字符再转 float。如果某行完全提取不到数字比如“面议”提取结果会是 NaN后续用dropna()或fillna()兜底处理。这种“先转字符串再正则提取”的顺序不能反直接astype(float)会在纯数字字符串上侥幸成功但一旦混入一个“面议”就全列抛异常。踩坑三随机切分训练集和测试集R² 虚高到 0.95换时间切分掉到 0.7现象同一个模型用随机切分 R² 是 0.95按时间排序切分后只有 0.75落差极大。原因二手房挂牌价存在明显的时间趋势和小区热度波动。随机切分让测试集混入了训练集同期的样本模型等于靠着“记住这段时间的行情”完成了预测这就是典型的数据泄漏。时间切分则迫使模型用 3 月之前的数据预测 4 月之后的房价信息不对称更接近真实场景。解决任何带时间戳的房价数据都必须按时间排序后切分。代码见 4.1 节核心一行是train_test_split(..., shuffleFalse)。如果发现时间切分后模型分数下降过大不是模型坏了而是随机切分的结果本来就是幻觉。踩坑四GridSearchCV 训练超时跑了一小时没出结果现象网格搜索一直不结束CPU 占满但看不到任何输出。原因大概率是参数网格太大加上cv5的叠加组合数量爆炸了。比如 3 个参数各有 4 个选项5 折交叉验证就是 5×64320 次完整训练每次训练还带 n 棵树。实测在中等数据量下一次搜索可能超过半小时。解决先按 4.4 节的三个参数小网格跑等确定了一个范围再做第二轮精细搜索。把n_estimators缩小到两三个选项。另外加verbose1或者verbose10每 10 次迭代输出一次至少能看到进度心里有数。如果还是太慢改成RandomizedSearchCV随机搜索用迭代次数控制总时间。踩坑五模型在训练集 R² 是 0.98测试集只有 0.6现象训练集分数远高于测试集测试集预测误差偏大并且房价高点位全部预测偏低。原因模型过拟合房价长尾分布的双重问题。训练集里高端豪宅样本少模型拟合这些样本的方式是硬记遇到测试集的豪宅样板就崩了同时房价右偏导致常规的 MSE 优化目标都在为多数普通房价服务高端点位根本不在优化的主范围里。解决先压缩模型复杂度——降低max_depth提高min_samples_leaf。如果还不够把回归目标从“总价”改为“log(总价)”让长尾分布变成接近正态分布。千万别忽视日志变换在房价预测项目中的位置它对长尾数据几乎必定有效。预测完再把结果np.exp()还原成万元即可。在时间切分的设置下这种修正对高端价位的 MAE 改进通常非常显著。6. 模型解释与验证用 SHAP 讲清房价预测的“为什么”房价预测项目做到模型 R² 不错只能算完成了 70%。面试官问你“哪些特征最重要”你如果回答“每个特征都重要”这就暴露了你并不理解自己建的模型。最后一步把特征贡献度量化出来验证模型行为是否符合业务常识用 SHAP 是主流做法——它对树模型的解释力清晰且稳定。import shap explainer shap.TreeExplainer(xgb_model) # 取测试集前 200 条样本避免运行过慢 sample_X X_test.iloc[:200] shap_values explainer.shap_values(sample_X) shap.summary_plot(shap_values, sample_X)逻辑说明TreeExplainer专门用于树模型的解释计算出的 SHAP 值表示“该特征对本条样本的预测值贡献了多少”。summary_plot会生成一张散点图每个样本一个点横轴是 SHAP 值正负表示推高/拉低房价颜色是该特征的值大小。你可以用这个图来验证模型是否学到了符合常识的逻辑。验证逻辑如果图里“面积”特征的 SHAP 值和面积大小呈明显正相关说明模型学到的“面积越大房价越高”是符合业务直觉的。如果“到市中心距离”这一特征的 SHAP 方向与常识相反——距离越远房价反而越高——那大概率特征工程有 bug要回溯检查经纬度坐标的处理步骤。这是一道关键的验证流程它能帮你挡住模型默默学错规律的情况。对于线性回归模型SHAP 图可能更直观地看出来是LinearExplainer。但两个模型在特征重要性排序上通常大体一致面积、房龄、距市中心距离、所在区域。如果出现排名异常优先怀疑 One-Hot 编码后的区域列是否带了时序特征。将 SHAP 结果作为模型验收依据之一放进项目 README 的可视化结果里整个项目的完整性就立住了。最后提一个我的个人习惯保存模型后顺手在 README 里记录训练时间、测试集 R²、MAE 以及模型文件大小。如果你把这份代码打包发给下一任同事或未来的自己这才是最有价值的修复起点。希望这些踩坑记录和参数边界能帮你少走一步弯路尽快跑通属于你自己的房价预测项目。本文还有配套的精品资源点击获取
返回列表