☰
Python二手房房价预测实战:从数据清洗到模型部署
2026/9/28 6:24:13 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与项目实战学习者的二手房房价数据分析与预测完整项目源码,源自经导师指导并认可的98分毕业设计,可直接用于毕设、课程设计或期末大作业。项目围绕链家二手房数据展开,涵盖数据爬取、清洗、可视化分析与房价预测建模等环节,帮助读者理解从原始数据到模型输出的完整流程。压缩包共17个文件,以12个csv数据文件、3个ipynb交互式笔记本、1个py脚本和1份docx说明文档为主,整体约6.23MB,结构清晰,便于按数据、代码、文档分模块查阅。目前已有336人学习下载。读者可获得可直接运行的完整源码、分步实现的爬虫与分析脚本、配套说明文档以及项目调试思路,既能作为毕设参考模板,也适合作为数据分析与机器学习入门的实战练习素材。

1. 二手房房价数据从哪来:一份能跑通的 Python 分析预测项目该长什么样

二手房房价数据分析与预测,说白了就三件事:把散落在各平台的挂牌数据弄成一张干净表,用统计和可视化把价格规律讲清楚,再训一个能对新房源估价的模型。很多人搜 Python 二手房房价数据分析与预测项目源码,下载完解压一看,要么数据是编的,要么爬虫跑不起来,要么模型 R² 高得离谱——因为拿训练集当测试集了。这篇不吹某个不存在的仓库,而是按一线做这类项目的真实路径,把数据获取、清洗、特征工程、建模、评估、踩坑全串一遍,代码可以直接抄。适合刚学完 pandas 和 sklearn、想拿一个完整项目练手的人,也适合已经会调库、但模型一上线就翻车的熟手。下面所有代码基于 Python 3.10 + pandas + scikit-learn,环境用 vscode 配 python 或 pycharm 配 python 环境都行,装库就一句 pip install。

2. 数据获取与清洗:从原始挂牌表到可建模数据

2.1 数据从哪来,以及为什么我不建议一上来就写爬虫

二手房数据来源常见三类:一是公开数据集(如链家历史成交、Kaggle 上的北京上海二手房),二是自己爬挂牌页,三是业务方直接给的导出表。新手最容易踩的坑是:标题写着「项目源码」,结果爬虫依赖的页面结构早改了,跑起来全是空值。我的建议是,练手阶段优先用公开数据集或自己手动导出几百条,把清洗和建模跑通,再回头补爬虫。爬虫本身不是这个项目的核心,特征工程和模型评估才是。

如果你确实要爬,注意三点:控制频率、只取公开可见字段(总价、单价、面积、户型、楼层、朝向、建成年代、区域)、把原始 HTML 或 JSON 落盘再解析,别边爬边解析,否则页面一变你连原始数据都没了。下面假设你已经有一份ershoufang_raw.csv,字段是中文列名,这是最常见的形态。

2.2 用 pandas 做第一轮清洗:缺失、异常、类型

原始表里最脏的是面积、总价、楼层。面积常带「㎡」「平米」,总价带「万」,楼层写成「中楼层/共18层」。先统一成数值。

