☰
AQI预测实战:数据清洗、特征工程与XGBoost/LSTM建模
2026/10/13 1:34:25 网站建设 项目流程

简介:面向机器学习初学者与环境数据分析人员,这份资料以空气质量指数AQI为完整案例,系统演示了从数据清洗、缺失值处理、特征可视化到回归建模与模型评估的典型项目流程。压缩包共含33个文件,包括两个可运行的Jupyter Notebook代码笔记、一份CSV原始数据、24张分析可视化图表、交互式HTML地图以及说明文档,总大小约4.23MB,目录结构清晰,便于按数据、图表、笔记分类查找。目前已有197人学习下载,适合课程设计、毕业设计或入门实战,也可作为环境科学相关课题的参考起点。通过学习,可完整掌握pandas处理缺失值与标准化、scikit-learn构建线性回归与随机森林模型、用MSE/RMSE/R²等指标评价模型的方法;同时还能借可视化图表理解PM2.5、PM10、NO2等污染物对AQI的影响,并参考说明文档中的思路,将整套流程迁移到其他环境数据项目。

1. AQI预测为什么值得做:从“看天”到“算天”的这一步

空气质量预测这类任务,很多从业者觉得是环境部门的专活,但放到机器学习视角里,它其实是一个非常典型的回归项目——数据维度不高、规律周期性明显、评价指标清晰,特别适合用来打通“从数据到模型”的完整流程。AQI分析与预测的核心流程包括数据清洗、特征工程、模型训练与验证,而本文将围绕这套流程给出可直接复现的代码和数据处理思路。无论你是刚学完机器学习基础、想拿真实数据做一遍完整项目的新手,还是需要对短期空气质量做预判的业务技术人员,这篇文章都能帮你省掉大量试错的时间。

2. 把数据先弄干净:AQI数据集清洗与缺失值插补的实操路径

2.1 先搞清楚你的数据长什么样:字段、粒度和时间跨度

拿到AQI数据集的第一步,不是急着写训练代码,而是先用pandas把数据的“底细”盘清楚。市面上常见的AQI数据来源分两类:一类是环保监测站点的逐小时记录,字段通常包含时间戳、PM2.5、PM10、SO2、NO2、O3、CO等污染物浓度;另一类是已经聚合好的日粒度AQI指数。这两种粒度的处理方式完全不一样——小时粒度可以构建滞后特征、做短时预测,日粒度只能做趋势分析或中长期预测。

先做一次基础体检,看看数据规模、列类型和时间轴是否完整:

import pandas as pd df = pd.read_csv('aqi_data.csv', parse_dates=['timestamp']) print(df.shape) print(df.dtypes) print(df.head()) # 检查时间戳是否连续,按小时检查 full_range = pd.date_range(start=df['timestamp'].min(), end=df['timestamp'].max(), freq='h') missing_times = full_range.difference(df['timestamp']) print(f"时间戳缺失数量: {len(missing_times)}")

这个操作的核心逻辑是:先确认数据规模和每列的类型,再核对时间轴是否连续。AQI预测的根基是时间连续性,如果时间戳本身缺了一大截,后面构建的滞后特征全是错位的。参数上,parse_dates指定要解析的列名,freq='h'表示按小时检查连续性——如果你拿到的是日粒度数据,要改成freq='D'。

2.2 缺失值不是随便填:时间序列插值的三种做法

AQI数据的缺失主要来自两种情况:传感器暂时离线、数据上报丢失。填缺失值的方式会直接影响后续所有特征计算,这里没有“万能方案”,只有适合场景的方案。

我常用的做法是分三种情况处理:

# 方法1:线性插值,适合短时间缺口 df['pm25'] = df['pm25'].interpolate(method='linear', limit=12) # 方法2:前向填充,适合传感器短时停摆 df['pm25'] = df['pm25'].fillna(method='ffill', limit=6) # 方法3:同小时中位数回填,适合长时间缺失 hour_median = df.groupby(df['timestamp'].dt.hour)['pm25'].transform('median') df['pm25'] = df['pm25'].fillna(hour_median)

逻辑说明:线性插值在处理小段缺失时最自然,因为污染物浓度在短时间内是连续平滑变化的;前向填充适合传感器掉线几分钟的场景——拿最近一个观测值顶上去,误差可控;同小时中位数回填适合连续缺失几个小时甚至更久的情况,因为AQI有明显的日内周期性,拿历史同小时的中位数填充比线性外推更可靠。

