☰
空气质量预测实战:从数据清洗到Django部署的完整流程
2026/10/22 2:32:46 网站建设 项目流程

简介:这是一份基于机器学习构建空气质量监测与预测系统的技术方案文档,面向环保科技从业者、空气污染研究人员、数据分析师及大数据工程师。内容围绕系统全流程展开,涵盖多源数据采集、清洗与预处理,并综合统计分析与时间序列模型,应用随机森林、支持向量机等算法完成空气质量指标预测;文档还详细阐述了Django框架下的系统架构、MySQL数据库设计及模型评估方法,并附有斯皮尔曼相关性分析、贝叶斯决策等具体算法实现,可作为类似项目开发与部署的参考模板。压缩包内仅有一个文档,大小约595KB,适合需要搭建空气质量分析平台或开展环境政策研究的政府及企业单位参考。目前该资源已有163人学习,可用于指导实际项目中的参数调整与方案落地。

1. 空气质量预测这件事:从数据到模型再到网页的全流程记录

空气质量数据看着只是时间戳加污染物浓度,但真正把它放进机器学习流程里做时间序列分析和预测建模时,大多数开发者的第一反应是拿模型直接训练,结果通常在两个位置翻车:特征之间相关性没有先做检验、训练集和测试集的时间顺序被随机打乱。这套系统把数据清洗、斯皮尔曼相关性分析、朴素贝叶斯二分类、随机森林回归和 Django 页面展示串成一条完整链路,用的全是常见开源组件,不依赖付费服务。它适合环保科技从业者做污染溯源分析,适合数据分析师把多城市时序数据处理流程作为参考模板,也适合刚接触 Django 的开发者在同一个项目里同时看到建模和 Web 集成是怎么协作的。下面按实际拆解顺序展开,能直接照着做。

2. 数据获取与预处理:一张可训练的时间序列表是怎么炼成的

空气质量数据最大的特点不是“脏”,而是“乱”。不同城市、不同时间段、不同传感器采集的数据粒度不一致;某一天某个监测站可能故障,导致整列 NaN;沙尘暴期间 PM10 出现异常高值,却又不能简单当噪声剔除。这套系统在数据入库前做了一次完整加工,把问题前置,后面建模才顺。

2.1 字段设计与数据源:日粒度 vs 小时粒度的取舍

原始数据采集自多个气候区代表城市,覆盖华北、华东、西部高原和热带海岛等不同环境特征。在设计数据库表时,核心是空气质量表,字段包括:date、city、quality_level、AQI_index、pm2.5、pm10、so2、no2、o3、co。这里的 city 字段不是简单的展示字段,而是后续做多城市对比分析和分组建模的主键之一,所以在设计时就把它跟 date 一起做成联合索引。

字段名数据类型默认值说明
idint自增主键
datedate无观测日期
cityvarchar(20)无城市标识,联合索引组成之一
quality_levelvarchar(64)无优良/轻度污染等分级
AQI_indexint无AQI 指数,建模目标字段
pm2.5 / pm10int无颗粒物浓度
so2 / no2 / o3 / coint无气态污染物浓度

采集周期上容易出现一个隐蔽问题:有些城市是小时级数据,有些是日级。如果直接合并,时间对齐会出现大量缺失。常见做法是统一降采样到日粒度,用resample('D').mean()聚合;如果项目硬要保留小时级,则需要额外处理早晚高峰、昼夜温差对浓度的周期影响。从模型复杂度来看,首批版本先做日粒度更稳妥。

用户信息表单独建一张,包含 user_ID、reg_time、username、password、now_city 等字段。now_city 是用户注册时选定的所在区域,系统登录后默认展示该城市的空气质量预测卡片,这个设计在后面的 Django 视图中会用到。

2.2 缺失值与离群值处理:用代码保住时间趋势

拿到入库的数据后,第一步是检查缺失比例并排序,这里不要直接dropna(),因为时间序列中的空值删除会打破连续性。处理结构如下:

