
简介面向空气质量预测方向的数据爱好者与机器学习初学者这套资源以北京35个站点未来两天PM2.5、PM10、O3浓度预测为场景利用XGBoost与LSTM两类模型完整走通数据清洗、特征工程、模型训练与效果评估流程。包内共7个文件三个ipynb文件分别承担数据预处理、XGBoost建模与LSTM建模一个py脚本用于模型效果评估另附三个CSV数据集涵盖站点监测、天气观测与网格气象数据便于按步骤复现实验。整个压缩包约9.95MB轻量易用适合在校学生、初级算法工程师快速上手也便于课堂演示和自学演练。已有734人学习可见其实用性。读者可同时获得完整源码、整理好的数据集以及分阶段笔记能较快理清特征筛选、模型调参与结果对比的关键点也能减少环境配置和数据清洗方面的踩坑时间。1. 污染物浓度预测为什么值得用XGBoost和LSTM一起做做空气质量预测的人大多有这种体会单一模型跑到最后误差曲线总卡在一个平台上不去。用XGBoost能拿到稳定的特征重要性排序却抓不住污染物浓度在凌晨和早晚高峰的时序惯性换成LSTM序列记忆是有了可遇到气象突变时又容易跟着异常值一起飘。把这两种模型串起来做污染物浓度预测本质上是让树模型负责“这小时和上一小时差多少”的回归拟合让循环神经网络负责“过去十几个小时是怎么演变的”的时序记忆——两者互补比单模型在RMSE上低 10%20% 是常见结果。这篇笔记面向手里已经有监测数据、想跑通一套完整 Python 源码的从业者从数据切分讲到 XGBoost 基线、LSTM 时序建模再到融合与验证。你会拿到能直接改路径就跑的代码也会看到我在真实数据上踩过的几个坑。2. 先备料拿到监测数据后做特征工程与样本切分2.1 数据长什么样浓度序列加气象外生变量污染物浓度预测的输入通常是两类数据一是目标污染物自身的历史浓度序列比如 PM2.5、NO2 或 O3 的逐小时监测值二是外生变量常见的是温度、湿度、风速、风向、气压这些气象观测值。部分场景还会加入时间特征——小时、星期几、是否节假日。我自己一般会把数据整理成一张宽表每一行是一个时间点列依次是time, pm25, temp, humi, wind_speed, pressure, hour, weekday。这样后面做特征工程和样本切分都方便。数据来源无论是国控站点 CSV 还是气象站导出的 Excel第一步都统一转成这种格式列名用英文小写加下划线避免后面编码问题。import pandas as pd df pd.read_csv(air_quality_hourly.csv, parse_dates[time]) df df.sort_values(time).reset_index(dropTrue) # 统一列名缺失值先用前向填充顶住 df.rename(columns{ PM2.5: pm25, TEM: temp, RH: humi, WSPD: wind_speed, PRES: pressure }, inplaceTrue) # 时间特征 df[hour] df[time].dt.hour df[weekday] df[time].dt.weekday # 缺失值处理先向前填充再线性插值补中间空洞 df df.interpolate(methodlinear, limit_directionboth) print(df.isnull().sum())这里有个细节interpolate在浓度序列中间有连续缺失时比fillna(0)靠谱得多因为污染物浓度是连续变化的物理量线性插值至少不会制造假的数值跳变。但要注意limit_directionboth只对边界缺失有效如果中间是一整段停机维护造成的空洞线性插值会画出虚假的直线这种情况应该直接删掉那几行而不是补出来。2.2 特征滞后与窗口构造让模型看见“昨天”树模型和 LSTM 对“历史信息”的利用方式不同。XGBoost 需要我们把过去几小时的浓度作为显式特征喂给它比如pm25_lag1表示上一小时浓度pm25_lag24表示昨天同一时刻的浓度。LSTM 则更希望输入是一个时间窗口序列把连续多个时间步的原始终端进去由循环结构自己提炼时序依赖。我常用的构造方式是同时准备两份数据。一份是扁平特征表给 XGBoost包含滞后 1、2、3、24 小时的浓度和气象数据另一份是三维张量给 LSTM形状是(样本数, 时间步长, 特征数)时间步长通常取 12 或 24。def make_lag_features(df, targetpm25, lags[1, 2, 3, 24]): df df.copy() for lag in lags: df[f{target}_lag{lag}] df[target].shift(lag) # 删除前 24 行因为滞后特征里会出现 NaN return df.dropna().reset_index(dropTrue) df_lag make_lag_features(df) print(df_lag.head(3))2.3 时间序列切分不能乱 shuffle按时间顺序切很多人在这一步翻车。普通回归任务随机切分训练集和测试集没问题但时间序列数据一旦 shuffle模型就偷偷看到了未来的信息测试集上的表现会虚高。正确做法是按时间顺序切成三段训练集、验证集、测试集验证集用来调超参和早停测试集放在最后模拟“未知未来”。我会按 7:2:1 切但这里有个关键点——切分点必须落在“完整一天”上避免把同一天的数据切进训练和测试两个集合里。污染物浓度的日周期性很强如果中午 12 点的数据进了训练集、下午 1 点进了测试集那测试集就失真了。train_size int(len(df_lag) * 0.7) val_size int(len(df_lag) * 0.2) test_size len(df_lag) - train_size - val_size train_df df_lag.iloc[:train_size] val_df df_lag.iloc[train_size:train_size val_size] test_df df_lag.iloc[train_size val_size:] print(ftrain: {len(train_df)}, val: {len(val_df)}, test: {len(test_df)})3. XGBoost做基线三行代码跑起来再调四个关键参数3.1 为什么先跑XGBoost而不是直接上LSTM每次做预测项目我习惯先拿 XGBoost 打底。原因有三一是训练快CPU 上几分钟就能出一版结果方便快速验证特征是否有效二是它能直接输出特征重要性帮助判断哪些变量对污染物浓度影响最大有没有冗余三是 XGBoost 对缺失值和异常值的鲁棒性比线性模型好很多拿到脏数据也能先跑通流程。XGBoost 在这里的角色不是“最终模型”而是基线与特征筛选工具。如果 XGBoost 的 RMSE 已经不错说明特征里有足够的信息量如果 XGBoost 表现很差那大概率是特征工程没做好这时候调 LSTM 也不会化腐朽为神奇。import xgboost as xgb from sklearn.metrics import mean_squared_error feature_cols [temp, humi, wind_speed, pressure, hour, weekday, pm25_lag1, pm25_lag2, pm25_lag3, pm25_lag24] X_train train_df[feature_cols] y_train train_df[pm25] X_val val_df[feature_cols] y_val val_df[pm25] X_test test_df[feature_cols] y_test test_df[pm25] model_xgb xgb.XGBRegressor( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, early_stopping_rounds20, random_state42 ) model_xgb.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse) y_pred_xgb model_xgb.predict(X_test) rmse_xgb mean_squared_error(y_test, y_pred_xgb, squaredFalse) print(fXGBoost RMSE: {rmse_xgb:.2f})3.2 四个必调参数树深、学习率、子采样、早停XGBoost 参数很多但做污染物浓度预测这类中等规模表格数据真正影响结果的主要是四个max_depth控制树深度。浓度预测的特征一般是几十个到上百个深度设到 46 就够。太深容易记住滞后特征里的噪声我见过有人设到 12训练集 RMSE 很低但测试集一塌糊涂。learning_rate和学习率配合n_estimators一起看。学习率 0.05 时300500 棵树是比较稳的组合学习率调到 0.1 就要把树数降到 150 左右否则过拟合。subsample和colsample_bytree是防止过拟合的双保险。分别控制样本采样和特征采样比例0.70.9 之间常见。污染物数据往往有站点特异性和季节特异性这两个参数能帮模型不那么“死记硬背”。early_stopping_rounds不是调参参数是训练控制参数。设 20 表示验证集误差连续 20 轮没改善就停止训练。这个参数能省大量时间也能自动确定最佳树数不需要手动调n_estimators到底。# 用早停后的最佳迭代次数重新查看特征重要性 importance pd.Series(model_xgb.feature_importances_, indexfeature_cols).sort_values(ascendingFalse) print(importance)这里feature_importances_输出的是增益占比不是次数占比。数值表示每个特征对模型损失的贡献比例可以用来判断哪些特征值得留、哪些可以丢。比如如果weekday的贡献不足 0.01说明星期几对这个站点的浓度影响微弱可以去掉减少噪音。4. LSTM补上时序记忆构造时间步样本与训练循环4.1 把表格数据变成时间步样本XGBoost 每个样本是独立的即便加了滞后特征它对昨天同一时刻的浓度变化的利用也只是“特征值”而不是“演变过程”。LSTM 的输入需要是连续窗口让模型看到浓度从昨天中午到今天中午是怎么一步步变化的。构造 LSTM 样本时我用时间步长 24意思是拿过去 24 小时的数值预测下一个小时的浓度。这个 24 不是拍脑袋定的它对应一天的周期能让模型学到“今天的浓度和昨天同一时刻相似”这类规律。特征窗口包含浓度滞后序列和同期气象数据LSTM 会自己决定哪些时刻更重要不需要我们显式构造滞后项。import numpy as np from sklearn.preprocessing import MinMaxScaler # 归一化LSTM 对输入尺度敏感必须缩放到 0~1 feature_list [pm25, temp, humi, wind_speed, pressure, hour, weekday] scaler MinMaxScaler() scaled_data scaler.fit_transform(df[feature_list].values) def create_sequences(data, seq_len24): X, y [], [] for i in range(seq_len, len(data)): X.append(data[i - seq_len:i]) y.append(data[i, 0]) # 第 0 列是 pm25 return np.array(X), np.array(y) X_seq, y_seq create_sequences(scaled_data, seq_len24) print(X_seq.shape, y_seq.shape) # (样本数, 24, 7)注意这里有几个细节。第一y取的是data[i, 0]因为feature_list第 0 列是pm25目标变量必须在第 0 列后续反归一化才方便。第二hour和weekday也被一起归一化了这没问题LSTM 能把它们当作循环特征来理解周期性。第三切分要在序列构造之后做不能切完再构造序列否则会有穿越。4.2 搭建 LSTM 网络两层结构加 DropoutLSTM 的层数、神经元数与数据量直接相关。污染物浓度数据量一般是几千到几万条两层 LSTM 是稳妥选择——单层往往表达能力不足三层以上在小数据上又容易过拟合。第一层设 64 个神经元返回序列给第二层第二层设 32 个神经元只返回最终输出。每层之间加 Dropout 0.2抑制过拟合。输出层是单节点线性激活因为回归问题不需要压缩到 01。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model_lstm Sequential([ LSTM(64, return_sequencesTrue, input_shape(X_seq.shape[1], X_seq.shape[2])), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) ]) model_lstm.compile(optimizeradam, lossmse, metrics[mae]) model_lstm.summary()4.3 训练参数与反归一化训练时我用batch_size32、epochs100搭配早停。batch_size太小会造成梯度震荡太大则收敛慢32 是中小数据集的常见起点。损失函数用 MSE因为均方误差对大误差的惩罚更重这对污染物浓度预测是合理的——浓度突变往往是污染事件宁可保守也别离谱。早停设置在验证集损失上patience10。污染物数据噪声大验证损失曲线跳跃明显patience 太短容易在局部低点停下。early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model_lstm.fit( X_train_seq, y_train_seq, validation_data(X_val_seq, y_val_seq), batch_size32, epochs100, callbacks[early_stop], verbose1 ) # 反归一化只还原目标变量那一列 y_pred_lstm model_lstm.predict(X_test_seq) # 构造一个和 scaled_data 同形状的数组只取第 0 列做 inverse_transform y_pred_full np.zeros((len(y_pred_lstm), scaled_data.shape[1])) y_pred_full[:, 0] y_pred_lstm[:, 0] y_pred_inv scaler.inverse_transform(y_pred_full)[:, 0] rmse_lstm mean_squared_error(y_test_actual, y_pred_inv, squaredFalse) print(fLSTM RMSE: {rmse_lstm:.2f})这段反归一化是新手最容易卡住的地方。scaler.inverse_transform需要输入和原数据有相同的列数所以先造一个空数组把预测值填到第 0 列再整体转换最后取第 0 列得到真实尺度的预测值。5. XGBoost与LSTM的5个高频踩坑现象、原因与解决5.1 测试集 RMSE 低得离谱原来是归一化泄漏现象LSTM 在验证集上表现正常测试集 RMSE 比训练集还低明显不对劲。原因scaler.fit_transform用全量数据拟合了最小值和最大值包括测试集的信息。归一化参数里混入了未来数据等于是偷偷给模型泄露了测试集的取值范围。解决先切分再归一化。用训练集的数据fit然后用训练集的参数transform验证集和测试集保证归一化参数只来自训练数据。scaler MinMaxScaler() scaler.fit(train_df[feature_list].values) train_scaled scaler.transform(train_df[feature_list].values) val_scaled scaler.transform(val_df[feature_list].values) test_scaled scaler.transform(test_df[feature_list].values)5.2 XGBoost 特征重要性全是滞后项气象特征贡献为零现象特征重要性排序里前几名全是pm25_lag1、pm25_lag2温度湿度重要性接近 0。原因滞后项与目标变量的相关性极强树模型每次都优先选它们做分裂。气象特征不是没用而是被滞后项“抢了功劳”。解决把滞后项删掉一部分再训练一版看气象特征的贡献是否恢复。如果想保留滞后项就用feature_importance做参考但别直接删气象特征——在预测未来 24 小时时滞后项会逐渐失去作用气象特征反而成为主要信号。5.3 LSTM 训练损失降到 0.001预测值却是一条水平线现象训练过程 loss 很低但画出预测曲线后发现模型输出几乎是一条直线完全没有波动。原因损失函数用了 MSE污染物浓度存在大量低值时段模型发现“预测均值附近的值”比“预测准确峰值”更划算。这是回归任务里典型的均值回归问题。解决两种方案。一是把目标变量做差分预测“下一小时相对上一小时的增量”让模型不再直接预测绝对浓度这个我试下来效果最明显二是对峰值样本加权重在损失函数里按真实浓度大小加权让模型更关注高浓度时刻的拟合精度。# 差分目标预测增量而非绝对值 df[pm25_diff] df[pm25].diff() df df.dropna().reset_index(dropTrue) # 后续建模以 pm25_diff 为目标预测结果累加回去5.4 时间步长从 12 改成 24效果反而变差现象把 LSTM 时间步长从 12 加到 24期望模型能学到日周期结果验证集 RMSE 不降反升。原因时间步长翻倍意味着每个样本的输入维度翻倍模型参数量相应增加但训练样本总数没变。数据量不足时长窗口带来的表达能力提升抵不过过拟合的风险。解决先跑短窗口 12 做基线再加到 24 对比效果。如果数据量在 5000 条以上一般 24 步是安全的如果只有 2000 条左右12 步是更稳妥的选择。5.5 模型对极端污染事件预测严重偏低现象遇到重污染天气真实浓度从 50 跳到 200模型预测只有 80严重跟不上。原因极端事件在数据集中占比小模型在训练时没见够足够多样的极端样本自然学不会这种跳跃。解决数据层面可以做重采样把高浓度样本复制几份模型层面可以调高subsample让每棵树见过的极端样本比例不降得太低特征层面可以加入“上游站点浓度”这个外生变量重污染是区域性的上游站点的浓度是下游站点突变的最强预报因子。6. 两种模型的融合策略与预测效果验证方法融合不是简单平均。XGBoost 和 LSTM 的误差结构不同取平均虽然能降一点 RMSE但往往不是最优解。我常用的做法是先把两个模型的预测值作为新特征用岭回归或线性回归学习“什么时候该信谁”。from sklearn.linear_model import Ridge # 用训练集的预测结果训练融合权重 stack_train np.column_stack([pred_xgb_train, pred_lstm_train]) stack_val np.column_stack([pred_xgb_val, pred_lstm_val]) meta_model Ridge(alpha1.0) meta_model.fit(stack_train, y_train_actual) stack_test np.column_stack([pred_xgb_test, pred_lstm_test]) final_pred meta_model.predict(stack_test)验证时不要只看 RMSE要看三个东西整体 RMSE、峰值时段的 RMSE、以及预测残差的自相关性。如果残差存在明显的时间相关性说明模型没把时序信息榨干还有改进空间。我会画一张残差分布图按小时分组看一天中哪些时段预测偏差最大——通常早晚高峰是重灾区这时候单独对高峰时段调权重比全局调参更有效。最后说一个我的习惯每跑一版模型就把当时的特征列表、参数、RMSE 记在一个 CSV 里。半年后回头翻记录你能清楚看到哪些调整真正有效哪些是白费功夫。希望帮到你。本文还有配套的精品资源点击获取