简介:这是一套面向数据挖掘初学者及空气质量分析实践者的完整项目资料,围绕随机森林算法构建污染预测模型,覆盖数据清洗、特征探索、模型训练与结果评估的实战闭环,适合具备一定Python基础、想通过真实项目巩固机器学习流程的读者。压缩包共包含3个文件:ipynb是可交互的建模分析代码,html为导出后的结果展示页,csv为整理后的污染数据集;整体仅616KB,体量轻便,便于快速部署与本地复现。该资源已有129人学习浏览,适合用于课程设计、期末项目或数据挖掘入门阶段的动手训练。读者可获得完整的数据预处理与特征分析思路、随机森林参数调优策略、污染物浓度预测及可视化结果,还可结合csv数据自行扩展对比实验,从数据到结论形成可复现的完整项目方案。
1. 一个压缩包里的数据挖掘实战:随机森林预测空气质量,核心不在调参
“数据挖掘实战-基于随机森林算法的空气质量污染预测模型(数据集+代码).rar”这类压缩包,在数据挖掘课程、毕业设计和转行项目里出现频率极高:数据集是某地监测站多年的逐小时PM2.5记录,代码是围绕随机森林算法搭的一套训练脚本。表面看,任务是用随机森林把空气质量污染预测出来;实际上,这个项目的成败几乎不取决于算法本身,而在于你有没有把时间序列的数据性质处理对。空气质量数据不是一堆独立样本,前一小时的浓度和后一小时强相关,随机划分、漏造滞后特征、把缺失值一把清零,都会让模型指标虚高或直接报废。这个方向适合第一次完整跑通回归建模流程的人,也适合想用一份带数据集和代码的项目练手数据挖掘的人。本文按实际落地的顺序,从数据清洗、特征工程、模型评估一路讲到五个高频翻车点,最后给出一套能验证模型真实水平的调参习惯。
2. 先把数据集读明白:字段结构、缺失值和目标分布
2.1 空气质量数据集的常见表格结构:列名与时间粒度
这类空气质量预测项目所用的数据,通常是一份逐小时记录的多变量表格。常见字段构成如下:
- No:行号,一般无预测意义
- year、month、day、hour:时间戳,需要合并成一个 datetime
- pm2.5:目标变量,即当前小时的PM2.5浓度,单位 μg/m³
- DEWP:露点温度,单位 ℃
- TEMP:气温,单位 ℃
- PRES:大气压,单位 hPa
- cbwd:组合风向,常见值为 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(drop=True) print(df.info()) print(df.isnull().sum())这段代码做了两件关键事:第一,把分开的年月日时列合并成真正的 datetime 对象;第二,按时间排序并重置索引。排序这一步容易被跳过,但如果不排序,后续 shift 和 rolling 构造出的“滞后”特征实际是乱序的,训练出的模型没有任何时间语义。
2.2 PM2.5缺失怎么处理:插值、前向填充还是直接丢弃
空气质量数据几乎必然有缺失,PM2.5 缺失尤其常见,因为监测仪器会定期校准、故障或断电。先看缺失分布比直接填重要得多:
miss = df["pm2.5"].isnull() print(f"pm2.5缺失总数: {miss.sum()}") print(df.loc[miss, "datetime"].head(30))如果缺失点分散在少数几个小时内,比如某天连续缺 3 小时,这种情况可以用线性插值补上。如果缺失连续一个月甚至更长,插值会把仪器故障状态伪装成真实大气过程,这种长段缺失直接丢弃更安全。
df["pm2.5"] = df["pm2.5"].interpolate(method="linear", limit=6) met_cols = ["DEWP", "TEMP", "PRES", "Iws", "Is", "Ir"] df[met_cols] = df[met_cols].ffill() df = df.dropna(subset=["pm2.5"]).reset_index(drop=True)interpolate 的 limit=6 表示最多线性填补连续 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"], prefix="wind", drop_first=False) 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_first=False 会保留全部四个风向列。特征清单里包含时间、气象、滞后浓度三类信息,最终特征数量不超过 20 个,对随机森林来说规模很友好。需要注意的是,feature_cols 手抄列表在后续迭代时容易漏,更稳的写法是从代码里动态筛选,后面 5.4 节会讲一个相关坑。
4. 随机森林训练与评估:从基线到可用结果
4.1 为什么这个场景先选随机森林
表格型数据做回归,随机森林是性价比很高的起点。PM2.5 与气象、时间之间的关系是非线性的,线性回归很难刻画;同时特征数量不到 20 个,样本量 6 到 8 万,随机森林能在几秒到几十秒内完成训练,不需要像神经网络那样做特征归一化和复杂的调参。相比之下,XGBoost 和 LightGBM 精度通常略好,但需要更多参数调整,对于这类课程或项目导向的数据挖掘实战,随机森林更稳健,也不容易过拟合得离谱。
4.2 训练参数起点、oob_score与第一次拟合
随机森林在 sklearn 里的训练代码很简洁,但参数起点不要用默认值,默认的 max_depth=None 在这种长序列数据上容易过拟合。
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=300, max_depth=20, min_samples_leaf=5, min_samples_split=10, max_features="sqrt" if X.shape[1] > 10 else 1.0, n_jobs=-1, random_state=42, oob_score=True ) rf.fit(X_train, y_train) print(f"OOB R²: {rf.oob_score_:.4f}")参数有几个值得说明。n_estimators 设为 300,在这个样本量下已经接近收敛,再多只会线性增加训练时间。max_depth=20 限制树深,防止单个树记住极端污染样本。min_samples_leaf=5 要求每个叶子至少 5 个样本,预测曲线更平滑。max_features 在特征数大于 10 时用 sqrt,否则用全部特征,空气质量数据特征偏少,全特征也可以。n_jobs=-1 让所有 CPU 核参与训练。oob_score=True 会在训练过程中用袋外样本计算一个近似泛化分数,这比直接看训练集 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(f"MAE={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, bins=bins, labels=labels) p_cut = pd.cut(pred, bins=bins, labels=labels) 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_splits=5) for train_idx, val_idx in tscv.split(X): rf_temp = RandomForestRegressor(n_estimators=300, max_depth=20, 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 带 center=True 时,窗口中心对齐到当前行,会用到未来几个小时的数据。在时间序列预测里,这就是未来数据泄漏。
解决:统一使用右对齐窗口,并对特征列做一次检查。
df["pm_mean6"] = df["pm2.5"].rolling(6).mean() # 默认右对齐 df["pm_mean6_center"] = df["pm2.5"].rolling(6, center=True).mean() # 错误示例,勿用 print(df[["pm_mean6", "pm_mean6_center"]].head(10))右对齐的 pm_mean6 在第 6 行才产生值,center=True 的版本从第 1 行开始就有值,这就是它提前使用了未来信息的直接证据。遇到精度异常高的结果时,检查一下有没有类似 center=True 的窗口配置。
5.3 树深不设上限:训练集满分、测试集拉胯
现象:训练集 R² 接近 0.98,测试集只有 0.72,差距悬殊。
原因:默认 max_depth=None 让每棵树无限制生长,叶子可以细分到只剩一个样本,把训练数据里的噪声也记下来。空气质量数据本身有测量误差和局地波动,过拟合后对未见数据表现很差。
解决:限制树深并增加叶子最小样本数。
from sklearn.model_selection import GridSearchCV param_grid = { "max_depth": [10, 15, 20, 30], "min_samples_leaf": [2, 5, 10], } gs = GridSearchCV( RandomForestRegressor(n_estimators=200, random_state=42), param_grid, cv=3, scoring="neg_mean_absolute_error", n_jobs=-1 ) gs.fit(X_train, y_train) print(gs.best_params_)GridSearchCV 中的 cv=3 在 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(columns=X_train.columns, fill_value=0)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_estimators=300, max_depth=20, min_samples_leaf=5, n_jobs=-1, random_state=42) rf_log.fit(X_train, y_train_log) pred_log = rf_log.predict(X_test) pred_exp = np.expm1(pred_log)log1p 是 ln(1+x),预测后要用 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,才意识到之前的结果全是时间泄漏撑起来的。后来凡是涉及时间序列的回归项目,我都先检查数据切分和滚动窗口对齐,再谈模型调参。这个教训帮我省下了后续无数个“指标好看、落地翻车”的夜晚。随机森林本身不玄学,真正玄学的是让数据里的时间信息保持诚实。希望帮到你。
本文还有配套的精品资源,点击获取