资讯动态

PM2.5预测大作业:用线性回归稳拿高分的完整指南

发布时间:2026/10/24 2:40:54 来源:尧图企业网站定制
简介基于线性回归的细颗粒物PM2.5浓度预测项目是一份面向计算机专业学生、毕业设计人员和机器学习初学者的课程设计类完整资料适合用于期末大作业或项目实战练习。该项目以空气质量预测为场景完整演示了从数据清洗、特征拼接、线性回归建模到预测结果分析的整个流程能帮助理解线性回归在真实数据上的应用方式。压缩包共19个文件包括12个CSV格式的数据集训练数据、测试数据、预测结果等、3个Python程序文件核心算法与执行脚本、1个Markdown格式的说明文档项目介绍与使用说明、1张预测效果示意图、1个测试文件以及1个NumPy模型参数文件整个压缩包大小约2.39MB结构清晰便于按需学习与复用。该项目经导师指导并获得99分的评审成绩代码完整可运行适合作为高分参考模板灵活迁移到其他回归预测任务中目前已有154人学习使用对于正在完成课程设计或准备毕业设计的读者具有实用参考价值。1. 把 PM2.5 预测做成线性回归大作业反而是最稳的选择临近机器学习大作业截止一种让人头皮发麻的场景是同组同学用 LSTM 调了一周还没收敛你手里只有一台笔记本和三天时间。基于线性回归的 PM2.5 预测这类题目恰恰是机器学习大作业里最稳的出口——数据公开、python 源码逻辑短、每一步都有明确的可视化产物高分靠的不是模型花哨而是数据定义干净、评估闭环完整。这篇笔记按预测目标怎么定、数据处理怎么做、线性回归怎么调、坑在哪展开适合想几天内交出一份能答辩项目、并把 R² 稳定做到 0.85 以上的同学。2. 数据集与问题定义先搞清楚预测目标和高分作业的评分逻辑2.1 数据字段与预测目标PM2.5 预测到底预测的是哪个数绝大多数大作业用的都是 UCI 的 Beijing PM2.5 数据集字段固定year、month、day、hour、pm2.5 浓度、露点 DEWP、温度 TEMP、气压 PRES、风向 cbwd、风速 Iws、积雪 Is、降雨 Ir。也有人用本地环境监测站导出的历史 CSV字段更乱但处理思路一样先把时间整理成连续索引再决定用什么预测什么。我见过太多翻车的大作业第一步就栽在目标定义上。你要预测的是下一小时浓度还是未来 24 小时的平均浓度线性回归在长步长上误差累积非常快把多步预测硬塞给普通最小二乘结果往往不如取过去 24 小时均值当作预测值这种朴素基线。所以常见做法是只做单步预测用 t 时刻及之前的信息预测 t1 时刻的 pm2.5文档方法部分写一句预测步长为 1 小时评分人一眼就懂。如果题目明确要求 24 小时预测那关键点是直接用 t 时刻及之前的数据拟合 t24 的浓度中间第 t1 到 t23 小时的数据只能用于生成历史滞后特征绝不能混进训练标签。这里有个容易被答辩追问的细节预测目标的列名和生成方式必须留在源码里可见。我一般会写 df[pm2.5_next] df[pm2.5].shift(-1)放在数据加载函数的最前面并在文档里画一条时间轴说明特征的观测截止时间。这样即便预测结果不理想评分人也知道你的问题定义是清楚的。2.2 滑动窗口构造样本把时间序列切成监督学习的形状线性回归是监督学习模型输入是 X 矩阵、输出是 y 向量每一行是一条独立样本。PM2.5 原始数据是时间序列本质上只有一列带时间戳的浓度想让模型看到历史上发生了什么就必须自己动手把历史观测和未来目标拼到同一行。这一步在机器学习里的专业叫法是滑动窗口构造在大作业里是最容易被省略、却又直接影响预测质量的一步。下面这段是构造样本的核心代码我建议单独放一个函数方便在文档里对着截图讲import pandas as pd import numpy as np def build_samples(df, target_shift-1, lag_hours(1, 3, 6, 12)): # target_shift-1 表示预测下一小时若做提前 24 小时预测改成 -24 df df.sort_values(time).reset_index(dropTrue) df[y] df[pm2.5].shift(target_shift) for h in lag_hours: df[fpm2.5_lag_{h}] df[pm2.5].shift(h) return df.dropna().reset_index(dropTrue)逻辑说明sort_values(time) 是前提如果原始文件日期没有严格排好shift 出来的滞后值就是错位的很多爬来的 CSV 会按站点分组先按时间排序再做窗口是必须的。shift(target_shift) 把目标值上移shift(h) 把历史值下拉对齐dropna 删除头部没有完整历史窗口的样本。参数说明lag_hours 决定模型能看到多远的过去。小时级数据我一般先取 1、3、6、12 四档1 小时捕捉短期突变3 到 6 小时覆盖污染过程演变12 小时引入昼夜特征。滞后窗口越大有效样本起始点越靠后数据量损失也越多加特征前先算一下手里还剩多少行。顺带一提pandas 2.x 对字符串列和缺失值的行为更严格跑前先 df.info() 瞄一眼类型。2.3 训练集/验证集切分时间序列乱 shuffle 是第一个翻车点切分这一节几乎每个线性回归大作业里都有人在这里摔跤。原因是 sklearn 的 train_test_split 默认 shuffleTrue原始数据是时间序列一旦随机打乱2015 年的样本可能进训练集2010 年的样本进测试集模型等于提前看到了未来测试集 R² 飙到 0.99。老师画一张时间轴图当场拆穿。split_idx int(len(df) * 0.8) # 按时间先后切分不 shuffle X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] print(ftrain period: {df[time].iloc[0]} ~ {df[time].iloc[split_idx-1]}) print(ftest period: {df[time].iloc[split_idx]} ~ {df[time].iloc[-1]})逻辑说明手写切片而不是给 train_test_split 传 shuffleFalse好处是区间边界显式出现在源码里可读性和答辩安全性都更好。print 出两段时间范围这份日志截屏放进作业文档就是最好的数据划分说明。参数说明8:2 是默认习惯但我建议先看测试集的月份分布别让测试集恰好落在污染严重的供暖季。如果训练集和测试集的季节分布严重失衡模型在测试集上会系统性偏低这时候不是线性回归不行而是划分方式有问题。改成按自然年切分用 2014 全年训练、2015 全年测试通常比固定 8:2 更抗辩。除了切分加载时还有一个脏数据坑部分版本的 CSV 把缺失值写成 NA 字符串pd.read_csv 不指定 na_values 会把这一列读成 object 类型后面做回归直接报错。我习惯在加载时统一声明df pd.read_csv(BeijingPM20100101_20151231.csv, na_values[NA, --, ]) df[pm2.5] df[pm2.5].ffill().fillna(df[pm2.5].mean())这样 pm2.5 列的缺失值在进入建模前就已经统一处理成 float后面做滑动窗口也不会因为字符串拼接报错。这两行代码的意思后面会细讲这里只强调一点数据处理顺序出了问题线性回归再标准也救不回来。3. 特征工程与数据清洗线性回归能不能拿高分七成在特征3.1 缺失值与异常值PM2.5 数据的脏比想象中多pm2.5 传感器在某些时段会整体掉线恢复后浓度又从低处回升这类结构性缺失如果直接 dropna会一次丢掉连续几十个小时的样本数据集容量大幅缩水。常见做法是前向填充因为 PM2.5 是强自相关变量上一个有效值比全局插值更接近真实浓度填充完再对整个列做一个均值兜底防止序列开头连续缺失导致填充链断裂。import pandas as pd import numpy as np df[pm2.5] df[pm2.5].replace(-999, np.nan) # 传感器哨兵值先还原成 NaN df[pm2.5] df[pm2.5].ffill().fillna(df[pm2.5].mean()) print(df[pm2.5].describe())逻辑说明replace(-999, np.nan) 处理的是某些版本里用 -999 表示仪器异常的情况不替换的话这个数值会被当成真实浓度参与回归单条样本就能把 RMSE 拉高几十。ffill 是前向填充fillna(mean()) 是最后兜底两行合起来覆盖了单个空洞和整段缺失两类情况。参数说明前向填充不是万能的如果缺失段超过 6 小时建议改用线性插值 df[pm2.5].interpolate(methodlinear)避免把一段 12 小时的缺失全填成同一个值。判断标准很简单填充后打印 describe看 min 和 max 是否还在合理区间比如 pm2.5 浓度出现负数就说明还有脏数据没清干净。3.2 相关性筛选与类别变量处理风向不能直接乘系数特征不是越多越好线性回归的一大特点就是特征之间一旦共线性明显系数估计的方差会变大单个特征的系数符号都可能反掉。我先用 df.corr()[y] 把特征和目标的相关性排序挑出相关性强的天气变量和滞后浓度弱相关的先不管防止它们在报告里制造噪音。年份、月份这类列我一般直接剔除因为 year 和 month 对建模没有增益还会让标准化后的系数解释变得很别扭。风向 cbwd 是字符型类别变量常见的取值有 cv静风、NE、NW、SE 几种不能直接参与乘法运算必须独热编码。独热编码有一个经典坑叫哑变量陷阱四类风向用四列二进制编码后四列之和恒等于 1与截距项产生完全共线性sklearn 不报错但系数解释会失真。解决方法是 drop_first把第一个类别作为参考类只保留其余三列。df pd.get_dummies(df, columns[cbwd], drop_firstTrue) print([c for c in df.columns if c.startswith(cbwd)])逻辑说明get_dummies 会把 cbwd 展开成 cbwd_NE、cbwd_NW、cbwd_SE、cbwd_cv 等列drop_firstTrue 丢弃第一类剩下三列都是 0/1 独热标志。打印出来的列名列表要写进文档后面建模时特征列表照着填少一列模型也能跑但结果解释就歪了。参数说明如果类别变量不是风向而是城市名、站点名drop_first 同样适用。独热编码的代价是维度膨胀几十个类别就是几十列样本量不够时线性回归会过拟合这时可以考虑改用目标编码或者直接把这类信息从特征里去掉。3.3 标准化为什么线性回归必须做以及一个防泄漏细节线性回归本身对量纲不敏感把温度从摄氏度换成华氏度系数会按比例缩放预测结果不变。但大作业里还是建议做标准化理由有两个一是如果后面对比 Ridge 或 Lasso正则化惩罚项对量纲敏感不做标准化时取值范围大的特征会被惩罚得更重二是标准化之后系数大小才能横向比较作为特征重要性的粗略证据放进报告。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)逻辑说明这里故意用 fit_transform 和 transform 两种方法强调训练集 fit、测试集只 transform。StandardScaler 计算的是均值和标准差如果对整个数据集先 fit 再切分测试集的统计信息会混进训练过程这属于数据泄漏是答辩时的高频提问点。参数说明StandardScaler 默认 with_meanTrue、with_stdTrue对应 z-score 标准化。如果特征里有大量稀疏 0/1 列可以改成 with_meanFalse 避免破坏稀疏性但对 PM2.5 这种稠密表格数据默认参数足够。想换 MinMaxScaler 也可以它把数据压缩到 [0,1] 区间对线性回归预测结果影响不大但画图时 z-score 的均值 0 更容易讲清楚。特征工程做完最好把相关系数矩阵存成一张图放进文档。这一步不增加代码复杂度但能向评分人证明你不是把全部列一股脑塞给模型的黑匣子选手筛选前后各跑一次模型把 R² 的变化写在表里比空口说我做了一堆特征有用得多。4. 用 Python 实现 PM2.5 预测一份可以直接交作业的源码流程4.1 最小可运行版本从加载到出预测结果前面做了大量准备工作到这里把它们串成一个能跑的 python 脚本。我一般把所有步骤收进 main() 函数数据清洗、特征构造、训练、评估各自独立成函数这样在命令行跑和 Jupyter 里逐格跑结果一致也方便换数据集后一键重跑。import pandas as pd import numpy as np from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score FEATURES [DEWP, TEMP, PRES, Iws, Is, Ir, pm2.5_lag_1, pm2.5_lag_3, pm2.5_lag_6, pm2.5_lag_12, hour_sin, hour_cos, cbwd_NE, cbwd_NW, cbwd_SE] def load_data(path): df pd.read_csv(path, na_values[NA, --, ]) df[time] pd.to_datetime(df[[year, month, day, hour]]) df df.sort_values(time).reset_index(dropTrue) df[pm2.5] df[pm2.5].ffill().fillna(df[pm2.5].mean()) df[y] df[pm2.5].shift(-1) for h in (1, 3, 6, 12): df[fpm2.5_lag_{h}] df[pm2.5].shift(h) df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) df pd.get_dummies(df, columns[cbwd], drop_firstTrue) return df.dropna().reset_index(dropTrue) def main(): df load_data(BeijingPM20100101_20151231.csv) X, y df[FEATURES], df[y] split_idx int(len(df) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] scaler StandardScaler().fit(X_train) X_train_s scaler.transform(X_train) X_test_s scaler.transform(X_test) lr LinearRegression() lr.fit(X_train_s, y_train) y_pred lr.predict(X_test_s) mae mean_absolute_error(y_test, y_pred) rmse mean_squared_error(y_test, y_pred, squaredFalse) r2 r2_score(y_test, y_pred) print(fMAE{mae:.2f} RMSE{rmse:.2f} R2{r2:.4f}) if __name__ __main__: main()逻辑说明load_data 里先拼接 time 列并排序解决原始文件乱序问题hour_sin 和 hour_cos 属于周期编码把 hour12 和 hour0 的相似性表达出来避免 23 点和 0 点被当成两个完全不同的数值整个函数以 dropna 收尾返回的每一行都有完整的目标和滞后特征后续建模不会再被 NaN 中断。参数说明FEATURES 列表是全局可改的做消融实验时比如去掉所有滞后特征再看 R² 掉多少只需要改这一个列表不用动模型代码。split_idx 的 0.8 建议改成按时间边界切比如用 2010 到 2014 年底做训练、2015 全年做测试答辩时解释起来更有说服力。4.2 算法选型与参数线性回归、岭回归和 SGD 怎么选作业主模型一般用 sklearn 的 LinearRegression它默认走最小二乘的封闭解几十个特征的规模完全够用不需要调学习率。但如果你想在文档里增加对比实验常见配置是 Ridge 和 SGDRegressor。三者适用场景差别很大我一般列成一张表放在方法部分模型数学形式适合的场景最值得调整的参数LinearRegression最小二乘特征少、共线性弱fit_intercept默认 TrueRidge最小二乘 L2 惩罚特征多、共线性明显alpha通常 0.1 到 10SGDRegressor梯度下降数据量几十万以上learning_rate、max_iter线性回归在这个项目里不是最简单的凑数模型而是baseline 必须稳的选择。Ridge 的存在是为了在文档里写出加 L2 正则后测试集 R² 从 0.86 提到 0.87这样的对比结论SGDRegressor 则适合你在源码里额外准备一个大数据量版本体现工程意识。参数说明Ridge 的 alpha 从 0.01、0.1、1、10 各试一遍画一条 alpha 对 R² 的曲线就够写成一小节实验分析SGDRegressor 注意三个参数max_iter 不够或 learning_rate 太大时不收敛打印 loss 曲线确认这几个参数也是答辩老师最可能追问的地方。4.3 评估指标R²、MAE、RMSE 对应不同评分逻辑指标这节我见到的常见误用是只写 R² 一个数。R² 衡量模型解释的方差比例但它的计算依赖残差平方和对极端值非常敏感。PM2.5 数据里有沙尘暴这类极端事件时个别大误差样本会在 R² 里占到很大权重。MAE 是平均绝对误差单位与真实浓度一致老师最直接的感受是我的模型平均偏差 15 微克RMSE 放大了大误差的惩罚适合用来暴露极端事件的预测失败。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score print(MAE%.2f % mean_absolute_error(y_test, y_pred)) print(RMSE%.2f % mean_squared_error(y_test, y_pred, squaredFalse)) print(R2%.4f % r2_score(y_test, y_pred)) # MAE 和 RMSE 单位都是 ug/m3R2 无量纲逻辑说明mean_squared_error 在较新的 sklearn 版本里用 squaredFalse 返回 RMSE如果环境是老版本写成 np.sqrt(mean_squared_error(...)) 结果一样但要确认环境版本并把这个细节留在环境说明里。三个指标最好都打印并写进文档因为评分标准经常是MAE 低于某阈值或R² 高于某阈值只报一个数可能直接失分。参数说明如果测试集覆盖了一整年按季度分别算指标会更有说服力冬天的 R² 通常比夏天低这是模型在逆温高发期的系统性短板把它写进局限与改进章节反而是加分项。指标计算时注意 y_test 和 y_pred 都必须是 floatsklearn 不接受带 NaN 的数组评估前再做一次 dropna 双保险。5. 高分项目避坑指南5 个让老师一眼识破的常见问题5.1 数据随机 shuffle验证集分数虚高到 0.99现象测试集 R² 高达 0.98画预测对比图却明显错位答辩一追问就露馅。原因train_test_split 默认 shuffleTrue模型看到了未来数据等价于拿答案考试。解决按时间索引连续切分并在文档里写明划分区间。我检查别人源码时第一眼就找 train_test_split 有没有 shuffleFalse这一步几乎能筛掉一半翻车项目。正确写法和上章一致切片后顺手打印两段的时间范围日志放进文档从根源上杜绝说不清划分方式的问题。5.2 标准化时混入测试集统计量信息泄漏不自知现象R² 偏高但系数解释乱套把 scaler.mean_ 打出来发现均值明显不属于训练集分布。原因先对整个数据集 fit 了 StandardScaler再切分训练测试集测试集的均值方差提前进入了训练过程。解决先切分后 fit训练集用 fit_transform测试集只用 transform。这条代码顺序问题是机器学习中的数据处理考得最细的点之一属于那种说出来人人都懂、不说就踩坑的细节。提交前可以在源码里临时加一行 print(scaler.mean_)对照训练集真实均值确保数字一致。5.3 只报 R²被沙尘暴极端值带偏现象删掉某一天污染爆表的样本后R² 从 0.78 跳到 0.92老师质疑你数据造假。原因MSE 和 R² 对极端误差非常敏感一条 800 微克的异常预测就能把指标拉垮。解决同时报 MAE 和 RMSE极端样本在文档里单独讨论不要偷偷删除。如果确实删了必须在数据清洗章节写明删除条件和条数比如共删除 3 条浓度超过 900 的传感器异常记录这种透明度反而会让评分人信任你的处理流程。5.4 滞后特征边界没写清被质疑用了未来数据现象老师看完特征列表质问你拿 t 时刻的 pm2.5 预测 t1这不就是已经知道答案了原因滞后特征和预测步长的关系在文档里没画清楚评分人默认你泄漏了未来信息。解决在方法部分放一张对齐表写清特征最大观测时刻为 t预测目标为 t1并强调滞后特征全部来自历史观测。用当前可获得的观测去预测未来在工作场景里正是常规做法但文档里不写就是你理亏一句话加一张表就能避免最尴尬的答辩场景。5.5 文档与源码参数脱节复现直接失败现象文档写岭回归 alpha0.1代码里根本没有 Ridge老师按文档去跑直接报错。原因报告和代码是两个人写的或者从模板复制后参数没同步。解决每个出现在文档表格里的模型和参数在源码里必须能找到赋值位置提交前从零环境跑一遍 main()把实际输出值填回文档。这条不算加分项而是拿高分的基本功很多高分项目最后死在复现这一步太可惜了。6. 从 85 到 95残差分析、时间序列交叉验证与可视化叙事6.1 残差图是文档里性价比最高的一张图模型跑完别急着写结论先画残差图。横轴是预测值纵轴是真实值减预测值散点如果随机分布在零线上下说明模型没有系统性偏差如果出现漏斗形说明预测值越大误差越分散这是异方差信号提示你考虑对目标取 log 再回归。import matplotlib.pyplot as plt residual y_test - y_pred plt.scatter(y_pred, residual, s6, alpha0.6) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted pm2.5) plt.ylabel(Residual) plt.savefig(residual.png, dpi150)残差图放进作业里表达的是我知道模型在哪一类场景失效这比多调一个参数更能加分。6.2 用 TimeSeriesSplit 代替单一切分单一固定切分很容易被质疑为什么选这个边界。常见做法是改用 sklearn 的 TimeSeriesSplit做 5 折交叉验证每一折训练集在前、测试集在后最后报的是 5 组指标的均值和标准差。这个数字比单次 0.86 更有说服力而且几乎不用改代码把原来的手写切片替换成迭代器即可。6.3 预测曲线叠加原序列用图讲清模型边界最后一张建议放的图是取测试集前 200 小时把真实 pm2.5 和预测值画在同一条时间轴上x 轴用 datetime。这张图能直观看到模型在污染峰值处的滞后——它总是慢半拍。这不是模型的锅而是滞后特征的固有缺陷但你能在论文里主动点出预测峰值存在 1 到 2 小时延迟评分人会认为你对方法边界有清醒认知而不是只会报喜不报忧。我当年交这类作业时模型分数不差就是没画残差图丢了文档分后来每次落地任何预测模型都会先画残差再谈调参。这个习惯一直留到了今天希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