随机森林空气质量预测实战:从数据清洗到模型调优的完整指南 简介这份资源面向数据挖掘初学者与希望掌握空气质量预测建模的开发者提供一套基于随机森林算法的完整实战方案帮助读者理解从数据预处理到模型评估的全流程。压缩包共3个文件包含1个ipynb代码笔记、1个csv数据集和1个html结果页面整体约616KB轻量便于快速上手。其中csv文件为更新后的污染数据集ipynb记录特征工程与随机森林建模过程html则呈现分析结果方便对照阅读。目前已有129人学习下载适合作为课程设计或入门项目的参考。读者可从中获得可运行的建模代码、真实空气质量数据以及完整的分析思路既能复现随机森林预测流程也能在此基础上调整参数、替换数据拓展自己的数据挖掘实践能力。1. 空气质量预测这件事随机森林为什么能先跑起来空气质量预测的刚需场景很具体环保部门要提前 24 小时判断是否启动重污染预警工厂要据此安排限产节奏城市管理者要决定洒水降尘的时段。这些决策都卡在一个问题上——明天 PM2.5 会不会超标。传统做法是跑 CMAQ、CAMx 这类化学传输模型物理机制清楚但输入数据要求极高算一次动辄几小时中小团队根本扛不住。数据挖掘的思路反过来不追求把大气化学反应写清楚而是从历史监测数据里学出「什么样的气象条件加前一天的污染物浓度会推出第二天的值」。随机森林回归算法在这个任务上几乎是默认起点原因有三它对特征量纲不敏感温度、湿度、风速、PM2.5 浓度混在一起不用归一化它能输出特征重要性帮你判断哪个气象因子在主导它不容易过拟合调参空间比 XGBoost 小得多新手跑通的门槛低。这篇笔记就围绕「数据集代码」这个组合把从拿到一份空气质量历史数据到跑出一个能用的随机森林污染预测模型的完整路径讲清楚包括特征怎么构造、参数怎么设、评估指标怎么看、哪些坑会让你的 R² 虚高。2. 拿到数据集先别急着建模字段清洗与特征构造2.1 空气质量数据集里到底有什么哪些字段能用一份典型的城市空气质量历史数据集通常来自国控站点的小时级或日级记录字段大致分四类。第一类是污染物浓度PM2.5、PM10、SO2、NO2、CO、O3单位一般是 μg/m³CO 是 mg/m³。第二类是气象条件温度、气压、湿度、风速、风向。第三类是时间戳年、月、日、小时。第四类是站点标识和空气质量等级AQI 或等级标签。真正能进模型的是前三类里经过构造的数值特征。AQI 等级这种标签字段要小心——如果你用当天 AQI 去预测当天 PM2.5那是数据泄漏模型会给出漂亮但毫无意义的结果。常见做法是预测目标设为「未来 24 小时的 PM2.5 浓度」输入特征只用当前时刻及之前的数据。风向是个特殊字段。它本质是角度0° 和 360° 在数值上差很远但物理上是一回事。直接当数值喂进去模型会学出错误的关系。我一般把它拆成 sin 和 cos 两个分量import pandas as pd import numpy as np # df 为原始数据含 wind_direction 角度字段0-360 df[wd_sin] np.sin(np.deg2rad(df[wind_direction])) df[wd_cos] np.cos(np.deg2rad(df[wind_direction])) # 原始角度列丢弃避免模型误用线性关系 df df.drop(columns[wind_direction])这段代码的逻辑是把周期性变量映射到单位圆上让 0° 和 359° 在特征空间里距离很近。参数上np.deg2rad负责角度转弧度如果你数据里已经是弧度就直接用。拆完之后 sin 和 cos 两列都要保留只留一个会丢失方向信息。2.2 时间滑窗特征让模型看到「趋势」而不是「快照」单看当前时刻的 PM2.5模型学不到累积效应。空气污染有很强的滞后性——昨天没散掉的污染物会叠加到今天。所以要做滑窗统计把过去若干小时的均值、最大值、变化量作为新特征。# 按站点分组后做时间滑窗避免不同站点数据串行 df df.sort_values([station_id, datetime]) window_sizes [3, 6, 12, 24] for w in window_sizes: df[fpm25_roll_mean_{w}h] ( df.groupby(station_id)[PM2.5] .transform(lambda x: x.rolling(w, min_periods1).mean()) ) df[fpm25_roll_max_{w}h] ( df.groupby(station_id)[PM2.5] .transform(lambda x: x.rolling(w, min_periods1).max()) ) # 构造变化量特征当前值减去 24 小时前 df[pm25_delta_24h] df[PM2.5] - df.groupby(station_id)[PM2.5].shift(24)逻辑说明groupby(station_id)是关键多站点数据如果不分组滑窗会把 A 站点的数据算到 B 站点头上。min_periods1保证序列开头不会产生 NaN。transform保证返回的索引和原表对齐。参数上窗口大小 3/6/12/24 小时是常见配置覆盖短时波动到日周期如果你的数据是日级窗口就改成 2/3/7 天。pm25_delta_24h捕捉的是「比昨天同一时刻好了还是坏了」这个特征在重污染过程预报里往往比绝对值更重要。2.3 缺失值处理空气质量数据的插补策略空气质量数据缺失是常态——设备校准、断电、通信故障都会造成空洞。处理方式取决于缺失比例。单列缺失低于 5%用前后值线性插值5% 到 20%用该站点同时刻的历史均值填充超过 20%这一列直接弃用硬填会引入偏差。# 按站点分组做时间序列插值 df[PM2.5] ( df.groupby(station_id)[PM2.5] .transform(lambda x: x.interpolate(methodlinear, limit6)) ) # 剩余缺失用同站点同小时的月中位数填充 df[PM2.5] df.groupby([station_id, hour])[PM2.5].transform( lambda x: x.fillna(x.median()) ) # 仍然缺失的行直接删除 df df.dropna(subset[PM2.5])limit6表示连续缺失超过 6 个点就不插值了避免用线性外推编造长段数据。第二步用同站点同小时的中位数是因为污染物浓度有明显的日变化规律用全局均值会抹掉这个规律。最后dropna是兜底保证目标列干净。3. 随机森林回归模型的搭建与参数调优3.1 特征矩阵和目标列的划分以及训练集的时间切分随机森林回归算法在 sklearn 里的接口是RandomForestRegressor。特征矩阵 X 包含前面构造的所有数值列目标列 y 是未来 24 小时 PM2.5 浓度。这里有个容易翻车的地方不能用train_test_split随机切分。时间序列数据必须按时间顺序切否则未来数据会泄漏到训练集里评估结果会虚高。from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 构造目标列未来 24 小时的 PM2.5 df[target] df.groupby(station_id)[PM2.5].shift(-24) df df.dropna(subset[target]) feature_cols [c for c in df.columns if c not in [datetime, station_id, target, PM2.5]] X df[feature_cols].values y df[target].values # 按时间 8:2 切分前 80% 训练后 20% 测试 split_idx int(len(df) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:]shift(-24)是把当前行往上移 24 行得到「24 小时后的值」作为标签。dropna去掉最后 24 行没有未来值的记录。特征列排除掉原始 PM2.5 列因为滑窗特征已经包含了它的信息保留原始列容易让模型过度依赖当前值而忽略趋势。3.2 随机森林的四个核心参数怎么设随机森林看着参数多真正影响结果的就四个n_estimators、max_depth、min_samples_leaf、max_features。下面这张表是我在空气质量数据上反复试出来的经验范围。参数作用经验范围设太大/太小的后果n_estimators树的数量100~500太少方差大太多训练慢且收益递减max_depth单棵树最大深度8~20太深过拟合太浅欠拟合min_samples_leaf叶节点最少样本数3~10太小噪声敏感太大欠拟合max_features每次分裂考虑的特征数0.3~0.6太小单棵树弱太大树之间相关性高rf RandomForestRegressor( n_estimators300, max_depth15, min_samples_leaf5, max_features0.4, oob_scoreTrue, # 用袋外样本评估省一次交叉验证 n_jobs-1, # 用满所有 CPU 核 random_state42 ) rf.fit(X_train, y_train) y_pred rf.predict(X_test) print(fMAE: {mean_absolute_error(y_test, y_pred):.2f}) print(fRMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.2f}) print(fR2: {r2_score(y_test, y_pred):.4f}) print(fOOB R2: {rf.oob_score_:.4f})oob_scoreTrue是随机森林的一个便利特性每棵树训练时约有 36.8% 的样本没被抽到用这些袋外样本评估相当于免费的交叉验证。n_jobs-1在数据量大时能显著缩短训练时间。random_state固定住是为了结果可复现调参阶段建议固定最终模型可以去掉。3.3 用网格搜索找参数组合以及特征重要性怎么看手动调参效率低用GridSearchCV批量试。但要注意时间序列数据不能用默认的 K 折交叉验证得用TimeSeriesSplit。from sklearn.model_selection import GridSearchCV, TimeSeriesSplit param_grid { n_estimators: [200, 300, 500], max_depth: [10, 15, 20], min_samples_leaf: [3, 5, 8], max_features: [0.3, 0.4, 0.5] } tscv TimeSeriesSplit(n_splits5) grid GridSearchCV( RandomForestRegressor(random_state42, n_jobs-1), param_grid, cvtscv, scoringneg_mean_absolute_error, verbose1 ) grid.fit(X_train, y_train) print(grid.best_params_)TimeSeriesSplit把训练集按时间切成 5 段每次用前面的段训练、后面的段验证模拟真实预测场景。scoringneg_mean_absolute_error是因为 sklearn 的评分函数统一取「越大越好」MAE 本身越小越好所以取负。搜索完拿到最优参数后用全部训练集重新拟合一次。特征重要性直接调feature_importances_配合特征名排序输出importances pd.Series( rf.feature_importances_, indexfeature_cols ).sort_values(ascendingFalse) print(importances.head(15))我跑过的数据里排前面的通常是pm25_roll_mean_24h、pm25_roll_mean_12h、湿度、风速、温度。如果发现某个特征重要性异常高先检查它是不是泄漏了目标信息。4. 模型评估与结果解读别被高 R² 骗了4.1 回归指标怎么选MAE、RMSE、R² 各看什么三个指标各有侧重。MAE 是平均绝对误差单位跟 PM2.5 一样是 μg/m³最直观——MAE 等于 15 就是说平均预测偏差 15 微克。RMSE 是均方根误差对大误差惩罚更重如果 RMSE 远大于 MAE说明存在少数预测得特别离谱的样本通常是重污染突发过程。R² 是拟合优度取值越接近 1 越好但空气质量预测里 R² 能到 0.7 以上就算不错了别拿线性回归那种 0.95 的标准来要求。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) r2 r2_score(y_test, y_pred) print(fMAE: {mae:.2f} μg/m³) print(fRMSE: {rmse:.2f} μg/m³) print(fR²: {r2:.4f}) print(fRMSE/MAE 比值: {rmse/mae:.2f})RMSE/MAE 比值在 1.2 到 1.5 之间算正常超过 2 就要去查那些预测误差最大的样本看看是不是有极端污染事件没被特征捕捉到。4.2 残差分析找出模型在什么条件下失效光看总体指标不够得看残差分布。把预测值减真实值画出来按真实浓度分段统计。residuals y_pred - y_test result_df pd.DataFrame({ true: y_test, pred: y_pred, residual: residuals }) # 按真实浓度分箱看偏差 bins [0, 35, 75, 115, 150, 500] labels [优, 良, 轻度污染, 中度污染, 重度及以上] result_df[level] pd.cut(result_df[true], binsbins, labelslabels) print(result_df.groupby(level)[residual].agg([mean, std, count]))如果发现「重度及以上」这一档的 mean residual 是很大的负数说明模型系统性地低估了重污染峰值。这是随机森林的固有局限——它做的是平均意义上的拟合极端值样本少树的分裂不会专门为它们优化。解决办法后面第 6 章会讲。4.3 预测值与真实值的时间序列对比图画图不是为了好看是为了看模型在污染过程的上升沿和下降沿跟不跟得上。import matplotlib.pyplot as plt plt.figure(figsize(14, 5)) plt.plot(y_test[:500], label真实值, alpha0.8) plt.plot(y_pred[:500], label预测值, alpha0.8) plt.xlabel(样本序号) plt.ylabel(PM2.5 (μg/m³)) plt.legend() plt.title(随机森林预测值与真实值对比前500个测试样本) plt.tight_layout() plt.savefig(pred_vs_true.png, dpi150)重点看两处峰值有没有被削平谷值有没有被抬高。如果峰值普遍被低估 20% 以上说明模型偏向保守用于预警会漏报。5. 避坑与排查随机森林做空气质量预测最容易翻车的五个地方5.1 R² 高到 0.95 以上大概率是数据泄漏现象测试集 R² 超过 0.95MAE 低到个位数看起来完美。原因特征里混入了目标时刻的信息。最常见的泄漏源是滑窗特征没做 shift——比如你算pm25_roll_mean_24h时包含了当前时刻而目标又是未来 24 小时的值中间有重叠。另一个常见源是用了train_test_split随机切分未来数据混进了训练集。解决所有滑窗特征在计算后统一 shift(1)确保只用历史信息切分必须按时间顺序。检查方法把特征重要性打出来如果某个特征重要性超过 0.5逐个排查它是否泄漏。5.2 多站点数据没分组滑窗特征跨站点串了现象模型在训练集上表现正常但换一个站点测试就崩。原因rolling或shift操作没有按station_id分组A 站点序列末尾接到了 B 站点开头。解决所有时间序列操作前先sort_values([station_id, datetime])然后用groupby(station_id).transform(...)。验证方法随便挑一个站点的前几行手动算一下滑窗均值跟代码输出对一下。5.3 风向直接当数值用模型学出错误关系现象风向的特征重要性很低但气象常识告诉你风向对污染物输送很关键。原因0° 和 360° 数值上差 360 但物理上是一回事树模型基于数值大小分裂学不到这种周期性。解决拆成 sin/cos 两个分量或者做独热编码分成 8 个方位。我一般用 sin/cos维度低且保留了连续性。5.4 缺失值填充用了全局均值抹掉了日变化规律现象模型在白天时段预测偏差明显大于夜间。原因PM2.5 有强烈的日变化早高峰和夜间边界层变化都会影响浓度。用全局均值填充缺失等于把不同时刻的差异抹平了。解决按station_idhour分组算中位数填充保留日变化结构。如果数据量够用相邻站点的同时刻数据做空间插值效果更好。5.5 重污染峰值被系统性低估预警漏报现象整体 MAE 可以接受但所有 PM2.5 超过 150 的样本预测值都偏低 30% 以上。原因随机森林的损失函数是均方误差极端值样本占比少对整体损失的贡献有限树的分裂优先照顾多数样本。解决三个方向——对目标做对数变换压缩量纲给高浓度样本加权sample_weight参数或者单独训练一个针对高浓度区间的分类器做二次判断。我一般先试对数变换改动最小。6. 让模型真正能用于预警几个我踩过坑才定下来的做法模型跑出指标只是第一步要让它能用于实际预警还有几件事得做。第一件是给预测结果加不确定区间。随机森林的每棵树都会给出一个预测值把这些值的分位数拿出来就能得到预测区间。比如 90% 分位数对应的值可以作为「乐观上限」参考。# 获取每棵树的预测值 all_preds np.array([tree.predict(X_test) for tree in rf.estimators_]) lower np.percentile(all_preds, 10, axis0) upper np.percentile(all_preds, 90, axis0) # 如果上限超过预警阈值即使点预测没超也要关注 threshold 150 alert_mask upper threshold print(f需要关注的样本数: {alert_mask.sum()})这个做法的价值在于点预测是 130 但上限到了 180说明模型内部对这次预测分歧很大实际可能超标值得人工复核。第二件是定期用新数据重新训练。空气质量数据的分布会随季节漂移冬天燃煤采暖期的特征关系和夏天完全不同。我一般每季度用最近一年的数据重新拟合一次参数不用大改但特征分布变了旧模型会退化。第三件是保存完整的预处理管道。训练时用的均值、中位数、滑窗参数预测时必须一模一样否则线上线下不一致。用joblib把模型和预处理参数一起存import joblib joblib.dump({ model: rf, feature_cols: feature_cols, fill_values: df.groupby([station_id, hour])[PM2.5].median().to_dict() }, air_quality_rf_v1.pkl)加载时先按fill_values填充缺失再按feature_cols的顺序取列顺序错了预测结果会完全不对。这个坑我踩过一次——训练时特征顺序是 A、B、C线上加载时字典顺序变了变成 C、A、B模型没报错但预测全偏了。后来我在保存时显式存了feature_cols列表加载后强制X X[feature_cols]对齐。最后说一个习惯每次重新训练后我会把新模型在最近一个月的重污染过程上的表现单独拉出来看而不是只看整体指标。整体 R² 0.75 但三次重污染过程全部漏报这个模型对预警来说就是不可用的。希望帮到你。本文还有配套的精品资源点击获取