AQI预测实战:特征工程与LightGBM时序建模避坑指南
发布时间:2026/10/18 1:28:12 锦皓数字建站

简介这是一份以空气质量指数AQI为案例的机器学习分析与预测实战资料面向正在学习Python数据分析与建模的初学者和进阶者帮助掌握从数据清洗、探索性分析到回归建模、模型评估的完整流程。压缩包共33个文件约4.23MB其中包含2个Jupyter Notebook源码、1份CSV原始数据、多张分析图表和说明文档代码与可视化输出搭配便于对照理解每一步操作。已有197人学习使用。资料中涵盖PM2.5、PM10、NO2、SO2等污染物浓度及气象因素的探索处理涉及线性回归、随机森林等常见算法的建模实践并配有MSE、RMSE、R²等指标评估和可视化展示。无论是课程作业还是入门环境预测项目这套资料都能提供可直接运行的代码、清晰的目录结构和完整分析思路让读者快速复现并迁移到类似的数据分析场景。1. AQI预测值不值得做先搞清楚它的数据坑再决定AQI空气质量指数预测是机器学习里少见的「数据和模型同等重要」的任务。很多人上来就调模型参数结果发现洛杉矶和北京的模型都不能通用——不是算法不行而是AQI的计算口径、监测站点的数据缺失模式、以及时间序列的泄漏问题哪一个都比模型本身更容易让你翻车。这个项目方向适合两类人一类是想用真实数据练手特征工程和时序交叉验证的初学者另一类是做环境监测、智慧城市、健康预警相关业务需要把预测结果落到日报或App里的工程师。我见过太多人卡在同一个地方拿到的数据是逐小时站点监测记录但AQI的官方口径是「24小时滑动均值 分项取最大」如果直接用原始小时值训练预测的就不是AQI而是某个污染物的浓度——这两个目标差了很远。这篇文章就围绕「怎么把原始监测记录变成可靠的AQI预测样本」展开代码和数据组织按最常见的从业方案来讲你可以直接照着落地。2. 把AQI的计算口径讲透为什么分项取最大会改变你的预测目标2.1 AQI的分项计算逻辑与标签构造方式AQI不是单一污染物的浓度而是六项污染物PM2.5、PM10、SO₂、NO₂、CO、O₃各自计算IAQI分指数后取最大值。每一项的浓度区间对应不同的IAQI分段超过100即进入轻度污染。这里有个关键细节PM2.5和PM10用的是24小时滑动平均浓度O₃用的是8小时滑动平均或1小时值取两者中较大的IAQI而SO₂、NO₂、CO用的是1小时浓度。也就是说你手里逐小时的监测数据要经过「滑动平均 → 分段线性插值 → 取最大」三步才算出一个合法的AQI值。很多初学者直接把站点原始数据里的aqi字段拿来用——这个字段通常是站点已计算好的结果但它依赖前24小时的数据如果你做的是「预测未来24小时」的任务这个字段就包含了未来信息直接当标签用就是泄漏。我一般会这样构造标签按监测站点分组对每个小时计算原始六项污染物的滑动平均浓度再按国标分段函数算出IAQI最后取最大作为该小时的AQI。预测目标有两种常见定义——预测未来1小时的AQI用于应急预警或者预测未来24小时的日均AQI用于日报发布。两者对应的特征完全不同前者可以用当前小时的全部特征后者要考虑气象预报数据和周期性特征。2.2 预测任务的数学形式与特征窗口设计把问题定义为监督回归给定t时刻及之前的特征窗口X预测tτ时刻的AQI值。τ一般取1、3、6、24小时。特征窗口W的选择决定了模型能看到多长的历史常见做法是取过去24小时或48小时。特征分三类。第一类是污染物浓度统计过去3小时、6小时、12小时、24小时的均值、最大值、最小值。第二类是气象特征温度、湿度、风速、风向、气压、降水——这些明显影响污染物扩散。第三类是时间特征小时、星期几、是否工作日、月份用来捕捉交通排放和季节变化的周期性。风向这类环形特征要拆成sin和cos两个分量直接丢数值进模型会出现「359度和1度被当成完全不同的值」的问题。还有两个实践中很容易被忽略的设计。一是滚动预测时的「自回归特征」即把模型在t时刻的预测值作为t1时刻的输入特征这会累计误差所以通常只在短期预测τ1或3时用长周期预测建议只用观测特征。二是特征的时间对齐预测t24小时意味着你只能用t时刻及之前的数据t到t24之间的任何观测值都不能进特征——这是个硬约束很多翻车事故都出在这里后面避坑章节会展开。模型选择上我优先推荐LightGBM这类梯度提升树。原因有三个第一AQI和污染物浓度之间是分段线性关系树模型能逼近非线性分段第二特征里有大量缺失值站点停电、仪器校准树模型对缺失值有原生处理第三数据量通常只有几万到几十万行深度学习训练成本高且不稳定而树模型在同等数据量下表现更稳。如果你要预测的是小时级序列且站点数据超过三年才值得考虑LSTM或Transformer方案但特征工程依然要先做好。3. 数据清洗与特征构建从原始监测记录到监督学习样本3.1 原始数据的字段结构与清洗脚本常见的空气质量原始数据是CSV格式按站点逐小时一条记录。核心字段包括站点编号、时间精确到小时、PM2.5浓度、PM10浓度、SO₂、NO₂、CO、O₃浓度以及气象字段温度、湿度、风速、风向、气压。拿到数据后的第一件事不是建模而是先做「能算出一个合法AQI」的验证——用官方算法算一遍和你数据里已有的aqi字段对齐差得太多就说明数据有脏值或者单位不一致。import pandas as pd import numpy as np df pd.read_csv(aqi_raw.csv, parse_dates[time]) df df.sort_values([station_id, time]).reset_index(dropTrue) # 浓度单位统一CO是mg/m³其余是μg/m³ unit_map {CO: 1000} # 假设原始CO单位是mg/m³转为μg/m³ for col, factor in unit_map.items(): if col in df.columns: df[col] df[col] * factor # 缺失值标记负值被视为无效测量 for col in [PM2.5, PM10, SO2, NO2, CO, O3]: df.loc[df[col] 0, col] np.nan # 按站点分组计算滑动均值时跳过NaN df df.groupby(station_id).apply( lambda g: g.assign( pm25_24hg[PM2.5].rolling(24, min_periods12).mean(), pm10_24hg[PM10].rolling(24, min_periods12).mean(), o3_8hg[O3].rolling(8, min_periods4).mean() ) ).reset_index(dropTrue)这段代码做了三件事按站点和时间排序、单位统一、以及6小时以上有效数据才计算滑动均值。min_periods12表示24小时滑动窗口至少要有12个有效值否则记为NaN——这样避免因短时间缺失导致滑动均值严重偏离。注意groupby.apply在全量数据上会比较慢如果数据超过几十万行建议改用groupby rolling组合操作。还有一个容易被忽略的问题跨天滑动窗口——rolling默认按行数滑动如果你的数据有缺失行某小时整行缺失按行数滑动就会出错。我先用reindex把每个站点的时间序列补全成连续小时再算滚动特征。3.2 构建监督学习样本的完整流程清洗完原始数据下一步是构造特征矩阵。这个步骤是AQI预测的核心工作量通常占整个项目70%的时间。我按「滞后特征 → 滑动统计 → 气象特征 → 时间特征」四步来写每一步都有对应的pandas操作。def build_features(df): df df.copy() # 1. 滞后特征过去1/3/6/12小时的关键污染物 for col in [PM2.5, PM10, O3, NO2]: for lag in [1, 3, 6, 12]: df[f{col}_lag{lag}] df.groupby(station_id)[col].shift(lag) # 2. 滑动统计过去24小时的均值/最大值/变化率 for col in [PM2.5, PM10]: for win in [6, 12, 24]: df[f{col}_mean{win}] df.groupby(station_id)[col].transform( lambda x: x.rolling(win, min_periods6).mean()) df[f{col}_max{win}] df.groupby(station_id)[col].transform( lambda x: x.rolling(win, min_periods6).max()) # 3. 气象特征风速越大越利于扩散温度与O3正相关 df[wind_dir_sin] np.sin(np.deg2rad(df[wind_dir])) df[wind_dir_cos] np.cos(np.deg2rad(df[wind_dir])) # 4. 时间特征小时周期性早晚高峰、星期、季节 df[hour] df[time].dt.hour df[dayofweek] df[time].dt.dayofweek df[is_weekend] (df[dayofweek] 5).astype(int) df[month] df[time].dt.month return df feature_df build_features(df_clean)滞后特征的shift是按站点分组的但这里有个重要细节shift是在resample补全后的连续时间序列上做的如果中间有缺失行shift(lag)取到的就不是「lag小时前」而是「lag行前」。所以建特征之前必须保证每个站点的小时序列连续——这个步骤上一步已经做了。滑动统计用了transform rolling的组合和直接rolling的区别在于它保留了原始行数适合后续合并特征。你在实际项目中建议把特征构建封装成函数这样换数据集时不用重写逻辑只要确认字段名一致即可。特征做完后检查一下特征列数——一般会生成70到90个特征如果少于30个大概率是字段名不匹配导致很多特征没建出来。3.3 训练集和验证集的切分方式时序预测的数据切分不能随机打乱。我用TimeSeriesSplit做交叉验证这是和普通K折最核心的差异。随机K折会把未来的数据混进训练集模型相当于开了天眼验证集分数虚高线上表现稀烂。from sklearn.model_selection import TimeSeriesSplit # 按时间先后切分每折训练集都在验证集之前 tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(feature_df): train_data feature_df.iloc[train_idx] val_data feature_df.iloc[val_idx] # 确保train_data最大时间 val_data最小时间 assert train_data[time].max() val_data[time].min() # 处理站点相关性同一时刻不同站点的样本不能跨折 # 如果站点少建议按时间整体切分而不是按站点切分这里有个容易踩的坑同一时间点上有多个站点的数据TimeSeriesSplit按行切分时可能把某天的A站点分到训练集、B站点分到验证集而A和B在同一时刻的AQI是高度相关的。更严谨的做法是按时间块切分比如前80%时间的所有站点进训练后20%时间的所有站点进验证。特征工程完成后再做这一步避免在切分好的数据上重新构造滞后特征导致泄漏。4. 用LightGBM训练AQI预测模型参数怎么调才算到位4.1 基线模型的训练与评估代码先跑一个LightGBM基线目标是用默认参数把流程走通确认数据没有泄漏和格式问题再进入调参阶段。评估指标用RMSE均方根误差和MAE平均绝对误差同时看AQI分级准确率——把预测值和真实值都映射到优良轻度污染等五个等级计算分类准确率这个指标更贴近业务感受。import lightgbm as lgb from sklearn.metrics import mean_squared_error, mean_absolute_error # 剔除标签和ID列特征列统一为float feature_cols [c for c in feature_df.columns if c not in [time, station_id, aqi]] X feature_df[feature_cols].astype(float) y feature_df[aqi].astype(float) # 前80%时间训练后20%验证 split_time feature_df[time].quantile(0.8) train_mask feature_df[time] split_time X_train, y_train X[train_mask], y[train_mask] X_val, y_val X[~train_mask], y[~train_mask] model lgb.LGBMRegressor( n_estimators600, learning_rate0.05, num_leaves63, max_depth7, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit(X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)]) y_pred model.predict(X_val) rmse mean_squared_error(y_val, y_pred, squaredFalse) mae mean_absolute_error(y_val, y_pred) print(fRMSE: {rmse:.2f}, MAE: {mae:.2f})这段代码里值得注意的点early_stopping用的是验证集但验证集必须有时间顺序LGBMRegressor里不传categorical_feature时所有特征都会被当成数值特征如果站点编号这类ID特征混进去了树会利用ID做记忆化导致严重过拟合——所以我在特征列筛选时把它排除掉了。另外AQI标签是浮点数不是分类标签所以用LGBMRegressor而不是LGBMClassifier。跑通基线后看一下RMSE的量级如果AQI均值在60左右RMSE低于15算合理高于25说明特征或切分有问题。常见情况是验证集RMSE远高于训练集这提示过拟合先降低num_leaves或增大min_child_samples而不是盲目加更多特征。4.2 五个关键参数的具体调法LightGBM的参数对于这个任务我一般按重要性排序调整n_estimators和learning_rate决定拟合程度和速度num_leaves控制模型复杂度min_child_samples控制过拟合subsample和colsample_bytree做随机化。下面是一组我在多个城市站点数据上调过的基础参数参数初始值调整方向说明n_estimators600结合early_stopping越多越容易过拟合用早停确定learning_rate0.05调低到0.02配合加大n_estimators低学习率泛化更好但训练慢num_leaves63数据量小降到31和max_depth配合控制树的复杂度过大直接过拟合min_child_samples20上调到50-100防止节点分裂到只有几条样本feature_fraction0.80.7-0.9随机选特征比例增强鲁棒性调参顺序上我有个习惯先固定learning_rate0.05和n_estimators600在num_leaves31/63/127三个档位快速跑一遍观察验证集RMSE选最优档位后调min_child_samples最后再调subsample和feature_fraction。不要直接上网格搜索AQI数据特征多、样本量中等全网格跑一次可能花费数小时而手动按重要性调三轮通常在半小时内能拿到接近最优的结果。另一个重要的操作是特征重要性分析importance pd.DataFrame({ feature: feature_cols, gain: model.booster_.feature_importance(importance_typegain) }).sort_values(gain, ascendingFalse) print(importance.head(15))gain类型的重要性反映特征对分裂带来的增益头部特征一般是PM2.5和PM10的滞后项、气象特征中的湿度和风速。如果时间特征排在头部说明周期性信号很强如果某个站点的lag特征排第一可疑检查是否是按行切分导致同一时刻站点特征混进了验证集。做完这步把重要性后50%的特征先删掉再训一次通常RMSE不会变差甚至更好同时训练速度提升明显。4.3 评估指标的解读与等级准确率回归指标RMSE和MAE之外一定要看等级准确率Accuracy of AQI Level。AQI分六级优0-50、良51-100、轻度101-150、中度151-200、重度201-300、严重300。把连续预测值映射到等级后计算分类准确率这是业务方最能理解的指标。def aqi_level(aqi): if aqi 50: return 0 elif aqi 100: return 1 elif aqi 150: return 2 elif aqi 200: return 3 elif aqi 300: return 4 else: return 5 y_val_level y_val.map(aqi_level) y_pred_level pd.Series(y_pred).map(aqi_level) level_acc (y_val_level y_pred_level).mean() print(fAQI等级准确率: {level_acc:.3f})等级准确率通常在0.6到0.75之间边界值比如AQI95和AQI105很难分准所以把等级准确率和RMSE一起看才有意义——RMSE下来但等级准确率没涨说明误差集中在了等级边界附近这时候可以给损失函数加权或者把预测值向边界外推一点。5. AQI预测避坑指南五条血泪经验照着查5.1 「我预测得很准」——结果发现用了未来数据现象验证集RMSE低到难以置信比同行的公开结果好一大截。原因特征或标签构造中用了未来时间点的数据。最典型的三个来源——标签用了站点计算好的aqi字段而该字段本身就是24小时滑动均值包含了未来信息滞后特征的shift忘了按站点分组把其他站点的数据错位到本站点切分时随机打乱了时间顺序。解决建特征时统一用groupbyshift切分用TimeSeriesSplit且验证集时间严格晚于训练集最后做一次特征泄漏自检——把每个特征的构建过程在纸上画一遍时间线确认每一步用到的数据都在预测时刻之前。5.2 气象站点和空气质量监测站点距离太远现象模型预测暴走出现极端值尤其冬季静稳天气。原因空气质量站和气象站往往不在同一位置你直接把气象站的温度湿度当作污染站点位置的数值两者相差几公里到几十公里时数据基本是噪声。解决先做站点匹配优先选距离最近且有完整时间覆盖的气象站。如果项目对精度要求高考虑引入气象预报数据而不是实测数据——因为预测未来24小时时你手头并没有未来的实测气象值这是一致性要求。用历史实测气象特征训练只能做t1或t3的预测做t24就得换成气象预报特征。5.3 节假日和重大活动导致预测系统性偏差现象平时RMSE在15以内一到春节出现RMSE超过40的异常点。原因工业、交通、餐饮排放的时间模式发生剧变模型没见过这种分布外样本。解决补充节假日特征不只是is_weekend还要包含春节、国庆这类长假标识把它做成一个单独的0/1特征或节日序号。如果数据足够可以训练一个「节假日单独模型」但通常加特征就够了。5.4 滑动窗口的min_periods设置不合理现象某个月份预测偏差比其他月份大检查后发现那个月的有效监测小时数只有正常月份的一半。原因min_periods设太低比如4导致滑动均值基于很少的样本计算方差极大设太高比如23则该月大量滑动均值变成NaN样本量骤减。解决min_periods设窗口的50%左右24小时窗口设128小时窗口设4。更重要的是对NaN样本不要直接删行——可以尝试用同站点前后几个小时的值做插值或者用最近一次有效值填充但一定要在填充后检查分布有没有畸变。5.5 重复训练和评估流程不稳定现象每次跑同一个脚本验证集RMSE波动超过2。原因模型初始化随机性加上验证集样本量小比如只有几千行单次分割的结果方差大。解决多跑几次取平均或者用5折TimeSeriesSplit的均值报告结果。LightGBM里固定random_state只影响初始状态特征子采样和样本子采样仍然有随机性。推荐的稳定做法跑3次不同随机种子报告RMSE的均值和标准差这比单次结果更有参考价值也能在调参时避免被噪声误导。6. 进阶把预测结果校准到AQI等级边界上模型调参到瓶颈后真正提升业务效果的是后处理校准。AQI预测的误差集中在等级边界附近一个巧妙的做法是给预测结果加一个「边界惩罚」——在训练损失函数里对处于等级边界附近比如95-105、145-155的样本加大权重让模型优先学准这些难例。另一个落地常用手段是「时序集成」训练两个模型一个用全部特征LightGBM一个只用污染物滞后特征线性回归或简单树模型最终预测取加权平均。做法不复杂但需要做的是用验证集确定权重系数而不是拍脑袋定0.5/0.5。我给一个通用代码框架from scipy.optimize import minimize def blend_weight(w): pred w * model_lgb.predict(X_val) (1 - w) * model_lr.predict(X_val) return mean_squared_error(y_val, pred, squaredFalse) # 权重限制在[0,1]初始值为0.7 res minimize(blend_weight, x0[0.7], bounds[(0, 1)], methodNelder-Mead) best_w res.x[0] print(f最优集成权重: {best_w:.2f})用验证集找到权重后在测试集上评估混合效果。我实际项目里的经验是树模型和线性模型的误差相关性低时混合能稳定降低3%到8%的RMSE如果两者相关性高混合基本没提升这时不要强行融合。如果你预测的目标是日均AQI而非小时值还有个更简单的技巧——对24小时的预测结果做指数移动平均平滑能消除夜间峰值带来的抖动让日报数字更稳定。我自己的习惯是永远保留一份「只用了当前时刻之前数据」的备份特征集每次复现实验都用它验证避免在堆特征的过程中悄悄引入泄漏。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。