import pandas as pd import numpy as np df = pd.read_csv('air_quality_raw.csv', parse_dates=['date']) print(df.isnull().mean().sort_values(ascending=False)) pollutants = ['pm2.5', 'pm10', 'so2', 'no2', 'o3', 'co'] # 按城市分组,先在组内做前向填充,再用后向填充兜底 df[pollutants] = df.groupby('city')[pollutants].ffill().bfill()

逻辑说明:groupby('city')保证填充只在同一个城市内进行,避免把沿海某市的低浓度值填到北方某市的缺失位上。ffill()用前一天浓度补缺失,符合污染物在相邻日期间的惯性;bfill()解决序列开头仍有缺失的情况。

参数说明:parse_dates=['date']必须写在最前面,否则后面的groupby和排序都基于字符串,时间轴的先后判断会出错。ffill/bfill的默认参数不需要额外设置,但要注意如果某个城市整段数据都缺失,填充后仍是 NaN,需要在特征工程阶段单独处理,不会报错只会带病进模型。

离群值处理我用的不是硬阈值,而是 IQR 截断,且系数取 3 而不是常见的 1.5:

def cap_outliers(s): q1, q3 = s.quantile(0.25), s.quantile(0.75) iqr = q3 - q1 return s.clip(q1 - 3 * iqr, q3 + 3 * iqr) df['pm10'] = cap_outliers(df['pm10'])

这里的考虑是:PM10 在沙尘暴天气时会出现真实高值,如果按 1.5 倍 IQR 截断,会把真实的环境事件也削掉,模型就永远学不到极端情况。3 倍 IQR 只兜住传感器故障级别的异常,保留真实极端样本。如果你做的是稳健回归场景,截断系数还可以再放宽。

2.3 多城市数据对齐与探索性数据可视化

多城市数据合在一张表后,要确认每个城市的时间范围覆盖一致。某些城市可能只收录了两年数据,另一个城市有五年,训练模型时如果直接合并,模型容易被数据量大的城市带偏。常见做法是先按城市做覆盖面统计:

coverage = df.groupby('city')['date'].agg(['min', 'max', 'count']) print(coverage)

输出后人工确认时间范围差异是否在可接受范围内。如果某个城市只有几个月数据,建议先不做训练集只做展示,避免它成为噪声源。

探索性可视化建议在建模前就做,不是建模后再补。举例:

import matplotlib.pyplot as plt sample = df[df['city'] == 'north_city'].tail(90) plt.figure(figsize=(12, 6)) plt.plot(sample['date'], sample['AQI_index'], label='AQI', color='#2c3e50') plt.plot(sample['date'], sample['pm2.5'], label='PM2.5', color='#e67e22', alpha=0.8) plt.legend() plt.title('90-day AQI and PM2.5 trend') plt.show()

这段代码的价值不在于画图本身,而在于人工核对曲线与已知环境事件的对应关系。比如某沿海城市夏季臭氧浓度曲线持续走高,这是光化学反应导致的,对应关系对了再进模型;如果曲线出现瞬间跃升又垂直掉落的尖峰,大概率是传感器量程切换,需要回到原始记录确认。这步操作属于“后悔药”——发现问题越早,后面返工的次数越少。ECharts 在网页端负责展示,而 matplotlib 只负责开发阶段的快速确认,两者定位不同,不冲突。

3. 斯皮尔曼相关性分析:确定哪些污染物真正影响 AQI

系统在特征分析阶段用的是统计学方法,对 AQI 与各污染物计算斯皮尔曼秩相关系数,并配合 t 检验判断显著性。这一步做得好,后面模型选特征的依据就硬。

3.1 为什么是秩相关:空气质量数据里常见的非线性场景

先看公式。斯皮尔曼相关系数的计算基于秩而不是原始值:

ρ = Σ(Ri - R̄)(Si - S̄) / sqrt(Σ(Ri - R̄)² * Σ(Si - S̄)²)

