毕业设计选题目的时候,我见过太多同学在“纯网页管理系统”和“纯算法研究”之间反复纠结。前者比如“XX管理系统”,开发门槛低,但工作量一眼见底,很多成品就是一张表的增删改查,答辩老师翻来覆去问不出核心竞争力;后者比如“基于深度学习的XX识别”,名字好听,但没GPU、没数据,三个月下来容易陷在调参泥潭里,最后展示的时候也说不出工程价值在哪儿。我最后落地的题目是“基于多元线性回归的新能源汽车销量分析预测与应用”,后端用Django,前端用Vue,从数据采集、特征处理、模型训练、可视化展示到预测应用,串成了一条完整链路。这个项目做下来我的体会是:它既有可解释的算法,又有能看得见的Web应用,还能名正言顺地跟“大数据分析”挂上钩。今天把整个实现过程、选型逻辑和答辩时踩过的坑完整写一遍,给准备做类似毕设的同学一个可以直接参考的范本。
1. 为什么选“新能源汽车销量预测”当毕设题目
1.1 选题背后的实际考量
选题目不能只凭兴趣,得同时看三个维度:是否有数据可拿、是否有算法可讲、是否有系统可看。新能源汽车销量预测这三个维度全占。
销量数据本质上是典型的业务时序数据。新能源车近几年的产销量变化非常明显,影响因素也足够丰富——油价、充电桩保有量、电池装机量、消费者信心指数、补贴政策调整等,每一个都有公开的历史趋势数据可以整理。对毕设而言,这意味着“分析”和“预测”两个环节都有源源不断的写作素材,不会出现论文写到第三章就无话可说的窘境。
更重要的是,这个题目能引出“模型为什么这样预测”的业务解释。比如充电桩数量上涨、油价走高,都可能在数据层面表现为正向影响,这类因果关系能让答辩时的口头表达非常顺。相比一个只输出分类结果的图像识别模型,销量预测的每一步都有业务逻辑兜底。
1.2 分析预测闭环才算完整
我给这个项目定的目标是“从数据到应用的分析预测闭环”,而不是只训练一个模型交差。闭环拆开是四步:先拿到真实感足够强的销量与因子数据,做清洗和特征工程;再基于多元线性回归建立销量与多个影响因素的映射关系;然后用Django后端提供训练和预测API;最后用Vue前端把历史趋势、相关性和预测结果可视化出来。
很多同学习惯先做页面,最后随便塞一个训练代码进去,结果答辩演示时只能点几个静态页面,一被追问“预测数据从哪来”就露馅。把闭环做完的好处是,你从数据讲到模型再讲到页面,整个过程是连贯的,老师往哪一步深挖,你都有真实内容应对。
2. 技术栈定下之前,我做了哪些对比选型
2.1 后端框架:为什么是 Django
后端框架我对比过Flask、Django、Spring Boot。Flask轻量,但登录、ORM、后台管理都要自己拼,对毕设来说重复造轮子太多;Spring Boot工程感强但上手曲线陡,代码量也大。最后选Django,是因为它自带的认证体系、ORM、后台管理和admin站点几乎覆盖了毕设项目的全部基础需求,可以让我把精力集中在模型服务和接口设计上。
Django还有一个很适合本项目的点:它的ORM可以直接映射MySQL或SQLite,和pandas清洗后的结构化数据对接非常顺畅。实际开发中,我用joblib把训练好的模型保存成pkl文件,Django接口在启动时加载一次,后续每次请求只需要把前端参数转成特征向量喂给模型,推理过程写起来不超过20行。
2.2 前端框架:Vue 3 + Element Plus 的组合逻辑
前端选型没太多纠结,直接用了Vue 3 + Element Plus + ECharts。Vue的单文件组件机制很适合把数据看板、趋势图表、预测表单拆成独立模块,ECharts则直接解决了可视化问题,不需要自己用Canvas从零画图。
对前端经验一般的人而言,Vue的接入成本也没想象中高。按官方脚手架创建项目后,用axios封装请求,把后端返回的数据填进ECharts的option选项里,这套模式固定之后,后面做新页面基本就是复制粘贴再微调。Element Plus的表格、表单、卡片组件让页面不至于太“裸”,答辩视觉效果有保障。
2.3 算法选择:多元线性回归为什么够用
很多同学在选题时会陷入“算法越难越好”的误区,但毕设的算法核心标准是“讲得清楚、验得明白”。多元线性回归虽然简单,但优点非常明确:数学形式直观、每个系数都有具体业务含义、能直接回答“哪些因素影响了销量”这类问题。
我列过一个备选算法对比表:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 多元线性回归 | 可解释性强、实现简单、训练快 | 对强非线性关系拟合有限 | 特征与目标之间近似线性且有解释需求 |
| 随机森林 | 非线性拟合好、无需标准化 | 黑盒、调参点较多 | 特征多、关系复杂但解释性要求不高 |
| LSTM / GRU | 能建模时序依赖 | 需要大量样本、训练慢、可解释性差 | 高频长序列数据 |
我做特征相关性分析时发现,新能源车销量与充电桩保有量、油价等变量的关系在月度粒度上并没有特别复杂的非线性模式,线性回归在测试集上的决定系数R²能到0.9以上,已经足以支撑学位论文的论证和应用展示。盲目上深度学习只会增加工作量,还会引入过拟合风险,答辩时解释起来也更加被动。
2.4 整体架构设计
系统按四层划分:数据层用CSV数据集承载原始记录,算法层用Python和pandas、scikit-learn完成清洗与建模,服务层由Django REST Framework提供接口,展示层由Vue和ECharts完成页面渲染。
数据从CSV读入后先做预处理,训练脚本独立运行并保存模型文件;Django后端负责登录认证、数据查询、训练触发和预测推理;前端通过axios调用后端API,把训练指标和历史趋势展示在页面上。这个架构的好处是每一层都能单独测试,答辩时可以按层逐步展示。
3. 数据是这类项目的命根子:采集与预处理
3.1 数据结构与字段设计
我的数据以月为单位,时间范围覆盖2016年1月到2024年12月,共整理出100多条月度记录。每条记录包含目标字段和多个影响因子。字段结构大致如下:
- sales:当月新能源乘用车销量(万辆)
- oil_price:全国92号汽油月均价(元/升)
- charging_piles:公共充电桩保有量(万台)
- battery_capacity:当月动力电池装机量(GWh)
- consumer_index:消费者信心指数
- subsidy_policy:补贴政策虚拟变量,0表示无补贴,1表示有补贴
- month_index:月份编号,从0开始递增
数据来源在不同平台上是分散的,需要手动对齐口径和时间轴。如果实在拿不到完整匹配的数据,一个可接受的做法是基于公开历史趋势构造结构合理的研究用模拟数据,但论文里必须明确写清楚口径,不能假装成官方统计。我最终使用的是公开可查的月度销量序列,配合行业报告里的价格与保有量数据,整理成统一的CSV文件。
3.2 缺失值与异常值处理
月度数据最烦的就是某个月因为统计口径调整出现缺失。我的处理原则是:连续型字段缺失少于5%时用相邻月份线性插值补齐;虚拟变量缺失直接补0;极端的异常值如果有明确口径变动说明,就在论文里写明并做边界截断。
处理代码大概是这样的:
import pandas as pd df = pd.read_csv("ev_sales.csv") print(df.isnull().sum()) for col in ["oil_price", "charging_piles", "battery_capacity", "consumer_index"]: if df[col].isnull().sum() > 0: df[col] = df[col].interpolate(method="linear") for col in df.columns: mean = df[col].mean() std = df[col].std() df[col] = df[col].clip(mean - 3 * std, mean + 3 * std)这里值得说明的是,我没有用删除缺失行的方法,因为月度样本本身不多,删一条就少一条。插值虽然会引入一点平滑偏差,但对于回归建模来说,比缺数据导致模型收敛不稳定要好得多。
3.3 相关性分析:先把变量间关系看明白
做模型之前,一定要先做Pearson相关系数分析。我画出的相关矩阵热力图显示:充电桩保有量与销量的相关系数超过0.9,这显然符合直觉,基础设施是销量的支撑条件;油价与销量是正相关,油价高了,用户转向电车的动力更强;消费者信心指数的影响相对偏弱。
这个环节的产出可以直接放进论文的“数据探索”章节。相关性热力图比任何文字都直观,答辩时老师看到你有做数据探索这一步,印象分会明显提高。同时它也帮我筛掉了一批冗余变量,为后面的模型稳定性打下了基础。
3.4 训练集测试集划分:避免数据泄露
时间序列数据不能用随机打乱的方式切分训练集和测试集。这一点务必注意,如果随机打乱,模型会“偷看”未来数据,测试集上的精度会虚高,答辩时一旦被问“你怎么切的数据”,答错就会成为致命伤。
正确的做法是按时间顺序切分,前80%个月份做训练,后20%个月份做测试:
train_size = int(len(df) * 0.8) train = df.iloc[:train_size] test = df.iloc[train_size:] X_train = train.drop(columns=["sales", "date"]) y_train = train["sales"] X_test = test.drop(columns=["sales", "date"]) y_test = test["sales"]这样处理后,模型的测试集指标反映的是真正“未来预测”能力,而不是在历史数据里自嗨。
4. 多元线性回归:从公式到能跑通的代码
4.1 数学表达式与业务含义
多元线性回归的数学表达式是 y = b0 + b1x1 + b2x2 + ... + bn*xn + ε。落到本项目里,就是把月度销量表示为油价、充电桩数量、消费者信心指数、补贴政策等因素的加权求和,权重就是回归系数。
这个模型的业务价值在于系数直接可解读。比如充电桩保有量的回归系数,可以解释成“在其他条件不变时,充电桩保有量每增加1万台,月度销量平均增加约多少万辆”。老师问“你的模型告诉了我们什么”,就可以从这个角度讲,这是树模型和神经网络都很难给出来的答案。
4.2 最小二乘法:模型到底在解什么
线性回归的标准求解方式是最小二乘法,目标是找到一组参数,使得所有样本真实值与预测值之间的残差平方和最小。数学解为 beta = (X^T X)^(-1) X^T y,虽然不需要在代码里手写矩阵求逆,但答辩时老师极大概率会问原理,至少要能讲明白“最小化误差平方和”的基本思想。
如果连解析解都忘了,就直接说“通过最小化预测值与真实值的平方误差来求解权重”,这也是个完整回答。关键是别把线性回归说成“一种神经网络”,这种表述在答辩里很减分。
4.3 模型评价指标怎么选
我使用了三个评价指标:R²决定系数、均方误差MSE、平均绝对误差MAE。R²越接近1说明模型解释能力越强;MSE对大误差惩罚更重,适合发现是否有极端预测偏差;MAE最直观,直接告诉我们“平均每个月预测偏差多少万辆”。
这三个指标的数值和对应的“预测值vs真实值”折线图,是论文和答辩PPT里的核心材料。截一张测试集预测曲线对比图,配上指标表,整段模型评价就非常有说服力。
4.4 训练代码与实测结果
模型训练代码用scikit-learn的LinearRegression,保存模型用joblib:
from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error import joblib model = LinearRegression() model.fit(X_train, y_train) train_pred = model.predict(X_train) test_pred = model.predict(X_test) print("R2 train:", r2_score(y_train, train_pred)) print("R2 test:", r2_score(y_test, test_pred)) print("MSE test:", mean_squared_error(y_test, test_pred)) print("MAE test:", mean_absolute_error(y_test, test_pred)) joblib.dump(model, "ev_sales_model.pkl")我实际跑出来的结果,测试集R²大约0.90,MAE约0.8万辆。刚开始看到这个数字我反而有点警惕,担心是不是数据泄露,后来确认了是按时间切分,主要原因是选取的因子与销量确实强相关。这里要提醒一句:如果R²做出来是0.99甚至1.0,先怀疑是否把销量本身的滞后变量或者销量拆分的子项当成了特征,这种“假性高精度”答辩一查就穿。
4.5 多元共线性是最大的坑
这个项目里我踩过最大的技术坑是多重共线性。充电桩保有量和动力电池装机量都在随时间同步上涨,两者相关系数接近0.95,放进模型后部分回归系数的符号不符合常识——比如某个一直正相关的变量系数突然变成负数。
原因是自变量之间高度相关,导致系数估计极不稳定。我的处理方式分两步:先计算方差膨胀因子VIF,把VIF大于10的特征视为强共线性;再在业务逻辑上做取舍,最终保留充电桩保有量,剔除了动力电池装机量。剔除后系数符号恢复合理,模型解释也清晰了很多。这段排查过程我完整写进了论文的“模型优化”小节,答辩时反而成了亮点,因为它是真实存在的分析问题,不是网上抄来的套路。
5. 后端:Django 训练与预测接口的设计细节
5.1 项目结构与模块划分
Django项目我用的是4.x版本,配合Django REST Framework开发接口。目录结构大概长这样:
ev_analysis/ manage.py ev_sales/ models.py views.py urls.py services/model_service.py frontend/ datasets/ev_sales.csv models/ev_sales_model.pkl把训练逻辑和视图逻辑分开是我觉得比较重要的一个习惯。模型文件只在服务启动时加载一次,不要放在视图函数里每次请求都load,否则并发稍微上来页面延迟就会非常明显。
5.2 接口分组设计
后端接口按功能分成五组:用户登录注册、历史销量查询、特征因子查询、模型训练触发、销量预测。登录接口走Django自带的认证体系,前端登录后拿到token,后续请求在Header里带上。
历史销量查询返回按月排列的销量列表,前端画趋势柱状图;特征因子查询返回各变量的数值序列,前端画多折线图;模型训练接口比较关键,它会在线读取当前数据集重新训练,并把R²、MSE、MAE返回给前端刷新显示;预测接口接收表单特征,返回模型预测的销量值。
5.3 预测接口的核心实现
预测接口的代码量其实很小,但格式和异常处理必须严谨:
import joblib from rest_framework.decorators import api_view from rest_framework.response import Response model = joblib.load("models/ev_sales_model.pkl") @api_view(["POST"]) def predict(request): data = request.data try: oil_price = float(data["oil_price"]) charging_piles = float(data["charging_piles"]) consumer_index = float(data["consumer_index"]) subsidy_policy = int(data["subsidy_policy"]) except (KeyError, ValueError): return Response({"error": "参数错误"}, status=400) features = [[oil_price, charging_piles, consumer_index, subsidy_policy]] result = model.predict(features)[0] return Response({"predicted_sales": round(float(result), 2)})前端调用时往这个接口POST一组JSON数据,例如:
{ "oil_price": 7.8, "charging_piles": 260.5, "consumer_index": 106.5, "subsidy_policy": 1 }返回结果示例:
{ "predicted_sales": 78.35 }单位统一是万辆。异常处理必须单独考虑,前端清空表单时可能传空字符串,后端如果不做校验,float("")会直接抛错导致接口返回500。
5.4 数据库表设计
数据库用MySQL,通过Django ORM管理。核心表包括:用户表User、历史销量表SalesRecord、特征因子表FactorRecord、预测记录表PredictRecord。
PredictRecord表特别值得说明,它保存每一次前端预测请求的输入特征和预测结果,字段包括id、oil_price、charging_piles、consumer_index、subsidy_policy、result、created_at。前端预测页下方会展示一个历史记录表格,每次预测完成自动刷新。这个功能虽然逻辑不复杂,但体现的是系统闭环和可用性,答辩演示时现场预测一条,再打开历史列表看到刚才的记录,比单纯一个“吐出数字”的Demo更有记忆点。
6. 前端:Vue 页面与后端联调的实操记录
6.1 页面功能与布局
前端页面划分四个模块:登录页、数据看板、模型训练页、销量预测页。
数据看板是重点页面,用ECharts绘了三张图:历史销量柱状趋势图、特征因子折线图、相关矩阵热力图。布局用Element Plus的栅格系统,左侧放图表,右侧放统计摘要卡片,展示年平均增长率、最高月度销量等关键数字。这个页面的定位是“让评委30秒内看到数据价值”。
模型训练页展示训练中状态和评价指标,前端点击“重新训练”后,轮询训练状态接口,完成之后自动刷新三个指标数字。这里的加载状态和完成反馈很重要,否则用户不知道训练是否还在跑。
销量预测页是核心交互页,提供四个表单输入项:油价、充电桩保有量、消费者信心指数、补贴政策下拉框。点击预测后显示结果数字,同时把当次输入和结果追加到下方历史记录表格中。
6.2 axios请求封装与跨域处理
前端所有请求统一封装在一个request模块里,baseURL设为后端地址。开发阶段最麻烦的是跨域,Django需要安装django-cors-headers并在settings.py里配置允许跨域来源。毕设演示阶段直接放开所有来源,等真正上生产再收紧。
请求封装示例:
import axios from "axios"; const request = axios.create({ baseURL: "/api", timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Token ${token}`; } return config; }); export default request;统一的请求入口还有个好处:如果后端返回401,可以在响应拦截器里统一跳转登录页,不用在每个页面里重复写鉴权逻辑。
6.3 联调阶段的高频报错
第一类是数据格式问题。Django返回的Decimal类型或者pandas的numpy.float64没法直接被JSON序列化,必须在视图里统一转float,或者配置自定义的JSONEncoder,否则前端拿到的是空响应。
第二类是日期格式问题。前端传“2024-01”这种字符串,后端解析容易出问题,统一改成ISO格式“2024-01-01”最省心。
第三类是Content-Type问题。axios的POST请求如果不显式设置请求头为application/json,后端request.data可能拿不到数据。遇到这种诡异报错时,我的排查习惯是先在后端打印request.data看请求体长什么样,再决定是改前端还是改后端,不要只看前端报错瞎猜。
6.4 图表渲染的细节
ECharts的option在数据更新后不会自动重绘,必须拿到新数据后调用chart.setOption(option)。如果新旧数据结构差异较大,还要加第二个参数true做合并,否则图上会出现残留的旧数据。另一个容易踩的坑是多图表页里用Tab切换,容器在隐藏状态下宽度计算会出错,图表会被压缩变形,解决方案是在Tab切换事件里调用chart.resize()重新计算尺寸。这些细节不大,但直接影响演示观感。
7. 答辩实战:演示流程与评委提问应对
7.1 演示流程设计
答辩时不要一上来就打开页面乱点,要按讲故事的方式走。我的演示路径是:登录进入系统,先停一停,说明你的数据来源、时间范围和数据量;然后打开数据看板,展示历史销量趋势和特征因子趋势,顺势讲数据清洗时处理过哪些问题;再切到模型训练页,现场触发一次训练,让指标真实跑出来;最后到预测页输入一组特征值,把预测结果的业务含义讲清楚。
整套演示控制在6到8分钟,重点放在数据清洗逻辑和模型优化过程上,这两块比页面炫不炫更能体现工作量。如果担心现场网络,提前把前端build后的文件用Django的静态文件机制托管,保证只依赖本机服务就能跑起来。
7.2 评委高频提问与回答思路
整理几个被问概率极高的问题,每一个都有对应的回答逻辑。
第一类:“为什么用多元线性回归,不用更复杂的模型?”回答核心是强调可解释性和数据特点。介绍当前数据在月度粒度下与因子呈近似线性关系,同时业务分析需要了解具体因素的量化影响,因此线性回归在保证精度的同时提供了更清晰的因素解读,这一点比黑盒模型更适合销量分析场景。
第二类:“数据怎么来的?样本量够吗?”要准确说出数据来源、覆盖周期、样本数量,以及为什么按月度聚合。千万别敷衍说“网上找的”,评委一定会追问数据可信度和清洗过程。
第三类:“预测结果怎么验证?”紧扣R²和MAE,说明测试集是按时间序列划分的,不存在数据泄露,并展示预测值与真实值的对比曲线。
第四类:“特征之间有共线性怎么办?”把VIF计算和系数符号异常这一整套排查流程讲出来,这一段如果你真的做过,回答会非常有底气。
7.3 论文与图表准备
论文在需求分析前加了一章“数据探索”,把相关性热力图、销量趋势、特征分布图全放进去。模型章节除了公式推导,还放了一页参数估计结果表,列出每个特征的回归系数、标准误和显著性水平,这样做会使论文看起来更规范。
措辞上建议严谨一些,少用“精准预测”这种过度承诺表述,改用“在历史数据上取得一定的拟合精度,可为销量走势提供参考”这类说法,在形式上更稳妥,也不容易抓漏洞。
8. 项目做完之后回头看的经验
最后说几点做完这个项目之后才真正理解的体会。
如果一个毕设项目时间紧,与其铺开多个半成品模块,不如优先保证一条完整链路的顺畅跑通。数据看板、训练、预测、历史记录,这个闭环通了,整个系统的完成度就有了;如果五个页面里有三个靠假数据撑场面,评委一问就散架。
模型文件pkl不要跟代码混在一起往仓库根目录塞,单独放进models目录里并加入.gitignore。有的模型文件几十上百MB,留在版本控制里会让后续clone变得非常慢。
本地开发时Django的DEBUG模式保持开启,否则静态文件和报错信息会带来一堆额外问题。如果本地演示一切正常,就不要在答辩前一晚折腾服务器部署,我见过好几个同学把本来稳定的项目葬送在部署环节。
最想说的是:第一次跑通从数据清洗到前端展示的整条链路时,那种“数据在系统里流动起来”的感觉,比背十遍讲解词都管用。多花时间亲手处理真数据、亲手调一次模型、亲手联调一个接口,答辩站上台的时候,自然就不虚了。