简介这是一套面向数据挖掘初学者及空气质量分析实践者的完整项目资料围绕随机森林算法构建污染预测模型覆盖数据清洗、特征探索、模型训练与结果评估的实战闭环适合具备一定Python基础、想通过真实项目巩固机器学习流程的读者。压缩包共包含3个文件ipynb是可交互的建模分析代码html为导出后的结果展示页csv为整理后的污染数据集整体仅616KB体量轻便便于快速部署与本地复现。该资源已有129人学习浏览适合用于课程设计、期末项目或数据挖掘入门阶段的动手训练。读者可获得完整的数据预处理与特征分析思路、随机森林参数调优策略、污染物浓度预测及可视化结果还可结合csv数据自行扩展对比实验从数据到结论形成可复现的完整项目方案。1. 一个压缩包里的数据挖掘实战随机森林预测空气质量核心不在调参“数据挖掘实战-基于随机森林算法的空气质量污染预测模型数据集代码.rar”这类压缩包在数据挖掘课程、毕业设计和转行项目里出现频率极高数据集是某地监测站多年的逐小时PM2.5记录代码是围绕随机森林算法搭的一套训练脚本。表面看任务是用随机森林把空气质量污染预测出来实际上这个项目的成败几乎不取决于算法本身而在于你有没有把时间序列的数据性质处理对。空气质量数据不是一堆独立样本前一小时的浓度和后一小时强相关随机划分、漏造滞后特征、把缺失值一把清零都会让模型指标虚高或直接报废。这个方向适合第一次完整跑通回归建模流程的人也适合想用一份带数据集和代码的项目练手数据挖掘的人。本文按实际落地的顺序从数据清洗、特征工程、模型评估一路讲到五个高频翻车点最后给出一套能验证模型真实水平的调参习惯。2. 先把数据集读明白字段结构、缺失值和目标分布2.1 空气质量数据集的常见表格结构列名与时间粒度这类空气质量预测项目所用的数据通常是一份逐小时记录的多变量表格。常见字段构成如下No行号一般无预测意义year、month、day、hour时间戳需要合并成一个 datetimepm2.5目标变量即当前小时的PM2.5浓度单位 μg/m³DEWP露点温度单位 ℃TEMP气温单位 ℃PRES大气压单位 hPacbwd组合风向常见值为 cv、NW、NE、SE是分类变量Iws累计风速Is累计雨雪时长Ir累计雨雪量时间跨度常见为几年到十几年样本量在 8 万到 10 万行左右。拿到这份数据后我一般不会急着建模先把时间列合并并排序因为后面所有滞后特征都依赖行顺序。import pandas as pd df pd.read_csv(AirQuality.csv) df[datetime] pd.to_datetime(df[[year, month, day, hour]]) df df.sort_values(datetime).reset_index(dropTrue) print(df.info()) print(df.isnull().sum())这段代码做了两件关键事第一把分开的年月日时列合并成真正的 datetime 对象第二按时间排序并重置索引。排序这一步容易被跳过但如果不排序后续 shift 和 rolling 构造出的“滞后”特征实际是乱序的训练出的模型没有任何时间语义。2.2 PM2.5缺失怎么处理插值、前向填充还是直接丢弃空气质量数据几乎必然有缺失PM2.5 缺失尤其常见因为监测仪器会定期校准、故障或断电。先看缺失分布比直接填重要得多miss df[pm2.5].isnull() print(fpm2.5缺失总数: {miss.sum()}) print(df.loc[miss, datetime].head(30))如果缺失点分散在少数几个小时内比如某天连续缺 3 小时这种情况可以用线性插值补上。如果缺失连续一个月甚至更长插值会把仪器故障状态伪装成真实大气过程这种长段缺失直接丢弃更安全。df[pm2.5] df[pm2.5].interpolate(methodlinear, limit6) met_cols [DEWP, TEMP, PRES, Iws, Is, Ir] df[met_cols] df[met_cols].ffill() df df.dropna(subset[pm2.5]).reset_index(dropTrue)interpolate 的 limit6 表示最多线性填补连续 6 个缺失点超过部分保持 NaN稍后统一删除。气象列使用 ffill 前向填充对缓慢变化的气象指标基本安全。注意不要用 fillna(0) 处理气象缺失0 会被模型误读成“极端干燥”或“完全无风”引入虚假信号。2.3 目标变量分布右偏直接决定评估指标怎么选清洗完后先看 PM2.5 的分布这决定后面怎么评估模型。print(df[pm2.5].describe()) print(f重度污染(200)占比: {(df[pm2.5] 200).mean():.4f})绝大多数城市空气质量数据会呈现明显右偏均值在 80 到 100 左右中位数低于均值超过 200 的重度污染样本占比可能只有 1% 到 3%。这意味着模型天然会把注意力放在常见的中低浓度区间学习到一个对高污染事件不够敏感的均值回归结果。后面评估时不能只看 R²还需要看高污染日子的查全率或者直接对目标做 log1p 变换缓解右偏。提示插值完成后仍可能有缺失行最后统一用 dropna 收尾避免模型训练时遇到 NaN 报错。3. 特征工程与数据切分别把时间序列切成随机的3.1 空气质量是强自相关的时间序列随机划分等于数据泄漏很多第一次做这个项目的人会用 train_test_split 的默认参数即随机 shuffle 后划分结果测试集 R² 高达 0.9 以上觉得自己调参天赋异禀。真实原因是空气质量有极强的自相关随机划分后同一天前后几个小时的数据被拆进了训练集和测试集测试集里藏着训练集“同一场污染过程”的答案模型等于开卷考试。正确的做法是在时间轴上切分前 80% 的历史数据做训练后 20% 做测试。cut_idx int(len(df) * 0.8) train df.iloc[:cut_idx].copy() test df.iloc[cut_idx:].copy() print(f训练集时间范围: {train[datetime].min()} ~ {train[datetime].max()}) print(f测试集时间范围: {test[datetime].min()} ~ {test[datetime].max()})这段代码把数据按物理时间分成前后两段测试集中的每条记录都晚于训练集所有记录。这样评估出的指标才接近真实部署场景用历史数据训练预测未来。3.2 时间特征和滞后特征让随机森林知道“现在是几点昨天什么浓度”随机森林无法从 year、month 这种数值里自动读出“季节”的语义需要显式构造时间特征。更关键的是滞后特征PM2.5 浓度有很强的惯性今天上午的污染过程往往会延续到下午昨天的同时段浓度也有参考价值。def add_time_features(df): df[hour] df[datetime].dt.hour df[dayofweek] df[datetime].dt.dayofweek df[month] df[datetime].dt.month df[season] df[month].map({12: 0, 1: 0, 2: 0, 3: 1, 4: 1, 5: 1, 6: 2, 7: 2, 8: 2, 9: 3, 10: 3, 11: 3}) return df def add_lag_features(df): df[pm2.5_lag1] df[pm2.5].shift(1) df[pm2.5_lag24] df[pm2.5].shift(24) df[pm_mean6] df[pm2.5].rolling(6).mean() df[pm_mean24] df[pm2.5].rolling(24).shift(1).mean() return df df add_time_features(train) df add_lag_features(df)shift(1) 取上一小时浓度shift(24) 取昨天同一小时浓度。rolling(6).mean() 是过去 6 小时滑动平均默认右对齐也就是当前时刻往前数 6 小时。pm_mean24 加了 shift(1) 是为了避免把当前时刻自己的浓度算进“历史平均值”里虽然对回归目标来说这不算泄漏但会让特征信息容量虚高。滞后特征构造完数据开头会产生 NaN比如 lag24 让前 24 行全部为空。训练前需要 dropna。3.3 风向与类别变量编码以及最终特征清单cbwd 是分类变量常见做法是 OneHot 编码。四种风向没有数值大小关系不能用 LabelEncoder 硬编号。如果训练集和测试集分开编码测试集可能缺少某个风向类别导致列数不一致因此在全量数据编码后再切分是比较省心的做法。df pd.get_dummies(df, columns[cbwd], prefixwind, drop_firstFalse) feature_cols [ hour, dayofweek, month, season, DEWP, TEMP, PRES, Iws, Is, Ir, wind_cv, wind_NW, wind_NE, wind_SE, pm2.5_lag1, pm2.5_lag24, pm_mean6, pm_mean24 ] X df[feature_cols] y df[pm2.5]get_dummies 的 drop_firstFalse 会保留全部四个风向列。特征清单里包含时间、气象、滞后浓度三类信息最终特征数量不超过 20 个对随机森林来说规模很友好。需要注意的是feature_cols 手抄列表在后续迭代时容易漏更稳的写法是从代码里动态筛选后面 5.4 节会讲一个相关坑。4. 随机森林训练与评估从基线到可用结果4.1 为什么这个场景先选随机森林表格型数据做回归随机森林是性价比很高的起点。PM2.5 与气象、时间之间的关系是非线性的线性回归很难刻画同时特征数量不到 20 个样本量 6 到 8 万随机森林能在几秒到几十秒内完成训练不需要像神经网络那样做特征归一化和复杂的调参。相比之下XGBoost 和 LightGBM 精度通常略好但需要更多参数调整对于这类课程或项目导向的数据挖掘实战随机森林更稳健也不容易过拟合得离谱。4.2 训练参数起点、oob_score与第一次拟合随机森林在 sklearn 里的训练代码很简洁但参数起点不要用默认值默认的 max_depthNone 在这种长序列数据上容易过拟合。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth20, min_samples_leaf5, min_samples_split10, max_featuressqrt if X.shape[1] 10 else 1.0, n_jobs-1, random_state42, oob_scoreTrue ) rf.fit(X_train, y_train) print(fOOB R²: {rf.oob_score_:.4f})参数有几个值得说明。n_estimators 设为 300在这个样本量下已经接近收敛再多只会线性增加训练时间。max_depth20 限制树深防止单个树记住极端污染样本。min_samples_leaf5 要求每个叶子至少 5 个样本预测曲线更平滑。max_features 在特征数大于 10 时用 sqrt否则用全部特征空气质量数据特征偏少全特征也可以。n_jobs-1 让所有 CPU 核参与训练。oob_scoreTrue 会在训练过程中用袋外样本计算一个近似泛化分数这比直接看训练集 R² 可信得多。4.3 MAE、RMSE、R²和污染等级命中率评价要配合业务回归任务最常见的三个指标是 MAE、RMSE 和 R²但对空气质量预测来说还要看污染等级命中率。PM2.5 的绝对值误差在重度污染时段会被放大单独看 R² 无法反映漏报情况。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import numpy as np pred rf.predict(X_test) mae mean_absolute_error(y_test, pred) rmse np.sqrt(mean_squared_error(y_test, pred)) r2 r2_score(y_test, pred) print(fMAE{mae:.2f} μg/m³, RMSE{rmse:.2f} μg/m³, R²{r2:.4f}) bins [0, 35, 75, 115, 150, 250, 400] labels [优, 良, 轻度, 中度, 重度, 严重] y_cut pd.cut(y_test, binsbins, labelslabels) p_cut pd.cut(pred, binsbins, labelslabels) acc (y_cut p_cut).mean() print(f污染等级命中率: {acc:.2%}) heavy_miss ((y_test 250) (pred 250)).mean() print(f重度污染漏报率: {heavy_miss:.2%})参数上MAE 反映平均偏差RMSE 加大了较大误差的惩罚R² 看整体拟合优度。污染等级命中率把连续预测转成 6 个等级贴近实际的空气质量发布口径。重度污染漏报率更直接说明模型会不会在高污染日失效。一个能落地的模型常见水平大致是 MAE 在 25 到 35 μg/m³、R² 在 0.8 左右同时重度污染漏报率控制在 15% 以内。如果 R² 超过 0.93先怀疑泄漏而不是高兴。5. 避坑随机森林预测空气质量的5个常见问题5.1 随机划分时间序列导致精度虚高现象train_test_split 默认随机划分测试集 R² 达到 0.92模型表现“惊艳”。原因空气质量是连续时间过程随机划分后训练集和测试集高度重叠测试样本的时间相邻样本出现在训练集中等价于泄漏。解决使用按时间切分或 sklearn 自带的时间序列交叉验证。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): rf_temp RandomForestRegressor(n_estimators300, max_depth20, n_jobs-1) rf_temp.fit(X.iloc[train_idx], y.iloc[train_idx]) print(rf_temp.score(X.iloc[val_idx], y.iloc[val_idx]))TimeSeriesSplit 的特点是每次验证集都排在训练集之后不会把未来样本混入历史训练。判断一个模型是否可靠先用这种切分方式重新评估一遍如果分数大幅下降说明之前的精度是数据划分造成的假象。5.2 滚动窗口用了中心对齐特征里混进未来值现象预测精度高得异常但换个时间段重新训练就崩或者模型在单变量预测里表现比合理值更好。原因pandas 的 rolling 默认右对齐值由当前时刻及之前的数据计算但 rolling 带 centerTrue 时窗口中心对齐到当前行会用到未来几个小时的数据。在时间序列预测里这就是未来数据泄漏。解决统一使用右对齐窗口并对特征列做一次检查。df[pm_mean6] df[pm2.5].rolling(6).mean() # 默认右对齐 df[pm_mean6_center] df[pm2.5].rolling(6, centerTrue).mean() # 错误示例勿用 print(df[[pm_mean6, pm_mean6_center]].head(10))右对齐的 pm_mean6 在第 6 行才产生值centerTrue 的版本从第 1 行开始就有值这就是它提前使用了未来信息的直接证据。遇到精度异常高的结果时检查一下有没有类似 centerTrue 的窗口配置。5.3 树深不设上限训练集满分、测试集拉胯现象训练集 R² 接近 0.98测试集只有 0.72差距悬殊。原因默认 max_depthNone 让每棵树无限制生长叶子可以细分到只剩一个样本把训练数据里的噪声也记下来。空气质量数据本身有测量误差和局地波动过拟合后对未见数据表现很差。解决限制树深并增加叶子最小样本数。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [10, 15, 20, 30], min_samples_leaf: [2, 5, 10], } gs GridSearchCV( RandomForestRegressor(n_estimators200, random_state42), param_grid, cv3, scoringneg_mean_absolute_error, n_jobs-1 ) gs.fit(X_train, y_train) print(gs.best_params_)GridSearchCV 中的 cv3 在 8 万行数据上会训练不少轮如果机器性能一般可以把 n_estimators 降到 150 或只搜索 max_depth 和 min_samples_leaf 两个参数。搜索结果一般落在 max_depth 15 到 25、min_samples_leaf 5 到 10 这个区间。5.4 OneHot编码后测试集列数不一致导致预测崩溃现象模型训练正常但 predict 时报错提示特征数量不匹配。原因训练集和测试集分别做了 get_dummies某个风向类别只出现在训练集或只出现在测试集导致两边的稀疏矩阵列数不一致。树模型非常依赖特征位置列数错位会造成灾难性失败。解决对训练集生成特征列然后用 reindex 让测试集对齐。X_test X_test.reindex(columnsX_train.columns, fill_value0)reindex 会把训练集有的列保留测试集缺失的列补 0测试集多出的列被丢弃。经过这一步predict 不会再报特征数量不匹配。更稳的做法是对全量数据先编码再切分但如果是从外部导入新数据进行预测reindex 是必须的兜底操作。5.5 重度污染日系统性漏报目标分布右偏的解法现象整体 RMSE 不算差但把所有超过 250 μg/m³ 的重度污染日都预测成了 150 到 200 左右漏报率很高。原因PM2.5 目标分布右偏高污染样本极少均方损失对极端值的惩罚让模型倾向于把预测值压向中位数导致极端事件被平滑掉。解决对目标变量做 log1p 变换缩小极端值与常见值之间的尺度差异。y_train_log np.log1p(y_train) rf_log RandomForestRegressor(n_estimators300, max_depth20, min_samples_leaf5, n_jobs-1, random_state42) rf_log.fit(X_train, y_train_log) pred_log rf_log.predict(X_test) pred_exp np.expm1(pred_log)log1p 是 ln(1x)预测后要用 expm1 还原。训练时损失在 log 空间计算极端污染样本的误差不再主导梯度方向模型更容易把高污染日预测值向上拉。这个技巧适合右偏严重的污染数据但评估指标仍然要在还原后的原始尺度上计算否则数字会让人误判。6. 调参与验证的落地技巧用逐周误差和特征重要性判断模型能不能用6.1 先RandomizedSearchCV粗调再看oob收敛曲线网格搜索在 8 万行数据上很慢我一般先用 RandomizedSearchCV 跑一轮粗调把 n_estimators 从 200 到 800 之间选几个点、max_depth 从 10 到 30 选几个点找到大致区域后再小范围精调。这里的核心不是找到绝对最优参数而是确认模型处于欠拟合和过拟合之间的稳定区间。判断的方法是观察 oob_score样本量足够时oob_score 和测试集分数通常接近如果两者差距超过 0.1说明切分方式或参数仍有问题。6.2 逐周切片验证和特征重要性给模型“做体检”全局 RMSE 会掩盖季节性问题。我会把测试集按周切块逐周计算 RMSE然后按月份或季度绘制误差曲线。空气质量在冬季往往误差更大这与静稳天气和采暖排放有关如果误差峰值都集中在某个季节说明模型对那个季节的污染机制刻画不够。这时再看特征重要性排序随机森林训练完成后直接取 feature_importances_常见排序中气压、滞后PM2.5浓度、风速会排在前面。如果时间特征 month 排名异常靠前而气象特征很弱要检查是不是数据年份跨度太小或季节信息被过度放大了。我第一次跑这个方向时被随机划分带来的 R²0.92 迷惑了好几天以为随机森林的预测能力就是这么强。直到换成按时间切分分数掉到 0.78才意识到之前的结果全是时间泄漏撑起来的。后来凡是涉及时间序列的回归项目我都先检查数据切分和滚动窗口对齐再谈模型调参。这个教训帮我省下了后续无数个“指标好看、落地翻车”的夜晚。随机森林本身不玄学真正玄学的是让数据里的时间信息保持诚实。希望帮到你。本文还有配套的精品资源点击获取