import pandas as pd import numpy as np # 读原始表,中文列名直接读 df = pd.read_csv("ershoufang_raw.csv", encoding="utf-8-sig") print(df.shape) print(df.dtypes) # 1) 面积:去掉单位,转 float df["面积"] = ( df["面积"].astype(str) .str.replace("㎡", "", regex=False) .str.replace("平米", "", regex=False) .str.extract(r"(\d+\.?\d*)")[0] .astype(float) ) # 2) 总价:去掉「万」,转 float df["总价"] = ( df["总价"].astype(str) .str.replace("万", "", regex=False) .str.extract(r"(\d+\.?\d*)")[0] .astype(float) ) # 3) 单价如果缺失,用总价/面积补 df["单价"] = pd.to_numeric(df["单价"], errors="coerce") mask = df["单价"].isna() & df["面积"].gt(0) df.loc[mask, "单价"] = df.loc[mask, "总价"] * 10000 / df.loc[mask, "面积"] # 4) 楼层:拆出「总层数」和「所在层」 df["总层数"] = df["楼层"].astype(str).str.extract(r"共(\d+)层")[0].astype(float) df["所在层"] = df["楼层"].astype(str).str.extract(r"(低|中|高)楼层")[0] df["所在层"] = df["所在层"].map({"低": 1, "中": 2, "高": 3}) # 5) 丢掉关键字段缺失的行 df = df.dropna(subset=["面积", "总价", "单价"]) df = df[(df["面积"] > 5) & (df["面积"] < 1000)] df = df[(df["单价"] > 1000) & (df["单价"] < 200000)] print(df.shape) df.to_csv("ershoufang_clean.csv", index=False)

逻辑说明:面积和总价用正则抽数字,比str.strip稳,因为单位位置不固定。单价缺失用总价反推,注意乘 10000 是把「万」换算成元。楼层拆成两个特征,因为「中楼层」在 6 层楼和 30 层楼里含义完全不同。最后用面积和单价的合理区间做粗筛,这一步会砍掉 5%~15% 的脏数据,别心疼。

参数说明:面积下限 5 平米是排除车位、储藏间;上限 1000 排除别墅误标;单价下限 1000 元/㎡ 排除明显错误,上限 20 万/㎡ 排除极端豪宅。这些阈值按你所在城市调整,一线城市上限可以放到 30 万。

2.3 特征工程:把「朝向」「装修」变成模型能吃的数

模型不认识「南北通透」,只认识数字。常见做法是独热编码和有序编码混用。朝向用独热,装修用有序。

# 朝向:取第一个方向做主朝向,独热 df["主朝向"] = df["朝向"].astype(str).str[0] df = pd.get_dummies(df, columns=["主朝向"], prefix="朝向") # 装修:有序编码 df["装修等级"] = df["装修"].map({"毛坯": 0, "简装": 1, "精装": 2, "豪装": 3}) df["装修等级"] = df["装修等级"].fillna(1) # 房龄:用当前年减建成年代 df["房龄"] = 2025 - pd.to_numeric(df["建成年代"], errors="coerce") df["房龄"] = df["房龄"].clip(lower=0, upper=80) # 户型:拆成室、厅 df["室"] = df["户型"].astype(str).str.extract(r"(\d+)室")[0].astype(float) df["厅"] = df["户型"].astype(str).str.extract(r"(\d+)厅")[0].astype(float) # 区域:独热,但类别多,先保留 top20 top_area = df["区域"].value_counts().nlargest(20).index df["区域"] = df["区域"].where(df["区域"].isin(top_area), "其他") df = pd.get_dummies(df, columns=["区域"], prefix="区") df.to_csv("ershoufang_feature.csv", index=False) print(df.columns.tolist())

逻辑说明:朝向只取第一个字,因为「南北」和「南」对采光影响接近,全排列会让特征爆炸。装修用 0-3 有序编码,因为毛坯到豪装确实有强弱关系。房龄用 2025 减建成年代,clip 到 0-80 防止 1900 年这种脏数据。区域独热前先归并长尾,否则几百个区域会让矩阵稀疏到没法训。

参数说明:top20 是经验值,城市越大可以放到 30。房龄上限 80 年,超过的基本是数据错误。如果你要做的是「预测单价」,目标列选单价;做「预测总价」,目标列选总价,但总价受面积影响极大,建议先预测单价再乘面积。

3. 建模与评估:线性回归、随机森林、XGBoost 怎么选

3.1 先跑一个线性回归当基线,别一上来就上深度学习

很多人觉得房价预测必须用神经网络,其实二手房这种表格数据,树模型往往更强,线性回归则是最好的基线。先跑线性回归,看 R² 和 MAE,心里有个底。

