
简介这份资源是面向计算机相关专业学生的机器学习大作业完整源码以线性回归为核心方法完成PM2.5浓度预测任务适合正在准备课程设计、期末大作业或需要项目实战练习的学习者使用。项目经导师指导并认可通过可作为高分作业参考模板帮助读者快速理解从数据到建模的完整流程。压缩包共18个文件约2.4MB包含12个csv数据文件、3个py源码文件、1个npy模型文件、1个png图片及测试相关文件覆盖训练集、测试集、预测结果与评估脚本等环节结构清晰便于按模块查阅。目前已有1069人学习下载说明该方案在同类作业中具有较高参考价值。读者可据此掌握线性回归建模思路、数据预处理与结果评估方法并对照源码完成自己的预测项目减少从零搭建的时间成本。1. 从一份 95 分大作业说起线性回归做 PM2.5 预测到底在做什么很多同学搜「机器学习大作业-基于线性回归的PM2.5预测项目python源码」的时候心里其实有两个诉求一是赶紧交上一份能拿高分的作业二是别被老师一问三不知。我当年第一次做这个题也是先去找现成源码结果跑通了却讲不出为什么用线性回归、特征怎么选、R² 为什么只有 0.6。后来带学弟学妹做了几轮才把这条链路摸清楚PM2.5 预测本质是一个多变量回归问题用历史气象和污染物数据去拟合下一时刻的 PM2.5 浓度。线性回归之所以常被选作大作业基线不是因为它最强而是因为它可解释、可推导、可手写梯度下降老师能从公式一路问到代码。这篇笔记就按「数据怎么来 → 特征怎么造 → 模型怎么训 → 指标怎么读 → 坑在哪」的顺序把一份能上 95 分的方案拆开讲新手能照着复现熟手能拿去改造成 Ridge、Lasso 或者加多项式特征。适合正在做机器学习期末项目、想搞懂线性回归落地细节的人。2. 数据与特征工程PM2.5 预测的输入到底长什么样2.1 先搞清楚数据集结构和字段含义常见的 PM2.5 数据集比如经典的北京 PM2.5 数据通常是逐小时记录字段包括 pm2.5、pm10、SO2、NO2、CO、O3 六种污染物外加 TEMP、PRES、DEWP、RAIN、wd、WSPM 六个气象字段还有 year、month、day、hour 四个时间字段。做线性回归之前第一件事不是写模型而是把数据读进来、看缺失、看分布。很多人直接dropna()完事结果样本从 4 万掉到 3 万模型在测试集上表现忽好忽坏这就是没做缺失分析的血泪经验。import pandas as pd import numpy as np # 读取数据注意编码和列名 df pd.read_csv(PRSA_Data_Aotizhongxin_20130301-20170228.csv) print(df.shape) print(df.isnull().sum().sort_values(ascendingFalse).head(10)) # 查看 PM2.5 的分布判断是否需要处理异常值 print(df[PM2.5].describe()) print((df[PM2.5] 0).sum()) # 0 值往往代表缺失不是真实浓度这段代码先看形状和缺失再看 PM2.5 的统计量。参数上isnull().sum()按列统计缺失数量describe()给出均值、四分位数和极值。逻辑上如果 PM2.5 缺失比例超过 10%直接删行会损失太多样本常见做法是用前后时刻插值或者用同站其他污染物做 KNN 填充。注意 PM2.5 等于 0 的记录要警惕传感器故障或校准期会产生大量 0直接当真实值喂给模型回归线会被拉偏。2.2 时间特征和滞后特征怎么造线性回归本身不会自动理解「小时」和「月份」的周期性所以要把时间拆成可用的数值特征。更关键的是滞后特征当前时刻的 PM2.5 和前一小时、前两小时高度相关把 lag1、lag2 加进去R² 通常能从 0.6 提到 0.8 以上。这是这个项目最容易拉开分数的地方也是老师最爱问的点。# 时间特征 df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) df[month_sin] np.sin(2 * np.pi * df[month] / 12) df[month_cos] np.cos(2 * np.pi * df[month] / 12) # 滞后特征用前一小时、前两小时的 PM2.5 df[PM2.5_lag1] df[PM2.5].shift(1) df[PM2.5_lag2] df[PM2.5].shift(2) # 风向 one-hot df pd.get_dummies(df, columns[wd], prefixwd) # 构造完滞后特征后再删缺失避免误删 df df.dropna(subset[PM2.5, PM2.5_lag1, PM2.5_lag2])这里用 sin/cos 编码小时和月份是为了保留周期性又不引入 23 和 0 之间的虚假距离。shift(1)生成前一小时的值注意必须在删缺失之前做否则第一行会变成 NaN。风向用 one-hot是因为它是类别变量直接映射成数字会让模型误以为北风和东风有大小关系。参数上lag 阶数不是越多越好加到 lag3、lag4 后共线性会变严重VIF 飙升系数解释性下降一般 lag1 和 lag2 就够。2.3 特征标准化和训练集划分的先后顺序线性回归用梯度下降求解时特征量纲差异大会导致收敛慢甚至震荡。PM2.5 数值在几十到几百而 hour_sin 在 -1 到 1不做标准化学习率很难调。但标准化必须先划分训练测试集再在训练集上 fit否则测试集信息泄漏分数虚高答辩时被问到就露馅。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler feature_cols [c for c in df.columns if c not in [PM2.5, date]] X df[feature_cols].values y df[PM2.5].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleFalse ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)shuffleFalse是时间序列的常见做法保持时间顺序避免用未来数据预测过去。fit_transform只在训练集上调用测试集用transform这是标准流程。如果数据量不大也可以按时间切分比如前 80% 做训练、后 20% 做测试更贴近真实预测场景。特征列里要排除目标本身和日期字符串否则会报类型错误。3. 线性回归模型从手写梯度下降到 sklearn 调参3.1 手写批量梯度下降把公式变成代码大作业如果只调LinearRegression().fit()分数可能不低但老师一问「梯度怎么推的」就尴尬。稳妥做法是手写一版批量梯度下降再和 sklearn 结果对比。线性回归假设是 h(x) θ₀ θ₁x₁ … θₙxₙ损失函数是均方误差梯度更新公式为 θ θ - α * (2/m) * Xᵀ(Xθ - y)。def gradient_descent(X, y, lr0.01, epochs1000): m, n X.shape X_b np.c_[np.ones((m, 1)), X] # 加偏置列 theta np.zeros(n 1) loss_history [] for i in range(epochs): y_pred X_b.dot(theta) error y_pred - y grad (2 / m) * X_b.T.dot(error) theta - lr * grad loss np.mean(error ** 2) loss_history.append(loss) return theta, loss_history theta, losses gradient_descent(X_train, y_train, lr0.01, epochs1000)np.c_[np.ones((m,1)), X]给特征矩阵加一列全 1对应偏置项 θ₀。学习率 lr 取 0.01 是经验值太大容易发散太小收敛慢。epochs 设 1000 后看 loss_history 是否还在下降如果早就平了就减如果还在降就加。这段代码的价值在于你能画出损失曲线答辩时直接展示「模型确实在收敛」比空口说强得多。3.2 sklearn 的 LinearRegression 和正则化版本怎么选手写版用来讲原理实际出分还是用 sklearn 更稳。LinearRegression用最小二乘闭式解速度快如果特征多、共线性强换成Ridge或Lasso更合适。Ridge 加 L2 正则系数收缩但不为 0Lasso 加 L1 正则能把不重要特征系数压到 0顺便做特征选择。from sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error models { OLS: LinearRegression(), Ridge: Ridge(alpha1.0), Lasso: Lasso(alpha0.1) } for name, model in models.items(): model.fit(X_train, y_train) pred model.predict(X_test) print(name, RMSE:, np.sqrt(mean_squared_error(y_test, pred)), MAE:, mean_absolute_error(y_test, pred), R2:, r2_score(y_test, pred))alpha是正则强度Ridge 的 alpha 越大惩罚越重一般从 0.1、1、10 里试。Lasso 的 alpha 取 0.1 时如果很多系数变 0说明特征冗余。评价指标里 RMSE 和 MAE 单位是 μg/m³R² 看拟合优度。注意 RMSE 对异常值敏感MAE 更稳健两个都报更专业。如果 Ridge 比 OLS 好说明有共线性如果 Lasso 稀疏后 R² 掉太多说明被压掉的特征其实有用。3.3 用交叉验证挑超参数别只切一次测试集单次 train_test_split 的分数波动可能很大换个 random_state 就差几个点。用 K 折交叉验证能给出更稳的估计也方便选 Ridge/Lasso 的 alpha。from sklearn.model_selection import GridSearchCV param_grid {alpha: [0.01, 0.1, 1, 10, 100]} grid GridSearchCV(Ridge(), param_grid, cv5, scoringneg_root_mean_squared_error) grid.fit(X_train, y_train) print(Best alpha:, grid.best_params_) print(Best CV RMSE:, -grid.best_score_)cv5表示 5 折scoring用负 RMSE 是因为 sklearn 的评分函数越大越好。best_params_给出最优 alphabest_score_是交叉验证平均分。注意时间序列严格来说要用TimeSeriesSplit普通 KFold 会打乱时间顺序导致用未来预测过去分数偏乐观。如果老师不深究KFold 也能用但自己心里要清楚这个边界。4. 结果评估与可视化让 95 分有据可依4.1 残差分析和预测对比图怎么画光报 R² 不够老师想看的是你懂不懂模型哪里错了。残差图能暴露系统性偏差如果残差随预测值增大而扩散说明异方差如果残差有周期性说明时间特征没提干净。import matplotlib.pyplot as plt pred models[Ridge].predict(X_test) residuals y_test - pred fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].scatter(pred, residuals, alpha0.3) axes[0].axhline(0, colorred, linestyle--) axes[0].set_xlabel(Predicted PM2.5) axes[0].set_ylabel(Residual) axes[1].plot(y_test[:200], labelTrue) axes[1].plot(pred[:200], labelPredicted) axes[1].legend() plt.show()左图残差应该围绕 0 随机分布如果呈喇叭形考虑对 PM2.5 取对数再回归。右图取前 200 个点看趋势预测线如果总是滞后真实线说明 lag 特征还不够或者模型欠拟合。这两张图放进报告比单纯堆指标有说服力。4.2 特征重要性怎么解释线性回归的系数就是特征重要性但必须在标准化之后比较否则量纲不同没法比。标准化后的系数绝对值越大说明该特征对 PM2.5 影响越大。coef_df pd.DataFrame({ feature: feature_cols, coef: models[Ridge].coef_ }).sort_values(coef, keyabs, ascendingFalse) print(coef_df.head(10))通常 PM2.5_lag1 系数最大符合直觉CO、NO2 次之因为它们是燃烧排放的伴随物风速 WSPM 系数为负风越大扩散越好。如果出现某个特征系数符号反直觉先查共线性再看是否标准化漏了。这份系数表是答辩时解释「模型学到了什么」的核心材料。5. 避坑与排查这份大作业最容易翻车的五个地方5.1 现象R² 高得离谱超过 0.98原因把 PM2.5 的当前值或未来值泄漏进了特征比如 lag 特征用了shift(-1)或者标准化时用了全量数据。 解决检查所有特征的时间戳确保预测 t 时刻只用 t 之前的信息标准化严格在训练集 fit。5.2 现象模型跑出来 RMSE 几百比均值预测还差原因学习率太大导致梯度爆炸或者特征没标准化量纲差异让梯度下降震荡。 解决把 lr 降到 0.001 试确认 loss 曲线单调下降所有数值特征做 StandardScaler。5.3 现象换了 random_stateR² 波动超过 0.1原因数据量小或者测试集划分不均匀单次划分偶然性大。 解决改用 5 折交叉验证报平均分和标准差或者用时间序列切分报告里写清楚划分方式。5.4 现象PM2.5 等于 0 的记录很多模型预测偏低原因0 值多为传感器缺失或校准期不是真实浓度直接参与训练拉低了回归线。 解决把 0 值替换为 NaN 再插值或者只保留 PM2.5 大于 0 的样本并在报告里说明处理依据。5.5 现象答辩时被问「为什么不用神经网络」答不上来原因只背了代码没想过选型理由。 解决准备一句话——线性回归作为基线可解释、训练快、能给出系数方向后续可以在此基础上加多项式特征或换树模型对比本项目重点在完整流程和特征工程。6. 进阶技巧把线性回归大作业做出区分度如果只做到上面这些拿 85 分没问题想冲 95 以上得在「对比」和「解释」上多走一步。我一般会加两个动作一是构造多项式特征和交互项看 R² 有没有提升同时观察是否过拟合二是用 statsmodels 输出完整回归报告把 p 值和置信区间放进去让统计味更足。import statsmodels.api as sm X_train_sm sm.add_constant(X_train) model_sm sm.OLS(y_train, X_train_sm).fit() print(model_sm.summary())summary()会给出每个特征的系数、标准误、t 值和 p 值。p 值小于 0.05 说明该特征统计显著置信区间不跨 0 说明方向稳定。这份表放进报告比 sklearn 的默认输出更专业。注意 statsmodels 对共线性敏感如果 VIF 超过 10先删掉冗余特征再跑。另一个技巧是做分季节建模。PM2.5 在冬季和夏季的成因不同冬季取暖排放多夏季光化学反应强。把数据按月份拆成采暖季和非采暖季分别训练对比系数变化能讲出「同一特征在不同季节作用不同」的故事。这个分析不复杂但能让老师看到你在思考数据背后的物理意义而不是机械调包。df[season] df[month].apply(lambda m: winter if m in [11,12,1,2,3] else other) for s in [winter, other]: sub df[df[season] s] X_s sub[feature_cols].values y_s sub[PM2.5].values X_tr, X_te, y_tr, y_te train_test_split(X_s, y_s, test_size0.2, random_state42) sc StandardScaler() X_tr sc.fit_transform(X_tr) X_te sc.transform(X_te) m Ridge(alpha1.0).fit(X_tr, y_tr) print(s, R2:, r2_score(y_te, m.predict(X_te)))这段代码按季节分组后重新标准化和训练对比两组 R² 和系数。如果冬季 R² 明显高说明冬季 PM2.5 更依赖滞后浓度和气象条件夏季低可能是光化学反应等非线性因素没被线性模型捕捉这正好是你讨论模型局限性的素材。最后说个习惯我每次交大作业前都会把代码从干净环境重跑一遍确认没有依赖本地缓存文件随机种子固定输出结果和报告里写的一致。这个动作救过我两次一次是忘了固定 random_state一次是测试集路径写成了绝对路径。希望帮到你。本文还有配套的精品资源点击获取