简介:一份基于Python机器学习的二手房交易预测及可视化系统毕业设计项目,面向计算机相关专业准备毕设的学生和需要项目实战练习的初学者,可实现二手房价格预测与数据可视化,代码完整可直接运行,配套答辩PPT,也可用于课程设计或期末大作业。资源共157个文件,压缩包约40MB,包含18个Python源码文件、18个CSV数据集、15个HTML页面和11个JavaScript脚本组成的可视化界面,另有65张截图和PPT演示文稿,便于按模块学习与答辩展示。项目完整覆盖二手房数据清洗、特征处理、模型训练、房价预测和可视化分析,数据集提供原始及多种清洗版本,方便复现实验细节。通过该系统的完整代码和说明,读者可掌握机器学习项目从数据到展示的整套流程,并借助高分毕设的答辩PPT梳理设计思路。这套项目已有166人学习,整体属于经导师认可、评审99分的高分毕业设计。 去年做毕设选这个题目,说实话不是因为它多热门,而是被链家上那串数字勾起了好奇心——同一栋楼,楼层差两层,总价能差二十万;同一个区域,朝向差一个窗户,报价能差十几万。谁在定价?规则藏在数据里吗?带着这个疑问,我把毕设题目定成了“基于Python机器学习的二手房交易预测及可视化系统”。
这套系统做的事情很直接:用Python采集或获取一批真实的二手房挂牌数据,完成清洗和特征工程,训练若干机器学习回归模型来预测房屋总价,然后通过Web页面把数据分布、特征关系、模型效果和在线预测完整展示出来。整个过程覆盖了一个机器学习项目从数据到落地的全链路,很适合计算机、数据科学、信息管理相关专业的毕业生作为毕设主体,也适合想补一个完整项目经历、或者想找一个“数据→建模→部署→展示”闭环案例练手的学习者。下面的内容就是我完整跑通之后的实现思路、选型理由、踩坑记录和答辩应对方案,照着复现基本能少走一半弯路。
1. 选题与系统整体设计:这个题目为什么“好做又好看”
1.1 选题逻辑:难度适中,故事完整,答辩不慌
毕设选题最怕两种:一种是太简单,一个月写完但没含金量,答辩老师问两句就露馅;另一种是太难,做一半卡住,最后只能硬着头皮糊弄。二手房交易预测这个方向,刚好卡在两者中间一个非常舒服的位置。
数据来源现成且真实。链家、贝壳这类平台的挂牌数据是公开的,字段齐全,有总价、单价、面积、户型、楼层、朝向、装修、区域、建筑年代等等。这些字段既有数值型又有类别型,天然适合做机器学习回归任务。哪怕不写爬虫,直接找一份整理好的公开数据集,也能撑起整个数据预处理和建模环节。
模型对比空间大。房价预测是典型的回归问题,线性回归能跑,决策树能跑,随机森林、XGBoost、LightGBM都能跑。模型之间可以做效果对比,出对比表格和图表,这正好是答辩老师最爱看的东西——有实验、有数据、有结论。
可扩展性强,能讲业务故事。系统不只是“训练一个模型交差”,还能落地成一个带页面的小工具:用户输入面积、户型、楼层、区域,系统返回一个预估总价。这件小事放到答辩现场演示,说服力比十页PPT都强。
1.2 系统架构与模块划分
我的实现把整个系统拆成了四个模块,按数据流动的顺序依次衔接:数据采集与预处理、模型训练与评估、结果持久化、可视化与在线预测展示。这样拆的好处是每块都能单独调试,出了问题不需要从头排查。
技术栈方面,我当时选了这样一套组合:
| 模块 | 选型 | 说明 |
|---|---|---|
| 数据获取 | requests + BeautifulSoup / 公开数据集 | 写爬虫练手可以,赶进度建议直接用整理好的CSV |
| 数据处理 | pandas + NumPy + scikit-learn | 清洗、特征工程、编码、划分数据全靠它 |
| 模型训练 | scikit-learn + LightGBM | 先跑基线模型,再上集成模型,对比出结论 |
| 模型持久化 | joblib | 保存训练好的模型,供Web端加载 |
| 可视化展示 | Flask + ECharts / Pyecharts | 图表交互性强,页面效果比matplotlib好一个档次 |
| 在线预测 | Flask + 已训练模型 | 表单输入 → 特征转换 → 模型预测 → 返回价格 |
整体数据流是一条直线:原始数据 → 清洗 → 特征工程 → 训练集/测试集划分 → 模型训练 → 评估对比 → 保存最优模型 → Flask加载模型 → Web页面展示图表和在线预测功能。顺着这条线往下做,逻辑上不会有大的偏离。
提示:环境版本尽量固定。我当时用Python 3.9 + scikit-learn 1.2.x + pandas 1.5.x,整套流程非常稳。版本太新某些API有变动,太老又跟课程内容脱节。新建一个虚拟环境,一次性装齐依赖,避免后面边写边补包。
装依赖就一条命令:
pip install pandas numpy scikit-learn lightgbm flask pyecharts joblib下载慢的话加清华镜像源:-i https://pypi.tuna.tsinghua.edu.cn/simple。这个细节很多人忽略,但能帮你省下不少时间。
2. 数据获取与预处理:模型的天花板在数据里
2.1 数据源与获取思路:能动手爬,但别陷进去
数据获取有三条路,效率完全不同。
第一条路,直接用公开数据集。Kaggle上有波士顿房价、加州房价等经典数据集,阿里天池、和鲸社区也有国内二手房数据。这些数据已经过初步整理,缺失值和异常值不多,适合把精力聚焦在建模和系统上。如果毕设时间紧张,这是最推荐的选择。
第二条路,自己写爬虫抓链家或贝壳。这个方案最有“项目感”,展示的时候可以说“数据是我自己采的”,但同时也是最容易翻车的环节。一是平台有反爬机制,访问频率太高会被封IP;二是页面结构经常调整,今天能跑的选择器明天可能就失效;三是数据量太大反而增加清洗负担。
我当时写了一个简单的爬虫思路,代码不复杂,关键是控制频率、带好Headers:
import requests from bs4 import BeautifulSoup def parse_lianjia_url(url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") # 提取小区名称、户型、面积、朝向、装修、总价、单价等字段 # 遍历列表页,翻页时在URL上替换页码参数 # 每次请求之间加随机延时,比如 time.sleep(random.uniform(1, 3)) return items if __name__ == "__main__": # 指定城区的二手房列表页 # 循环抓取并保存到CSV pass第三条路,找导师要数据。部分学校有合作企业的脱敏数据,这种数据最干净、最有说服力,但可遇不可求,有的话直接选它,没有也别强求。
我的建议很直接:爬虫可以作为系统的“数据获取模块”写进论文里展示思路,但实际建模用的数据,优先用整理好的公开数据集。把时间留给后面更重要的事情,而不是在网页解析上死磕。
2.2 清洗与特征工程实操:细节决定模型上限
数据拿到手之后,第一件事不是建模,而是看看数据长什么样。我当时用df.info()、df.describe()扫了一遍,发现问题比想象中多。整理了一份处理清单,基本覆盖了二手房数据的常见坑:
| 字段 | 处理方式 | 原因 |
|---|---|---|
| 面积 | 过滤掉小于20㎡或大于500㎡的记录 | 极小或极大值多为录入错误或非住宅类房产 |
| 总价/单价 | 用箱线图识别极端值,按需剔除 | 个别挂牌价严重偏离市场,影响模型训练 |
| 朝向 | 把“南、北、东南、西南”保留,其余归为“其他” | 类别太散会导致独热编码维度爆炸 |
| 楼层 | 从字符串中提取“低/中/高/顶层/底层” | 楼层高低对房价影响显著,属于核心特征 |
| 装修 | 映射为“毛坯/简装/精装/豪装” | 这四个档位业务含义清晰,直接做序数编码 |
| 建筑年代 | 计算房龄(当前年份-建筑年代),缺失用同小区众数填充 | 房龄比绝对年份更有建模意义 |
| 区域/商圈 | 区域做目标编码或标签编码 | 位置是房价最核心因素,但独热编码维度太高 |
关键一步是处理价格长尾分布。房价数据通常右偏严重,几百万的房子占多数,几千万的豪宅拉高了均值,直接丢给线性模型很容易被极端值带偏。我当时对总价做了对数变换:
import pandas as pd import numpy as np df = pd.read_csv("house_data.csv", encoding="utf-8") df = df[df["面积"] >= 20].copy() df["房龄"] = 2025 - df["建筑年代"] df["朝向_group"] = df["朝向"].apply( lambda x: x if x in ["南", "北", "东南", "西南"] else "其他" ) df["总价_log"] = np.log1p(df["总价"]) df.to_csv("house_clean.csv", index=False, encoding="utf-8")np.log1p相当于log(1+x),好处是即使价格为0也不会出现负无穷,还原的时候用np.expm1就行。
区域这种高基数类别特征,直接独热编码会让特征矩阵变得非常稀疏。我当时先把区域做了标签编码,后来又试了目标编码——用该区域的历史平均单价替代类别值。效果提升很明显,但这里有一个必须注意的禁忌:目标编码的均值只能在训练集上计算,再映射到测试集,否则会造成数据泄漏,测试指标会虚高得离谱,答辩时被老师点出来就很难收场。
注意:特征工程的顺序千万记牢——先切分训练集和测试集,再在训练集上做各种编码和填充。任何用到全局统计量(均值、中位数、频次)的操作,都不能把测试集的信息混进来。
3. 机器学习模型选型与调优:从基线到最优的完整路径
3.1 先跑基线模型,再上集成模型
很多初学者上来就调XGBoost、LightGBM,结果超参调了一整天,效果也没比线性回归好多少。正确做法是先跑几个基线模型,建立一个参照系,再逐步上更复杂的模型。
我当时按“线性回归 → 随机森林 → GBDT → LightGBM”的顺序一步步来。每一步都计算MAE、RMSE、R²三个指标,记录到表格里。这样产生的模型对比表非常扎实,答辩时直接呈现“我的模型是逐步优化出来的”,而不是“我随便选了一个模型”。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score X = df_encoded.drop(columns=["总价", "总价_log"]) y = df_encoded["总价_log"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) models = { "LinearRegression": LinearRegression(), "RandomForest": RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42), "GBDT": GradientBoostingRegressor(n_estimators=200, learning_rate=0.05, random_state=42), } for name, model in models.items(): model.fit(X_train, y_train) pred = model.predict(X_test) mae = mean_absolute_error(y_test, pred) r2 = r2_score(y_test, pred) print(f"{name}: MAE={mae:.4f}, R2={r2:.4f}")注意我在划分数据之前,把总价和总价_log都从特征里剔掉了,这是防止数据泄漏的基本操作。训练目标是总价_log,评估出来的MAE是在对数空间里的,想转回真实的万元单位,要用np.expm1(pred)还原后再算一次误差。
3.2 LightGBM与XGBoost的参数实践
基线模型跑完,正常情况下LightGBM或XGBoost应该能拿到最优结果。我当时选的是LightGBM,主要原因是对特征工程的要求相对宽松、训练速度快,而且原生接口支持early stopping,省去手动调参的大量时间。
LightGBM的初始参数和调参思路,我整理成了一张表:
| 参数 | 建议初值 | 说明 |
|---|---|---|
| learning_rate | 0.05 | 学习率,越小精度上限越高,但训练更慢 |
| n_estimators / num_boost_round | 1000 | 配合early stopping,实际训练轮数由验证集决定 |
| num_leaves | 31 | 叶子节点数,越大越容易过拟合 |
| max_depth | -1 | 不限制深度时靠num_leaves控制复杂度 |
| feature_fraction | 0.8 | 每轮迭代随机使用80%特征,防过拟合 |
| bagging_fraction | 0.8 | 每轮迭代随机采样80%样本,防过拟合 |
| lambda_l2 | 0.1 | L2正则,对高维稀疏特征有稳定作用 |
核心训练代码:
import lightgbm as lgb params = { "objective": "regression", "metric": "rmse", "learning_rate": 0.05, "num_leaves": 31, "max_depth": -1, "feature_fraction": 0.8, "bagging_fraction": 0.8, "lambda_l2": 0.1, "verbose": -1, "seed": 42 } train_data = lgb.Dataset(X_train, label=y_train) valid_data = lgb.Dataset(X_test, label=y_test, reference=train_data) model = lgb.train( params, train_data, num_boost_round=1000, valid_sets=[valid_data], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)] )early_stopping(50)的意思是连续50轮验证集RMSE没有下降就停止训练,返回的最优模型会被保留。这一招对控制过拟合特别有效,我在多个项目里实测下来,比手动指定轮数稳定得多。
3.3 模型评估与特征重要性解读
模型跑完,要输出一份类似这样的对比表,这是论文和答辩的核心素材:
| 模型 | MAE(万元) | RMSE(万元) | R² | 整体表现 |
|---|---|---|---|---|
| 线性回归 | 23.5 | 35.1 | 0.71 | 解释性强,拟合不足 |
| 随机森林 | 18.2 | 28.7 | 0.81 | 稳健,无明显过拟合 |
| GBDT | 16.4 | 25.9 | 0.84 | 比随机森林更好 |
| LightGBM | 15.8 | 24.3 | 0.86 | 综合最优,训练最快 |
看到这个结果先别急着高兴,一定要看看特征重要性,这是答辩时的加分项。LightGBM原生接口能直接输出:
importance = pd.Series( model.feature_importance("gain"), index=X_train.columns ).sort_values(ascending=False) print(importance.head(10))我当时的Top特征基本是区域编码、面积、房龄、楼层、“是否南向”。这些特征放到业务里完全说得通:位置决定基础价位,面积和房龄决定单价,楼层和朝向决定同一小区内部的差价。能用自己的数据解释业务规律,这个项目就不再是“调包”,而是真正有业务理解力的作品。
提示:R²不是唯一标准。房价这种波动大的场景,MAE的业务含义更直观——预测误差15.8万,意味着模型给出的估价与真实挂牌价平均只差15.8万。答辩时先用MAE解释模型价值,再用R²说明拟合优度。
4. 可视化子系统:让数据和模型“讲人话”
4.1 技术选型:为什么选Flask + ECharts
可视化是这套系统的门面,也是答辩时最容易出彩的部分。可选的方案不少,我对比过三条路:
| 方案 | 优点 | 缺点 |
|---|---|---|
| matplotlib / seaborn 静态图 | 简单、经典、论文插图顺手 | 交互性差,展示效果平淡 |
| Pyecharts 生成HTML | 中文友好,生成图表方便,适合快速做出Demo | 版本差异大,部分组件定制受限 |
| Flask + ECharts | 交互强、可自定义、能结合表单预测 | 前后端联调需要自己写代码 |
最终选了Flask + ECharts。原因很简单:Flask作为轻量级Web框架,几百行代码就能搭起一个完整的展示站点;ECharts是纯前端图表库,散点图、热力图、地图、漏斗图全都开箱即用,交互缩放、悬停提示都是现成的。前后端通过JSON交互,逻辑清晰,答辩时现场演示一套流程非常流畅。
4.2 核心图表与在线预测页面设计
系统页面我拆成了两部分:数据展示页面和在线预测页面。
数据展示页面放了6个核心图表,都是答辩时必须能讲清楚的:
- 总价分布直方图/KDE曲线,展示价格呈长尾分布,这也是做对数变换的原因
- 面积-总价散点图,按区域着色,可以看到不同区域的价格带差异
- 特征相关性热力图,展示总价与面积、房龄等特征的相关关系
- 各区域平均单价柱状图,支持按商圈下钻,直观对比区域价格
- 真实值与预测值对比散点图,越接近对角线说明预测越准
- 特征重要性条形图,展示模型到底“看中”哪些因素
在线预测页面更关键。用户填写面积、户型、楼层、朝向、装修、区域等字段,后端把输入转成和训练时完全一致的特征格式,调用已保存的LightGBM模型预测总价,返回结果。核心代码:
from flask import Flask, request, jsonify import joblib app = Flask(__name__) model = joblib.load("lgb_model.pkl") @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json() # 把用户输入转换为训练时完全一致的特征格式 features = transform_input(data) pred_log = model.predict([features])[0] pred_price = round(float(np.expm1(pred_log)), 2) return jsonify({"price": pred_price}) if __name__ == "__main__": app.run(debug=True, port=5000)其中transform_input是整个预测接口的灵魂——它必须把用户在网页上填的“原始值”转换成训练时的“模型输入”,包括相同的编码映射、相同的填充逻辑、相同的顺序。这段函数直接复用训练脚本里的特征处理函数,不要重写一遍,否则很容易出现线上预测结果离谱的问题。
模型保存我用的是joblib:
import joblib joblib.dump(model, "lgb_model.pkl")相比pickle,joblib对大数组和模型对象的序列化更高效,加载速度也更快。
5. 答辩PPT设计与项目表达:高分的关键在逻辑
5.1 PPT结构:像讲故事一样讲项目
做完系统只是成功了一半,答辩PPT决定老师怎么评价你的工作。一个高分毕设PPT,核心不是堆砌代码和技术名词,而是把“为什么做、做了什么、怎么做、结果如何”讲清楚。
我的PPT结构是8个部分:
- 选题背景与研究意义(用真实数据引出房价预测的实际价值)
- 国内外研究现状(简单回顾,引用3到5篇文献,别铺开)
- 系统总体设计(架构图 + 技术栈说明)
- 数据获取与处理(展示原始数据长什么样,清洗前后对比)
- 模型构建与实验对比(核心页,放模型对比表和特征重要性图)
- 可视化系统展示(截图 + 现场演示或录屏)
- 总结与展望(突出可扩展方向:接入实时房价数据、推荐引擎等)
- 致谢
第5页模型对比那一页,至少留两分钟讲:从基线到LightGBM每一步的优化点在哪里,最后一页放“模型对比表”,直接对比四个模型的MAE、RMSE、R²。这一页就是你的核心论据。演示环节建议提前录制一份完整录屏备份,防止现场网络出问题或端口被占用。
注意:PPT每页的讲解时间控制在10到15秒,重点页可以到30秒以上。答辩规定时间通常8到10分钟,讲完记得留出时间给老师提问。
5.2 老师最爱追问的5个问题
答辩前我把老师可能问的高频问题整理成一个速查表,尤其针对“创新点”和“模型选择”这两个方向做了准备:
| 高频追问 | 回答思路 |
|---|---|
| 为什么选这几个特征? | 从业务相关性讲:面积、区域、房龄是房价的核心影响因素;再补充数据上的证据,如相关性热力图和特征重要性 |
| 为什么不用神经网络? | 样本量有限,树模型可解释性强,模型对比实验更符合毕设工作量;如果换更大数据集可以尝试DNN |
| 如何避免数据泄漏? | 所有编码和填充操作都只在训练集上拟合后再映射到测试集,线上预测复用同一个转换函数 |
| 模型泛化能力如何? | 交叉验证 + 独立区域测试集验证,展示模型在未见过的数据上依然稳定 |
| 创新点是什么? | 不追求算法创新,强调业务闭环:从数据采集、特征工程到模型部署可视化一个完整链路 |
提前找人模拟答辩,把自己当听众,用最简洁的语言把项目讲一遍,直到不卡壳为止。这个环节比多跑十个模型都有用。
6. 常见问题与调试排查:这些坑我替你踩过了
6.1 数据与模型阶段的经典问题
整套流程走下来,我把最容易踩的坑分类整理了一下,都附上了解决方案和问题根源。先看数据和模型部分:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| pandas读取CSV中文乱码 | 文件编码是GBK,读取时默认UTF-8 | pd.read_csv(path, encoding="gbk"),或者先转成UTF-8再读 |
| OneHotEncoder报错 | 新版sklearn默认返回稀疏矩阵直接塞进DataFrame会报错 | 指定sparse_output=False,旧版本参数名是sparse=False |
| joblib加载模型报错 | 训练和部署环境的sklearn版本不一致 | 保持同一环境运行,写清楚requirements.txt |
| 预测结果出现负数 | 对数空间还原后极小值导致 | 用np.clip(pred, 0, None)限制下界 |
| LightGBM安装失败 | 网络源不稳定或平台不兼容 | 用清华镜像源,或conda install -c conda-forge lightgbm |
另外提一个容易被忽视的问题:Windows下用matplotlib画图时中文显示成方块,原因是没有设置中文字体。需要手动指定:
plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows自带黑体 plt.rcParams["axes.unicode_minus"] = False6.2 可视化与前后端联调的常见问题
Web端的排查思路和模型端完全不同,核心是搞清楚问题出在浏览器还是服务器:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Flask端口被占用 | 默认5000被上次进程占用 | 换端口:app.run(debug=True, port=5001),或先杀掉占用进程 |
| ECharts图表加载不出来 | CDN文件路径失效或网络不通 | 把echarts.min.js下载到本地static目录,用相对路径引用 |
| 表单提交后返回500 | 后端特征处理函数抛异常 | 开启debug模式,看浏览器的Network面板和Flask控制台栈信息 |
| 刷新后页面数据丢失 | 图表数据存在内存变量里 | 用sessionStorage临时存储,或者直接重新请求后端接口拉数据 |
排查的顺序也有讲究:先看Flask控制台有没有报错,再看浏览器Network面板的请求状态码,最后看Console的JS错误。大多数问题三步之内就能定位。
还有一种情况我遇到过一次,前端传过来的字段名和后端函数对不上,导致模型预测结果完全不对。后来我在transform_input函数内部加了一行打印,把转换后的特征打印出来,和训练时的特征做对比,问题一眼就暴露了。这种“先打印再调试”的习惯,在前后端联调阶段特别管用。
提示:演示环境记得把Flask的debug关掉,否则出错时弹出的调试器页面会暴露源码路径,而且不安全。
我个人在实际操作中的最大体会是:这个题目的价值不是算法有多新,而是它逼着你走完一个机器学习项目的完整生命周期——从拿数据、清洗、建模到部署、展示、答辩。房价预测很难做到绝对准确,影响一个房子成交价的因素,远多于我们数据里能拿到的字段,比如学区变动、业主急售、成交周期这些信息根本不在挂牌数据里。但“用数据逼近规律”这个过程本身就很有说服力,也是面试官和答辩老师真正看重的能力。如果你也正在做这个题目,我最后多说一句:不要在爬虫和花哨组件上花超过三分之一的时间,把精力留给特征工程和模型对比,那才是整个系统真正的核心。
本文还有配套的精品资源,点击获取