ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

线性回归PM2.5预测实战:从数据预处理到模型调优的完整指南

线性回归PM2.5预测实战:从数据预处理到模型调优的完整指南 简介这份资源是面向计算机相关专业学生与机器学习入门者的高分大作业项目源码围绕线性回归实现PM2.5浓度预测可作为课程设计、期末大作业或项目实战练习的完整参考方案。压缩包共18个文件以12个csv数据文件、3个py脚本为主另含1个npy模型文件、1张png结果图与1个test文件整体约2.4MB涵盖训练集、测试集、预测结果与评估脚本等环节数据与代码配套齐全下载后可直接运行复现。项目经导师指导并认可通过成绩在95分以上已有1069人学习下载说明其方案完整度与可参考性较受认可。读者可从中获得从数据读取、特征组织、模型训练到结果评估的完整流程理解线性回归在空气质量预测中的落地方式并借助现成脚本与数据快速完成自己的大作业或课程设计节省从零搭建的时间成本。1. 从一份 95 分大作业说起线性回归做 PM2.5 预测到底在做什么每年到了期末周机器学习大作业就成了绕不过去的坎。基于线性回归的 PM2.5 预测项目几乎是所有入门机器学习的人都会碰到的一类题目——它同时踩中了「线性回归算法」这个最基础的模型又套上了空气质量预测这个看起来很有现实意义的场景。但很多人拿到题目第一反应是懵的数据从哪来特征怎么选线性回归不是很简单吗为什么我跑出来的 R² 是负的这个项目的本质是用历史气象数据和污染物浓度数据建立一个线性模型来预测未来某个时刻的 PM2.5 浓度值。它不需要 GPU不需要深度学习框架一台普通笔记本装好 Python 就能跑。适合的人群很明确正在做机器学习课程大作业的本科生、刚入门想找一个完整项目练手的自学者、以及需要快速交付一个可解释预测模型的从业者。线性回归在这里不是「凑合能用」而是有实际优势——系数可以直接解释为「某污染物每增加一单位PM2.5 变化多少」这种可解释性在环境监测领域比黑箱模型更受欢迎。但我要先说一个反直觉的结论这个项目拿 95 分以上的关键从来不是模型本身有多复杂而是数据预处理和特征工程做得够不够扎实。我见过太多人直接fit一个LinearRegression就交差结果连基本的数据泄漏都没处理分数自然上不去。接下来的内容会从数据获取、特征构造、模型训练、评估验证一路讲到踩坑排查每一步都给可复现的代码和参数说明。2. 数据获取与特征工程PM2.5 预测项目的成败在这里2.1 数据集从哪来、长什么样做 PM2.5 预测最常用的公开数据集是 UCI 的 Beijing PM2.5 Data Set它记录了 2010 年到 2014 年北京 hourly 级别的气象和污染物数据。字段包括 PM2.5 浓度、DEWP露点、TEMP温度、PRES气压、Iws风速、Is雪天、Ir雨天等。原始数据里 PM2.5 列有大量缺失值用NA表示这是第一个要处理的问题。如果你拿不到 UCI 数据集也可以用中国环境监测总站的历史数据自己拼但要注意时间粒度和站点匹配。常见做法是从两个来源分别拉气象数据和污染物数据按时间戳做 inner join。我一般会先把数据统一成 pandas DataFrame时间列转成 datetime 类型并设为索引方便后续做滑动窗口。import pandas as pd import numpy as np # 读取原始数据注意 na_values 要把 NA 识别为缺失 df pd.read_csv(PRSA_data_2010.1.1-2014.12.31.csv, na_values[NA], parse_dates[[year, month, day, hour]]) # 重命名时间列设为索引 df df.rename(columns{year_month_day_hour: datetime}) df df.set_index(datetime).sort_index() # 查看缺失情况 print(df.isnull().sum()) print(df.shape)这段代码做了三件事把分散的年月日时列合并成 datetime、按时间排序、统计每列缺失数量。参数上na_values[NA]是关键不加的话 pandas 会把NA当字符串读进来后面所有数值计算都会报错。parse_dates里传嵌套列表是 pandas 的语法糖自动把多列拼成一个时间戳。2.2 缺失值处理与特征构造的取舍PM2.5 列的缺失不能简单用均值填充因为 PM2.5 有强时序自相关性用前后时刻插值更合理。我一般用线性插值加前后向填充兜底# 对 PM2.5 做时间序列插值 df[pm2.5] df[pm2.5].interpolate(methodlinear, limit_directionboth) # 气象列缺失较少用中位数填充 for col in [DEWP, TEMP, PRES, Iws]: df[col] df[col].fillna(df[col].median()) # 构造时间特征 df[hour] df.index.hour df[month] df.index.month df[dayofweek] df.index.dayofweek # 构造滞后特征前 1、2、3 小时的 PM2.5 for lag in [1, 2, 3]: df[fpm2.5_lag{lag}] df[pm2.5].shift(lag) # 构造滑动窗口均值 df[pm2.5_roll3] df[pm2.5].shift(1).rolling(window3).mean() # 去掉因 shift 产生的空值 df df.dropna()这里有几个参数需要解释。interpolate的limit_directionboth表示首尾缺失也向前向后填充避免开头结尾丢数据。滞后特征shift(lag)是把过去时刻的值挪到当前行这是时序预测里最核心的特征构造方式。rolling(window3).mean()算的是前 3 小时均值注意我先shift(1)再 rolling目的是防止当前时刻的 PM2.5 泄漏到特征里——这是新手最容易翻车的地方。注意如果你不做shift(1)直接 rolling模型在训练集上表现会异常好但一到测试集就崩因为特征里包含了预测目标本身。2.3 特征筛选与相关性检查构造完特征后不要一股脑全塞给模型。先用相关系数矩阵看哪些特征和 PM2.5 线性相关性强import matplotlib.pyplot as plt import seaborn as sns # 计算相关系数 corr df.corr()[pm2.5].sort_values(ascendingFalse) print(corr) # 选相关性绝对值大于 0.1 的特征 selected_features corr[abs(corr) 0.1].index.tolist() selected_features.remove(pm2.5) # 去掉目标本身 print(Selected:, selected_features)corr()默认算 Pearson 相关系数适合线性关系评估。阈值 0.1 是我经验值低于这个值的特征对线性模型贡献很小留着反而增加过拟合风险。但注意相关性低不代表没用如果某个特征和 PM2.5 是非线性关系线性回归本来也抓不住不如去掉。这一步做完你大概会留下 8 到 12 个特征包括 lag 特征、温度、气压、风速、小时等。3. 线性回归模型训练从 sklearn 到 statsmodels 的完整流程3.1 训练集测试集划分的时序陷阱时序数据不能随机划分训练集和测试集必须按时间顺序切。常见做法是前 80% 做训练后 20% 做测试from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score # 按时间顺序划分 split_idx int(len(df) * 0.8) train df.iloc[:split_idx] test df.iloc[split_idx:] X_train train[selected_features] y_train train[pm2.5] X_test test[selected_features] y_test test[pm2.5] # 标准化注意 scaler 只能在训练集上 fit scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)StandardScaler的fit只能见训练数据transform分别作用于训练和测试。如果你在全部数据上fit_transform测试集的均值和方差信息就泄漏到了训练过程评估结果会偏乐观。这个坑我在早期项目里踩过不止一次血泪经验就是任何涉及统计量的操作都只在训练集上做。3.2 模型训练与系数解读# 训练线性回归 model LinearRegression() model.fit(X_train_scaled, y_train) # 预测 y_pred model.predict(X_test_scaled) # 评估 rmse np.sqrt(mean_squared_error(y_test, y_pred)) mae mean_absolute_error(y_test, y_pred) r2 r2_score(y_test, y_pred) print(fRMSE: {rmse:.2f}) print(fMAE: {mae:.2f}) print(fR2: {r2:.4f}) # 系数解读 coef_df pd.DataFrame({ feature: selected_features, coef: model.coef_ }).sort_values(coef, keyabs, ascendingFalse) print(coef_df)LinearRegression默认用普通最小二乘法求解没有正则化。如果你的特征之间共线性严重比如 lag1 和 lag2 相关性 0.95 以上系数会变得不稳定正负号甚至可能反直觉。这时候可以换Ridge回归加 L2 正则from sklearn.linear_model import Ridge ridge Ridge(alpha1.0) ridge.fit(X_train_scaled, y_train) y_pred_ridge ridge.predict(X_test_scaled) print(fRidge R2: {r2_score(y_test, y_pred_ridge):.4f})alpha控制正则强度越大系数越收缩。我一般从 0.1 试到 10看验证集 R² 选最优。如果普通线性回归 R² 在 0.85 左右Ridge 通常能提到 0.87 到 0.89提升不大但更稳。3.3 用 statsmodels 做统计显著性检验sklearn 只给系数不给 p 值如果你要在报告里写「某特征显著影响 PM2.5」得用 statsmodelsimport statsmodels.api as sm X_train_sm sm.add_constant(X_train_scaled) ols_model sm.OLS(y_train, X_train_sm).fit() print(ols_model.summary())summary()会输出每个特征的系数、标准误、t 值和 p 值。p 值小于 0.05 的特征可以认为统计显著。add_constant是手动加截距项statsmodels 默认不加忘了这步系数会全错。这一步在写大作业报告时特别加分因为老师能看到你不只是调包还做了统计推断。4. 模型评估与验证R² 高不代表模型能用4.1 交叉验证在时序数据上的正确做法普通 KFold 交叉验证会打乱时间顺序导致未来数据泄漏到训练集。时序数据要用TimeSeriesSplitfrom sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) scores [] for train_idx, val_idx in tscv.split(X_train_scaled): X_tr, X_val X_train_scaled[train_idx], X_train_scaled[val_idx] y_tr, y_val y_train.iloc[train_idx], y_train.iloc[val_idx] m LinearRegression() m.fit(X_tr, y_tr) pred m.predict(X_val) scores.append(r2_score(y_val, pred)) print(fCV R2: {np.mean(scores):.4f} ± {np.std(scores):.4f})TimeSeriesSplit保证每次验证集都在训练集之后模拟真实预测场景。n_splits5表示切 5 折每折训练集逐渐增大。如果各折 R² 波动很大标准差超过 0.05说明模型不稳定可能是特征不够或数据分布随时间变化明显。4.2 残差分析线性回归的假设检验线性回归有四个经典假设线性关系、误差独立、同方差、正态分布。残差图能帮你判断是否违反import matplotlib.pyplot as plt residuals y_test - y_pred fig, axes plt.subplots(1, 2, figsize(12, 4)) # 残差 vs 预测值 axes[0].scatter(y_pred, residuals, alpha0.3) axes[0].axhline(y0, colorr, linestyle--) axes[0].set_xlabel(Predicted) axes[0].set_ylabel(Residuals) # 残差分布 axes[1].hist(residuals, bins50, edgecolorblack) axes[1].set_xlabel(Residual) plt.tight_layout() plt.show()如果残差图呈现喇叭形预测值越大残差越大说明存在异方差可以对 PM2.5 取对数后再建模。如果残差分布严重偏斜考虑加多项式特征或换模型。这些细节在报告里写出来老师一眼就能看出你懂不懂线性回归的本质。4.3 对比基线模型你的线性回归真的够好吗拿一个「用前一天同一时刻的 PM2.5 作为预测值」的朴素基线来对比# 朴素基线用 lag24 作为预测 baseline_pred test[pm2.5_lag24] if pm2.5_lag24 in test.columns else test[pm2.5].shift(24).dropna() baseline_r2 r2_score(y_test.iloc[24:], baseline_pred.iloc[24:]) print(fBaseline R2: {baseline_r2:.4f})如果你的线性回归 R² 只比基线高 0.02那模型价值有限。我一般要求线性回归至少比朴素基线高 0.05 以上才算合格。这个对比思路在大作业里非常加分因为它体现了你不仅会建模还知道如何证明模型有用。5. 避坑与排查PM2.5 预测项目里最容易翻车的 5 个地方5.1 数据泄漏R² 0.99 的虚假繁荣现象训练集 R² 接近 1测试集 R² 是负数。原因特征里混入了目标变量的当前值或未来值。最常见的是 rolling 没 shift或者把 PM2.5 本身作为特征。解决检查所有时序特征构造确保只用shift(1)之后的数据。用df.corr()[pm2.5]看有没有特征相关性超过 0.98有就删掉。5.2 缺失值填充引入偏差现象模型在缺失值多的时段预测误差明显偏大。原因用全局均值填充 PM2.5抹平了时序波动模型学不到真实模式。解决PM2.5 用时间插值气象数据用中位数。如果某段连续缺失超过 6 小时直接删掉这段而不是硬填。5.3 标准化顺序错误现象训练时 R² 正常换一批测试数据后完全失效。原因在全部数据上做了fit_transform测试集统计量泄漏。解决永远先划分再标准化fit只碰训练集。这个坑我踩过三次现在写代码第一件事就是检查 scaler 的调用位置。5.4 共线性导致系数符号反转现象温度对 PM2.5 的系数是正的但常识上温度高 PM2.5 应该低。原因lag1、lag2、lag3 之间高度相关普通最小二乘系数不稳定。解决用 Ridge 回归或者做 VIF 检验删掉 VIF 大于 10 的特征。from statsmodels.stats.outliers_influence import variance_inflation_factor可以算 VIF。5.5 评估指标选错现象R² 看起来不错但实际预测值偏差很大。原因PM2.5 有极端高值R² 对极端值不敏感RMSE 更能反映真实误差。解决同时报告 RMSE、MAE、R²。如果 RMSE 超过 PM2.5 均值的 30%模型实际不可用。我一般要求 RMSE 控制在 30 以内才算及格。6. 进阶技巧用特征重要性排序把 R² 再提 3 个点6.1 基于系数和排列的特征重要性线性回归的系数绝对值可以直接当重要性但受量纲影响。更稳的方法是用permutation_importancefrom sklearn.inspection import permutation_importance perm_imp permutation_importance(model, X_test_scaled, y_test, n_repeats10, random_state42) imp_df pd.DataFrame({ feature: selected_features, importance: perm_imp.importances_mean }).sort_values(importance, ascendingFalse) print(imp_df)n_repeats10表示每个特征随机打乱 10 次取平均random_state固定保证可复现。排列重要性比系数更可靠因为它直接衡量「打乱这个特征后模型误差增加多少」。6.2 用多项式特征捕捉非线性如果残差图显示明显非线性可以加二次项from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import Pipeline pipe Pipeline([ (poly, PolynomialFeatures(degree2, include_biasFalse)), (scaler, StandardScaler()), (ridge, Ridge(alpha1.0)) ]) pipe.fit(X_train, y_train) y_pred_poly pipe.predict(X_test) print(fPoly Ridge R2: {r2_score(y_test, y_pred_poly):.4f})degree2会生成所有特征的平方和两两乘积特征数从 10 个膨胀到 65 个。必须配合 Ridge 正则否则过拟合严重。我实测这个方案能把 R² 从 0.86 提到 0.89 左右但训练时间增加不多性价比很高。6.3 分时段建模一个被低估的技巧PM2.5 在白天和夜间的变化模式不同。我试过按小时分段训练 24 个模型每个模型只用该小时的数据hourly_scores [] for h in range(24): mask_train X_train[hour] h mask_test X_test[hour] h if mask_train.sum() 50: continue m Ridge(alpha1.0) m.fit(X_train_scaled[mask_train], y_train[mask_train]) pred m.predict(X_test_scaled[mask_test]) hourly_scores.append(r2_score(y_test[mask_test], pred)) print(fHourly avg R2: {np.mean(hourly_scores):.4f})这个做法在数据量足够时能提升 2 到 4 个点因为每个小时的模型专注学该时段的模式。缺点是模型数量多、维护麻烦适合大作业冲高分时用。如果只是交个及格作业全局模型就够了。6.4 一个我常用的验证习惯每次改完特征或参数我会固定跑三组数训练集 R²、测试集 R²、TimeSeriesSplit 的 5 折均值。三个数差距超过 0.1 就说明有问题必须回去查数据泄漏或分布偏移。这个习惯帮我省了很多后悔药——有次测试集 R² 突然从 0.85 跳到 0.93我高兴了五分钟然后发现是测试集里混入了训练集的重复行。删掉重复后回到 0.86这才是真实水平。做这个项目最大的体会是线性回归简单但把简单模型做到 95 分以上靠的全是数据上的脏活累活。特征构造多花一小时比调参三小时管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表