基于线性回归的PM2.5预测系统实战:从原理到Python实现
简介一份基于线性回归实现PM2.5浓度预测的完整Python源码包面向机器学习初学者、数据挖掘课程设计者及空气质量预测相关开发者。项目以监测数据为训练集通过多维度特征拼接与线性回归模型完成对PM2.5的预测涵盖数据读取、特征工程、模型训练与结果输出等关键环节。压缩包共19个文件包含Python源代码、12个CSV数据文件训练数据、测试数据、预测结果与真实答案对照、模型权重npy文件以及5张示意说明图片整体大小仅2.14MB轻量便携。源码结构清晰变量与函数命名规范并配有中间处理步骤的可视化截图便于对照学习。目前已有594人学习下载适合作为课程设计、毕业设计或入门机器学习回归任务的参考实现可直接运行调试并快速替换为自己的数据集。1. 为什么PM2.5预测系统值得用线性回归起手基于线性回归的PM2.5预测系统是环境数据分析和机器学习入门里最能落地的场景之一数据是公开的历史监测记录目标是连续的数值评价标准简单直接——预测值和实测值之间的误差。很多从业者一上来就去试LSTM、Transformer这类复杂时序模型忽略了线性回归。实际上线性回归在处理小时级PM2.5浓度时基线效果并不差而且它有一个巨大的优势每个特征的系数都可以解释你能清楚地看到风速、湿度、昨日同期浓度各自贡献了多少。这个标题里的“python源码.rar”指的是一套完整可跑的预测流程从数据加载、特征构造到模型训练和结果评估。适合谁适合刚接触时间序列预测、想用最小成本搭出第一个可解释预测模型的开发者也适合需要快速做基准线的数据工程师后面换复杂模型时有对比锚点。2. 线性回归预测PM2.5原理与数据准备2.1 回归假设与PM2.5数据的匹配度线性回归的核心假设是目标变量y与特征x之间存在线性关系误差项独立同分布且服从正态分布。PM2.5浓度受气象条件和污染物扩散影响并不严格满足线性假设——比如湿度很高时颗粒物的吸湿增长是非线性的。但小时尺度的预测里滞后浓度与当前浓度之间存在明显自相关这部分可以用线性关系很好地捕获。我不建议大家纠结于假设是否完全满足而是关注残差是否符合预期。常见做法是先跑一版线性回归作为基线再画残差图。如果残差呈现明显的非线性模式比如U形分布说明有重要非线性特征没进模型此时再考虑加特征或换模型。对于PM2.5预测线性回归能解释数据中很大一部分方差尤其是加入滞后项之后。2.2 数据来源与时间序列监督化PM2.5预测系统的输入端通常是监测站逐小时记录。数据一般包含两大块空气质量指标PM2.5、PM10、SO₂、NO₂、CO、O₃和气象指标温度、湿度、风速、风向、气压。如果你拿到的是原始时间序列第一件事是把它从“一行一个时刻”转换成“一行一个样本”的监督学习格式。所谓监督化就是构造特征矩阵X和目标向量y。以单步预测为例如果用过去3小时的浓度来预测下一小时那么t时刻的目标yₜ是PM2.5在t1时刻的值特征就是t时刻能看到的PM2.5在t-3、t-2、t-1时刻的值。在Python里常见做法是用pandas的shift函数import pandas as pd df pd.read_csv(pm25_hourly.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 构造滞后特征过去3小时的PM2.5浓度作为当前时刻可见信息 for lag in range(1, 4): df[fpm25_lag{lag}] df[pm25].shift(lag) # 预测下一小时浓度把t1时刻的值拉到当前行 df[target] df[pm25].shift(-1) # 去掉没有完整滞后信息的头尾行 df_clean df.dropna().reset_index(dropTrue) feature_cols [pm25_lag1, pm25_lag2, pm25_lag3] X df_clean[feature_cols] y df_clean[target] print(X.head()) print(f样本量: {len(df_clean)})shift(lag)表示把浓度序列向下移动lag行lag1时前一小时的浓度对齐到当前行这就是模型在t时刻能拿到的“最近1小时”信息。target用shift(-1)把下一小时浓度拉到当前行再把最后一行没有目标值的样本drop掉。这里有一个容易被忽略的细节dropna的位置很关键因为滞后特征会在数据开头产生3行NaNtarget会在末尾产生1行NaNdropna会把它们一次性清理干净。2.3 缺失值与异常值的处理策略监测站的数据不是完美连续流。设备校准、通信中断、极端污染事件都会导致某几个小时的数据缺失或异常。我的处理顺序是先处理异常值再补缺失值顺序不能反过来。异常值方面PM2.5小时浓度超过600μg/m³的情况在严重沙尘或特殊污染事件中会出现但数量极少。常见做法是先用百分位数定位超过99.5%分位数的值通常先检查是否是设备问题而不是直接截断。我一般用中位数窗口平滑的方式修正而不是删除整行——时间序列删除行会打断连续性给后面构造滞后特征留下隐患。# 用前后12小时的中位数窗口修正严重离群点 def smooth_outlier(series, lower0.005, upper0.995): q_low series.quantile(lower) q_high series.quantile(upper) mask (series q_low) | (series q_high) # 窗口大小为25中心值前后各12小时中位数天然抗离群点 smoothed series.rolling(25, centerTrue, min_periods1).median() series[mask] smoothed[mask] return series df[pm25] smooth_outlier(df[pm25])缺失值用前向填充ffill和后向填充bfill组合处理。常见错误是直接用线性插值——但PM2.5的突变性强线性插值会把一个真实的高浓度尖峰抹平。前向填充更符合数据生成逻辑设备短暂离线时浓度大概率维持在前一水平如果连续缺失超过6小时ffill就不合适了此时按小时滑动平均法补值更稳妥。3. 核心代码实现用sklearn搭出最小可运行预测系统3.1 最小可用代码单特征基线用线性回归预测PM2.5最简版本只需要不到20行代码。用当前小时的实测浓度预测下一小时的浓度这是所有预警系统的最小逻辑import pandas as pd import numpy as np from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score df pd.read_csv(pm25_hourly.csv, parse_dates[date]) # 目标下一小时浓度 df[target] df[pm25].shift(-1) df_valid df.dropna(subset[target]).reset_index(dropTrue) X df_valid[[pm25]].values y df_valid[target].values # 时间顺序切分前80%训练后20%测试 split_idx int(len(X) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(fR2: {r2_score(y_test, y_pred):.4f}) print(fMAE: {mean_absolute_error(y_test, y_pred):.4f} μg/m3) print(fRMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.4f} μg/m3) print(f系数: {model.coef_[0]:.4f}, 截距: {model.intercept_:.4f})这段代码的关键点在于切分方式按时间顺序切分而不是随机切分。原因很简单——时间序列的测试集必须是未来随机切分会把未来的数据混进训练集预测成绩虚高部署到线上立刻崩坏。单特征基线的R²通常在0.90-0.95之间MAE在15-25μg/m³之间系数接近0.96说明当前小时浓度对下一小时有很强的解释力这就是时间序列的“惯性效应”。3.2 多变量扩展加入气象特征与时间特征单特征模型虽然能跑但预测尖峰事件时会明显滞后——模型只知道当前浓度无法预判气象条件突变。这时加入气象特征能改善上限import pandas as pd import numpy as np from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline df pd.read_csv(pm25_hourly.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 滞后特征过去6小时 for lag in range(1, 7): df[fpm25_lag{lag}] df[pm25].shift(lag) # 时间特征小时和星期捕获日周期和星期周期 df[hour] df[date].dt.hour df[dayofweek] df[date].dt.dayofweek feature_cols [fpm25_lag{i} for i in range(1, 7)] [hour, dayofweek, temp, humidity, wind_speed, pressure] df[target] df[pm25].shift(-1) df_clean df.dropna().reset_index(dropTrue) X df_clean[feature_cols] y df_clean[target] split_idx int(len(X) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] # 标准化 线性回归管道 model make_pipeline(StandardScaler(), LinearRegression()) model.fit(X_train, y_train) y_pred model.predict(X_test) print(fR2: {r2_score(y_test, y_pred):.4f}) print(fMAE: {mean_absolute_error(y_test, y_pred):.4f} μg/m3)特征列的设计是这套系统的核心我把它们分成三类特征组列名作用滞后特征pm25_lag1 ~ pm25_lag6捕获污染趋势的惯性时间特征hour、dayofweek捕获交通排放的日周期和星期周期气象特征temp、humidity、wind_speed、pressure捕获扩散条件变化这里用StandardScaler包在LinearRegression外面原因有两个。第一风速、气压、湿度这些字段的量纲不同如果不做标准化回归系数的大小不能直接比较你还以为是某个特征更重要实际是单位的影响。第二标准化后后续换岭回归或Lasso时惩罚项对每个特征的约束是公平的。加入滞后6小时、气象和时间特征后MAE通常能比单特征基线下降15%-20%。从这个版本开始模型不再只是“惯性复制”而是具备了一定的事前感知能力湿度升高、风速降低等扩散条件恶化时预测值会提前升高。3.3 保存与加载模型把训练好的系数落地训练的最终目的不是留在内存里而是落地成可部署的文件。用joblib保存和加载模型import joblib import json # 训练完成后保存模型和特征列顺序 joblib.dump(model, pm25_predictor.pkl) with open(feature_cols.json, w) as f: json.dump(feature_cols, f) # 新进程中加载模型 loaded_model joblib.load(pm25_predictor.pkl) with open(feature_cols.json, r) as f: loaded_cols json.load(f) # 假设 new_features 是按 loaded_cols 顺序构造的 1×N 数组 new_pred loaded_model.predict(new_features)有一个细节必须重视保存特征列的顺序。加载模型做预测时如果把特征顺序排错sklearn不会报错但预测结果完全错了。我见过有人在新版本代码里添加了一个特征后忘记更新保存的列表导致线上服务悄悄用了错位的数据跑了一整天直到做误差对比才发现。所以特征列顺序文件要和模型文件放在一起任何特征变更都必须同时更新这两个文件。提示模型文件与feature_cols.json必须成对保存缺失任何一个都会让整个预测服务不可靠。4. 模型训练与评估让预测结果可信的指标选型4.1 时间序列验证为什么不能用K折随机划分K折交叉验证在默认配置下不考虑时间顺序如果开启shuffle则会把数据随机打乱再折分这在时间序列上是灾难。随机打乱后训练集里包含测试集时段的数据模型学会了“看到未来”评估分数虚高。预测类项目的血泪经验是所有验证方案都必须保持时间顺序。时间序列上常见的验证方式是滚动预测验证。用前80%的数据训练然后在后20%的每个时刻做预测不断对比import numpy as np from sklearn.linear_model import LinearRegression def rolling_validate(X, y, train_ratio0.8, step24): 滚动验证每次用最近的train_ratio比例数据训练预测未来step小时 n len(X) split int(n * train_ratio) predictions [] actuals [] for end in range(split, n, step): start max(0, end - split) X_train, y_train X[start:end], y[start:end] X_test, y_test X[end:endstep], y[end:endstep] model LinearRegression() model.fit(X_train, y_train) preds model.predict(X_test) predictions.extend(preds) actuals.extend(y_test) return np.array(predictions), np.array(actuals)step24表示每24小时重新训练一次。为什么不能每次预测都重新训练因为随着数据量增大全量重训会让验证本身变得很慢step值设得越大验证越快但也越粗。如果数据集只有几千行step可以设为1如果上了几万行step设24比较合理。提示时间序列的任何切分都不允许随机打乱否则模型会把“未来”写进训练集验证分数没有意义。4.2 评估指标的组合使用PM2.5预测的评估指标最常用的有三个RMSE、MAE、R²。单独看哪个都不够组合起来才能判断模型行为。RMSE对离群点敏感。如果预测系统在污染峰值时段出现数量不多的极端偏差RMSE会显著变大——这正好暴露了模型对尖峰事件的低估。MAE对整体平均偏差的刻画更稳健适合判断日常预测的实用程度。R²则回答相对问题模型相对“只用历史均值做预测”提升了多少。我在落地报告里习惯同时给出这三个指标再做一张预测值和实测值的散点图。散点图能直观看出系统误差如果点大部分落在对角线下方说明系统性低估如果点很分散说明随机误差主导。另一个容易被忽略的点评估指标必须按季节分开统计。PM2.5浓度在冬季和夏季的量级差异很大全年平均的MAE无法反映系统在污染季的表现。时段MAE基准关注点全年15-25 μg/m³总体可用性污染季11-3月20-35 μg/m³峰值捕捉能力清洁季6-9月8-15 μg/m³基线误差是否可接受4.3 调参正则化与特征缩放的作用当特征数量增大后线性回归容易过拟合表现就是训练集R²很高、测试集R²明显下降。此时换岭回归Ridge或Lasso是常规操作。Ridge通过L2惩罚约束系数大小Lasso通过L1惩罚产生稀疏解相当于自动做特征选择。用GridSearchCV调alpha参数时要特别注意验证方式必须用时间序列划分不能用默认的K折from sklearn.model_selection import TimeSeriesSplit, GridSearchCV from sklearn.linear_model import Ridge tscv TimeSeriesSplit(n_splits5) param_grid {alpha: [0.01, 0.1, 1, 10, 100]} ridge Ridge() grid GridSearchCV(ridge, param_grid, cvtscv, scoringneg_mean_absolute_error) grid.fit(X_train, y_train) print(f最佳alpha: {grid.best_params_}) print(f最佳MAE: {-grid.best_score_:.4f} μg/m3)TimeSeriesSplit是sklearn内置的时间序列交叉验证器它按时间顺序切出5个训练/验证折每折的训练集只包含验证集之前的数据。alpha的最佳值取决于数据量和特征尺度没有普适推荐值上面这个范围足够覆盖大部分PM2.5特征组的数量级。如果你发现最佳alpha落在边界值比如100说明搜索范围不够要扩大范围重新搜索。5. 避坑指南PM2.5预测中常见的5个翻车现场5.1 翻车现场一随机划分导致分数虚高现象用train_test_split默认参数切分出来的验证集R²高达0.97部署上线后实测误差几乎是验证集的两倍。原因train_test_split的shuffle参数默认为True把未来时刻的数据随机混进了训练集。模型在训练时见过测试时段的数据相当于开卷考试。解决所有时间序列切分必须按时间顺序用行索引切片shuffle必须为False。写成代码就是X[:split_idx]和X[split_idx:]这种切片方式不要走train_test_split的默认路径。5.2 翻车现场二特征与目标时间对齐错误导致“开卷考试”现象模型R²高达0.99几乎完美但部署后预测误差比验证集大3倍以上。原因最常见的是把目标变量所在的原始列也放进了特征矩阵——比如df[pm25]同时出现在X和y里模型学到的其实就是恒等映射或者是构造滞后特征时用了shift(-1)却把它放在了特征组里模型提前看到了未来。解决构造完特征后打印X.head()和y.head()逐行手动检查确认特征列的时间都不晚于目标时间。也可以直接写断言# 确保原始浓度列没有混入特征 assert pm25 not in feature_cols, pm25原始列不能作为特征必须用滞后列5.3 翻车现场三预测峰值时段系统性低估现象实际污染事件中浓度从100跳到250模型只预测到170明显偏低预警系统错过峰值告警。原因线性回归在训练时对离群值做了妥协因为离群值的数量少贡献的误差在损失函数里占比小。模型学到的是一条“平均线”预测值必然向均值回归——极端值被抹平。解决一个有效手段是把滞后窗口加长到6-12小时让模型看到变化趋势而不是单点状态。如果还不够考虑加入气象因子的交互项比如风速×湿度。如果有沙尘爆发这类特殊样本单独用虚拟变量处理也可以。5.4 翻车现场四气象特征缺失导致预测悬空现象某天监测站气象传感器故障湿度、风速字段全为NaN模型直接报错或输出没有意义的数值。原因sklearn的LinearRegression在输入有NaN时会报错如果不报错则是数据管道里某处把NaN替换成了0——但气象变量为0在物理上不可能预测结果完全失真。解决在预测服务的入口做特征完整性校验def validate_features(X_batch): if X_batch.isna().any().any(): raise ValueError(f存在缺失特征: {X_batch.columns[X_batch.isna().any()].tolist()}) return True线上系统还要对气象字段做合理性判断——湿度不在0-100之间、风速为负值就拒绝预测并报警而不是继续往下走。5.5 翻车现场五评估指标只报R²现象模型R²是0.88看起来不错但业务方反馈预测结果经常慢半拍。原因R²衡量的是相对方差解释比例数据本身方差大时R²很容易显得高。它没有反映绝对误差量级也没有体现时序上的滞后性。PM2.5序列的方差波动很大R²高不等于预测准。解决核心业务汇报必须带MAE和峰值时段的分层统计同时画一张时间序列对比图把预测线和真实线画在一起肉眼看滞后程度。我评估模型有一个习惯先看时间轴上的峰值对齐度再回到指标表格里找对应的数值。6. 进阶多步预测与滚动策略把线性回归用到极限基线模型做的是单步预测——用过去6小时预测下一小时。实际部署中你需要预测未来3-6小时甚至24小时否则系统只能提前一小时预警业务价值有限。多步预测有一个核心难题误差累积。如果采用“迭代预测”的方式——先预测t1再把t1的结果当作特征预测t2——预测误差会逐步放大预测时间越长越不可靠。更稳妥的方式是直接法为每个预测步长单独训练一个模型即训练5个回归器分别预测t1到t5。这个做法的代价是训练时间变成原来的5倍但线性回归的训练开销极小这个代价完全可以接受。而且每个步长的模型可以独立调参t5的模型如果效果太差就直接把它作为最长预警上限在系统里明确标注“第5小时后的预测仅供参考”。我在一个模拟项目中实践过这个策略6小时窗口特征训练5个独立模型预测t1到t5。结果t1的MAE在15μg/m³左右t3涨到22t5涨到28。虽然误差在放大但趋势判断仍然可用——更重要的是每个模型都有自己的系数你能精确地看到哪些特征的效应随预测步长变长而变化。风速的系数在t1模型里最大到了t3模型里湿度系数反超这种信息对理解污染传播机制很有价值这在复杂模型里几乎拿不到。最后说一个我养成的习惯每次跑完一版模型保存一份带特征列顺序、训练时间范围、数据版本的元数据文件放在模型旁边命名为model_meta.json。原因是我曾经在接手一版旧模型时因为没有这个文件花了三天才搞清楚训练数据里包含了哪些特征时段导致线上数据格式对不上预测结果一片乱。模型可以迭代这个元数据必须保留。希望这个习惯和前面的踩坑经验能帮到你让PM2.5预测系统从能跑到跑得稳少走几步弯路。本文还有配套的精品资源点击获取