参数说明:limit=12表示最多连续插值12个小时,超过这个长度就不再插值。为什么设这个值?因为超过12小时的缺口,趋势不确定性太大,线性插值会抹掉真实的环境变化过程。limit=6同理。这三种方法可以在同一套数据上组合使用,我一般的顺序是先linear插短缺口,再用ffill补小缝隙,最后用中位数回填长缺口。

2.3 异常值识别:把传感器的“抽风”和真实污染分开

在AQI数据里,“异常值”不一定是错误。比如沙尘暴天气,PM10可以一夜之间冲上800以上,这在统计上是异常,但在环境语义上是真实事件。所以异常值处理的原则是:只处理传感器故障类的异常,不处理环境突变类的异常,否则你会把真实的重污染事件当成脏数据删掉,模型就学不到极端情况的分布了。

import numpy as np # 基于滑动窗口的MAD异常检测 def flag_outliers(series, window=24): rolling_median = series.rolling(window=window).median() mad = (series - rolling_median).abs().rolling(window=window).median() threshold = 5 * mad return (series - rolling_median).abs() > threshold outlier_mask = flag_outliers(df['pm25']) print(f"疑似异常值数量: {outlier_mask.sum()}") # 对判定为传感器故障的异常值,置空后插值恢复 df.loc[outlier_mask, 'pm25'] = np.nan df['pm25'] = df['pm25'].interpolate(method='linear', limit=24)

为什么用中位数+MAD而不是均值+标准差?因为PM2.5的分布严重右偏——大部分时间浓度在30-80之间,但偶尔有几天冲到200以上。如果用均值和标准差做阈值,这些高浓度日会被误判成异常,而其实它们恰恰是预测任务里最重要的样本。window=24表示以24小时为滑动窗口,MAD(中位数绝对偏差)比标准差更抗极端值干扰。判定为异常之后,把数值置空再插值恢复,比直接删掉更稳妥——时间序列的连续性要求每一小时都有值,删掉会造成时间轴断裂。这一步做完,数据才能真正进入特征工程阶段。

3. 特征工程决定天花板:从原始污染物浓度到可用特征

3.1 时间特征:把“小时”“星期几”“季节”变成模型能用的东西

AQI有明显的多重周期性:一天之内早晚高峰导致污染物浓度呈双峰分布,一周之内工作日和周末的排放强度不同,一年之内秋冬高发季和夏季扩散季差异悬殊。这些周期如果不显式地告诉模型,线性模型和树模型都很难自己“发现”规律。所以时间特征往往是第一个要做的,也是最便宜的提升手段。

df['hour'] = df['timestamp'].dt.hour df['dayofweek'] = df['timestamp'].dt.dayofweek df['month'] = df['timestamp'].dt.month df['is_weekend'] = (df['timestamp'].dt.dayofweek >= 5).astype(int) # 对小时做sin/cos循环编码 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)

这里的关键是hour_sin和hour_cos。为什么要对小时单独做sin/cos编码?因为小时是一个循环变量:23点和0点在数值上相差23,但在周期性语义上是相邻的。如果直接喂原始整数,树模型会把23和0当作两个极端值去切分,而实际上它们应该共享相似的污染特征。sin/cos编码把这个循环关系展开成二维平面上的点,模型能更自然地学到周期性。dayofweek和month我一般直接用整数编码,它们的周期性不如小时那么强,做sin/cos的提升有限,反而可能增加特征维度。

3.2 滞后特征与滚动统计:让模型“看到”历史

AQI和股票价格逻辑类似——当前时刻的浓度和过去几小时、过去一天的浓度高度相关。构建滞后特征(lag features)和滚动统计特征是让模型具备“记忆”的关键手段。这一节是整个特征工程里最能提升模型效果、也最容易翻车的地方。

# 滞后特征:过去1、3、6、24小时的PM2.5 for lag in [1, 3, 6, 24]: df[f'pm25_lag_{lag}'] = df['pm25'].shift(lag) # 滚动统计:过去6小时和24小时的均值、标准差 for window in [6, 24]: df[f'pm25_rolling_mean_{window}'] = df['pm25'].rolling(window=window).mean() df[f'pm25_rolling_std_{window}'] = df['pm25'].rolling(window=window).std() # 删除因滞后产生的NaN行 df = df.dropna()