其中 Ri、Si 是第 i 个样本在两组数据中的秩次,R̄、S̄ 是平均秩次。

选秩相关而不是皮尔逊相关,核心原因是空气质量数据里不少关系是单调但非线性的。PM2.5 和 AQI 在低浓度段近似线性,但浓度升高后,AQI 计算的 IAQI 分段函数让两者的关系变成折线甚至近似对数形态。皮尔逊相关对这种形态的敏感度很低,可能算出来只有 0.6 左右,而斯皮尔曼基于秩次排序,能抓到“PM2.5 越高 AQI 越高”这种单调趋势,不再被数值间距干扰。

另外,空气质量数据中存在明显的离群点,沙尘暴当天的 PM10 可能比平时高几十倍。皮尔逊系数对离群点很敏感,哪怕只有一个极端值,也会把相关系数拉向 1,造成虚假的强相关;斯皮尔曼因为只看排序位置,被单个极端值带偏的幅度要小得多。这一点在现实数据里非常关键。

3.2 Python 中的斯皮尔曼计算与 t 检验实现

计算相关系数可以直接用 scipy 的spearmanr,但需要额外做显著性检验时,就手动算一遍:

from scipy.stats import spearmanr import numpy as np pollutants = ['pm2.5', 'pm10', 'so2', 'no2', 'o3', 'co'] results = {} for col in pollutants: rho, p_val = spearmanr(df['AQI_index'], df[col]) n = df[col].notna().sum() # t 统计量公式:t = rho * sqrt(n-2) / sqrt(1-rho^2) t_stat = rho * np.sqrt(n - 2) / np.sqrt(1 - rho**2) results[col] = {'rho': rho, 'p_value': p_val, 't_stat': t_stat} print(f"{col}: rho={rho:.4f}, p={p_val:.4e}, t={t_stat:.4f}")

逻辑说明:spearmanr返回两个值,rho 是相关系数,p_val 是显著性概率。t_stat是手动换算的 t 统计量,用于和临界值比对。这里的 n 用的是该特征非空样本数,不是总行数,避免因缺失值处理不当把样本量虚增。

参数说明:在 t 检验中,自由度是 n-2。当 t 的绝对值大于 t0.05 临界值时,认为相关性显著;p 值小于 0.05 也说明同样的结论。实际项目中我一般直接看 p 值,小于 0.05 就认为相关性在统计上显著,t 值只看符号方向。

这段代码跑完后会得到一个污染物与 AQI 相关性排序。从多个开题、毕设类似的真实项目统计来看,PM2.5 和 PM10 几乎总是排在前两名,SO₂ 在北方采暖季贡献明显,O₃ 在夏季成为主要相关因子。如果你的数据和这个规律差异巨大,比如 PM2.5 相关系数结果只有 0.2,大概率是前面预处理阶段出了问题,需要回头查。

3.3 筛选特征:把相关系数变成建模依据

计算相关性的目的不是看看数字,而是为模型输入做减法。我一般有三种处理方式:

  • 相关系数大于 0.7 且 p 值显著:保留,作为核心特征
  • 相关系数在 0.3 到 0.7 之间:保留,但标记为次要特征
  • 相关系数小于 0.3 或 p 值不显著:建议剔除,不值得为它付出模型复杂度

特别要注意的是共线性问题。PM2.5 和 PM10 的相关系数往往超过 0.8,它们之间存在高度共线性。如果同时作为特征进入线性模型,会放大参数估计方差;但在树模型里影响不大,随机森林可以自动处理特征冗余。所以特征筛选要看最终选什么模型。

这套系统的做法是先用斯皮尔曼结果做初筛,再结合业务知识确认。以 SO₂ 为例,它在某些南方城市与 AQI 相关性很低,但不代表二氧化硫不重要,只是当地燃煤排放少。如果你的服务对象是北方采暖城市,SO₂ 就必须保留。分析模块还需要给用户看“空气质量形成原因”,这部分依赖的就是相关系数的业务化解读:把数字翻译成“本周 AQI 升高主要由 PM2.5 驱动”这样的结论。