from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score import pandas as pd df = pd.read_csv("ershoufang_feature.csv") # 目标:单价 y = df["单价"] # 特征:去掉目标、总价、以及原始文本列 drop_cols = ["单价", "总价", "楼层", "朝向", "装修", "户型", "建成年代", "区域", "标题", "小区"] X = df.drop(columns=[c for c in drop_cols if c in df.columns]) X = X.select_dtypes(include=["number"]).fillna(0) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) lr = LinearRegression() lr.fit(X_train, y_train) pred = lr.predict(X_test) print("线性回归 R2:", r2_score(y_test, pred)) print("线性回归 MAE:", mean_absolute_error(y_test, pred))

逻辑说明:select_dtypes只留数值列,避免独热后还有字符串。fillna(0)是兜底,正常清洗后不该有缺失。random_state 固定 42,保证每次划分一致,方便对比模型。

参数说明:test_size 0.2 是常规,数据少于 1000 条时用 0.3。MAE 的单位是元/㎡,比如 MAE=3000 意味着平均估偏 3000 元/㎡,结合你城市均价判断能不能接受。

3.2 随机森林和 XGBoost:参数怎么设,特征重要性怎么看

线性回归跑完,换树模型。随机森林抗过拟合,XGBoost 精度通常更高但更容易调参翻车。

from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor import matplotlib.pyplot as plt rf = RandomForestRegressor( n_estimators=300, max_depth=12, min_samples_leaf=3, random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) rf_pred = rf.predict(X_test) print("随机森林 R2:", r2_score(y_test, rf_pred)) print("随机森林 MAE:", mean_absolute_error(y_test, rf_pred)) xgb = XGBRegressor( n_estimators=500, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8, random_state=42 ) xgb.fit(X_train, y_train) xgb_pred = xgb.predict(X_test) print("XGBoost R2:", r2_score(y_test, xgb_pred)) print("XGBoost MAE:", mean_absolute_error(y_test, xgb_pred)) # 特征重要性 imp = pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False) print(imp.head(15)) imp.head(15).plot(kind="barh", figsize=(8, 6)) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig("feature_importance.png", dpi=150)

逻辑说明:随机森林的min_samples_leaf=3是防止叶子节点只有一两条样本导致过拟合。XGBoost 的learning_rate=0.05配n_estimators=500是慢学多树的经典组合,比 0.1 配 200 更稳。特征重要性用随机森林的,因为 XGBoost 的 importance 默认按增益算,容易偏向高基数特征。

参数说明:max_depth随机森林给 12,XGBoost 给 6,因为 XGBoost 是加法模型,单棵树不用太深。subsample和colsample_bytree都 0.8,是防过拟合的常用值。如果 R² 比线性回归还低,先检查特征里有没有泄漏,比如把「总价」漏进去了。

3.3 交叉验证和残差分析:别只看一个 R²

单次划分的 R² 有随机性,用 5 折交叉验证看稳定性,再画残差图看模型在哪类房源上翻车。

from sklearn.model_selection import cross_val_score import numpy as np scores = cross_val_score(rf, X, y, cv=5, scoring="r2") print("5折R2:", scores, "均值:", scores.mean()) # 残差分析 residual = y_test - rf_pred plt.figure(figsize=(8, 5)) plt.scatter(rf_pred, residual, alpha=0.3) plt.axhline(0, color="red", linestyle="--") plt.xlabel("预测单价") plt.ylabel("残差") plt.tight_layout() plt.savefig("residual.png", dpi=150) # 看残差最大的10条 err_df = pd.DataFrame({"真实": y_test, "预测": rf_pred, "残差": residual}) print(err_df.reindex(err_df["残差"].abs().sort_values(ascending=False).index).head(10))

逻辑说明:交叉验证的均值比单次 R² 可信,标准差大说明模型不稳定。残差图如果呈喇叭形,说明大户型预测误差更大,可以考虑对面积取对数。看残差最大的样本,往往是数据错误或特殊房源,比如「满五唯一」没进特征。