滞后特征的原理是把时间序列“摊开”成一个表格:每一行的目标值是当前小时的PM2.5,特征里包含过去若干小时的数值。模型就能学到这样的规律:如果过去3小时浓度持续上升,那当前小时大概率也不会低。滚动统计则提供一种平滑化的历史视图,对短期波动不那么敏感——当你想预测的是一天之后的AQI,24小时均值比单点滞后值更有参考价值。

这里要特别提醒:所有的shift和rolling必须保证DataFrame的行顺序是严格按时间升序的。如果你前面做过任何排序或筛选,都要先df.sort_values('timestamp', inplace=True)再执行这一步。另外,构建完滞后特征后应该立即dropna(),否则模型会把NaN当成一个有效值去学习。

3.3 要不要外接气象数据:一个成本收益问题

这个问题在AQI预测的圈子里几乎没有争议:要,但不是必须的。温度、湿度、风速、气压对AQI短期变化影响显著——风速大有助于污染物扩散,湿度高容易促进二次颗粒物生成,逆温层会让污染物在地面堆积。但外接数据的成本也很高:获取渠道不稳定、气象站和监测站的空间覆盖不完全匹配、时间对齐需要额外处理。

我一般会建议分阶段推进。第一版模型只用污染物自身的滞后特征和滚动统计,跑出一个基线RMSE。然后尝试接入气象数据,看RMSE的降幅。如果下降超过5%,说明气象信息对当前站点的AQI变化有真实解释力,值得维护这条数据通路;如果只下降1-2%,说明这个站点的污染主要来自本地排放而非气象扩散条件,就不必在气象数据上投入时间。没有标准答案,数据会告诉你答案。

4. 建模选型与训练:XGBoost和LSTM在AQI预测上的分工

4.1 基线先行:线性回归告诉你能做到什么程度

任何一个预测任务,都应该先跑一个最简单的模型,目的是给后续的所有复杂模型定一个“及格线”。如果XGBoost连线性回归都赢不了太多,说明问题出在特征工程上,而不是模型不够强大。线性回归还有另一个额外价值:它的系数可以直接解释每个特征对AQI的影响方向,帮你提前发现特征工程里的低级错误。

from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error # 准备特征列和目标列 feature_cols = [c for c in df.columns if c.startswith('pm25') or c in ['hour_sin', 'hour_cos', 'dayofweek', 'month', 'is_weekend']] target_col = 'pm25' # 已通过shift(1)对齐到“下一小时” # 严格按时间顺序划分 cutoff = int(len(df) * 0.8) train, test = df.iloc[:cutoff], df.iloc[cutoff:] model = LinearRegression() model.fit(train[feature_cols], train[target_col]) pred = model.predict(test[feature_cols]) rmse = mean_squared_error(test[target_col], pred, squared=False) print(f"Linear Regression RMSE: {rmse:.2f}")

这里的两个关键点:第一,cutoff按时间切分而不是用train_test_split随机分隔,因为时间序列的前后样本高度相关,随机划分会在训练集里混入“未来”的信息,让验证结果虚高。第二,squared=False让mean_squared_error直接返回RMSE而不是MSE,省一步开方。跑完之后,看一眼线性模型的系数:如果pm25_lag_1的系数是负的,你就要警惕了——过去一小时浓度越高、当前小时反而越低,这不符合物理常识,通常意味着数据里有错位或填充不当的问题。

4.2 XGBoost:表格型预测的主流选择,参数这样调

对表格型数据,XGBoost是当前最稳的选择之一。它兼顾非线性拟合能力、对缺失值的容忍度和训练速度。相比线性回归,它最大的优势是能捕捉污染物浓度在不同区间的非线性关系——比如PM2.5在50以下时对风速不敏感,但超过100后风速的影响显著增强,这种阈值效应靠线性模型是学不出来的。