4. 预测模型构建:朴素贝叶斯、随机森林与 SVM 的取舍和实现

开题报告里明确写了三套算法:朴素贝叶斯做分类、随机森林和 SVM 做回归补充。这里不是简单堆算法,而是每个算法对应不同的使用场景。下面拆开讲实现细节。

4.1 朴素贝叶斯:AQI 二分类中的后验概率比较

在智能空气质量指数预测研究中,为了简化预测问题,先把空气质量按标准二分类:AQI 大于 150 认定为空气污染严重,其余认定为空气良好。然后用贝叶斯公式计算后验概率:

  • P(ω1|x) 表示在观测到污染物特征 x 时,空气质量为“良好”的概率
  • P(ω2|x) 表示在观测到污染物特征 x 时,空气质量为“严重污染”的概率

如果 P(ω1|x) > P(ω2|x),预测为“空气质量良好”,否则预测为“空气严重污染”。

代码实现:

from sklearn.naive_bayes import GaussianNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report df['polluted'] = (df['AQI_index'] > 150).astype(int) features = ['pm2.5', 'pm10', 'so2', 'no2', 'o3', 'co'] X = df[features] y = df['polluted'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) gnb = GaussianNB() gnb.fit(X_train, y_train) y_pred = gnb.predict(X_test) print(classification_report(y_test, y_pred))

逻辑说明:GaussianNB假设每个特征在给定类别下服从正态分布,基于贝叶斯定理计算后验概率。stratify=y保证训练集和测试集中严重污染样本的比例一致,避免因为划分不均匀导致测试集里没有一个正样本。

参数说明:test_size=0.2是经验值,数据集小时可以调到 0.3;random_state=42固定随机种子,保证每次跑结果一致,方便对比调参前后效果。

朴素贝叶斯的条件是特征独立假设,这在空气质量数据里显然不成立,PM2.5 和 PM10 相关性极高。但这个模型的优势是训练速度快到几乎不计时间,适合做实时预警的快速初筛。在系统界面里,用户输入当天污染物浓度,后台立刻能给出“疑似污染”或“疑似良好”的初步判断。

4.2 随机森林回归:预测 AQI 数值与特征重要性

分类只能给出“污染/良好”,用户真正想要的是“未来三天 AQI 大概多少”。随机森林回归负责预测具体数值:

from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score rf = RandomForestRegressor( n_estimators=300, max_depth=20, min_samples_leaf=3, n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train) # y_train 为 AQI 数值,不是二分类标签 y_pred_rf = rf.predict(X_test) print('MAE:', mean_absolute_error(y_test, y_pred_rf)) print('R2:', r2_score(y_test, y_pred_rf))

逻辑说明:随机森林对特征尺度不敏感,不需要做标准化,这是它比 SVM 好上手的地方。n_estimators=300棵树在几万条数据量级下计算量仍然可控;max_depth=20防止单棵树过深导致过拟合;min_samples_leaf=3保证叶子节点至少有 3 个样本才停止分裂。

参数说明:如果 MAE 偏高,优先调max_depth和min_samples_leaf,一般先从min_samples_leaf=5开始试。n_estimators超过 500 后提升很有限,只增加训练时间。n_jobs=-1表示用全部 CPU 核心,在只有双核的机器上不要这么写,会卡住其他任务。

特征重要性直接用rf.feature_importances_输出即可,通常 PM2.5 排第一。在可视化模块里,把特征重要性绘制成柱状图展示给用户,比干巴巴地列数字直观得多。

4.3 SVR 补充方案:小样本场景下的竞争模型

SVM 在小样本、特征维度不高时往往表现更好,但它的缺点是特征必须标准化,否则不同量纲的特征会让优化过程迟迟不收敛。

