简介:基于线性回归的PM2.5预测系统Python源码,面向环境数据分析、机器学习入门及课程设计人群,解决从原始监测数据到浓度预测的完整回归建模问题。压缩包共19个文件,以12个csv数据文件为主体,覆盖训练集、测试集及特征拼接、中间变量等处理结果;另有1个py主程序、1个npy模型参数文件和5张过程截图,大小仅2.14MB,轻量便于学习。该代码读取繁体编码的训练数据,按小时维度整理18类污染物指标,通过特征构造、最小二乘正规方程训练,最终输出预测值csv,流程完整。目前已有594人浏览学习。读者可直接调参运行并对照数据文件核验中间结果,借助截图与模型参数理解关键步骤;通过观察arrayx、concatenateX等中间矩阵,还能弄清特征拼接与数据标准化的具体实现,是机器学习课程设计、毕业论文实验或PM2.5回归预测入门的一份完整可运行参考资料。
1. 线性回归做PM2.5预测:先搞定基线,再谈花活
做空气质量预测,十有八九是从线性回归起步的。PM2.5浓度与历史浓度、气象条件之间存在大量可被线性模型捕捉的关联,小时级数据上前几小时的浓度对当前值有极强的自相关。这套基于线性回归的PM2.5预测系统,本质是把“历史浓度+气象特征”映射到未来浓度的回归模型,训练快、可解释、易部署,适合做业务初版和复杂模型的对标基线。它解决的不是“预测最准”,而是“用最小成本换来稳定可用的结果”。
适合两类人:刚接触时序预测的开发者,想看清从数据清洗、滞后特征到训练评估的完整链路;已有深度学习方案的工程师,想拿它当对照基线。这篇从原理讲到代码,再落到高频踩坑点,照着做就能把整套系统搭起来。
2. 线性回归预测PM2.5的原理与数据准备:特征矩阵决定上限
2.1 线性回归为什么能预测PM2.5:机理与适用边界
线性回归假设目标变量 y 是特征 x 的线性组合加噪声,写成公式是 y = β0 + β1x1 + … + βnxn + ε,训练时用最小二乘法求一组系数 β,让预测值与真实值的平方误差和最小。这个过程没有任何黑匣子,每个系数都对应一个特征的影响方向和大小,这也是它至今仍是工业界基线首选的原因。
PM2.5 的短期浓度变化能用线性模型描述,核心支撑有两点。第一是惯性:大气污染物的扩散和清除需要时间,当前小时浓度与过去几小时浓度高度相关,滞后项能捕捉这段自相关,而自相关是时序预测里最值钱的信息。第二是气象驱动:风速决定稀释能力,湿度影响二次生成,气压关系到垂直扩散,这些气象变量在正常波动范围内对浓度的贡献近似线性。把这两类信息编成特征,线性回归就能拟合出一条可用的预测线。
但必须认清边界。二次化学反应、逆温层、沙尘输送这类强非线性过程,线性回归天生预测不了极端峰值。实际项目里我一般把它定位成短期基线:预测未来1到24小时的浓度趋势,评估重点放在正常波动区间的拟合质量,而不是搏极端事件。如果业务需要预测重污染红警,再往上叠加树模型或深度学习,但线性回归这条基线必须存在——它是判断复杂模型是否真有用的尺子。
2.2 数据从哪来:监测字段、时间对齐与样本组织
这套系统最常用的数据源是空气质量监测站的小时级记录,字段一般包括 PM2.5、PM10、SO2、NO2、CO、O3 六项污染物浓度,加同时段的气温、气压、湿度、风速、风向。不同来源的数据粒度可能不同,有的按整点,有的按分钟,先统一再使用。
拿到数据先做三件事:按时间升序排序并去重、处理缺失、校正字段类型。监测设备断网重传经常产生重复记录,时间戳偏移也很常见,表现为整点记录落在 00:01 或 23:59。我一般先把时间戳取整到小时,再按“同一时刻只保留最后一条”去重。
# 数据加载与时间整理 import pandas as pd df = pd.read_csv('air_quality.csv', parse_dates=['datetime']) df['datetime'] = df['datetime'].dt.floor('h') # 取整到小时 df = df.sort_values('datetime').reset_index(drop=True) # 时间升序 df = df.drop_duplicates(subset=['datetime'], keep='last') # 同刻只留一条 print(df.shape, df.dtypes)dt.floor('h')会把 00:01 归到 00:00,避免滞后特征串位。sort_values加drop_duplicates是最低保障,后面所有特征构造都依赖这一步的正确性。字段类型也要顺手检查,湿度、风速这类列如果被读成字符串,后面所有运算都会报错。
提示:时间序列数据严禁在排序前做滚动或滞后操作,否则滞后值会串到别的时刻上,训练完的模型全是错觉。
2.3 特征矩阵构建:滞后项、滚动统计量与环形气象编码
特征工程是这套系统里性价比最高的环节。我常用的特征分三类:目标变量滞后项,取 lag1、lag3、lag6、lag24,覆盖短期惯性和日周期;滚动统计量,例如过去6小时 PM2.5 均值,相当于简易低通滤波,抑制单小时尖刺噪声;气象特征,其中风向是环形变量,0度和360度含义完全相同,直接拿数值塞进模型会让回归系数莫名其妙,推荐拆成 sin 和 cos 两个分量。
| 特征类别 | 特征名 | 构造方式 | 作用 |
|---|---|---|---|
| 滞后项 | pm25_lag1 / lag3 / lag6 / lag24 | shift(n) | 捕捉短期惯性与日周期 |
| 滚动统计 | pm25_roll6_mean | rolling(6).mean() | 平滑单小时尖刺 |
| 气象 | temp / pressure / humidity | 原始数值 | 气象驱动 |
| 气象 | wind_speed | 原始数值 | 扩散能力 |
| 风向 | wind_sin / wind_cos | 角度三角函数 | 环形变量编码 |
# 构造滞后、滚动与风向特征 import numpy as np def build_features(data, target='pm25', lags=(1, 3, 6, 24)): df = data.copy() for lag in lags: df[f'{target}_lag{lag}'] = df[target].shift(lag) df[f'{target}_roll6_mean'] = df[target].rolling(6).mean() df['wind_sin'] = np.sin(np.deg2rad(df['wind_direction'])) df['wind_cos'] = np.cos(np.deg2rad(df['wind_direction'])) return df.dropna().reset_index(drop=True) df = build_features(df) print(df.head())shift和rolling都会在序列头部产生 NaN,dropna会顺带丢掉最前面几行,这是正常现象,不是数据错误。滞后档位的选择跟预测步长强相关:预测下一小时,lag1 的权重通常最大;预测未来24小时,lag24 和滚动均值的作用更突出。这个函数默认给四档滞后,覆盖小时级预测最常用的配置,想预测更长周期就自己加 lag48、lag72。
3. 用Python源码搭训练流程:数据清洗、特征构造到预测一行不落
3.1 数据清洗:缺失值插值、异常值截断与脏数据兜底
监测设备断网、标定维护都会造成整段缺失。线性回归对缺失值零容忍,scikit-learn 的 LinearRegression 遇到 NaN 直接抛错。常见做法是线性插值加阈值截断组合:短缺失用前后值插值补上,长缺失整段丢弃,因为连续缺失超过6小时时插出来的值基本是猜,带进模型只会污染系数。
# 缺失值插值与异常截断 df = df.interpolate(method='linear', limit=6) # 最多连续插值6小时 df = df.dropna() # 超长缺失直接丢弃 cap = df['pm25'].quantile(0.995) # 99.5分位作为上限 df['pm25'] = df['pm25'].clip(upper=cap)interpolate的limit参数是关键,它限制连续插值长度,超过的部分保留为 NaN 再dropna丢掉。这个组合比一刀切fillna(0)或fillna(mean)靠谱得多,也避免把设备故障期间的假数据当成真实波动。异常处理上用分位数截断而非三倍标准差,原因是 PM2.5 分布右偏严重,正常高污染日的浓度就可能超过均值加三倍标准差,直接删会误伤样本,截断则保留样本量,同时压制极端值对回归线的拉扯。
3.2 特征与标签切分:时间顺序交叉验证,不能随机打乱
分类任务可以随机打乱数据,时间序列不行。随机划分会让模型在训练时看到测试时刻之前的未来信息,验证指标虚高,部署上线后立刻现原形。这是整套系统里最隐蔽的坑,后面 5.1 会细讲。这里先用 TimeSeriesSplit 按时间顺序做交叉验证。
# 时间顺序交叉验证 from sklearn.model_selection import TimeSeriesSplit features = ['pm25_lag1', 'pm25_lag3', 'pm25_lag6', 'pm25_lag24', 'pm25_roll6_mean', 'temp', 'pressure', 'humidity', 'wind_speed', 'wind_sin', 'wind_cos'] X = df[features].values y = df['pm25'].values tscv = TimeSeriesSplit(n_splits=5) for fold, (tr_idx, te_idx) in enumerate(tscv.split(X)): X_tr, X_te = X[tr_idx], X[te_idx] y_tr, y_te = y[tr_idx], y[te_idx] # 每一折训练集都严格早于测试集TimeSeriesSplit与KFold的本质区别就在这里:它不洗牌,且每一折的训练集时间上严格早于测试集。用它的前提是原始数据已经按时间升序排好,这一点忘了检查,后面所有评估指标都是白算。n_splits 设5到10都行,小时级数据样本量大,5折足够;折数太多会缩短每折训练长度,折数太少评估方差大。
3.3 训练与预测:LinearRegression 最小实现与结果解读
# 线性回归训练与评估 from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score import numpy as np model = LinearRegression() model.fit(X_tr, y_tr) y_pred = model.predict(X_te) rmse = np.sqrt(mean_squared_error(y_te, y_pred)) r2 = r2_score(y_te, y_pred) print(f'RMSE={rmse:.2f} ug/m3, R2={r2:.3f}')这段代码不需要标准化。普通线性回归对特征量纲不敏感,系数会自动适应不同尺度,滞后项浓度是几十到几百,气压是上千百帕,模型照样收敛。如果换成 Ridge 或 Lasso 正则化,才必须提前做标准化,否则正则项会对大数值特征惩罚过重,这一点很多人搞反。
跑完之后先看 RMSE 的量级:如果 RMSE 和测试集浓度的标准差差不多,说明模型几乎没学到信息,优先回头查特征和清洗流程,别急着换模型。加了 lag1 之后 R² 通常能做到 0.7 以上,达不到这个水平,数据链路大概率有 bug。每折都打印这两个指标,观察折与折之间的稳定性,比单看某一折的结果有意义得多。
4. 模型评估与调参:R²、RMSE、正则化与残差诊断
4.1 评估指标怎么选:R²看拟合度,RMSE看业务误差
评估回归模型,最常用的三个指标各有侧重。R² 衡量模型解释了多少方差,越高越好,但容易被极端值拉高;RMSE 是误差的均方根,单位和原始数据一致,直接对应业务上的“平均偏差多少微克”;MAE 是绝对误差均值,比 RMSE 更抗异常值干扰。小时级 PM2.5 预测里,我一般同时报 RMSE 和 R²,RMSE 给业务看,R² 给技术评审看。
| 指标 | 含义 | 适用判断 | 注意点 |
|---|---|---|---|
| R² | 1 - 残差平方和/总平方和 | 模型整体拟合度 | 极端值会虚高 |
| RMSE | 均方误差开根号 | 业务实际偏差 | 对异常值敏感 |
| MAE | 绝对误差均值 | 稳健偏差 | 不反映大误差 |
这三个指标都不能只看训练集。训练集 R² 很高、测试集掉一半,几乎可以断定过拟合,先用 4.3 的残差分析定位,再考虑加正则化或砍特征。还有一个常用技巧:把测试集按季度切片分别算 RMSE,能直观看到模型在冬夏两季的表现差距,这比一个全局指标更有业务参考价值。
4.2 正则化选型:Ridge还是Lasso,取决于共线性结构
特征多了之后,共线性会让最小二乘解的系数方差变大,训练集上拟合得很好,测试集上飘。两个常用解法:Ridge 加 L2 惩罚,把系数往零压但不真正归零,适合特征间存在相关性的场景;Lasso 加 L1 惩罚,会把不重要的系数直接压成零,自带特征选择。
# Ridge与Lasso对比 from sklearn.linear_model import Ridge, Lasso from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline scaler = StandardScaler() ridge = make_pipeline(scaler, Ridge(alpha=1.0)) lasso = make_pipeline(scaler, Lasso(alpha=0.01)) for name, pipe in [('ridge', ridge), ('lasso', lasso)]: pipe.fit(X_tr, y_tr) y_p = pipe.predict(X_te) print(name, f'R2={r2_score(y_te, y_p):.3f}', f'RMSE={np.sqrt(mean_squared_error(y_te, y_p)):.2f}')注意这里用了 StandardScaler,配合make_pipeline保证标准化和训练在同一条流水线里,避免在测试集上泄漏统计量。alpha 的取值要靠小网格搜索调,我一般从 0.01、0.1、1、10 几档里选交叉验证 RMSE 最低的。PM2.5 预测这个场景里,Ridge 通常比 Lasso 稳,因为气象特征之间多少都相关,L1 硬切容易把有用信息一起删掉。
4.3 残差分析:模型失效的信号灯
训练完别急着收工,画残差图是排查模型问题最直接的手段。把 y_te - y_pred 按时间顺序画出来,如果残差围绕零线随机波动,模型基本正常;如果出现明显的上升或下降趋势,说明有未建模的时间效应,比如季节漂移;如果残差方差随时间变大,说明模型在浓度高的时候系统性失灵。
# 残差趋势检查 import matplotlib.pyplot as plt residual = y_te - y_pred plt.figure(figsize=(12, 4)) plt.plot(residual, alpha=0.6) plt.axhline(0, color='red', linestyle='--') plt.title('Residual over time') plt.show()残差还有一个高频特征:重污染时段残差为正且很大,说明线性模型系统性低估峰值,这符合 2.1 里说的非线性边界。如果业务确实需要峰值预测,就得在特征里加入风速湿度等气象交互项,或者换树模型。残差分析不需要每折都做,但至少要在最后一折上做一次,作为上线的放行条件之一。
5. 避坑指南:PM2.5线性回归预测的五个真实踩坑记录
5.1 数据泄露:随机划分让指标虚高,上线就翻车
现象:训练集 R² 0.91,测试集 R² 0.89,漂亮得不像话,结果部署到线上实时预测,误差比测试时大了将近一倍。
原因:代码里用了train_test_split默认的随机划分。PM2.5 序列自相关极强,随机切分会让训练集里混入测试时刻前后的样本,模型等于提前看到了答案,测试指标纯属自欺欺人。
解决:按 3.2 的方式改用 TimeSeriesSplit,或直接按时间比例手动切分,保证训练集永远在测试集之前。所有涉及时间的特征构造(shift、rolling)也必须在切分之前完成,且不能引入未来信息。这是时序预测的头号血泪教训,没有之一。
5.2 预测值出现负数:浓度不可能是负的
现象:预测结果里出现 -10、-25 这类负数,业务方直接质疑数据有问题。
原因:线性回归没有非负约束。当特征组合落入训练数据稀疏的区域,回归平面外推,预测值就可能越过零轴。PM2.5 浓度分布左端紧贴零,外推时最容易出现负值。
解决:最简单的是np.clip(y_pred, 0, None)在输出端兜底。更讲究一点,把目标变量取对数再回归,预测完再指数还原,这样预测值天然为正,同时压缩右偏分布的影响。注意取对数后 RMSE 要在对数域计算,别拿还原后的值直接算。
5.3 多重共线性:PM10和PM2.5打架,系数符号反转
现象:PM2.5 和 PM10 高度相关,单独只放一个时 R² 差不多,两个都放时系数符号突然反转,PM10 系数变负。
原因:共线性特征之间可以相互替代,最小二乘解在相关方向上不稳定,系数估计方差被放大,符号翻转是典型表现。回归系数的“黑匣子”效应在这里体现得最明显。
解决:先看相关系数矩阵,把相关性超过 0.8 的特征合并,或保留其中业务意义更明确的一个。PM10 和 PM2.5 同源,实际项目里我通常保留目标滞后项,把 PM10 作为辅助特征或直接去掉。也可以用 Ridge 缩小系数方差,但不解决解释性问题。
5.4 异常值把回归线带偏:一次标定故障毁掉一折训练
现象:模型整体 RMSE 正常,但某天重污染时误差特别大,且系数明显偏向那个方向。
原因:监测设备标定异常会产出离谱的高值,比如某个小时 PM2.5 记录到 3000。线性回归的损失函数是平方误差,一个 3000 的样本对回归线的拉扯力度相当于几百个正常样本。
解决:用 3.1 的 clip 截断,或把极端值缩到分位数边界。更稳的做法是用 HuberRegressor 代替 LinearRegression,它对异常值的鲁棒性天生更强,损失函数在残差大于阈值时从平方退化为线性,高污染日的数据不会单点带偏整条回归线。
5.5 标准化后忘记还原预测值:数字对不上领导
现象:评估时 RMSE 看着挺小,但预测值和真实浓度画在同一张图上差了一个量级,全是零点几的小数。
原因:训练前对特征或者目标做了 StandardScaler,预测结果自然落在标准化后的尺度上,没做 inverse_transform 还原。
解决:目标变量不要标准化,保持原始浓度单位;如果真的标准化了,预测后必须用scaler.inverse_transform(y_pred)还原。如果用的是make_pipeline包标准化,预测输出会自动反标准化。凡是训练和预测走了两条路的,都要检查是否做了可逆变换,这条经验同样适用于其他回归项目。
6. 从静态模型到滚动预测小系统:重训练、可视化与输出
6.1 滚动预测与重训练:让模型跟上季节漂移
静态模型一次性训练完就上线,PM2.5 的浓度分布会随季节漂移,冬夏差异巨大,模型用久了精度必然下滑。常见做法是滚动预测加定期重训练:每天用最新数据预测未来24小时,每7天用最近三个月的数据重新训练一次,既保证样本量,又跟上漂移。
# 滚动预测示例:用当前特征递推未来24小时 def rolling_predict(model, df, steps=24): preds = [] temp = df.copy() for _ in range(steps): x = temp[features].iloc[-1:].values pred = model.predict(x)[0] preds.append(max(0, pred)) # 负数兜底 new_row = temp.iloc[-1:].copy() new_row['pm25'] = pred # 预测值接回序列 temp = pd.concat([temp, new_row], ignore_index=True) return preds滚动预测的关键是“预测即数据”:每预测出一步,就把结果接回序列尾部,供下一步的滞后特征使用,否则多步预测会退化成重复同一个值。这种递推方式误差会累积,预测步数越远越不可信,所以24小时以上的预测建议直接用 lag24 特征一次到位,而不是滚动24次。重训练我放在一个定时任务里,每次拉最新数据、重建特征、重新拟合,用模型文件覆盖旧的。
6.2 可视化与结果输出:把预测变成业务能用的东西
模型跑通只是第一步,落地要解决“预测结果给谁看、怎么用”。可视化我一般画两样:一是近48小时真实值与预测值的对比曲线,直接反映模型状态;二是未来24小时的预测曲线加置信带,置信带用历史残差的标准差近似,业务看到的是一个区间而不是一个单点。
# 预测结果导出 result = pd.DataFrame({'datetime': future_time, 'pm25_pred': preds}) result.to_csv('pm25_forecast.csv', index=False)导出格式保持与监测数据同构,带上时间戳和浓度单位,这样下游的告警、报表、大屏可以直接消费。我这套系统跑完给我的最大教训是:评估指标再漂亮,也不如一个能稳定输出的 CSV 文件有价值。所有后处理逻辑——负数截断、置信带、单位标注——必须在导出前统一处理好,别把原始预测丢给下游让人家二次加工。希望帮到你。
本文还有配套的精品资源,点击获取