参数说明:cv=5 是常规,数据少用 10。残差图横轴是预测值,纵轴是真实减预测,理想是随机分布。如果残差和预测值有相关性,说明模型有系统性偏差。

4. 避坑与排查:二手房预测项目里最容易翻车的 5 个地方

4.1 数据泄漏:R² 0.99 的模型基本都是假的

现象:模型 R² 高到 0.98 以上,MAE 小得离谱。原因:特征里混进了目标列的衍生变量,比如用「总价」预测「单价」,或者清洗时用全量数据算了均值再填充。解决:建模前明确哪些列在预测时拿不到,全部 drop;填充缺失用训练集统计量,别用全量。检查方法:看特征重要性,如果「总价」排第一,直接删。

4.2 区域独热后维度爆炸,模型训不动

现象:get_dummies后列数从 30 涨到 500,训练极慢,R² 还降了。原因:小区名、商圈名这类高基数类别直接独热。解决:先按目标编码或频次编码,只保留 top20 区域,其余归「其他」;或者用category_encoders做目标编码,但注意在交叉验证内部做,防止泄漏。

4.3 面积和单价的非线性关系没处理

现象:大户型预测普遍偏低,小户型偏高。原因:单价随面积增大而降低,线性模型抓不住。解决:对面积取对数np.log1p(df["面积"]),或者加面积平方项;树模型本身能抓,但特征里最好同时保留原始面积和对数面积。

4.4 时间维度没考虑,用未来数据预测过去

现象:随机划分后 R² 很好,但按时间划分就崩。原因:房价有上涨趋势,随机划分让模型「看到」未来价格。解决:按成交时间排序,前 80% 训练后 20% 测试;如果数据没有时间列,至少别用随机划分的结果下结论。

4.5 缺失值填充用 0 或均值,引入偏差

现象:房龄缺失填 0,导致一批「新房」;装修缺失填均值,毛坯和豪装被平均。解决:房龄缺失用中位数或单独设「未知」类别;装修缺失保留为单独一类,别硬填。树模型对缺失不敏感,XGBoost 能自动处理,但线性回归必须填。

5. 把模型用起来:估价接口、批量预测和持续迭代

模型训完不是终点,能对新房源估价才算落地。最简做法是把模型和特征处理管道存下来,写一个预测函数。

import joblib import pandas as pd # 保存模型和特征列 joblib.dump(rf, "rf_model.pkl") joblib.dump(X.columns.tolist(), "feature_cols.pkl") def predict_price(input_dict): cols = joblib.load("feature_cols.pkl") model = joblib.load("rf_model.pkl") row = pd.DataFrame([input_dict]) # 这里需要和训练时一样的特征工程,实际项目建议用 sklearn Pipeline 封装 row = row.reindex(columns=cols, fill_value=0) return model.predict(row)[0] # 示例:输入一套 89 平、南向、精装、房龄 5 年的房 sample = {"面积": 89, "室": 3, "厅": 1, "房龄": 5, "装修等级": 2, "朝向_南": 1} print("预测单价:", predict_price(sample))

逻辑说明:reindex保证输入特征顺序和训练一致,缺的补 0。实际项目里,特征工程要封装成sklearn.pipeline.Pipeline,把清洗、编码、模型串起来,否则线上和线下处理不一致,这是最常见的翻车点。

参数说明:joblib比 pickle 更适合存 sklearn 模型,大模型加载快。预测函数返回单价,总价乘面积即可。

进阶技巧:用Pipeline+ColumnTransformer把数值列和类别列分开处理,数值列做标准化,类别列做独热,整体存成一个对象。这样新数据进来直接pipeline.predict,不用手动对齐列。另外,模型上线后要定期用新成交数据回测,房价分布漂移很快,半年不更新,MAE 可能翻倍。我自己的习惯是每季度重训一次,每次记录 R² 和 MAE,掉超过 15% 就查数据源是不是变了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询