from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) svr = SVR(kernel='rbf', C=1.0, epsilon=0.1) svr.fit(X_train_s, y_train) y_pred_svr = svr.predict(X_test_s)

逻辑说明:StandardScaler把每个特征缩放到均值为 0、方差为 1 的分布。注意fit_transform只用训练集,测试集用transform,避免测试集信息泄漏到训练过程中。kernel='rbf'是默认径向基核,适合非线性关系;C=1.0是正则化系数,越大越容易过拟合;epsilon=0.1表示预测值与真实值差距在 0.1 以内不算误差。

参数说明:SVR 对 C 很敏感,数据量在几万行时可以从 C=1 开始,用 GridSearchCV 搜索 0.1 到 100 的范围。epsilon 建议不要低于 0.01,否则训练时间成倍增加但精度提升有限。

4.4 模型选型策略:什么场景下用哪个

三个模型不是只有谁优谁劣,而是各有定位:

模型适用场景主要优点容易踩的坑
朴素贝叶斯实时快速初筛、资源受限环境训练快、可解释性强特征独立假设不符合实际
随机森林日常 AQI 数值预测、特征重要性分析非线性拟合强、无需标准化数据量小时容易过拟合
SVR小样本、多特征的低维回归泛化能力好对特征尺度敏感、调参成本高

在部署时,我一般把朴素贝叶斯和随机森林同时上线:朴素贝叶斯给出等级判断,随机森林给出具体 AQI 数值,两个结果互相校验。如果模型间出现冲突,比如贝叶斯说“良好”但随机森林预测 AQI=162,就需要人工介入检查输入数据是否异常。

5. 常见问题与避坑:上线前最容易翻车的五个细节

这个项目从数据处理到 Django 页面展示的完整链路中,每一步都有隐藏的坑。下面按实际踩过或排查过的顺序记录,每条都按“现象、原因、解决”结构说明。

5.1 数据泄漏:时间序列被随机划分引发的幻觉

现象:训练集 R² 高达 0.97,测试集和训练集都好看,但模型部署到真实环境后预测结果完全不像样,误差比训练时翻了好几倍。

原因:用train_test_split默认的随机划分方式处理时序数据,相邻两天的数据被拆到训练集和测试集里。模型在训练时看到了“前一天的数据”,测试时同样出现在附近位置,相当于把答案提前告诉了模型。这是时间序列建模中最常见的翻车点,也是最隐蔽的。

解决:必须按时间顺序切割。用日期作为切分依据,确定一个时间点,之前的数据训练,之后的数据测试:

split_date = '2024-06-01' train = df[df['date'] < split_date] test = df[df['date'] >= split_date]

注意这里不能先做随机抽样再按时间排序,顺序必须保持原数据的自然时间流。从那以后我每次建训练集都会先确认数据集的时间范围,再考虑怎么划分。

5.2 缺失值填充方式改变了相关性分布

现象:斯皮尔曼相关系数计算结果异常,所有污染物和 AQI 的相关度都超过 0.85,明显不符合业务常识。

原因:全局用均值填充缺失值,大量重复的均值进入分布,把原本分散的秩次压平,导致相关系数虚高。尤其是 PM2.5 和 O₃ 这种季节性特征,均值填充会让夏季和冬季的浓度差异被抹平。

解决:改为分组前向填充,如第 2 章中代码所示。如果填充后仍有关键特征的缺失比例超过 20%,可以考虑删除该城市某时间段的数据,而不是硬填。缺失比例过高说明传感器长期故障,此时任何填充策略都是在编造数据。

5.3 Django 时区配置导致图表日期偏移

现象:用户在前端看到最近七天的折线图,最后一天数据显示是“昨天”,但数据库里对应记录的时间戳是正确的当天。

原因:Django 默认开启了USE_TZ=True,数据库存储的是 UTC 时间,前端模板渲染时没有做时区转换。系统服务的是国内城市,UTC 时间比北京时间慢 8 小时,日期边界因此错位。

