开头:
距离上一篇更新过去挺久了,很多追这个系列的朋友一直在问第四篇什么时候出。说实话,前面三篇花了大篇幅讲AQI数据的采集、清洗、EDA和基础特征工程,写完了之后我自己的感受是“地基打得越深,后面盖楼反而越没底”。因为到了真正做预测这一步,你会发现很多前面觉得“差不多能用”的数据准备,实际跑起模型来处处是坑。这篇干脆不再铺垫,直接用前三篇整理好的数据集,走一遍完整的AQI预测建模流程:从确认目标、划分数据,到多模型基线对比、参数调优,再到结果评价和落地注意事项。整个项目的代码、思路全部是基于我实际工作中改过很多版的方案,用的都是最常见的数据科学工具栈——Python、pandas、scikit-learn、LightGBM,画图用matplotlib和seaborn。没有上特别炫酷的深度学习方法,因为对于AQI这类时序性偏弱、气象驱动较强的回归问题,树模型和线性模型的组合往往已经能给出很好的效果,而且部署和解释都容易得多。不管你是刚开始接触空气质量预测,还是已经在做类似环境数据挖掘的项目,这篇都能给你一套可以直接照抄的实践模板。
1. 整体思路与方案选型
1.1 先明确预测任务类型
空气质量指数预测,本质上是一个回归问题还是分类问题,取决于你到底要预测什么。最常见的两种需求:一种是预测未来某时刻的AQI具体数值,另一种是预测未来某时刻的AQI等级(优、良、轻度污染、中度污染、重度污染、严重污染)。这两种任务对应的评估指标、模型选择都不一样。
我最终选择了以回归为主、等级分类为辅的双轨策略。回归模型的输出是连续值,可以直接用于计算MAE、RMSE;等级分类则是把回归预测的数值映射到《环境空气质量标准》定义的区间里,再做等级准确率评估。这个方案的好处在于,你既可以用数值告诉用户“明天下午的PM2.5浓度大概是75微克每立方米”,也可以用等级告诉用户“明天全天的空气质量是轻度污染”。实际项目中,给公众看的往往是后者,给专业部门做分析用的往往是前者。
1.2 特征体系怎么搭才不浪费
前三篇做特征工程的时候,我保留了这么几类特征作为建模输入:
- 气象特征:温度、湿度、气压、风速、风向(分箱后的正弦余弦值)、降水量
- 时间特征:小时、星期几、是否节假日、季节
- 历史污染特征:过去24小时AQI均值、滞后24小时AQI、过去7天AQI均值、前一天的PM2.5和PM10浓度
- 空间特征:相邻站点的同小时AQI均值(如果有多站点数据)
这套特征体系的搭建逻辑其实很朴素:气象因素决定污染物的扩散条件,时间因素决定排放强度的周期性变化,历史污染水平则直接承接了污染的惯性效应。没有做太多复杂的交叉特征,因为树模型本身具备一定的特征交互能力,线性模型则用多项式扩展来做补充。
1.3 模型选型的心路历程
我在这个项目里对比了五种模型,从简单到复杂挨个跑了一遍:
- 线性回归:作为最低基线,看数据里有多少线性信号
- 岭回归:加了L2正则,解决特征共线性问题
- 随机森林:非线性能力强,但外推能力弱
- LightGBM:训练快、效果好、调参空间大
- 多层感知机(MLP):简单神经网络,验证一下非线性拟合的上限
实测下来的结果是,LightGBM在验证集上的表现最好,岭回归和线性回归的误差虽然稍大,但胜在稳定和可解释。随机森林的表现反而有点让人意外,训练时间比LightGBM长,精度却没跟上。MLP的表现取决于网络结构和归一化方式,调好了和LightGBM接近,但稳定性差一些,换一份数据验证集误差波动很剧烈。
最终提交的模型结构采用了“LightGBM主模型+岭回归兜底”的组合。如果LightGBM因为数据分布突变产生明显偏大或偏小的预测值,就用岭回归的结果做混合回退。这种做法是我在一个类似项目里摸索出来的,处理AQI这种和气象条件强相关的数据时非常实用。
2. 数据预处理的几个关键细节
2.1 训练集和测试集不能随机切分
做时序相关的预测,最忌讳的就是用随机打乱的方式切分数据集。AQI数据有明显的自相关性和季节性趋势,如果用随机切分,模型会“偷看”到未来的一部分信息,验证集的表现会虚高,但部署上线之后立刻现出原形。
我采用的切分方式是按时间顺序严格划分:以数据的前80%作为训练集,后20%作为测试集。同时,为了保证调参阶段的评估稳定性,训练集内部又按时间顺序切成了前75%作为子训练集、后25%作为验证集。这个布局尽量模拟了真实场景:模型只管用过去的数据学习,未来的数据完全不可见。
另外还要处理一个细节:时间特征里的“星期几”在切分后训练集和测试集分布不均衡的问题。如果切分点刚好落在周末前,那训练集里周一到周四的数据就偏多,测试集里周五到周日的数据偏多。所以我额外做了一步画分布图检查训练集和验证集的星期几分布,发现偏差明显的时候,会把切分点往前挪几天重新切,确保两边的时间语义完整。
2.2 缺失值和异常值的取舍逻辑
AQI数据常见的缺失原因包括设备故障、通讯中断、数据审核标记无效等。我统计了一下,这个数据集的缺失率在1.8%左右,不算高,但集中在几个连续时间段。
针对缺失值,我根据特征类型区分处理:
- 气象特征缺失:用前一天同时刻的值向前填充,因为气象有较强的日周期性
- 历史污染特征缺失:用最近一周的滑动均值填补,避免单点瞬断带来的异常跳变
- 目标变量缺失:直接删除该行样本,不填充
异常值处理上,我没有一律用三倍标准差去截断,因为AQI这个指标本身就允许出现极端值(比如沙尘暴、春节燃放时段)。直接截断会把真实的重污染事件抹掉,导致模型在极端情况下的泛化能力下降。
处理方案是只剔除“物理不可信”的异常值,比如相对湿度小于0或大于100的记录,负浓度的记录,以及连续多天同一个数值完全不波动的“死数”区间。这种宽松过滤保住了极端样本,又去掉了明显脏数据。
2.3 特征归一化只做了一半
该不该做归一化,取决于用什么模型。线性回归和MLP需要把特征尺度拉到一个范围内,否则梯度下降和正则化项会歪掉;树模型则完全不受特征尺度影响,归一化对它没有任何帮助。
所以我只对连续型气象特征和滞后污染特征做了标准化,把所有树模型输入保持原始数值。此外,对于时间特征的编码方式,我用了循环编码(比如小时数转成sin和cos两个分量),而不是直接扔整数,因为直接用整数会让模型错误地认为23点和0点之间的差异是23。循环编码后,23点和0点在角度空间里就是相邻的,模型能够学到周期性规律。
import numpy as np def cyclical_encode(series, period): theta = 2 * np.pi * series.astype(float) / period return np.sin(theta), np.cos(theta)3. 模型训练与调参实战
3.1 LightGBM的核心参数配置
我最终LightGBM的完整训练配置可以给出一份直接可用的参数组合,这是经过网格搜索加经验判断综合出来的结果,使用5000轮早停轮次(early stopping rounds=200):
import lightgbm as lgb params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': 0.03, 'num_leaves': 63, 'max_depth': 7, 'min_child_samples': 30, 'subsample': 0.8, 'subsample_freq': 1, 'colsample_bytree': 0.8, 'reg_alpha': 0.05, 'reg_lambda': 0.3, 'n_estimators': 5000, 'random_state': 42, 'n_jobs': -1 }关于参数选择的一些心得:
learning_rate从默认的0.1降到0.03,训练轮数相应增加,换来的精度提升大概在MAE上能减少2%,代价是训练时间多了一倍。这一点对AQI这类数据量不大(几万条)的场景来说完全划算。num_leaves控制树的复杂度,63是比较适中的一个数。我试过更小的31片叶子,误差变大;试过更大的127片,出现过拟合。min_child_samples设在30,是为了抑制树在少数极端样本上过度分裂。AQI数据中重度污染样本比较少,如果这个值太小,树容易在稀有样本上记忆噪声。- 特征采样率
colsample_bytree设为0.8,可以有效减少树与树之间的相关性,对集成模型整体效果是正向的。
3.2 网格搜索和手工调参怎么配合
网格搜索直接用GridSearchCV在这个数据量上跑还能接受,但参数组合一多,整体耗时还是太长。我实际的做法是分阶段调:先用粗网格锁定几个关键参数的大致区间,再固定这些参数,用手动微调的方式去优化剩下的参数。
第一轮只调learning_rate和n_estimators,固定其他参数,观察早停轮数。因为这两个参数是学习轨迹的核心,直接影响后续调参时训练轮数和验证集误差的对应关系。
第二轮调num_leaves和max_depth,这是树结构的主体参数。我在num_leaves∈[31, 63, 127]和max_depth∈[5, 7, 9]的组合里做了搜索,效果最好的落在num_leaves=63, max_depth=7。
第三轮调正则化参数reg_alpha和reg_lambda,又固定其他参数。这一步的目的是看验证集误差和训练集误差之间的差距能不能缩小,也就是抑制过拟合,最终选在reg_alpha=0.05, reg_lambda=0.3。
全程使用MAE作为优化指标,因为它对异常值的敏感度比MSE低,更贴近实际场景里普通人对空气质量误差的感受。
3.3 简单模型做底线校验
LightGBM效果看起来不错,但我不放心,还是跑了一个岭回归和模型做交叉验证对比。这样做的价值在于:如果复杂模型和简单模型的验证集指标相差无几,那说明数据中缺乏足够的非线性信号,复杂的树模型就是多余的成本。
岭回归的MAE大约比LightGBM高10%到15%,这一个差距说大不大,说小不小,但足以证明数据中确实存在一些线性模型抓不到的非线性模式。这进一步坚定我选择LightGBM作为主模型的判断。MLP的结果和LightGBM基本持平,但训练稳定性差,同一份数据换随机种子,验证集误差波动范围比LightGBM大。所以最终生产环境的模型还是以LightGBM为主。
4. 结果评价与多模型效果对比
4.1 评价指标不能只看一个
模型跑完之后,我同时计算了以下指标:MAE、RMSE、MAPE(平均绝对百分比误差)、R²、以及等级分类准确率。每种指标从不同角度揭示模型表现。
| 模型 | MAE | RMSE | MAPE | R² | 等级准确率 |
|---|---|---|---|---|---|
| 线性回归 | 13.82 | 21.46 | 16.7% | 0.84 | 78.4% |
| 岭回归 | 13.57 | 20.92 | 15.9% | 0.85 | 79.1% |
| 随机森林 | 12.39 | 19.87 | 14.8% | 0.86 | 80.2% |
| LightGBM | 11.94 | 19.18 | 13.5% | 0.87 | 82.5% |
| MLP | 12.12 | 20.03 | 14.1% | 0.86 | 81.3% |
可以看到,LightGBM在各指标上都是最优的,但优势并非压倒性。与线性回归相比,MAE降低了约13.6%,这个提升幅度在污染预测场景里意味着更精准的预警边界判断。
MAPE指标这里要特别提醒一下:AQI数值在50以下的“优”等级区间,百分比误差天然会偏高,因为基数小。如果整个数据集里优等级样本占比特别大,MAPE会被这些低数值样本拉高。我是分组计算的MAPE,分不同AQI等级区间来看,优等级区间的MAPE高达百分之三十几,而轻度污染以上区间的MAPE只有百分之十几。这就说明单一百分数指标会误导人,必须和其他误差指标一起综合判断。
4.2 特征重要性解读
LightGBM训练结束后,可以直接输出特征重要性。我整理了Top10的重要特征,排序如下:
- 历史24小时AQI均值
- 滞后24小时AQI
- PM2.5滞后浓度
- 过去7天AQI均值
- 风速
- 湿度
- 温度
- 小时(循环编码的sin和cos分量)
- 气压
- 风向(sin和cos分量)
这个排序其实符合我对空气质量形成机制的认知。AQI本身具有很强的惯性,今天的浓度和过去24小时的平均浓度高度相关,这本质上是污染物在地理空间上积聚和消散的连续过程。风速和湿度紧随其后,因为风速直接决定水平扩散能力,湿度则和二次气溶胶生成速率密切相关。
4.3 时间维度上的误差分析
误差分布不看时间维度是不完整的。我按月份和小时两个维度拆分了验证集在测试集上的预测误差,发现了一个非常典型的规律:冬季和早晨8点到10点的时段,误差明显更大。
冬季误差大的原因是污染过程往往伴随静稳天气,边界层高度低,污染物在地面堆积,这种情况下AQI经常出现连续多日高位运行,模型预测的滞后性就会被放大。早晨时段误差则是通勤排放高峰和边界层抬升两个过程在时间上叠加重叠,大气化学过程变化最快,模型容易低估峰值。
如果模型要部署到实际业务中,对这两个场景需要针对性补救。我当时的方案是为冬季单独训练了一个模型副本,训练数据只保留11月、12月和1月,预测时如果时间落入冬季窗口,就使用冬季模型副本的结果。这个调整在冬季重污染事件预测上,MAE降低了差不多10%。
5. 常见问题排查与踩坑记录
5.1 为什么模型在重污染时段总是低估
这是整个项目中最头疼的问题。模型整体指标不错,但一旦遇到AQI超过200的重污染事件,预测值几乎总是低于真实值。原因在于样本不平衡:数据集中AQI超过200的样本占比不到5%,模型在训练时很少见到这个区间,自然不会花太多容量去拟合它。
想通过加权重缓解?LightGBM支持weight参数对样本加权,我试了一下,轻度改善,但以训练集整体误差上升为代价。更有效的办法是单独为重污染区间建模,或者在上层加一个增量修正规则——当预测的PM2.5浓度超过某个阈值时,应用一个轻量的岭回归修正块。这个方法有点粗糙,但效果稳定,后来我实际部署的模型就是带这个修正逻辑的。
5.2 节假日特征为什么没有显著提升
刚开始我加了“是否节假日”作为特征,特征重要性排名接近垫底,证明确实没帮上什么忙。分析原因后发现,数据集的节假日样本太少,一年里节假日最多十几天,对比全年几千条数据,模型根本没有足够的样本去学习节假日的独特污染模式。而且节假日排放下降的效果在气象条件较好的时段并不明显,反而是春节前后的烟花爆竹时段对数据影响大,但这类特殊情况样本量更少。
我试过拉长节假日窗口(比如把节前节后两天也标记为节假日),效果略有改善,但提升依然有限。如果数据量足够一个城市五到十年的监测数据,这个特征可能会变得更有意义,但靠这一两年的时间跨度确实榨不出更多信号。
5.3 站点间数据能不能直接用
如果有多个站点数据,很多人第一反应就是把相邻站点的AQI也塞进特征。这个做法可以做,但要在特征里明确区分“同一时刻本站点数据”和“同一时刻相邻站点数据”。我用过一个简化方案:直接计算全城所有站点的某时刻AQI均值作为空间背景特征,效果比用单一邻近站点要好,而且特征维度少、计算量小。
不过要注意,站点间数据的缺失时段往往高度重合(设备检修往往同步进行),这些时段的空间特征也会缺失,需要进行额外的插补,如果缺失时段范围太大,这一块空间特征的价值就要打个折扣。
5.4 调度预测的时间窗口怎么定
最终生产模型的预测窗口定为“提前24小时预测未来逐小时的AQI值”。这个时间窗口的确定更多来自业务需求:空气质量预警部门需要提前一天做出响应。但预测的时间跨度越长,误差累积越明显,尤其是24小时末端的预测误差通常比前端高出百分之二三十。
对策是把预测任务拆成两段:用短周期模型预测未来1到6小时,用一个独立的日尺度模型预测未来24小时的日均值。两段预测分开调优,评价指标也分开计算,效果明显比用一个模型硬扛全天要好。
6. 一些值得分享的实操心得
写到这里,我回顾了整个项目实施过程,挑几条最值得说的经验:
第一,拿到数据先别急着建模,先花时间把时间序列的可视化做扎实。日期、星期、节假日、季节、天气事件的污染画像都画出来,你会对数据有一个非常直觉的把握,后面的特征工程就不容易跑偏。
第二,树模型虽然号称不需要预处理,但实际项目中把时间特征做循环编码带来的提升是实打实的,这是我多少次实验下来都稳定有效的操作。不要偷懒直接丢整数小时列进去。
第三,评价指标的选择直接影响调参方向。在这个项目里我一开始用RMSE,后来换成MAE,模型调参方向明显变了,线下测试的结果也说明MAE更符合公众对空气质量误差的认知,说什么MSE大误差惩罚严格,都说不出这个场景的现实意义在哪里。
第四,对异常值的宽容处理要放在模型上线前认真思考。AQI数据里的“极端值”往往是最值得预警的事件,如果你图省事把尾部样本全截断了,模型在重污染事件里就会变成“睁眼瞎”。
第五,模型精度做到一定程度,边际收益越来越小,这时候把精力放在解释性上,给业务人员讲清楚为什么预测值偏高或偏低,比硬挤出那零点几个百分点的误差更有用。树模型的特征重要性和SHAP值分析结合起来,能很好支撑这类需求。
这个项目到目前这个阶段,已经能稳定产出可用的预测结果。如果想继续往下做,可以考虑引入更多外部数据源,比如卫星遥感气溶胶光学厚度、区域气象预报场、交通流量数据等,预测精度还有进一步提升空间。但就目前这套流程而言,对大多数城市的AQI预测需求已经能覆盖了。