空气质量预测实战:从数据清洗到Django部署的完整流程
简介这是一份基于机器学习构建空气质量监测与预测系统的技术方案文档面向环保科技从业者、空气污染研究人员、数据分析师及大数据工程师。内容围绕系统全流程展开涵盖多源数据采集、清洗与预处理并综合统计分析与时间序列模型应用随机森林、支持向量机等算法完成空气质量指标预测文档还详细阐述了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(ascendingFalse)) 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.5def 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], labelAQI, color#2c3e50) plt.plot(sample[date], sample[pm2.5], labelPM2.5, color#e67e22, alpha0.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_size0.2, random_state42, stratifyy ) gnb GaussianNB() gnb.fit(X_train, y_train) y_pred gnb.predict(X_test) print(classification_report(y_test, y_pred))逻辑说明GaussianNB假设每个特征在给定类别下服从正态分布基于贝叶斯定理计算后验概率。stratifyy保证训练集和测试集中严重污染样本的比例一致避免因为划分不均匀导致测试集里没有一个正样本。参数说明test_size0.2是经验值数据集小时可以调到 0.3random_state42固定随机种子保证每次跑结果一致方便对比调参前后效果。朴素贝叶斯的条件是特征独立假设这在空气质量数据里显然不成立PM2.5 和 PM10 相关性极高。但这个模型的优势是训练速度快到几乎不计时间适合做实时预警的快速初筛。在系统界面里用户输入当天污染物浓度后台立刻能给出“疑似污染”或“疑似良好”的初步判断。4.2 随机森林回归预测 AQI 数值与特征重要性分类只能给出“污染/良好”用户真正想要的是“未来三天 AQI 大概多少”。随机森林回归负责预测具体数值from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score rf RandomForestRegressor( n_estimators300, max_depth20, min_samples_leaf3, n_jobs-1, random_state42 ) 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_estimators300棵树在几万条数据量级下计算量仍然可控max_depth20防止单棵树过深导致过拟合min_samples_leaf3保证叶子节点至少有 3 个样本才停止分裂。参数说明如果 MAE 偏高优先调max_depth和min_samples_leaf一般先从min_samples_leaf5开始试。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(kernelrbf, C1.0, epsilon0.1) svr.fit(X_train_s, y_train) y_pred_svr svr.predict(X_test_s)逻辑说明StandardScaler把每个特征缩放到均值为 0、方差为 1 的分布。注意fit_transform只用训练集测试集用transform避免测试集信息泄漏到训练过程中。kernelrbf是默认径向基核适合非线性关系C1.0是正则化系数越大越容易过拟合epsilon0.1表示预测值与真实值差距在 0.1 以内不算误差。参数说明SVR 对 C 很敏感数据量在几万行时可以从 C1 开始用 GridSearchCV 搜索 0.1 到 100 的范围。epsilon 建议不要低于 0.01否则训练时间成倍增加但精度提升有限。4.4 模型选型策略什么场景下用哪个三个模型不是只有谁优谁劣而是各有定位模型适用场景主要优点容易踩的坑朴素贝叶斯实时快速初筛、资源受限环境训练快、可解释性强特征独立假设不符合实际随机森林日常 AQI 数值预测、特征重要性分析非线性拟合强、无需标准化数据量小时容易过拟合SVR小样本、多特征的低维回归泛化能力好对特征尺度敏感、调参成本高在部署时我一般把朴素贝叶斯和随机森林同时上线朴素贝叶斯给出等级判断随机森林给出具体 AQI 数值两个结果互相校验。如果模型间出现冲突比如贝叶斯说“良好”但随机森林预测 AQI162就需要人工介入检查输入数据是否异常。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_TZTrue数据库存储的是 UTC 时间前端模板渲染时没有做时区转换。系统服务的是国内城市UTC 时间比北京时间慢 8 小时日期边界因此错位。解决项目首次配置阶段就明确时区设置# settings.py USE_TZ False TIME_ZONE Asia/Shanghai如果坚持用USE_TZTrue则在查询数据后手动转换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默认的乱序交叉验证要改用TimeSeriesSplitfrom sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) 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 再出报表希望帮到你。本文还有配套的精品资源点击获取