基于线性回归的PM2.5预测系统Python源码实战解析
简介基于线性回归的PM2.5预测系统源码是一套面向Python学习者、机器学习入门者及大气环境数据分析场景的小型完整项目。代码以单文件Python脚本承载数据读取、特征构造、模型训练与结果预测等关键流程配套原始训练/测试CSV表、处理后的特征矩阵、训练模型参数文件以及多张过程可视化截图能够帮助读者完整理解线性回归从数据预处理到输出提交结果的全过程。包内共19个文件其中12个CSV数据文件覆盖原始数据与中间结果5张PNG图片用于展示数据分布或预测效果另含1个model.npy模型参数与1个主程序文件压缩包整体2.14MB便于快速下载和迁移。已有594人学习下载适合作为课程设计、毕业设计或机器学习入门练习的参考按本地路径读取繁体中文编码数据的写法以及从训练到提交预测结果的完整结构也具有较强的实战参考价值。1. 这套“线性回归预测系统”解决的是哪类 PM2.5 问题看空气质量 App 的时候你多半做过一个动作下午盯着 PM2.5 浓度的曲线心里盘算明天早上出门前是不是该戴上口罩。这套“基于线性回归的 PM2.5 预测系统 python 源码”要做的就是把这种经验判断交给程序——拿过去几十小时的气象观测和污染浓度用线性回归算法拟合出一个线性关系然后对接下来 3 到 24 小时的浓度给出预报数字整份代码可打包成一个 .rar 源码包交付。它解决的核心问题不是“预报准到小数点后两位”而是让工地扬尘管控、学校户外活动决策和错峰生产调度能提前半天看到趋势而不是等浓度超过 150 才想起启动应急预案。适合谁有 Python 基础、想直接抄一套能跑通的预测流程、又不打算为此搭深度学习训练环境的工程师和数据岗位人员。线性回归在这个场景里依然能打因为系数能解释“风速涨 1 m/s浓度大约降多少”一句话就能讲给业务部门听。2. 开工第一件事把小时级监测数据整理成可训练的特征表2.1 拿到手的数据长什么样小时级监测表的列结构这类型预测系统大多不会拿日报数据训练而是小时级监测数据。CSV 文件里通常有这几类字段时间戳 time、PM2.5 浓度 pm25、PM10 浓度 pm10、SO2、NO2、O3外加气象表里的温度 temperature、相对湿度 humidity、风速 wind_speed、风向 wind_direction、气压 pressure。先说两个容易栽跟头的点气体浓度字段一般是固定整点发布的气象字段却是分钟级上报比如风速每 10 分钟一条。如果你直接把两张表 join 起来去 fit行数都对不上更别说训练了。我一般会先把原始文件读进来把时间索引对齐到“整点”再合并。这一步在高精度上决定后面滞后特征是否成立。监测站的小时浓度本身就代表“这一个自然小时的平均污染水平”按小时对齐后模型的每一行才有明确的物理含义。import pandas as pd # 读取监测表和气象表统一将时间列解析为 datetime air pd.read_csv(air_quality.csv, parse_dates[time]) meteo pd.read_csv(meteo_hourish.csv, parse_dates[time]) # 气象数据按小时聚合取均值浓度字段直接取每小时第一个采样值 meteo_hour meteo.set_index(time)[ [temperature, humidity, wind_speed, pressure] ].resample(H).mean() air_hour air.set_index(time)[ [pm25, pm10, so2, no2, o3] ].resample(H).first()resample(H) 把分钟级气象记录压缩到整点mean 是求这一小时内所有采样点的平均。浓度字段用 first 而不是 mean是因为监测站的小时浓度本身就是整点发布的多个采样值之间更多是瞬时测量噪声取首值能避免把 23:50 的瞬时值强行平均成不真实的整点浓度。如果你用 mean每小时的值会向“这一小时中间时刻”漂移滞后特征的语义就不干净了。两列对齐之后下一步是构造滞后特征lag也就是“过去第 1 小时、第 3 小时、第 24 小时的浓度”。滞后特征对 PM2.5 预测非常重要因为污染过程有很强的惯性——今天的浓度往往决定明天的起点一个静稳天气持续两三天浓度就会一天比一天高。# 对齐后的完整小时表 df air_hour.join(meteo_hour) # 滞后特征用过去时刻的浓度作为当前时刻的输入 for lag in (1, 3, 24): df[fpm25_lag{lag}] df[pm25].shift(lag) # 未来3小时浓度作为预测目标shift(-3) 表示“用当前时刻可得到的信息预测3小时后” df[pm25_target] df[pm25].shift(-3) # 删除前24行没有足够滞后历史和后3行没有目标值 df df.iloc[24:-3]shift 是时间序列预测的标准姿势。我们要预测 t3站在 t 时刻能拿到 pm25、pm25_lag1、pm25_lag3、pm25_lag24 和气象观测shift(-3) 把 t3 的真实浓度搬到当前行上让模型学“t 时刻特征到 t3 浓度”的映射。这里顺手删掉前 24 行和后 3 行避免模型跑在一个全是 NaN 的矩阵上还浑然不觉。2.2 从原始表到特征表缺失值、滞后特征与周期的 pandas 代码实际数据不会那么干净。污染站点故障、网络传输中断、仪器校准都会产生缺失值尤其雨天高湿时段很多站点会出现连续两三小时的缺测。处理缺失值的第一个原则不能全补 0。PM2.5 缺测不代表浓度就是 0只是仪器没读到或超量程全补 0 会把“雨天低污染”这样的真实规律破坏掉。# 对数值列做线性插值连续最多补6小时防止把整周缺测硬补出来 df df.interpolate(methodlinear, limit6) # 气象列偶尔有负值湿度计漂移用前后值中位数修正 df.loc[df[humidity] 0, humidity] df[humidity].median()interpolate 只在数据缺口不大于 6 小时时填充缺口超过 6 小时直接保留 NaN这些行在训练前会被丢弃。为什么不直接用 mean 填充时间序列里均值填充会抹掉早晚高峰差异模型学到的就变成“不管几点浓度都贴近均值”对预测毫无帮助。接着加上时间周期特征。小时浓度有明显的日内周期早晚高峰浓度高、午后扩散条件好浓度低一周里工作日和周末也有差异。把这些写进特征# 时间周期特征小时、星期几模型可以直接学分段截距 df[hour] df.index.hour df[dayofweek] df.index.dayofweek # 湿度区间映射相对湿度高于70%时颗粒物吸湿增长把连续值切成有序分类 df[humidity_level] pd.cut( df[humidity], bins[0, 40, 70, 100], labels[0, 1, 2], ).astype(int)hour 和 dayofweek 在 sklearn 里会被当成数值特征但对于线性回归来说赋予它们“分段截距”的解释也能自洽系数为正的小时说明该时段污染容易累积。humidity_level 是我个人比较喜欢加的一个特征因为 PM2.5 在高湿度下会吸湿增长线性回归直接拟合连续湿度也能跑但把湿度按区间映射成 0/1/2 后模型在“湿度 70% 以上”这个区间能拿到一个独立的截距解释起来更直观。pd.cut 切出来的标签转成 int是为了能直接参与矩阵运算。风向这种环形变量也值得处理。直接把风向角度 0 到 360 度塞进线性回归模型根本学不到“西北风 vs 东南风”的差异因为它默认变量越大影响越强。常见做法是把风向拆成 sin 和 cos 两列保留方向周期性df[wind_dir_sin] np.sin(np.deg2rad(df[wind_direction])) df[wind_dir_cos] np.cos(np.deg2rad(df[wind_direction]))这两列加入特征矩阵后线性回归可以组合出“某方向来的风对浓度上升的影响”比把风向钉死成一个连续值合理得多。当然如果你的源数据里风向缺失太多直接用风速也够用。2.3 相关性与特征筛选先看哪几个特征值得进模型特征造了一堆不代表都能进模型。线性回归对共线性敏感两个高度相关的特征同时进入系数估计会变得不稳定甚至出现风速系数为正这种违反直觉的结果。这里先看相关性# 计算所有特征与预测目标之间的相关系数按绝对值降序 corr df.drop(columns[pm25_target]).corrwith(df[pm25_target]) corr_sorted corr.reindex(corr.abs().sort_values(ascendingFalse).index) print(corr_sorted)corrwith 输出每个特征与目标的皮尔逊系数。实际项目里pm25_lag1 和 pm25_lag3 通常排在最前面这是合理的污染有惯性湿度也经常排在中上游。如果一个特征相关系数低于 0.05又没有明确的物理意义可以先从特征列表里拿掉减少模型噪声。但这里有一个容易被忽略的坑pm25 当前值本身与目标 pm25_target3小时后的相关性也会很高但你在预测 t3 时t 时刻的 pm25 是已经观测到的值它进入特征矩阵没问题而 pm25_target 是被 shift(-3) 挪过来的“未来值”绝对不能混进特征。常见做法是把特征列单独存成一个列表不要用 drop 的方式反选避免手滑把目标带进去。feature_cols [ pm25_lag1, pm25_lag3, pm25_lag24, temperature, humidity, wind_speed, pressure, wind_dir_sin, wind_dir_cos, hour, dayofweek, humidity_level, ] X df[feature_cols].copy() y df[pm25_target].copy() print(X.shape, y.shape)到这一步X 和 y 已经是对齐的X 是 t 时刻能够拿到的全部信息y 是 t3 时刻的真实浓度。接下来的建模都建立在这张特征表上。3. 模型训练与指标解读用 sklearn 跑通最小可用的线性回归3.1 线性回归算法选择为什么不用随机森林和神经网络线性回归算法在 PM2.5 预测里经常被低估。很多人一上来就上 XGBoost、LSTM但忽略了这里的训练数据规模一个城市站点一年也就 8760 个小时去掉缺测只剩 7000 条左右样本特征也就十来个。对这么小的数据量线性回归已经能解释大部分方差更重要的是它的系数是透明的。随机森林在同样的数据上可能把训练集拟合得很好但在超参数没调对时对时序外推的泛化能力并不比线性回归强神经网络更麻烦调参成本高、部署依赖重、解释性差对一个“上午训练、下午出预报”的生产脚本来说线性回归是性价比最高的起点。我一般先用线性回归打底如果 RMSE 实在压不下去再回头查特征工程而不是急着换模型。就算换到极端天气场景比如沙尘暴或重污染过程线性回归预测出来的是“平均值附近的偏移”它也能告诉你“浓度会比常态高”这对预警来说已经足够。3.2 sklearn 训练LinearRegression 的核心代码与参数说明把特征标准化之后用 sklearn 训练线性回归模型非常直接。这里有一个细节标准化要 fit 在训练集上不能对全量数据 fit否则验证时会泄漏统计信息。from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.model_selection import TimeSeriesSplit import numpy as np # 保留最早的若干行做前窗避免滞后特征在训练集头部缺数据 X X.iloc[240:] y y.iloc[240:] # 按时间顺序拆分前 70% 训练后 30% 测试 split int(len(X) * 0.7) X_train, X_test X.iloc[:split], X.iloc[split:] y_train, y_test y.iloc[:split], y.iloc[split:] scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model LinearRegression(fit_interceptTrue) model.fit(X_train_scaled, y_train) print(训练集 R²:, model.score(X_train_scaled, y_train)) print(测试集 R²:, model.score(X_test_scaled, y_test)) for col, coef in zip(feature_cols, model.coef_): print(f{col}: {coef:.4f})这里有个常见误用需要提醒直接调用 train_test_split 默认参数它会随机洗牌。对时序数据一旦洗牌8 月的数据可能出现在训练集7 月的出现在测试集模型等于“提前预习了未来”测试 R² 虚高。上面用 iloc 按条数切分训练集永远是时间靠前的部分这只是起步姿势。LinearRegression 的参数不多实际会动的主要就几个fit_intercept 默认 TruePM2.5 浓度没有“特征全为零时浓度也为零”的物理基础建议保持positive 默认 False做源解析想要非负系数可以打开但会有拟合优度损失普通预测不开n_jobs 对线性回归几乎没影响不用管。normalize 参数在 sklearn 1.2 以后已经移除建议用 StandardScaler 处理这也是下面梯度下降能收敛的前提。3.3 从零实现梯度下降看懂回归系数背后的推导逻辑只看 sklearn 的模型很多人会把线性回归当成黑匣子。这里把梯度下降写一遍有助于后面调参。目标是最小化残差平方和 L(β) (1/n)Σ(yᵢ - xᵢβ)²对 β 求偏导得到梯度 (2/n)Xᵀ(Xβ - y)然后用学习率迭代更新。def gradient_descent(X, y, lr0.05, epochs500): n, m X.shape # 在前列加常数1对应截距项 Xb np.column_stack([np.ones(n), X]) beta np.zeros(m 1) history [] for _ in range(epochs): grad (2 / n) * Xb.T (Xb beta - y) beta - lr * grad history.append(np.mean((y - Xb beta) ** 2)) return beta, history注意这段代码假设 X 已经标准化且特征数量在十几个以内学习率 0.05 才比较稳。如果特征量纲差异很大比如温度 300K、风速 8 m/s梯度会在某些维度上震荡loss 曲线像心电图一样上下跳这时候把 lr 调到 0.001 或对特征单独做归一化。和 sklearn 的闭式解对比手写梯度下降更适合理解“系数如何滚出来”生产环境还是用 LinearRegression它的最小二乘解法数值稳定性更好也不依赖学习率。常见的翻车操作是拿原始特征直接跑梯度下降结果 loss 在 10⁴ 量级震荡然后怀疑代码写错了。实际上只要先 StandardScaler问题立刻消失。判断收敛不看 loss 是否为零而看连续几十轮 loss 变化是否小于 1e-5。线性回归的 loss 是凸函数不存在局部极小所以收敛不稳只有学习率和特征量纲两个原因。3.4 评估指标怎么设R2、RMSE、MAE 的阈值与业务换算先说结论只看 R² 会骗人它会随着滞后特征数量的增加而虚高。以 PM2.5 3 小时预测为例我一般同时盯三个指标RMSE均方根误差对大误差更敏感单位是 µg/m³MAE平均绝对误差更接近业务直觉R² 用来对比模型和“直接取均值”的差距训练集和测试集都要看差值超过 0.3 就说明过拟合或泄漏。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score 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} µg/m³, MAE{mae:.2f} µg/m³, R²{r2:.3f})把指标对到业务上PM2.5 的 24 小时平均浓度二级标准是 75 µg/m³。如果你的 RMSE 在 2030 µg/m³意味着预测值和真实值差出半个到一个空气质量等级这对提前预警已经够用。RMSE 比 MAE 大很多时说明误差分布里有少量极端错误值去查是不是个别日期发生了沙尘暴或突发扬尘事件。提示如果测试集 R² 超过 0.9而正常特征组合下这个场景期望值不会超过 0.75那么多半是把目标的未来值混进了特征。先按这条排查再怀疑模型。4. 从文件到系统模型保存、批量预测与可视化验证4.1 模型持久化用 joblib 保存模型和标准化器训练好的模型要是每次预测都现场重训一遍系统没法用。先把模型和标准化器一起保存下来。import joblib from datetime import datetime stamp datetime.now().strftime(%Y%m%d_%H%M) joblib.dump(model, fpm25_model_{stamp}.pkl) joblib.dump(scaler, fpm25_scaler_{stamp}.pkl) # 顺手把特征列名也存一份防止后面重构代码时列顺序对不上 joblib.dump(feature_cols, fpm25_features_{stamp}.json)joblib 是 sklearn 官方建议的持久化方案能直接序列化模型对象。pickle 也能用但 joblib 对大数组更友好。保存标准化器尤其关键预测时拿到的实时数据是原始量纲不经过同一个标准化器直接扔给模型特征分布变了结果会莫名其妙偏到天边这是很多模型上线后第一个掉链子的地方。4.2 批量预测与单条预报两种调用方式和它们的取舍批量预测用在“每天凌晨把过去三个月推一遍生成对比报告”的场景单条预报用在“每小时收一次实时数据给出未来 3 小时浓度”的场景。两种调用方式可以共用一套加载逻辑def load_artifacts(prefix): model joblib.load(f{prefix}_model.pkl) scaler joblib.load(f{prefix}_scaler.pkl) feature_cols joblib.load(f{prefix}_features.json) return model, scaler, feature_cols def predict_single(model, scaler, feature_cols, row): row: dict按键取特征保证和训练时的列顺序一致 feat np.array([row[c] for c in feature_cols]).reshape(1, -1) feat_scaled scaler.transform(feat) return model.predict(feat_scaled)[0]批量预测最容易踩坑的是用当年当时的字段顺序预测把 scaler 的列顺序搞乱。这里用 feature_cols 的固定顺序从 row 里逐个取值字典内部怎么排都没关系进到模型里的列顺序永远和训练时一致。单条预报有一个时序上的注意点滞后特征要自己算。比如你要预测 t3此刻是上午 10 点你需要拿到 9 点、7 点和昨天 10 点的实测浓度去填 pm25_lag1、pm25_lag3、pm25_lag24。如果系统收到的是实时监测流先用一个循环缓冲维护“过去 24 小时的浓度”from collections import deque # 缓冲区上限 25足够取 lag1、lag3、lag24 buffer deque(maxlen25) def append_observation(val): buffer.append(val) if len(buffer) 25: return None row { pm25_lag1: buffer[-2], pm25_lag3: buffer[-4], pm25_lag24: buffer[-25], # 气象特征从实时接口取 temperature: meteo_now[temperature], # ... } return row这里 buffer[-2] 是上一小时浓度buffer[-4] 是上上上小时buffer[-25] 是昨天同一时刻。deque 的长度限制让时间窗口自动滑动不会无限膨胀。这个细节写不好模型再准也会因为喂错滞后特征而翻车。4.3 可视化验证一张预测对比图暴露 90% 的问题评估指标只是数字数字背后的问题还得靠图来看。我最常画的是“最近一周预测 vs 实测”的折线图。import matplotlib.pyplot as plt # 取测试集最近 168 小时一周 plt.figure(figsize(14, 4)) plt.plot(y_test.values[-168:], labelobserved) plt.plot(y_pred[-168:], labelpredicted) plt.legend() plt.ylabel(PM2.5 µg/m³) plt.xlabel(hour since start) plt.title(PM2.5 forecast vs observation (last 7 days)) plt.tight_layout() plt.savefig(pm25_validation.png, dpi150)画图时要注意对位y_test 和 y_pred 索引要对齐。如果预测代码里做过 dropna两条曲线可能错位一小时看起来像滞后其实只是索引没对上。看到图之后重点看三件事第一曲线是否整体“跟着实测跑但慢半拍”如果是说明滞后历史特征权重过大需要加大气象特征权重或缩短滞后窗口第二高峰和低谷的幅度是否被明显压缩实测冲到 180预测只到 110这通常说明模型在平均化可以考虑加风速或边界层高度特征第三是否存在周期性单点误差比如每天清晨固定偏高这可能跟逆温层有关是特征工程需要补的方向。5. 避坑PM2.5 线性回归建模最容易翻车的五个细节5.1 现象训练集 R² 0.92滚动预测却一路漂移有段时间我调这套系统训练集 R² 0.92心里觉得这次稳了。结果拿过去一周真实数据做滚动预测第二天就开始漂移预测值整体偏高第三天直接高出一倍多。原因我用了随机 train_test_split而不是时间切分。8 月的样本被随机分到了训练集7 月的样本被分到了测试集模型早就提前见过了未来时段的趋势自然“考”得不错真到了线上只能拿历史训练它就原形毕露。解决改成时间顺序切分或者用 TimeSeriesSplit 交叉验证。写代码时先 sort_index 再切分这一步不能省。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, test_idx in tscv.split(X): model.fit(X_scaled[train_idx], y[train_idx]) score model.score(X_scaled[test_idx], y[test_idx])用 TimeSeriesSplit 代替 train_test_split它保证训练索引永远在测试索引之前。这段代码如果跑出来各折分数波动很大说明模型不稳定优先去查特征里的离群点。5.2 现象“预测”成了复制粘贴目标值进了特征矩阵一次收拾别人的工程包发现测试集 R² 0.98高得离谱。把特征列打出来一看里面有一列叫 pm25_lag0其实就是当前这一时刻的实测浓度。预测 t3 时用户确实知道当前浓度直接把当前浓度塞进特征模型学到的就是“当前浓度乘以一个接近 1 的系数”等于把预测变成了复制粘贴对 3 小时后的变化毫无解释能力。原因造特征时把原始表整列一起 shift没删掉“当前时刻”那一列或者用了整个 DataFrame 做 shift 后又把原始列合并了回来。解决特征列表里绝不包含和 pm25_target 采自同一时刻的浓度列滞后特征至少 shift(1) 起步。如果一定要用当前浓度把它定义为“最近一小时平均值”而不是“现在这一刻的瞬时读数”语义上是可观测值但列名不要叫 lag0。5.3 现象缺失值补 0R² 直接变负之前处理一套北方城市站点数据某台仪器连续三个小时故障我用 fillna(0) 把缺失值填成了 0。训练完一跑测试集 R² 是负的意味着模型预测比直接取平均值还差。原因缺测往往发生在雨天高湿或仪器超量程时段真实浓度并不低补 0 等于人为制造了一批低值样本线性回归为了迁就这些 0 值把整个超平面压低遇到正常高值全部预测偏低。解决缺测不超过 6 小时用线性插值超过 6 小时直接删数据段不要均值填充更不要填 0。对气象列优先用前一天同时刻的值做填充。这条经验在 python 3.8 环境里也一样适用和数据工具版本没关系。5.4 现象训练精度很高上线时取不到气象数据系统初版上线前模型跑得很好等到接入真实数据接口却发现模型需要的“未来 6 小时温度”在实时接口里根本不存在因为气象预报数据是收费且延迟的。原因建模时想着“如果我知道未来气象预测会更准”于是在训练集里用了真实未来气象做特征测试集也这么构造R² 自然好看。但生产环境拿不到未来时的实测气象能拿到的是气象台的预报值不是实测值特征语义直接从“实测”变成了“预报”。解决先列一个“特征可获取性清单”每个特征标注“实时可得/延迟可得/不可得”。训练时只用当前时刻能拿到的历史观测如果一定要用气象预报特征就用气象台提前发布的预报值去仿真训练数据把预报误差当成噪声一并学习。5.5 现象夏天训练的模型冬天一用就废有一年我做这套系统5 月训练并部署R² 0.72 相当不错跨到 12 月夜间浓度飙升到 200 以上预测曲线却一直追不上真实值R² 掉到 0.3 以下。原因PM2.5 污染生成的化学过程有季节性。夏天臭氧和二次气溶胶主导冬天燃煤取暖加静稳天气同样的湿度、风速和浓度输入对应的输出关系完全不同。模型系数是固定的等于整个夏天没学过“高湿度加静稳”这种冬季工况。解决把训练窗口改成滚动式保留最近 90 天数据重新训练而不是训一次用到天荒地老。同时把单月模型和全年模型做对比观察 RMSE 变化。定时任务加一步自动重训比任何模型调参都管用。# 滚动重训每月执行一次取最近90天样本 from datetime import timedelta recent df[df.index (df.index.max() - timedelta(days90))] X_recent recent[feature_cols].values6. 验证与进阶模型版本化与退化检测的日常操作6.1 一次训练封存四样产物回滚才有后悔药模型修完不是终点。每次训练至少封存四样东西模型本体、标准化器、特征列名、当时的效果指标。不要只存一个 model.pkl因为预测代码一改你不知道这份模型是拿哪些特征训出来的。snapshot { created_at: 2024-01-15T08:00:00, train_start: 2023-10-01, train_end: 2024-01-01, model: model, scaler: scaler, feature_cols: feature_cols, rmse: rmse, mae: mae, r2: r2, data_source: city_station_001, } joblib.dump(snapshot, fsnapshot_{stamp}.pkl, compress3)compress3 是速度和体积的折中。把这些打成一个字典存成 joblib比散存三个文件更不容易丢配套。这个习惯在预测系统里特别值哪天新特征工程把模型搞崩了回滚到上一个快照十分钟能恢复上线。6.2 退化检测的十分钟做法我不建议等人投诉了才去复盘。每周固定跑一次“上周预测 vs 上周实测”的对比算当前 MAE 和训练时的 MAE偏差超过 30% 就触发重训。def compute_weekly_mae(model, scaler, feature_cols, df_week): X_week df_week[feature_cols].values X_week_scaled scaler.transform(X_week) pred model.predict(X_week_scaled) return np.mean(np.abs(pred - df_week[pm25_target].values)) mae_now compute_weekly_mae(model, scaler, feature_cols, df_week) if mae_now snapshot[mae] * 1.3: print(模型退化触发重训)把这加到 crontab 每周一早上跑一次。我自己的习惯是周一到了单位先看一眼这个输出再决定这周要不要动模型。这个习惯救过我两次一次是冬季第一场大范围灰霾过程另一次是站点仪器校准导致数据基线偏移。平时看着稳定的模型最常在周末静默崩溃。把模型当消耗品去频繁维护这套线性回归预测系统的精度才会一直在线。希望帮到你。本文还有配套的精品资源点击获取