1. 项目全景拆解:这个毕设到底在做什么?
1.1 选题价值与核心亮点
先说结论:随机森林 + Django + Vue这个组合做空气质量指数预测系统,是一套性价比极高、答辩上限也很高的毕业设计选题。它既不是纯算法项目,也不是纯 CRUD 管理系统,而是把“机器学习模型训练”和“Web 可视化平台”串成了完整闭环——用户在浏览器里看到的每一个预测曲线、每一张空气质量等级卡片,背后都连着真实的历史污染物数据和训练好的回归模型。
这个项目能解决的问题很明确:基于过去一段时间的气象数据、污染物浓度数据,预测未来某个时刻的 AQI(Air Quality Index,空气质量指数),并实时展示当前空气质量等级、主要污染物贡献占比、趋势走向。它适合两类人参考:一类是计算机/大数据专业的学生,想做一个能同时体现算法能力和工程能力的毕设;另一类是已经工作但想把机器学习应用层打通的开发者,拿这个项目当练手模板。
我最喜欢这个选题的地方在于它“够具体”。空气质量和每个人相关,数据公开可获取,业务逻辑不绕弯,但又有足够的技术纵深可以挖掘——特征怎么构造、缺失值怎么补、模型怎么调参、前端怎么把高维数据可视化,每一环都有话可讲。答辩时评委随便问一个细节,你都能接得住。
1.2 谈点实在的:随机森林和“深度学习”到底是什么关系
这里必须先纠一个很多人会踩的坑。题目里写“大数据深度学习算法”,但随机森林本身不属于深度学习。随机森林是集成学习(Ensemble Learning)里的 Bagging 类算法,基学习器是决策树;深度学习指的是多层神经网络自动学习特征表示,典型代表是 CNN、RNN、Transformer 这类结构。
很多学生做毕设的时候容易把这两个词混用,甚至直接把“机器学习”笼统叫成“深度学习”,答辩时被老师一问就容易卡壳。我的建议是:在论文和 PPT 里用词严谨一点,写清楚“随机森林是机器学习中的集成学习算法”即可。如果你确实想让“深度学习”三个字站得住脚,可以考虑做模型对比实验——用随机森林和 MLP(多层感知机)分别训练同一个数据集,然后用图表对比 R²、MAE 等指标,这样深度学习和随机森林就都覆盖到了,答辩时既有的聊,也显得你知识面广。
至于“大数据”,这个概念在这个项目里更像是“大数据分析与可视化”的教学场景,而不是 Hadoop/Spark 那套分布式技术栈。这个项目的数据量通常在几万到几十万行量级,单机跑随机森林没有问题。你可以在论文里把“大数据”定位成“多维度环境监测数据的大规模分析与挖掘”,而不是真的去搭一个分布式集群——既省时省力,逻辑上也合理。
1.3 技术选型为什么是Django+Vue
前后端分离是现在 Web 项目的绝对主流。Django 负责后端 API 和数据持久化,Vue 负责前端页面渲染和交互,两者通过 JSON 接口通信。这个组合的优势有三点:
第一,Django 自带 ORM 和 Admin 后台,开发效率极高。空气质量数据天然适合关系型数据库存储,Django 的 models 写起来快,Admin 还能白嫖一个数据管理后台,毕设演示时非常加分。
第二,Vue 的组件化开发适合做数据可视化看板。预测系统的核心交互是“选城市-看曲线-看等级”,这本质上是组件状态管理的问题,Vue 的响应式数据流配合 ECharts 图表库,能非常舒服地把数据映射成视图。
第三,Python 生态直接贯通。Django 是 Python 框架,随机森林用 scikit-learn 训练,两者之间不需要跨语言桥接,模型保存、加载、预测的代码写起来一气呵成。如果换 Java Spring Boot,你还要用 PMML 或者单独起一个 Python 微服务来部署模型,平白增加复杂度。
关于前端工具链,我建议用 Vue 3 + Vite。相比 Vue 2 的 Webpack 配置,Vite 的开发服务器启动速度是秒级的,热更新也快,能省下大量等待时间。
2. 核心算法拆解:随机森林如何预测AQI
2.1 随机森林原理与“为什么选它”
随机森林回归的本质,是训练多棵决策树,然后让所有树对预测结果取平均。每一棵决策树都在原始数据集的随机抽样子集上训练,同时每次节点分裂时只看随机挑选的部分特征来寻找最优切分点。这两个“随机”叠加在一起,使得每棵树都长得不一样,单棵树可能过拟合,但一森林平均下来,方差就被压低了。
很多人把随机森林和决策树搞混,其实核心差异就在这。单棵决策树深度一大就特别容易过拟合——训练集上指标漂亮,一换数据就崩。随机森林通过 Bagging(Bootstrap Aggregating)机制做样本扰动,又通过随机特征选择做特征扰动,双管齐下控制过拟合。打个比方:一个人预测明天空气质量可能凭感觉走偏,但一百个背景各异的人投票取平均,结果就会稳健得多。
AQI 预测这个场景非常适合随机森林。第一,空气质量数据特征维度中等(七八个污染物浓度加气象特征),没有到图像、文本那种需要深度网络自动提特征的程度;第二,AQI 和各污染物浓度之间存在较强的非线性关系,决策树不用像线性回归那样手动构造交互项,树模型天然能切分出复杂模式;第三,训练快、调参少、可解释性强——scikit-learn 里可以直接输出特征重要性,答辩时讲“PM2.5 和 PM10 是影响 AQI 的最主要特征”时是有数据支撑的。
2.2 特征工程:AQI预测到底喂什么数据
AQI 预测不是直接把过去几天的 AQI 扔给模型就完事。一般会构造三部分特征。
污染物浓度特征:PM2.5、PM10、SO₂、NO₂、CO、O₃ 这六项是计算 AQI 的直接原料。注意,AQI 不是六项的平均值,而是先分别计算每一项的 IAQI(Individual Air Quality Index,分指数),再取最大值。所以模型学到的核心映射关系就是这些浓度值到分指数再到总分指数的计算逻辑。随机森林对这种固定函数关系的学习能力很强,只要数据质量没问题,这部分特征给足就能保证模型底线。
气象特征:温度、湿度、风速、风向、气压、降水量,这些特征反映污染物扩散条件。比如风速大、降水多的时候空气质量通常好,逆温层存在时污染物容易累积。加入气象特征能让模型学习到污染物浓度之外的因果线索。
时间特征:小时、星期几、是否节假日、是否采暖季。空气质量有明显的周期性——一天之内早晚高峰污染较重,秋冬采暖季比夏季差。把时间维度编码成特征,模型就能捕捉到这些周期性规律。
有一个关键细节:预测时刻的污染物浓度无法直接作为输入特征。假设你要预测明天上午 10 点的 AQI,可以用今天上午 10 点、昨天上午 10 点的数据作为滞后特征,但不能直接把“明天 PM2.5 浓度”喂进去,否则就是数据泄漏。正确做法是用过去 N 小时的滑动窗口统计量——均值、最大值、变化趋势,作为预测依据。
2.3 模型评估指标与调参实践
回归任务评估不看准确率,看以下三个指标:
R²(决定系数):模型解释了多少比例的数据方差,越接近 1 越好。一般 AQI 预测做到 0.85 以上就算不错。
MAE(平均绝对误差):预测值和真实值差值的绝对值取平均,单位是 AQI 指数。MAE 在 15-20 左右说明预测偏差不到一个空气质量等级,已经具备参考价值。
RMSE(均方根误差):对大误差更敏感,因为先平方再开方,会把偏差大的样本暴露出来。如果 RMSE 明显大于 MAE,说明存在少量预测特别离谱的样本。
随机森林的调参优先级是这样的:先定n_estimators(树的数量),从 100 开始,画学习曲线看 RMSE 是否收敛;再调max_depth(最大深度),限制深度能有效防止单棵树过拟合;最后微调min_samples_split和min_samples_leaf来控制树的细化程度。我把常用参数范围整理在下面:
| 参数 | 常调范围 | 作用 |
|---|---|---|
| n_estimators | 100 – 500 | 树越多越稳,但超过一定值收益递减 |
| max_depth | 10 – 30 或 None | 限制树深度,值越小越保守 |
| min_samples_split | 2 – 10 | 分裂所需最小样本数,增大可防过拟合 |
| min_samples_leaf | 1 – 5 | 叶节点最小样本数,增大可提高泛化性 |
| max_features | auto / sqrt | 每次分裂随机选的特征数,默认即可 |
| random_state | 42 | 固定随机种子,保证可复现 |
实操中我一般先固定其他参数将n_estimators调到损失曲线平台期,再去网格搜索max_depth和min_samples_split。有时候你以为是小参数,结果对结果的影响反而很大。
2.4 一段可以直接跑的模型训练代码
下面是训练随机森林回归模型的核心代码。这个代码片段可以直接放在 Django 项目的ml_train.py脚本文件里,独立于 Web 服务运行:
import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error df = pd.read_csv("aqi_train_data.csv") feature_cols = [ "pm25_lag1", "pm10_lag1", "so2_lag1", "no2_lag1", "co_lag1", "o3_lag1", "temperature", "humidity", "wind_speed", "pressure", "hour", "day_of_week", "is_heating_season" ] X = df[feature_cols] y = df["aqi"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False ) model = RandomForestRegressor( n_estimators=300, max_depth=20, min_samples_split=4, min_samples_leaf=2, random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("R2:", r2_score(y_test, y_pred)) print("MAE:", mean_absolute_error(y_test, y_pred)) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False)) # 特征重要性输出 for name, imp in zip(feature_cols, model.feature_importances_): print(f"{name}: {imp:.4f}")注意shuffle=False,时间序列数据不能像普通分类任务那样随机打乱再划分,否则会出现“拿明天预测今天”的数据泄漏,测试集永远“看起来很美”,一到真实场景就翻车。这是时间序列预测里最容易犯的错误,也是答辩时老师最爱挖的坑。
3. 后端Django:从ORM到模型服务化
3.1 项目骨架与数据库模型设计
Django 的后端工程建议按应用拆分。我习惯建三个 app:datasets负责原始数据和特征数据的管理,prediction负责调用模型、产出预测结果,visualization负责向前端聚合输出看板数据。
数据库模型方面,空气质量数据适合拆成几张表。核心的数据表结构我建议这样设计:
# apps/datasets/models.py from django.db import models class City(models.Model): name = models.CharField(max_length=50, unique=True) code = models.CharField(max_length=20) class PollutantRecord(models.Model): city = models.ForeignKey(City, on_delete=models.CASCADE) timestamp = models.DateTimeField(db_index=True) pm25 = models.FloatField(null=True) pm10 = models.FloatField(null=True) so2 = models.FloatField(null=True) no2 = models.FloatField(null=True) co = models.FloatField(null=True) o3 = models.FloatField(null=True) aqi = models.IntegerField(null=True) temperature = models.FloatField(null=True) humidity = models.FloatField(null=True) wind_speed = models.FloatField(null=True) pressure = models.FloatField(null=True) class Meta: unique_together = ("city", "timestamp") class PredictionResult(models.Model): city = models.ForeignKey(City, on_delete=models.CASCADE) timestamp = models.DateTimeField(db_index=True) predicted_aqi = models.FloatField() actual_aqi = models.FloatField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True)这里有几个设计细节值得说。第一,PollutantRecord里把 AQI 和污染物浓度放在同一张表,因为建模时需要同时用到它们;第二,co字段单位是 mg/m³,其他污染物是 μg/m³,单位不统一在计算 IAQI 时要小心换算,建模时建议统一做归一化;第三,PredictionResult单独建表,把预测值和真实值(未来回填)放在一起,方便后续评估模型在真实场景中的表现。
3.2 模型训练侧与预测侧的分离
很多人把训练和预测写在一个视图函数里,这个习惯很不好。随机森林训练需要完整数据集,可能要花几秒到几十秒,每次请求都重新训练,系统直接瘫掉。正确思路是:训练离线做,预测在线做。
训练侧用脚本完成:从数据库读取历史数据,构造特征集,训练模型,然后用joblib.dump把模型以二进制文件形式保存到项目目录下的models/文件夹里。
# ml_train.py(项目根目录脚本) import joblib # ... 上述随机森林模型训练代码 ... joblib.dump(model, "models/aqi_rf_v1.pkl") joblib.dump(feature_cols, "models/aqi_feature_cols.pkl")预测侧在 Django 视图里加载已保存的模型文件,做特征预处理和推理:
# apps/prediction/views.py import joblib import numpy as np from django.http import JsonResponse from django.utils import timezone from datetime import timedelta from ..datasets.models import PollutantRecord model = joblib.load("models/aqi_rf_v1.pkl") feature_cols = joblib.load("models/aqi_feature_cols.pkl") def predict_next(request, city_id): city_id = int(city_id) now = timezone.now() lag1_records = PollutantRecord.objects.filter( city_id=city_id, timestamp__date=(now - timedelta(days=1)).date() ) if not lag1_records.exists(): return JsonResponse({"code": 400, "msg": "no enough data"}, status=400) # 构造特征向量,注意特征顺序必须与训练时一致 latest = lag1_records.latest("timestamp") features = np.array([[ latest.pm25, latest.pm10, latest.so2, latest.no2, latest.co, latest.o3, latest.temperature, latest.humidity, latest.wind_speed, latest.pressure, now.hour, now.weekday(), 1 if (now.month in (11, 12, 1, 2, 3)) else 0 ]]) pred = model.predict(features)[0] result = PredictionResult.objects.create( city_id=city_id, timestamp=now, predicted_aqi=float(pred) ) return JsonResponse({ "code": 200, "data": { "city_id": city_id, "timestamp": now.strftime("%Y-%m-%d %H:%M"), "predicted_aqi": round(float(pred), 1), "level": aqi_level(float(pred)), } })这个过程中最容易出错的就是特征顺序不一致。训练时的特征列是pm25, pm10, so2, no2, co, o3, temperature...,预测时也必须按照完全相同的顺序排列,否则模型拿到的特征语义错位,预测结果毫无意义。所以我推荐把feature_cols列表和模型一起joblib.dump保存,预测时直接读取,避免手敲特征顺序导致出错。
3.3 API接口设计与前端数据契约
前后端分离项目必须定义清楚接口契约。我用 Django REST Framework(DRF)写接口,因为它自带序列化器和分页,省去很多手工 JsonResponse 的麻烦。核心接口我设计成这样:
| 接口名 | 路径 | 方法 | 用途 |
|---|---|---|---|
| 城市列表 | /api/cities/ | GET | 返回所有支持的城市 |
| 历史数据 | /api/cities/{id}/history/ | GET | 返回污染物浓度时序数据 |
| 预测结果 | /api/cities/{id}/predict/ | POST | 触发预测,返回预测 AQI |
| 模型指标 | /api/model/metrics/ | GET | 返回 R²、MAE 等指标 |
接口返回统一用{code, message, data}包裹,code为 200 表示成功,其他为失败。前端拿到非 200 状态码时直接弹错误提示,不解析 data。这样做的目的是把错误处理逻辑统一收口到拦截器里,避免每个页面重复写try-catch。
这里要特别提醒一下 DRF 配 CORS 的问题。开发阶段前端运行在http://localhost:5173,后端在http://localhost:8000,端口不同即跨域。需要在settings.py里配置django-cors-headers:
INSTALLED_APPS = [ ... "corsheaders", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", # 要在 CommonMiddleware 之前 ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ]跨域问题不解决,前端会一直报 “Blocked by CORS policy”,而且报错信息容易误导你以为是接口挂了。这个坑几乎必踩,先把它配好再写业务代码。
4. 前端Vue:可视化看板与交互细节
4.1 工程搭建与项目目录规划
前端我用 Vue 3 + Vite + Vue Router + Pinia + Axios + Element Plus + ECharts,这套组合是目前 Vue 生态最主流的选择,不用自己造轮子。
创建项目:
npm create vite@latest aqi-frontend -- --template vue cd aqi-frontend npm install npm install axios vue-router@4 pinia element-plus echarts依赖装完之后,把 vite.config.js 里的server.proxy配置好。开发环境下让 Vite 把/api请求代理到 Django,这样前端代码里统一写相对路径/api/...,不需要关心后端地址是 8000 还是别的端口,也顺带绕开了 CORS:
// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } })项目目录里,src/api放接口封装,src/views放页面组件,src/components放公共组件。我用 Pinia 管理城市维度和预测结果的全局状态,这样从“城市选择页”跳到“看板页”时,选中的城市信息不会丢失。
4.2 页面规划与核心交互逻辑
整个前端规划了三个核心页面。
首页概览页:进入系统后的默认页,展示全国(或选定区域)各城市今天的实时 AQI 排名、空气质量等级分布饼图、整体变化趋势折线图。这页的数据来自历史数据接口,前端拿到数据后按 AQI 从低到高排序,颜色按等级映射——绿色优、黄色良、橙色轻度污染、红色中度污染、紫色重度污染、褐红色严重污染。
城市详情页:路由形如/city/:id,展示某一个城市近 14 天的 AQI 趋势曲线,下面用多个折线图分别展示 PM2.5、PM10、SO₂、NO₂、CO、O₃ 的浓度变化,再配一个雷达图展示各污染物相对浓度水平。这一页是信息量最大的,也是答辩演示的主角。
预测对比页:调用预测接口,展示“未来 24 小时 AQI 预测”的结果——预测值、对应空气质量等级、置信度提示。我建议把模型在历史数据上的预测值和真实值画在同一张图里做对比,让用户直观看到模型的准确性。这个页面最能体现随机森林模型的价值。
组件间的数据传递我统一走 Pinia store,不在路由参数里塞复杂对象。路由只传 cityId,其他数据全部异步拉取。这样刷新页面后还能正常恢复状态,不会出现“路由有参数但 store 里没数据”的尴尬。
4.3 ECharts可视化如何接真实数据
ECharts 是前端可视化的核心。画图本身不难,难的是怎么把接口数据转换成 ECharts 期望的数据结构。我封装了一个fetchHistory方法:
// src/api/index.js import axios from 'axios' const client = axios.create({ baseURL: '/api', timeout: 10000 }) export function fetchCityHistory(cityId, days = 14) { return client.get(`/cities/${cityId}/history/`, { params: { days } }).then(res => res.data.data) }页面组件里拿到返回的数组后,按照 ECharts 的 xAxis 和 series 结构做映射。核心代码:
// src/views/CityDetail.vue import * as echarts from 'echarts' import { fetchCityHistory } from '../api' import { onMounted, ref } from 'vue' const aqiChartRef = ref(null) async function loadChart() { const data = await fetchCityHistory(cityId, 14) const chart = echarts.init(aqiChartRef.value) chart.setOption({ title: { text: '近14天AQI变化趋势', left: 'center' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.map(item => item.timestamp.slice(5, 10)) }, yAxis: { type: 'value', name: 'AQI指数' }, series: [{ name: '实际AQI', type: 'line', smooth: true, areaStyle: { opacity: 0.2 }, data: data.map(item => item.aqi), markLine: { data: [ { yAxis: 50, lineStyle: { color: '#e6c300' } }, { yAxis: 100, lineStyle: { color: '#ff8c00' } } ] } }] }) } onMounted(loadChart)两个细节经验。第一,chart实例要在组件卸载时dispose,否则在 Vue 路由切换时图表会占用内存不释放,页面开久了会卡顿;第二,ECharts 在容器尺寸变化时不会自动重绘,如果页面有侧边栏折叠这类布局变化,需要在nextTick里调用chart.resize(),否则图表会出现显示不全的问题。
5. 数据从哪来:数据获取与预处理实录
5.1 公开数据集和爬虫方案怎么选
空气质量预测项目的数据获取,有三个可选路径。
最省事的是用公开数据集。中国环境监测总站发布的全国城市空气质量数据、UCI 机器学习库里的 Air Quality 数据集,都是可以拿来直接用的。学术公信力强,来源写清楚,答辩时能站住脚。
第二是爬虫方案,爬取空气质量历史数据网站。Python 用requests + BeautifulSoup或者Selenium都能实现。这个方案的优点是数据量可以做得很大、覆盖的城市范围广;缺点是有一定的编码和运维成本,网站页面结构一变爬虫就崩。我在工程里会写一个通用的采集类,用 CSS 选择器解析列表页和详情页,并把采集到的数据写入 Django 的PollutantRecord表。
第三种方案是模拟数据兜底。如果目标爬取站点临时挂了,或者某个测试阶段不要求真实数据,可以使用numpy生成带噪声的模拟数据——设置基础浓度水平、加周期性波动和随机噪声,生成时间序列。这个方法在开发联调阶段特别有用,能让前后端开发不被数据问题阻塞。
5.2 缺失值处理和环境数据对齐
空气质量监测数据有个痛点:缺失和异常太多。设备维护、网络中断、极端天气,都会导致某一天的某个污染物浓度没有记录。如果直接把这个样本丢给模型,特征矩阵就会出现 NaN,训练直接报错。
我的处理策略分三步。第一步,删除所有特征列同时缺失超过 30% 的样本,这种样本信息量太低,补出来的也是噪声。第二步,对单特征缺失用最近时刻的有效值进行前向填充,因为污染物浓度在相邻时间点的取值有连续性,用后向填充反而会引入未来信息;第三步,对还没补上的极个别样本显示插值,多数情况下几行插值不影响模型整体效果。
数据对齐是另一个容易出问题的地方。污染物浓度数据发布通常是每小时一条,但气象数据可能每天固定几个时刻,如果你们是拿的气象站数据,就需要把时间戳统一到同一粒度。实际做法是把气象数据按小时做了重采样:风速取整小时平均、温度取整小时平均、降水量取累计值。
5.3 训练集/验证集划分的隐藏陷阱
前面提到时间序列数据不能随机 shuffle,我再把这个坑展开讲透。假设你用train_test_split(X, y, test_size=0.2, random_state=42)做默认划分,数据会被随机打乱,模型看到的训练集里可能包含未来时间的数据,测试集里反而包含过去时间的数据。由于污染物浓度具备时间连续性,模型表面上拿到了 0.98 的 R²,实际部署后预测能力惨不忍睹。
正确做法有两个。第一是保留时间顺序划分:前 80% 时间作为训练集、后 20% 作为测试集,用shuffle=False参数实现。第二个更好的做法是使用TimeSeriesSplit做交叉验证,它在不断扩展训练集的同时保持测试集永远在未来。用这个方式评估出的指标才代表模型的真实泛化能力。这部分内容我建议在论文里重点写,答辩时说出来非常加分——几乎所有时间序列预测项目的数据泄漏问题都在这里。
另一个隐患是特征的时间对齐。如果特征是前一天的污染物浓度,而标签是当天的 AQI,两者在数据库里查询拼接时,必须保证PollutantRecord中每条记录的时间戳精确对齐。我曾经因为时区设置不一致,导致某城市所有特征整体偏移了一个小时,模型 RMSE 高到离谱,排查了半天才在数据库里发现了时区错乱。Django 项目里TIME_ZONE = 'Asia/Shanghai'和USE_TZ = True必须配套配好,取数据时统一用本地时间。
6. 常见问题与排查技巧实录
6.1 Django+Vue联调时的CORS和端口问题
联调环节我遇到最多的问题就是 CORS。明明后端接口用浏览器直接访问没问题,前端axios一请求就报跨域错误。常见原因:django-cors-headers中间件位置不对,或者CORS_ALLOWED_ORIGINS忘记加端口号。解决办法是先在浏览器直接访问http://localhost:8000/api/cities/,确认后端正常,再检查中间件配置是否在CommonMiddleware之前。还有一个小细节:开发时如果用npm run dev起的 Vite 服务是 5173 端口,如果改了端口要同步更新配置。
另外,如果你把 Django 配在 8000 端口、Vue 配在 5173 端口,但同一个网络环境下有其他服务端口冲突,启动时会有OSError: [Errno 98] Address already in use的报错。直接用lsof -i :8000查占用进程再结束即可,不用重装饰端口。因为两个端口只是开发环境临时方案,联调完成后部署时统一走 Nginx 反向代理,就不存在跨域问题。
6.2 模型训练集指标好但预测趋势不对
有段时间我做出来的模型在测试集上 R² 有 0.9,但把预测曲线和真实曲线画在一起,发现预测值总是比真实值滞后一两个时间点,而且整体被“拉平”。这类问题典型的原因是时间序列的自相关性:模型学到的最有效特征往往是前一天的 AQI 值,所以预测结果近似于把昨天的值搬过来,趋势当然滞后。
解决办法是减少对滞后标签的依赖,增加外生特征——气象条件、季节、节假日等跟历史自回归信息弱相关的特征。我当时把未来一天的天气预报特征加入预测条件后,趋势跟随现象缓解不少。另外也可以考虑换用序列建模思路,比如 LightGBM 加滞后特征窗口,或做一阶差分让模型学习的是“变化量”而不是“绝对值”,效果往往更好。
6.3 Vue路由刷新404与图表样式错乱
Vue 项目部署到服务器后,用户点击浏览器刷新按钮,结果页面直接 404。这是因为前端路由是 history 模式的 client-side routing:页面切换不需要请求服务器,但刷新时会向服务器发出对应的 URL 请求,而后端服务器上根本没有这个路径对应的真实文件。
解决办法是配置 Nginx 的 try_files 指令,将所有路由请求都回退到index.html:
location / { root /var/www/aqi-frontend/dist; index index.html; try_files $uri $uri/ /index.html; }图表样式错乱的坑更容易被忽略:ECharts 图表渲染之后,如果异步加载的数据比组件挂载更慢,容器高度可能还没被 CSS 撑开,导致图表压缩成一小块。解决方法是给图表的容器设置固定高度(比如height: 400px),或者监听数据加载完成后再调用chart.resize()。
6.4 环境版本兼容性的血泪教训
Django、Python、scikit-learn、Node.js 的版本兼容问题,我踩过不少坑,整理成速查表如下:
| 症状 | 原因 | 解法 |
|---|---|---|
| pip install 报依赖冲突 | Python 3.12 与部分旧库不兼容 | 用 Python 3.10,明确写 requirements.txt |
| sklearn 模型保存后跨版本加载报错 | 版本间 pickle 格式变动 | 训练和预测必须用同一套环境 |
| npm install 报 engine 不匹配 | Node 版本过老或过新 | 用 nvm 统一 Node 18 |
| Django 4 里 pymysql 配置报错 | 需要显式调用 pymysql.install_as_MySQLdb() | 在项目__init__.py里加上 |
| Vue 3 项目里引入 Vue 2 组件库报错 | Element UI 和 Element Plus 不兼容 | 确认安装的是 element-plus 而非 element-ui |
我的建议是创建项目后第一时间导出环境依赖:后端pip freeze > requirements.txt,前端确认package-lock.json入库。这样即使换电脑重装环境,也能几分钟内恢复,不会因为版本不一致白耗一整天。
6.5 常见问题快速定位表
把开发中积攒的问题整理成速查表,遇到类似情况可以先对照:
| 现象 | 排查步骤 | 最终处理 |
|---|---|---|
| 前端请求报 500 | 看 Django 日志,检查是否数据为空 | 捕获异常返回空数组或提示 |
| 模型预测结果全为同一个值 | 特征顺序错位或特征全部缺失 | 比对 feature_cols 顺序,检查 NaN |
| 前端图表不显示 | 检查接口是否返回、data 字段是否存在 | 在 axios 拦截器里打印 res.data.data |
| 部署后静态文件 404 | Django 静态文件未 collectstatic | python manage.py collectstatic |
| 跨域请求 OPTIONS 报 403 | CORS 配置未加载 | 检查 Middleware 顺序和 allowed origins |
写在最后
踩过这么多坑之后,我对这个项目最深的体会是:毕设能不能拿高分,往往不取决于模型有多高级,而取决于你的工程链路有多完整。随机森林本身是个标准算法,你只要把“数据获取-特征构造-模型评估-可视化呈现”这条链路走通,把每一步里的权衡和取舍说清楚,就已经超过大多数只交个 notebook 的选手。
最后分享一个我自己答辩前做的小动作:把随机森林模型的特征重要性排序做成一张带颜色渐变的条形图,放在系统首页最显眼的位置。这张图能直观地回答“为什么用随机森林”这个问题——你一眼就能看到 PM2.5、PM10 对 AQI 贡献最大,模型决策依据一目了然。很多时候,评委不是要你证明算法比别人好,而是想确认你真的理解了模型在做什么。这个项目后续再想扩展,可以接实时爬虫数据流、加入更多城市的对比、接入短信预警推送——但先把地基打好,每一层都做得扎实,比什么都重要。