每年到毕设选题的旺季,群里总有人问同一个问题:题目怎么选才能既有技术含量、又不至于做不出来,还保证答辩时评委听得懂、问不倒?房价预测这个方向之所以被反复选中,正因为它把“大数据”“机器学习”这些关键词和真实生活场景绑在了一起,后端可以用 Django 撑起完整的数据服务,前端可以用 Vue 做出漂亮的可视化界面。如果你也在做或者正准备做这个题目,这篇文章可以帮你少走一大半弯路。从数据怎么来、特征怎么做、模型怎么选,到后端接口怎么写、前端图表怎么画、答辩现场怎么讲,我把整条链路完整过一遍。
我做的这套 Django + Vue 基于机器学习的房价预测数据分析系统,整体跑下来大概花了三周,中间踩了不少坑。文中会写清楚每一步的思路、能直接抄作业的代码片段、以及我在实际项目中验证过的参数。适合正在做毕设的同学,也适合想快速搭一个全栈机器学习 Demo、顺便学点工程化思路的开发者。内容偏实操,理论部分我只讲到够用的程度,太深的东西留给论文展开。
1. 选题逻辑与系统整体设计
1.1 选题思路:这个题为什么能讲满二十分钟
毕设答辩本质上是“讲一个完整的故事”,而不是秀一段代码。房价预测的天然优势在于,它的业务链条足够长:数据采集、数据清洗、特征工程、模型训练、后端接口、前端可视化,每一环都有东西可以讲,而且每一环都独立可演示。
更重要的是,房价预测是典型的回归问题,数据特征大多是结构化表格字段,评委不需要额外理解复杂的业务背景。他问“这几个特征是怎么影响房价的”,你就能从面积、房龄、地段、楼层这些直观因素入手解释,聊得深了还能扯到特征工程和模型选择。相比之下,“基于深度学习的图像识别”这类题目虽然听着高级,一旦被追问数据集规模和训练细节,很容易答不上来。
另外,这个题目和数据可视化天然契合。房价数据里有时间维度、地理维度、价格维度,折线图、散点图、热力图、地图,想画什么都有素材。而可视化恰恰是“大数据”这个概念里评委最容易感知的部分。我的经验是,评委会花大量时间盯着你的图表看,而不是盯着你的代码看,所以把可视化做好,比把模型调高零点几个点更划算。
1.2 技术选型:Django 与 Vue 的分工逻辑
先说明一下,这个项目用的是前后端分离架构,而不是 Django 传统的模板渲染方案。原因很直接:后端只负责提供数据和模型预测服务,前端专注做交互和可视化,两者解耦之后,答辩时可以分别演示,代码结构也更清晰。
后端选 Django,理由有三个。第一,Django 自带 Admin 后台和 ORM,毕设里最常见的“数据录入和管理”需求,不需要额外写页面,改改 Model 注册一下就能用。第二,Django REST framework 写 API 非常顺手,序列化器一配,JSON 接口直接出来。第三,Django 对 Channels 的支持比较成熟,后面做 WebSocket 增量推送时不用另起服务。
前端选 Vue,是因为它对新手友好、生态齐全。Vue 的单文件组件配合 Element Plus 这类 UI 库,可以快速搭出仪表盘页面;ECharts 的可视化图表接入成本低,数据格式调整一下就能渲染。我用的 Vue 3 + Vite + Vue Router + Pinia 这个组合,开发和构建体验都很顺。
至于为什么用机器学习而不是深度学习,我在第三章节详细说。简单先给个结论:在房价这种小规模表格数据上,调好特征的传统机器学习模型,效果大概率优于深度学习,而且可解释性更强,答辩时更好讲。
1.3 架构分层:从数据到展示的四层闭环
整个系统可以划分为四个层次,每一层的职责非常清晰:
- 数据层:负责数据采集与预处理,包括开源数据集、爬虫数据、数据清洗和特征工程产物。
- 模型层:负责训练房价预测模型,输出模型文件和特征配置,用 joblib 序列化保存。
- 服务层:基于 Django 提供 REST API,负责数据查询、统计分析、模型预测调用,以及 WebSocket 实时推送。
- 表现层:基于 Vue 构建前端应用,展示数据大盘、价格趋势、预测结果和管理页面。
这个分层的价值在于,答辩时你可以顺着数据流讲:原始数据进来之后经过清洗变成特征,特征喂给模型得到预测结果,预测结果通过接口暴露给前端,前端以图表形式呈现。评委沿着这条线提问,你很难被问倒,因为每一层你都实际做过。
2. 数据准备与特征工程
2.1 数据来源:开源数据集和爬虫怎么选
房价预测最常见的数据集是波士顿房价,但这个数据集在 scikit-learn 1.2 版本之后被移除了,原因是它存在一些伦理争议和统计问题。如果你用新版 sklearn,直接from sklearn.datasets import load_boston会报错。我用的是加州住房数据集fetch_california_housing,包含两万条左右样本、八个特征,刚好适合做毕设。它的特征是区域层面的统计值,比如该区域平均收入、房龄中位数、房间数中位数、人口等,目标值是该区域房价中位数,单位是十万美元。
另一个常用方案是爬取链家、贝壳这类平台的二手房数据。这种方式的好处是数据真实、字段丰富,有小区名、户型、朝向、楼层、装修、挂牌单价等,做特征工程时最有发挥空间。但要注意,爬虫要控制节奏,不要高频请求;数据量也不宜贪大,抓几百条到一千条足够支撑项目演示。我个人最后用了加州住房数据集做主模型数据,同时抓了几百条真实房源做补充展示,让答辩材料里既有标准数据集,也有真实业务数据,比单一来源更有说服力。
2.2 清洗规则:缺失值、异常值和单位换算
数据清洗是评委大概率会问到的环节,因为它在整个项目里最琐碎、最能体现工程素养。以爬取的房源数据为例,我最常遇到的几个问题:
- 总价和单价字段不一致,有的房子按总价挂牌,有的按单价挂牌,必须统一换算成“元/平方米”。
- 楼层字段是“18/33”这种格式,需要拆分成“当前楼层”和“总楼层”两个特征,再进一步生成“楼层率”。
- 朝向字段缺失,很多老旧小区没有写朝向,我选择填充“未知”,而不是删掉整行,这样能保留样本量。
- 面积字段存在异常值,比如 5 平方米的“杂物间”和 500 平方米的“别墅”出现在同一批数据里,需要加业务逻辑过滤,否则模型会被极端值带偏。
针对这些情况,我建了一张清洗规则表,每一步都记录处理逻辑,答辩时直接拿这张表讲,既清晰又有说服力。
| 字段 | 常见问题 | 处理方式 |
|---|---|---|
| 面积 | 极小或极大异常值 | 按小区位数的 1%/99% 分位截断 |
| 单价 | 与总价不一致 | 换算为统一单位:元/平方米 |
| 楼层 | 字符串形式“18/33” | 拆分为当前楼层、总楼层、楼层率 |
| 朝向 | 缺失、口径混乱 | 统一映射为枚举值,缺失填“未知” |
| 装修 | 精装/简装/毛坯 | 独热编码为多个二值特征 |
| 挂牌时间 | 时间格式不统一 | 统一为日期格式,计算挂牌天数 |
这个规则表是纯手工总结出来的,不同小区实际分布差异很大,建议大家先画箱线图看一眼分布,再决定截断阈值。一上来就删数据很容易把好样本误删。
2.3 特征工程:把业务直觉变成模型能懂的数值
特征工程做得好不好,直接决定模型分数的上限。加州住房数据集的特征已经处理过了,但爬虫抓到的原始字段不能直接用,需要做变换。
我处理的特征包括三组:
- 数值特征:面积、总楼层、当前楼层、房龄、卧室数、客厅数。这类特征我会先做标准化,把量纲统一,尤其对线性模型很重要。
- 类别特征:朝向、装修、电梯、所在城区。朝向和装修这类用独热编码,转换成多个 0/1 列;所在城区如果类别很多,可以用目标编码,用该城区历史平均房价作为编码值。
- 衍生特征:楼层率(当前楼层/总楼层)、单位面积价格、距离地铁站的直线距离、小区均价与整体均价的比值。
其中“楼层率”是个很实用的衍生特征。经验上,电梯房的中高楼层价格更高,楼梯房则是低楼层更受欢迎,单纯用“当前楼层”一个数字很难表达这种关系,但“楼层率”配合是否有电梯,就能让模型学到这个规律。
还有一个容易忽略的点:训练集和测试集的特征处理必须一致。很多人先对全量数据做标准化,再划分训练集和测试集,这是错的。正确做法是先切分,再用训练集的均值和标准差去变换测试集,否则会引入数据泄漏,测试指标虚高。
2.4 相关性分析:用热力图找到影响房价的关键变量
在做模型之前,我习惯先画相关性矩阵热力图,把每个特征和目标值的皮尔逊相关系数列出来。这一步看似简单,但价值很大:一方面帮你筛掉无关特征,另一方面答辩时可以证明你不是拍脑袋选特征的。
在我跑的加州住房数据上,几个关键变量和房价中位数的相关度排序大致是:收入水平 > 房龄 > 房间数量 > 人口密度。收入水平和房价呈明显正相关,房龄呈负相关,这和直觉一致。这个结果可以直接在答辩时展示,评委看到你的分析过程和业务逻辑对得上,第一印象就稳了。
画图我用的是 Python 的 matplotlib 和 seaborn。核心代码不超过十行,热力图输出后保存成图片,后续放到演示 PPT 里也好看。
import matplotlib.pyplot as plt import seaborn as sns corr = df.corr() plt.figure(figsize=(12, 10)) sns.heatmap(corr, annot=True, fmt=".2f", cmap="RdBu_r", square=True) plt.title("Feature Correlation Heatmap") plt.savefig("correlation.png", dpi=200, bbox_inches="tight")这里有一个实操细节:热力图上如果特征数量太多,每个格子的数字会挤成一团,看起来像马赛克。建议先手动筛选出和目标值相关性绝对值大于某个阈值的特征,再画子图,别把所有特征全丢进去。
3. 房价预测模型:从线性回归到集成学习
3.1 基线模型:先跑通线性回归,再谈优化
不要一上来就堆 XGBoost。我的习惯是先跑一个最基础的线性回归,作为性能基线和代码正确性的校验。如果线性回归结果一塌糊涂,说明前面数据处理或者特征工程一定出了问题,这时候去调复杂模型毫无意义。
线性回归的原理一句话就能讲清楚:假设目标值和特征是线性关系,通过最小化预测值与真实值的均方误差来求解系数。评价回归模型,我主要看两个指标:R² 决定系数,表示模型解释了多少方差,越接近 1 越好;RMSE 均方根误差,表示预测值和真实值的平均偏差,单位与目标值一致,便于理解。
在加州住房数据集上跑的第一个线性回归,测试集 R² 大约是 0.71,RMSE 约 0.62(目标值单位是十万美元)。这个结果不算好,但已经能说明特征整体有效。我把它记录下来作为基线,后面每个模型都跟它对比。
3.2 Lasso 正则化:牺牲一点拟合,换可解释性
第二个模型我选 Lasso 回归。Lasso 在线性模型上加了一个 L1 正则项,它有一个特性:会把部分特征的系数压缩到 0,相当于自动做特征选择。这在答辩时是一个非常值得展开的点——你用实际数据证明了模型自己挑出了哪些特征。
调参关键是正则化系数 alpha。我用交叉验证搜索了一组候选值,从 0.001 到 0.1 按对数刻度排列,最后选出的 alpha 在 0.01 这个量级。测试集 R² 比线性回归略高几个百分点,同时有几个系数为零的特征被剔除。
from sklearn.linear_model import LassoCV from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) lasso = LassoCV(alphas=None, cv=5, random_state=42) lasso.fit(X_train_s, y_train) print(f"best alpha: {lasso.alpha_:.4f}") print("coefficients:", lasso.coef_)一个常见的误区是 alpha 设得过大。之前我试过 alpha=1.0,结果所有特征系数全都变成 0,模型输出恒定值,R² 直接变成负数。从那次之后我养成了先画“系数随 alpha 变化路径图”的习惯,看特征先被压缩、后消失的过程。
3.3 树模型进阶:随机森林和 XGBoost 的可解释性
线性模型表现有限,因为特征和房价之间还有大量非线性关系。比如同样增加 10 平方米,小户型和大户型的价格变化幅度完全不同,这种交互效应线性模型很难捕捉。这时候就该上树模型。
我先后试了随机森林和 XGBoost。随机森林通过构建多棵决策树、对结果取平均来降低过拟合,对异常值和小噪声比较鲁棒。XGBoost 是梯度提升框架,每一棵树都在拟合前面所有树的残差,表达能力更强,但调参也更敏感。
在同一个测试集上,随机森林测试集 R² 到了 0.85 左右,XGBoost 到了 0.88 左右,已经明显超过线性基线。不过要注意,树模型如果不加约束,训练集分数很容易冲到 0.97 以上,而测试集分数跟不上,这就是过拟合信号。我的应对方式是对树的深度、叶子节点最小样本数做了限制,并用早停法控制提升轮数。
树模型还有一个很有价值的产出:特征重要性排序。随机森林可以输出每个特征对预测的贡献占比,我把它做成柱状图放到答辩 PPT 里。评委问“你凭什么说这几个特征是关键特征”时,直接把图亮出来就好。特征重要性的排序通常和相关性热力图一致,但又不完全相同,因为它捕捉的是特征在高阶交互中的价值。
3.4 模型对比:用一张表终结选型争论
我最终把所有模型的测试集指标汇总成了一张表。不同数据集上具体数值会有差异,但趋势固定:树模型明显优于线性模型,正则化和特征工程都能带来边际提升。
| 模型 | 测试集 R² | RMSE(十万美元) | 训练时间 | 说明 |
|---|---|---|---|---|
| 线性回归 | 0.71 | 0.62 | 秒级 | 基线,可用作正确性校验 |
| Lasso 回归 | 0.74 | 0.58 | 秒级 | 自动特征选择,可解释性强 |
| 随机森林 | 0.85 | 0.45 | 十几秒 | 稳定,调参简单 |
| XGBoost | 0.88 | 0.41 | 半分钟左右 | 集成树,效果最好 |
最后部署到 Django 里的是随机森林,原因有三:它的效果和 XGBoost 差距很小,对超参数不敏感,代码里不用写一堆调参逻辑;模型体积小,加载快;特征重要性已经足够支撑答辩。
部署前我用 joblib 把训练好的模型和标准化器一起打包:
import joblib joblib.dump(model, "model/random_forest.pkl") joblib.dump(scaler, "model/scaler.pkl")这里有个版本坑需要提醒:joblib 在跨 Python 小版本加载时偶尔会报不兼容错误,最稳妥的方案是训练环境和部署环境用同一个 Python 版本。我就是因为实验室电脑和笔记本 Python 版本不一致,部署时模型加载直接报错,折腾了半天才发现。
4. Django 后端接口与业务闭环
4.1 工程骨架与数据库设计
后端我用 Django 4 + Django REST framework + Channels 搭的。先创建项目和应用:
django-admin startproject house_project cd house_project python manage.py startapp house核心数据模型有两个。第一个叫 HouseInfo,存房源基础信息和实际成交单价,对应数据管理页面;第二个叫 PredictRecord,存每次预测的输入特征、预测价格和预测时间,用于在日志页面展示历史预测记录。
from django.db import models class HouseInfo(models.Model): area = models.FloatField("面积(㎡)") house_age = models.IntegerField("房龄(年)") floor_rate = models.FloatField("楼层率") has_elevator = models.BooleanField("有无电梯") total_price = models.FloatField("总价(万元)") unit_price = models.FloatField("单价(元/㎡)") district = models.CharField("所在城区", max_length=32) created_at = models.DateTimeField("入库时间", auto_now_add=True) class PredictRecord(models.Model): features = models.JSONField("预测输入特征") predicted_price = models.FloatField("预测价格(元/㎡)") created_at = models.DateTimeField("预测时间", auto_now_add=True)Django 的 JSONField 在存档输入特征时特别方便,直接把前端传过来的特征字典原样存进去,省去建一堆字段的麻烦,以后查记录也直观。
4.2 用 DRF 快速构建查询与统计接口
接口设计我用了 RESTful 风格,前后端通过 JSON 通信。主要接口有这几个:
GET /api/houses/:分页返回房源列表,支持按城区、价格区间过滤。GET /api/houses/statistics/:按月统计平均挂牌价走势,供前端画折线图。GET /api/houses/district/:按城区统计平均单价,供前端画柱状图或地图。POST /api/predict/:接收房源特征,返回模型预测价格。GET /api/predict/records/:查询历史预测记录。GET /ws/price/:WebSocket 通道,实时推送新增房源和预测结果。
以POST /api/predict/为例,核心逻辑是加载模型文件、标准化输入、返回预测值。
import joblib import numpy as np from rest_framework.views import APIView from rest_framework.response import Response model = joblib.load("model/random_forest.pkl") scaler = joblib.load("model/scaler.pkl") class PredictView(APIView): def post(self, request): features = request.data.get("features") if not features: return Response({"error": "missing features"}, status=400) scaled = scaler.transform(np.array(features).reshape(1, -1)) price = float(model.predict(scaled)[0]) PredictRecord.objects.create( features=features, predicted_price=round(price, 2) ) return Response({"price": round(price, 2), "unit": "元/㎡"})一个小经验:模型文件千万别放在 views.py 里重复加载。如果在模块顶部加载一次,所有请求共用同一份模型,节省内存和时间;如果放在每次请求里加载,接口响应会慢到一个不可接受的程度,答辩现场演示时就会很尴尬。
4.3 跨域配置与前端联调约定
前后端分离之后,跨域问题躲不开。前端开发服务器跑在http://localhost:5173,Django 跑在http://localhost:8000,浏览器出于同源策略会拦截请求。我的处理方式是后端直接配django-cors-headers,在前端开发阶段再用 Vite 的 proxy 做一层转发,双保险。
Django 侧的settings.py配置:
INSTALLED_APPS = [ # ... "corsheaders", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", # ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ]Vite 侧的代理配置,在vite.config.js里:
export default { server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } }用 Vite 代理之后,前端代码里可以直接请求/api/predict/,不需要写完整的http://localhost:8000前缀,后面部署时只需改代理目标,前端代码一行都不用动。
4.4 WebSocket 增量推送:让页面自己“长”出新数据
这个功能是整个项目里答辩最亮眼的一部分。我实现了这样一个场景:管理员在后台录入了一条新房源,或者用户刚发起一次新的预测,不需要刷新页面,前端所有在线的客户端就能立刻看到数据更新——折线图自动延伸、列表自动新增一行。背后的技术是 Django Channels 提供的 WebSocket 支持。
Channels 给 Django 加了一层异步能力,处理长连接。核心是一个消费者类,定义连接建立、断开、接收消息时的行为。
import json from channels.generic.websocket import AsyncJsonWebsocketConsumer class PricePushConsumer(AsyncJsonWebsocketConsumer): async def connect(self): await self.channel_layer.group_add("price_group", self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard("price_group", self.channel_name) async def price_update(self, event): await self.send_json(event["data"])在 Django 视图里,每次新增房源或完成一次预测后,向组推送消息:
from channels.layers import get_channel_layer from asgiref.sync import async_to_sync def notify_new_record(payload): channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( "price_group", {"type": "price.update", "data": payload} )这里有一个常见的坑:Channels 的默认 channel layer 是 Redis,但很多同学本地没有装 Redis,项目跑不起来。实际上 Channels 内置了InMemoryChannelLayer,只用于本地开发和演示,配置很简单。
CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer" } }用内存 channel layer 时,多个 uvicorn worker 进程之间没法互通消息,但毕设演示通常只开一个进程,完全够用。如果哪天要上生产环境,再换 Redis 就行。
前端接收推送的代码也很简单,原生 WebSocket 就够用:
const ws = new WebSocket('ws://localhost:8000/ws/price/') ws.onmessage = (event) => { const data = JSON.parse(event.data) pointList.push(data) chart.setOption({ series: [{ data: pointList }] }) }当 WebSocket 推送触发 ECharts 的setOption时,图表会自己动起来,没有任何刷新操作。这个效果在答辩现场特别加分,而且实现逻辑不复杂,几句话就能讲清楚。
5. Vue 前端可视化与交互
5.1 页面规划:仪表盘、趋势分析、单房预测、管理页
前端页面我分了四个主模块,每个模块对应一个 Vue 路由。
首页是数据总览仪表盘,顶部一行指标卡展示房源总数、平均单价、最高单价、在售套数;中间区域放城区均价柱状图和面积-单价散点图。次页是趋势分析页,用折线图展示近一年房价走势,用热力图展示特征相关性,这个页面的素材全部来自后端统计接口。
第三页是单房预测页,也是最核心的功能页。用户填写面积、房龄、楼层率、朝向、装修等表单,点击预测按钮,后端实时返回模型预测价格,前端展示预测结果卡片,同时把本次预测记录插入下方的历史记录表。第四页是数据管理页,通过 Django Admin 嵌入的方式管理房源数据,前端只展示入口,不重复开发。
工具类页面还有一个 WebSocket 演示页,专门用来展示增量推送直播效果。这个页面平时不体现在导航里,答辩时单独打开。
5.2 ECharts 图表配置:让数据自己说话
前端可视化选的是 ECharts,它最成熟、文档最全,遇到问题基本都能搜到答案。我只挑了四个核心图表:折线图、散点图、柱状图、热力图。每个图表的配置都不复杂,关键在于数据格式。
以散点图为例,配置一个双轴散点,X 轴是面积,Y 轴是单价,点的大小代表总价,颜色代表城区,一张图塞进三个维度。
import * as echarts from 'echarts' const chart = echarts.init(document.getElementById('scatterChart')) chart.setOption({ xAxis: { name: '面积(㎡)' }, yAxis: { name: '单价(元/㎡)' }, series: [{ type: 'scatter', data: houses.map(h => ({ value: [h.area, h.unit_price, h.total_price], itemStyle: { color: districtColor[h.district] } })) }] })这里有个细节:如果你的散点数据点很多,建议把data里的symbolSize改成按第三个维度映射,不然图表会变成一坨密度相似的圆点,视觉上完全不区分信息。
地图类可视化我没有用 ECharts 自带的地图,因为成都、长沙这类城市的手工地理数据需要额外加载并校准边界,折腾起来性价比低。更轻量的方案是用散点图叠加在城市底图的图片上,按经纬度映射坐标,演示效果轻盈且稳定。这个方案在毕设场景下非常推荐。
5.3 Vue Router 路由与请求封装
前端路由我用的 Vue Router,懒加载配置好之后,每个页面独立打包,首屏加载速度明显提升。路由配置类似这样:
const routes = [ { path: '/', component: () => import('@/views/Dashboard.vue') }, { path: '/trend', component: () => import('@/views/Trend.vue') }, { path: '/predict', component: () => import('@/views/Predict.vue') }, { path: '/manage', component: () => import('@/views/Manage.vue') } ]请求层我封装了一个 axios 实例,统一处理 baseURL、超时时间、错误提示,避免每个页面重复写拦截逻辑。
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.response.use( response => response.data, error => { ElMessage.error(error.response?.data?.message || '请求失败') return Promise.reject(error) } ) export default request用请求函数时,业务代码会变得非常干净。预测接口的调用就一行:
const res = await request.post('/predict/', { features: formFeatures })状态管理我用的 Pinia,主要存全局的房源列表和当前筛选条件。如果项目里只有两三个页面共享数据,其实用 Pinia 有点过度设计,但答辩时可以多一个“页面状态管理方案选型”的谈资,并且后续扩展不会乱套。
5.4 联调踩坑:接口字段约定与开发环境代理
前后端联调中最容易出的问题就是字段名对不上。后端返回unit_price,前端读price,结果页面上永远显示 undefined。我的解决办法是列一张接口字段对照表,前后端各留一份,开发时按表对接。我贴一个我自己的对照片段:
| 前端字段 | 后端字段 | 含义 | 类型 |
|---|---|---|---|
area | area | 面积 | float |
unitPrice | unit_price | 单价 | float |
district | district | 城区 | string |
houseAge | house_age | 房龄 | int |
这个对照表看似简单,但它让我少走了很多弯路。特别是当接口返回字段几十个时,不约定好规范,排查起来非常痛苦。
另一个坑是表单前后端校验不一致。前端限制了面积必须大于 0,后端接口也该同样校验。别人直接拿 Postman 绕过前端发送非法参数时,后端至少返回 400 而不是 500。我在 Django 里就吃了这个亏,用户输入一个超大负数面积,模型预测出荒谬结果,图表直接崩。后来我在接口层加了参数范围校验,非法输入统一返回 400 错误,前端再友好提示。
6. 答辩实操指南:常见问题与避坑清单
6.1 开场三分钟讲稿怎么组织
答辩开场的三分钟,直接决定评委的注意力。我的讲稿结构是这样组织的:先抛出业务背景,也就是“房价预测对购房者和市场分析者有什么价值”;紧接着说自己做了什么,从数据清洗、特征工程、模型选型一路带过;再演示系统,先看数据大盘,再演示单房预测,最后展示 WebSocket 实时推送;最后总结技术难点和下一步计划。
这里要注意,不要按代码文件一个个讲,而要按“数据怎么流转”讲:数据从哪里来、变成了什么特征、模型怎么用特征预测出价格、价格怎么显示在页面上。这条流水线讲清楚了,评委对你的系统全貌就有了准确认知,后续提问也不会跑偏。
我的实际操作中,开场大概一页 PPT 的背景介绍用了三十秒,整个流程控制在十分钟内,剩五分钟给评委提问。时间规划好了,答辩节奏基本可控。
6.2 高频问题速查表:几乎必问的十个问题
答辩中有些问题几乎是必考的,提前准备是性价比最高的复习方式。我整理了一张表,每一条都是评委大概率会追着问的点。
| 评委问题 | 回答思路 |
|---|---|
| 为什么用 MSE 而不是 MAE | MSE 连续可导,对较大误差惩罚更重,梯度优化更稳定;但解释性比 MAE 差,所以同时报告 RMSE |
| 为什么没用深度学习 | 表格数据规模小,树模型拟合能力强且可解释性更好;深度学习在小样本表格数据上常见劣势 |
| Lasso 和岭回归区别 | Lasso 用 L1 正则,会把系数压到 0,适合作特征选择;岭回归用 L2 正则,只缩小系数但不会归零 |
| 怎么判断模型过拟合 | 对比训练集和测试集 R² 差距,差距过大说明过拟合;再用交叉验证分数进一步确认 |
| 特征重要性怎么算的 | 随机森林基于袋外误差或节点纯度下降来度量,展示特征重要性排序图说明 |
| 数据量这么小够吗 | 加州住房约两万条,配合交叉验证可以给出可靠估计;实际部署中可通过爬虫和增量入库持续扩充 |
| 业务上预测价格有什么偏差 | 模型预测的是挂牌价的表层影响因素,不包含学区政策、市场情绪等非结构化因素 |
| 模型多久重新训练一次 | 设计了一个指标,当累积预测数据的 MAE 超过阈值,触发后台重跑训练脚本 |
| WebSocket 和 HTTP 区别 | WebSocket 是长连接,服务端可主动推送;HTTP 是请求-响应模式,服务端不能主动发数据 |
| 你项目里最大的技术难点 | 我梳理为跨域前后端联调,以及模型上线加载的体积与稳定问题,具体经验和优化过程展开讲 |
答辩时最忌讳的是背答案。每道题你都先用两三句话讲清逻辑,再结合自己项目里的数据说一个实例,评委会明显感觉到你真的做过了。
6.3 现场演示脚本:先展示什么,再展示什么
演示环节的先后顺序很有讲究。我的顺序是:
第一步,先打开首页数据大盘。展示房源总数、均价、分布图,评委一眼看到信息量,会觉得“数据量到位”。第二步,切到趋势分析页,用热力图和相关性分析引出特征工程,告诉评委“我理解哪些因素影响房价”。第三步,切到单房预测页,手动输入几个真实房源参数,点击预测。这时要选择一个和真实挂牌价接近的户型,让结果看起来可信。第四步,展示 WebSocket 页面。让管理员后台新增一条房源或提交一条预测,页面图表自动更新,不用刷新。评委看到这个动态效果,基本就对一个毕设项目的前端深度有了认可。
现场演示最大的风险是网络和依赖服务。我的建议是,所有接口全部走本地服务,不要依赖外网;演示前五分钟先清一遍流程,确认模型文件能正常加载。另外,如果 WebSocket 演示时前端页面没反应,不要着急重启服务,先检查端口是否被占用,多半是开发者工具里残留了旧连接。
6.4 我踩过的坑清单:给后来者的一份护身符
整个项目做下来,有几个坑我每次想起来都觉得值得提醒后来的同学。
第一个坑是 sklearn 版本问题导致开篇就卡住。旧教程全是load_boston,新版直接删了,翻文档才发现已经换成fetch_california_housing,这个改动让很多同学开局就怀疑人生。解决方案就是换数据集,没有任何兼容的捷径。
第二个坑是模型文件路径写死。之前我在 Windows 上开发,模型文件放在桌面某个路径,后来项目迁移到 Linux,路径找不到,模型加载直接抛异常。后来我统一改成用BASE_DIR拼接相对路径,项目迁移到哪都不怕。路径问题看着小,部署时才暴露,提前处理省心。
第三个坑是跨域配置漏了 OPTIONS 预检请求。当时前端 POST 请求一直报跨域,排查半天才发现CORS_ALLOWED_ORIGINS配了,但中间件顺序和预检响应头没配好。这个问题的排查思路是先在浏览器开发者工具里看Access-Control-Allow-Origin响应头,再决定到底是前端还是后端的问题,别乱改代码。
第四个坑是模型效果在演示时翻车。我训练时用的随机森林,测试集 R² 0.85 看着不错,但现场输入一个超出训练数据分布范围的极端户型,预测结果就特别离谱。后来我在前端表单里加了输入范围校验,并在后端也同步校验,保证演示时输入的参数都在训练数据范围内,预测结果自然稳定。
还有第五个坑,是答辩 PPT 里放模型代码太多。评委根本没时间看代码,应该放数据流转图、特征热力图、模型对比表、系统截图。代码最多贴一个预测接口的关键函数就够了。图比代码有说服力,数据比文字有说服力,这个颠扑不破。
最后再说一点个人体会。房价预测这个题目,做完之后回头看,最值钱的部分其实不是那几个模型指标,而是数据处理和系统集成的完整链路。很多同学交上去的毕设是从网上找一段模型代码,另找一份前端模板拼起来,自己根本不知道中间怎么衔接。你如果认真把数据清洗表、特征对比图、模型对比表、接口文档全部留档,答辩时材料一摆,评委自然会认为这是一个有全局视野的工程实践。真心建议后来者不要只盯着模型分数调参,把系统讲完整、把过程讲扎实,才是毕设答辩最稳的路线。