import xgboost as xgb X_train = train[feature_cols].values y_train = train[target_col].values X_test = test[feature_cols].values y_test = test[target_col].values dtrain = xgb.DMatrix(X_train, label=y_train) dtest = xgb.DMatrix(X_test, label=y_test) params = { 'objective': 'reg:squarederror', 'max_depth': 6, 'eta': 0.05, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_weight': 2, 'eval_metric': 'rmse', } model = xgb.train(params, dtrain, num_boost_round=500, evals=[(dtest, 'test')], early_stopping_rounds=50)

evals传入验证集后,每一轮迭代都会打印验证集RMSE;early_stopping_rounds=50表示连续50轮验证集没有改善就提前停止,你不需要手动扫描最优的num_boost_round。num_boost_round=500只是一个上限,实际训练通常用不到就会被早停截断。

调参的基本原则:先固定eta=0.05,扫描max_depth(从4到8),选定深度后再调subsample和colsample_bytree,最后微调min_child_weight。顺序不要反——深度对模型容量的影响最大,先调它最能拉开差距。subsample=0.8表示每棵树随机使用80%的样本,可以防止过拟合,尤其适合AQI这种周期性很强的数据。

4.3 LSTM:时间序列的另一种解法,适合什么样的场景

LSTM和XGBoost的路线完全不同。XGBoost需要你手工构造滞后特征,LSTM直接把原始序列喂进去,让网络自己学习“过去多久的信息对当前有用”。理论容量更大,但对数据量、训练时长和调参技巧的要求都更高。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from sklearn.preprocessing import MinMaxScaler import numpy as np # 用过去24小时的PM2.5序列预测下一个小时候的PM2.5 def make_sequences(data, lookback=24): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i]) y.append(data[i]) return np.array(X), np.array(y) scaler = MinMaxScaler() pm25_scaled = scaler.fit_transform(df[['pm25']]) X_seq, y_seq = make_sequences(pm25_scaled.flatten()) split = int(len(X_seq) * 0.8) X_train_seq, X_test_seq = X_seq[:split], X_seq[split:] y_train_seq, y_test_seq = y_seq[:split], y_seq[split:] model = Sequential([ LSTM(32, input_shape=(24, 1), activation='tanh'), Dense(1) ]) model.compile(optimizer='adam', loss='mse') history = model.fit(X_train_seq, y_train_seq, validation_data=(X_test_seq, y_test_seq), epochs=30, batch_size=64, verbose=1)

lookback=24使用过去24小时的数据预测下一小时,这是一个经验值——AQI的自相关通常在24到48小时范围内显著,超过这个长度会引入大量噪声。activation='tanh'是LSTM的默认激活函数,不建议改成relu,因为会导致梯度不稳定。MinMaxScaler把数据压缩到0到1区间,LSTM对输入尺度敏感,标准化后训练收敛更稳定。

LSTM的固有问题是:如果你的数据量只有几千行,它的效果大概率不如XGBoost——这不是模型本身的错,是数据容量撑不起深度模型的参数量。如果一定要用LSTM,建议至少积累两年以上的小时级数据,并且用24小时窗口做多步预测,这样比单步预测有实际指导意义,也能更好发挥LSTM在序列建模上的优势。

5. AQI预测避坑指南:五个常见坑和对应解法

5.1 数据泄漏的两种隐蔽形态:随机打乱和标准化泄漏

坑1:随机打乱数据集,验证集“偷看”未来

现象:模型在训练集上RMSE很低,测试集上表现也不错,可一放到真实场景里预测未来,误差立刻翻倍。

原因:很多人直接用train_test_split默认的随机分隔方式,AQI时间序列的相邻样本高度相关,训练集里混入了目标时间点前后的数据,模型等于“开卷考试”。

解决:换成按时间顺序切分,用df.iloc[:cutoff]和df.iloc[cutoff:],并且训练和推理时保证时刻一致。

坑2:标准化时把测试集信息泄漏进来

现象:用全量数据的均值和标准差做StandardScaler,再切分训练集和测试集,验证集效果不错,上生产环境就崩。

原因:标准化器的mean_和scale_参数包含测试集信息,模型在训练时“间接见过”测试集的分布,这属于典型的数据泄漏。

解决:先切分,再scaler.fit(train_data),然后分别transform训练集和测试集。推理时直接使用这个已保存的scaler,确认推理数据的字段顺序与训练时一致。

5.2 训练和评估层面的坑:普通K折与滞后特征堆叠

坑3:用普通K折交叉验证给时间序列打分

现象:5折交叉验证每折的得分都很好,换成滚动预测验证效果却一塌糊涂。

原因:KFold随机分块破坏了时间顺序,训练集里可能包含未来的样本,模型学到了时间上的“穿越”规律。

解决:改用TimeSeriesSplit做交叉验证,它保证每一折的训练集都严格早于验证集。如果不介意额外工作量,直接用滚动预测窗口做评估,更贴近实际使用场景。

坑4:滞后特征堆太多,模型“只会念旧”

现象:XGBoost的特征重要性排名里,pm25_lag_1排在第一位,预测曲线看起来平滑,但提前24小时的预测几乎就是现状平移,没有真正的预判能力。

原因:滞后特征和目标变量之间的相关性太高,模型偷懒了——既然lag_1和目标高度相关,它不需要学习真正的气象驱动机制,只要照着上一个小时的数据平移就能把训练误差压得很低。

解决:如果要预测未来24小时,把滞后特征的窗口整体往前推24小时——比如用shift(25)、shift(27)、shift(30)、shift(48),确保训练时特征和目标之间有真实的时间间隔。模型被迫去学习趋势变化,而不是复制粘贴。

5.3 结果判断层面的坑:只盯着整体RMSE

坑5:只看整体RMSE,忽略高值段的预测能力

现象:整体RMSE在30左右,看着不错,但把预测值和真实值画成散点图,发现AQI超过150的高值段,预测值全部偏低,重污染事件几乎完全错过。

原因:RMSE被大量“普通时段”的数据主导。AQI在50到100的时间占了绝大多数,模型为了最小化整体损失,倾向于把预测值压缩到均值附近,用普通时段的精度换极端时段的误差。

解决:增加分段评估指标,对AQI大于100的子集单独计算RMSE。如果项目要求重点预报重污染过程,在损失函数里给高值段加权——XGBoost里可以设置scale_pos_weight,或者通过自定义损失函数实现。

6. 验证和落地:从模型出来到“敢不敢真用”的最后一公里

6.1 滚动预测的可视化验证

模型训练完后,把测试集的预测值和真实值画在一张图上,很多人只扫一眼整体拟合程度就算完事。我一般会盯三个位置:夜间低谷、早晨高峰、极端污染事件的上升段。夜间低谷代表模型是否记住了周期性,早晨高峰代表特征提取是否有效,极端污染事件的上升段代表模型泛化能力的上限。这三个位置如果同时画得好,这个模型才有资格进入下一步。

6.2 我给模型打分的三个固定习惯

第一个习惯:永远单独留一个“最差时段”的验证集——比如冬季高污染月的数据不参与训练,专门用来做压力测试。普通时段的RMSE好看没有意义,重污染过程能不能提前预警才是AQI预测的价值所在。

第二个习惯:预测步长从短到长逐一验证。单步预测误差小说明模型学到了短期惯性,24小时预测仍然稳定说明模型学到了真正的驱动因子。两步之间的差距往往会暴露模型的短板。

第三个习惯,也是我最看重的:检查预测曲线的“物理合理性”。AQI不会在半小时内从50跳到300,除非有沙尘暴或突发的工业事故。如果预测曲线出现不该有的剧烈抖动,哪怕RMSE结果很好,我都要回去检查特征工程的窗口设置。模型可以没有物理直觉,但人必须替它把关。

6.3 一个教训换来的固定检查

我第一次做AQI预测时,把滞后特征的shift方向搞反了——本应该用t-1时刻的值预测t时刻,结果写成了用t时刻的值预测t-1时刻,模型在验证集上拿到了极低的RMSE。当时还以为是模型调参的功劳,后来才发现这其实是数据泄漏的一种变体。那次之后我养成了一个习惯:每次构建特征工程,先把所有特征列和目标列的滞后偏移量打印成一张配置清单,保存到项目目录里。之后每次跑模型前都要核对一遍,确认特征确实在时间上早于目标。

这个习惯后来救过我很多次,不只是AQI项目,所有时间序列任务我都这么做。做预测这行,很多时候翻车不是因为模型不够先进,而是因为数据准备阶段一个不经意的错位。希望这次的AQI分析与预测流程梳理和避坑记录能帮到你——尤其是那些看起来不起眼、却能让模型“虚假繁荣”的细节。

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

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

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

立即咨询