简介:这份资源面向数据挖掘与机器学习入门及进阶学习者,围绕空气质量预测这一典型回归/分类场景,提供从数据到建模的完整实战方案。包内共3个文件,以csv数据集、ipynb代码笔记本和html分析报告为主,压缩包约1.39MB,体积轻便,便于本地快速运行与复现。数据集包含空气质量相关特征字段,代码部分完整呈现数据清洗、特征工程、模型训练与评估流程,html报告则直观展示分析结果与可视化图表,方便对照理解每一步建模思路。目前已有129人学习下载,适合课程设计、竞赛练手或自学机器学习算法的读者参考。通过这份资料,读者可掌握空气质量预测的完整建模链路,理解特征处理与模型调参要点,并借助现成代码快速迁移到其他环境数据预测任务中。
1. 空气质量预测这件事,为什么值得用数据挖掘重做一遍
空气质量预测模型听起来像是环保部门的专属课题,但真正做过一轮你会发现,它其实是数据挖掘和机器学习里少有的「全链路练手场」:数据脏、特征多、时序强、评价指标还特别讲究。很多做机器学习入门的朋友拿鸢尾花、泰坦尼克号练完手,转头就想找一个既有真实业务价值、又能把特征工程到模型调参全流程跑通的项目,空气质量预测正好卡在这个位置上。它要处理缺失值、异常值、时间滑窗、气象耦合,还要面对「预测明天下午三点的 PM2.5」这种带时间约束的回归任务,比单纯分类任务更接近工业场景。
这套「数据集+代码」的组合,核心价值不在于模型本身有多深,而在于它把数据挖掘的完整链条摊开给你看:原始监测数据怎么清洗、气象因子怎么拼接、滞后特征怎么构造、树模型和神经网络怎么选、评估为什么不能只看 R²。适合已经会写 Python、跑过 sklearn 基础流程,但没独立做过端到端时序回归的从业者。如果你正卡在「模型能跑但结果不可信」的阶段,这个方向能帮你把每个环节的坑踩明白。
2. 先搞清楚预测目标:空气质量预测到底在预测什么
2.1 预测对象与时间粒度的选择
动手之前必须先定死一件事:你预测的是哪个污染物、提前多久、按什么粒度。常见做法是预测未来 1 小时、未来 24 小时或未来 72 小时的 PM2.5 浓度,粒度分小时级和日级。这三者对应的建模思路完全不同。小时级预测更依赖近期自相关,滞后 1 到 6 小时的特征权重极高;日级预测则更吃气象条件,风速、湿度、气压的影响会被放大。
我一般建议新手从「小时级、提前 1 小时、预测 PM2.5」起步。原因很实际:这个设定下,前一小时的浓度本身就是极强特征,模型容易收敛,你能快速拿到一个看起来不错的基线,再逐步加难度。如果一上来就做 72 小时预测,误差会大到让你怀疑代码写错了,其实是任务本身难度决定的。
数据集里通常包含时间戳、监测站点、PM2.5、PM10、SO2、NO2、CO、O3 这些污染物列,外加温度、湿度、风速、风向、气压等气象列。你要做的第一件事不是建模,而是确认时间戳是否连续、站点是否唯一、缺失比例有多高。
2.2 回归还是分类,指标怎么定
空气质量预测可以是回归,也可以是分类。回归直接输出浓度值,分类则把 AQI 分成优、良、轻度污染等档位。两者没有优劣,取决于业务要什么。如果下游要做预警,分类更直接;如果要做趋势展示,回归更细腻。
指标上有个血泪经验:不要只报 R²。空气质量数据存在明显的季节性和突发峰值,R² 很容易被大量平稳样本拉高,掩盖模型在污染峰值上的糟糕表现。我一般同时看三个指标:MAE 看平均偏差,RMSE 对大误差更敏感,再单独统计「真实值超过阈值时模型的命中率」。最后这个指标才是预警场景真正关心的。
提示:如果数据集里 PM2.5 缺失率超过 20%,先别急着插值,先看缺失是不是集中在某些时段或站点,集中缺失往往意味着设备故障,盲目填充会引入系统性偏差。
3. 数据清洗与特征工程:决定模型上限的一步
3.1 缺失值、异常值与时间对齐
拿到数据后,第一步是统一时间索引。常见做法是把时间戳转成 pandas 的 datetime 并设为索引,然后按小时重采样,确认没有重复时间点。缺失值处理分两种:短缺口(连续少于 3 小时)用线性插值,长缺口直接标记为缺失段,训练时跳过而不是硬填。
异常值检测用物理范围加统计方法双保险。物理范围就是 PM2.5 不可能为负、不可能超过 1000;统计方法用 IQR 或 3σ,但要注意污染峰值本身可能就是真实极值,别一刀切把峰值当异常删了。我的习惯是先把超物理范围的值置为 NaN,再用前后均值填补,统计异常只做标记不做删除,留给模型自己判断。
import pandas as pd import numpy as np # 读取原始数据,时间列解析为 datetime df = pd.read_csv("air_quality.csv", parse_dates=["timestamp"]) df = df.sort_values("timestamp").set_index("timestamp") # 按小时重采样,避免时间戳不连续 df = df.resample("1H").mean() # 物理范围过滤:负值和超上限置为缺失 for col in ["PM2.5", "PM10", "SO2", "NO2", "CO", "O3"]: df.loc[(df[col] < 0) | (df[col] > 1000), col] = np.nan # 短缺口线性插值,限制最多填 3 个连续缺失 df = df.interpolate(method="linear", limit=3, limit_direction="both") # 打印各列缺失比例,决定后续处理 print(df.isna().mean().sort_values(ascending=False))这段代码的逻辑是:先保证时间轴连续,再做物理过滤,最后只对短缺口插值。limit=3是关键参数,它防止把长时间故障段用线性插值糊过去。limit_direction="both"保证序列首尾的缺失也能被处理。跑完看缺失比例,如果某列还剩大量 NaN,说明该列要么弃用,要么单独做缺失指示特征。
3.2 滞后特征与滑动窗口构造
时序预测的核心特征来自历史。对 PM2.5 这类强自相关变量,滞后 1、2、3、6、12、24 小时的值几乎必加。构造方式是用 shift,但要注意 shift 之后会产生新的缺失,需要在建模前统一 drop 或填充。
滑动窗口统计量同样重要:过去 6 小时均值、过去 24 小时最大值、过去 24 小时标准差。这些特征能帮模型捕捉累积效应和波动剧烈程度。风向这种周期性变量不能直接当数值用,常见做法是拆成 sin 和 cos 两个分量,否则 359 度和 1 度在数值上差很远,实际却几乎同向。
# 构造滞后特征 for lag in [1, 2, 3, 6, 12, 24]: df[f"PM25_lag_{lag}"] = df["PM2.5"].shift(lag) # 滑动窗口统计 df["PM25_roll_mean_6"] = df["PM2.5"].shift(1).rolling(6).mean() df["PM25_roll_max_24"] = df["PM2.5"].shift(1).rolling(24).max() df["PM25_roll_std_24"] = df["PM2.5"].shift(1).rolling(24).std() # 风向拆成 sin/cos,避免周期性断裂 df["wind_sin"] = np.sin(np.deg2rad(df["wind_dir"])) df["wind_cos"] = np.cos(np.deg2rad(df["wind_dir"])) # 构造预测目标:下一小时 PM2.5 df["target"] = df["PM2.5"].shift(-1) # 去掉因 shift 和 rolling 产生的缺失行 df = df.dropna()这里有个容易翻车的地方:滑动窗口必须先 shift(1) 再 rolling,否则当前时刻的值会混进特征里,造成标签泄漏。shift(-1)构造目标时方向是负的,表示取下一时刻。风向的 sin/cos 转换是处理角度类特征的通用手法,气象数据里很常见。
3.3 特征筛选与共线性处理
特征不是越多越好。PM2.5 的多个滞后项之间相关性极高,全塞进去会让树模型的特征重要性分散,也让线性模型系数不稳定。我一般先算相关矩阵,把相关系数超过 0.95 的特征对里保留业务含义更清晰的那个。再用树模型跑一遍特征重要性,砍掉重要性接近零的列。
注意:特征筛选必须在训练集上做,不能拿全量数据算相关性再切分,否则验证集信息会泄漏进特征选择过程,评估结果虚高。
4. 模型选型与训练:从基线到调参的完整路径
4.1 基线模型与树模型的取舍
任何预测任务都该先有一个基线。空气质量预测最朴素的基线是「用上一小时的值直接当预测值」,学术上叫 persistence model。如果复杂模型的 MAE 打不过这个基线,说明特征工程或建模有问题,别急着调参。
基线之后,树模型是首选。XGBoost、LightGBM 这类梯度提升树在表格型时序特征上表现稳定,对缺失值和特征缩放不敏感,训练快,还能直接输出特征重要性。相比之下,线性回归对滞后特征的多重共线性很敏感,神经网络则需要更多数据和调参耐心。我一般先用 LightGBM 跑通全流程,拿到一个可信的 MAE,再考虑要不要上 LSTM 或 Transformer。
from sklearn.model_selection import TimeSeriesSplit from lightgbm import LGBMRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error # 时序数据必须按时间切分,不能随机打乱 split = int(len(df) * 0.8) train, test = df.iloc[:split], df.iloc[split:] features = [c for c in df.columns if c not in ["target", "PM2.5"]] X_train, y_train = train[features], train["target"] X_test, y_test = test[features], test["target"] model = LGBMRegressor( n_estimators=500, learning_rate=0.05, max_depth=6, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_train, y_train) pred = model.predict(X_test) print("MAE:", mean_absolute_error(y_test, pred)) print("RMSE:", mean_squared_error(y_test, pred) ** 0.5)切分用TimeSeriesSplit或直接按时间点切,绝不能随机切。随机切会让未来数据进入训练集,评估结果好看但上线就崩。learning_rate=0.05配n_estimators=500是稳妥组合,max_depth=6和num_leaves=31控制模型复杂度,防止过拟合。subsample和colsample_bytree做行和列采样,提升泛化。
4.2 时序交叉验证与超参数调整
单次切分的评估波动大,尤其是空气质量数据有季节性。更稳的做法是滚动窗口验证:用前 N 个月训练,预测下一个月,然后窗口前移。这样能看出模型在不同季节的表现差异。
超参数调整用 Optuna 或 GridSearch 都行,但搜索空间别开太大。LightGBM 重点调learning_rate、num_leaves、min_child_samples和正则项lambda_l1、lambda_l2。学习率越低,需要的树越多,训练越慢,但通常更稳。我一般先固定学习率 0.05 粗调树的数量和叶子数,再降到 0.01 精调。
import optuna def objective(trial): params = { "n_estimators": trial.suggest_int("n_estimators", 200, 1000), "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.1), "num_leaves": trial.suggest_int("num_leaves", 15, 63), "min_child_samples": trial.suggest_int("min_child_samples", 5, 50), "lambda_l1": trial.suggest_float("lambda_l1", 1e-3, 10, log=True), "lambda_l2": trial.suggest_float("lambda_l2", 1e-3, 10, log=True), } model = LGBMRegressor(**params, random_state=42) model.fit(X_train, y_train) return mean_absolute_error(y_test, model.predict(X_test)) study = optuna.create_study(direction="minimize") study.optimize(objective, n_trials=50) print(study.best_params)n_trials=50是性价比比较高的起点,再多收益递减。log=True用于正则项这种跨数量级的参数。注意这里为了演示简洁用了单次切分,实际项目里 objective 内部应该换成滚动验证的平均 MAE,否则调出来的参数只对某一段数据好。
4.3 神经网络方案的适用边界
如果数据量足够(几年以上的小时级数据)、且你愿意花时间调参,LSTM 或 Temporal Fusion Transformer 这类时序模型能捕捉更复杂的长期依赖。但多数空气质量数据集只有一两年数据,树模型往往更划算。神经网络的坑在于:对缺失值敏感、需要归一化、训练不稳定、调参空间大。我见过不少人一上来就搭 LSTM,结果 MAE 还不如 LightGBM,问题多半出在数据量不够和特征没对齐。
提示:如果一定要上神经网络,先把树模型的特征工程结果直接喂进去,保证输入一致,再对比两者。这样你才能确定差距来自模型本身,而不是特征处理。
5. 避坑与排查:那些让模型结果不可信的细节
5.1 标签泄漏:现象是评估极好,上线极差
现象:验证集 MAE 低到离谱,比如个位数,但换一段时间预测就崩。原因:特征里混入了未来信息,最常见的是滑动窗口没 shift、或者用全量数据做了标准化和插值。解决:所有涉及时间聚合的操作先 shift 再 rolling;标准化参数只用训练集拟合;插值只在训练集内部做,测试集的缺失用训练集统计量填。
5.2 时间切分错误:现象是随机切分指标虚高
现象:用 train_test_split 随机切分,R² 到 0.95,但按时间切分只有 0.7。原因:相邻小时的样本高度相似,随机切分让训练集和测试集共享了几乎相同的时间片段。解决:一律按时间顺序切分,或用 TimeSeriesSplit,确保测试集时间晚于训练集。
5.3 缺失值填充引入偏差:现象是模型在故障时段预测异常平稳
现象:某段时间真实值波动大,模型预测却一条直线。原因:长缺口被线性插值填成了平滑序列,模型学到了假的平稳模式。解决:长缺口不插值,改为添加缺失指示列,或直接在这些时段不计算损失。
5.4 评价指标单一:现象是 MAE 好看但峰值全错
现象:整体 MAE 很低,但污染峰值时段预测值远低于真实值。原因:平稳样本占多数,模型偏向预测均值。解决:对峰值样本加权,或单独统计阈值以上样本的召回率和 MAE,把它作为模型选择依据。
5.5 特征重要性误读:现象是删了「重要」特征模型反而变好
现象:按特征重要性删掉低分特征,模型效果没提升甚至下降。原因:树模型的特征重要性会被高基数特征和相关性特征分散,不代表因果贡献。解决:结合业务含义和相关性分析做筛选,别只信重要性排序。
6. 让预测真正可用的两个进阶技巧
第一个技巧是残差建模。空气质量数据里,气象突变和节假日效应很难被常规特征完全捕捉。我的做法是先让主模型预测,再对残差单独建一个轻量模型,输入是节假日标记、天气突变指标(如风速变化率、温度骤降幅度)。两个模型叠加后,峰值时段的 MAE 通常能降一截。这个思路本质是让主模型学平稳规律,残差模型学突发事件,分工明确。
第二个技巧是预测区间而不是单点。业务方真正关心的往往不是「明天 PM2.5 是 85」,而是「大概率在 70 到 110 之间」。用分位数回归或对树模型做 bootstrap 采样,能给出预测区间。LightGBM 支持分位数损失,把 objective 设成 quantile,分别训练 0.1 和 0.9 分位,就得到 80% 预测区间。这个区间比单点值有用得多,也更容易让业务方接受模型的不确定性。
# 分位数回归给出预测区间 lower = LGBMRegressor(objective="quantile", alpha=0.1, n_estimators=500, learning_rate=0.05) upper = LGBMRegressor(objective="quantile", alpha=0.9, n_estimators=500, learning_rate=0.05) lower.fit(X_train, y_train) upper.fit(X_train, y_train) pred_low = lower.predict(X_test) pred_high = upper.predict(X_test) # 检查真实值落在区间内的比例,理想接近 80% coverage = ((y_test >= pred_low) & (y_test <= pred_high)).mean() print("区间覆盖率:", coverage)alpha控制分位点,0.1 和 0.9 对应 80% 区间。覆盖率是验证区间质量的核心指标,如果算出来只有 50%,说明区间太窄,需要调整 alpha 或增加模型复杂度。这个技巧我在实际项目里用得最多,因为它直接回答了业务方「你这个预测靠不靠谱」的追问。
最后说个我自己的习惯:每次跑完模型,我都会把预测值和真实值按时间画出来,肉眼扫一遍。指标再好看,图上如果出现系统性偏移或峰值全错,那模型就是不能用的。这个动作花不了几分钟,但帮我躲过了好几次「指标漂亮、上线翻车」的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取