解决:项目首次配置阶段就明确时区设置:

# settings.py USE_TZ = False TIME_ZONE = 'Asia/Shanghai'

如果坚持用USE_TZ=True,则在查询数据后手动转换df['date'] = df['date'].dt.tz_convert('Asia/Shanghai')。但最简单可靠的是关闭 UTC,因为项目本身只服务一个时区,不需要全球化支持。

5.4 特征尺度不一致,SVM 训练始终不收敛

现象:SVR 模型训练时间超过 10 分钟,控制台输出警告“ConvergenceWarning: Solver terminated early”,预测结果全是一个常数。

原因:CO 浓度单位是 mg/m³,数值在个位数级别;PM2.5 浓度单位是 μg/m³,数值范围在 0-500。SVM 内部使用距离度量,量纲差异导致梯度方向被大数值特征主导,优化过程在 CO 的微小尺度上反复震荡。

解决:训练前强制标准化,并且只对测试集做transform而不是重新fit_transform。标准化的另一个好处是岭回归、KNN 这类距离敏感模型也能直接复用同一套预处理结果。

5.5 跨城市通用模型在新城市失效

现象:在华北某市训练好的模型,直接部署到沿海某市后,AQI 预测值系统性偏低,而且在夏季经常把“轻度污染”判成“良好”。

原因:各城市污染物结构不同。北方城市冬季 PM2.5 和 SO₂ 主导,南方沿海城市夏秋季 O₃ 主导。单一城市训练出的模型没有见过高浓度 O₃ 与中低浓度 PM2.5 的组合模式,遇到就预测不准。

解决:按城市分组训练模型,每个城市保留独立的模型文件。如果数据量不够,就把 city 作为类别特征拼进向量中,让树模型自己能学习城市间差异。系统设计时,把 city 字段作为模型输入的一部分,同时在前端提供城市切换按钮,比强行训练一个全国通用模型靠谱得多。

6. 模型验证与持续优化:时间序列交叉验证和定期重训的关键动作

上线不是终点,真正进入常态化运行后,核心工作量都耗在两个地方:验证方法是否严谨、模型多久重训一次。

对于时间序列模型,不能使用KFold默认的乱序交叉验证,要改用TimeSeriesSplit:

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, valid_idx in tscv.split(X): X_train_ts, X_valid_ts = X.iloc[train_idx], X.iloc[valid_idx] y_train_ts, y_valid_ts = y.iloc[train_idx], y.iloc[valid_idx] # 训练并记录验证集指标

这个做法保证每一折的训练集时间都早于验证集,模拟的是真实部署后的“用过去预测未来”。我一般在模型上线前用 TimeSeriesSplit 跑完整轮对比,最终选择 MAE 最稳定的一版。

重训节奏方面,空气质量数据有明显的季节周期,我通常每月重训一次,并把最近 30 天数据加进训练集。如果模型在监控图表上连续一周出现预测偏差超过 20%,提前触发重训,不用等月度周期。Django 后端可以接一个定时任务,每晚拉取当天数据存入 MySQL,第二天凌晨触发增量训练,训练完成自动替换模型文件。这样预测接口永远用的是最新模型。

前端展示部分,ECharts 的折线图同时绘制历史真实值和未来七天预测值,预测区间用虚线区分。用户登录后根据now_city字段默认跳转到对应城市页面,查看近七天污染物浓度走势和未来三天 AQI 预报,这比直接甩一个数值表格更符合实际业务场景。

全套流程跑通后,后续功能扩展空间很大。可以加季节性分解,把趋势项、周期项和残差分开建模;也可以把气象预报数据(温度、湿度、风速)接进来作为额外特征。但不管怎么扩展,有一条原则不能丢:任何新特征上线前都要先跑一遍时间序列交叉验证,对比加特征前后的 MAE。从那以后我每次交付空气预测项目都会强制先跑一遍 TimeSeriesSplit 再出报表,希望帮到你。

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

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

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

立即咨询