线性回归做疫情预测:从特征构造到特征泄漏的完整实践指南 拿到一份每日新增病例的历史数据后很多人第一反应是直接上高级模型LSTM、Prophet、SIR 微分方程感觉不堆几个看起来高级的算法就不算做过预测。如果你正在头歌这类机器学习实验平台上刷线性回归题目可能已经习惯了那种干净的小数据集几十条样本、几个特征调一下 LinearRegression 打印 R² 就收工。但把线性回归放到真实的疫情走势预测里事情会立刻变味。这篇博文我想完整记录一次用线性回归做疫情预测的实操过程包括怎么把时间序列改造成模型能吃的特征、怎么训练、怎么评估以及真实预测里几乎必然踩到的几个坑。文章里的数据是模拟生成的仅用于算法演示不构成任何现实疫情判断依据。如果你刚学完线性回归基础、想拿一个完整项目练手这篇文章应该能帮你少走不少弯路。1. 疫情预测问题的本质为什么先用线性回归1.1 任务定义与数据形态先明确我们要解决什么问题。给定过去 N 天的每日新增病例数预测未来 M 天的大致走势。原始数据通常只有两列日期date和每日新增病例数new_cases。这是一个典型的时间序列预测问题。所谓时间序列预测就是数据点之间有时间先后关系第 t 天的数值和它前面的第 t-1 天、第 t-7 天乃至过去一段时间的值都有联系。和普通回归任务不一样我们手里没有现成的 X 和 y只有一个孤零零的时间序列。线性回归本身并不认识时间它只能学习特征和目标之间的映射关系。所以第一步不是打开训练函数而是先想明白我该构造什么样的特征才能让一个线性模型从历史数据里学到趋势信息。这也是很多人第一次做时序预测时最容易卡住的地方。习惯分类问题里每行一个样本、每列一个特征的思路后面对一长串日期和数字会不知道从哪里下手。解决办法是有监督化把时间序列改造成多行特征表格。具体来说就是用过去几天的数值来预测今天的数值那么过去的数值就变成特征今天的数值就变成标签。1.2 线性假设在疫情数据上到底成立吗线性回归的核心假设是目标变量和特征之间存在近似线性关系。如果把日期序号 t 当作唯一特征模型就是在找一条直线 y w·t b让它尽量贴近历史的每日新增病例曲线。现实的疫情数据几乎不可能是一条直线。传染病早期往往是指数增长随后受到各类干预措施、人群免疫力、季节因素影响会出现增速放缓、到达峰值、然后回落的形态。一条直线无论如何都拟合不出这种先升后降的走势。那为什么还非要先用线性回归因为它是成本最低的 baseline。在把 LSTM 和 SIR 模型搬出来之前你需要一个最简单、最稳定、可解释性最强的模型来检验整个数据处理流程是否通畅。线性回归训练只需要几毫秒结果一目了然能立刻暴露出特征构造、数据划分、评估方式里的bug。如果线性回归的误差已经小到可以接受那复杂模型大概率也不会带来质的提升这时候更应该回头检查数据而不是继续堆模型。如果线性回归误差很大那它的误差模式也会告诉你下一步该往哪个方向优化。2. 数据准备把时间序列改造成线性模型能吃的表格2.1 特征构造滞后特征、日期序号和滚动统计我从模拟数据开始演示。下面这段代码生成 120 天数据走势大致是先缓慢上升、然后波动、最后回落比较接近真实疫情数据里暴发—控制—下降的形态但完全不含真实信息import numpy as np import pandas as pd np.random.seed(42) days 120 t np.arange(days) trend 20 * np.sin(t / 25) 0.6 * t noise np.random.normal(0, 10, days) cases np.clip(trend noise, 0, None).astype(int) df pd.DataFrame({ date: pd.date_range(2023-01-01, periodsdays), new_cases: cases }) df.head()拿到原始序列后至少要构造三类特征日期序号特征 t把日期转成从 0 开始的连续整数让模型能捕捉整体随时间的上升或下降趋势。滞后特征lag1前一天新增病例、lag7一周前同一天的新增病例。疫情数据通常有周内波动lag7 往往比 lag1 更有参考意义。滚动统计特征过去 7 天平均新增病例数。它的作用是平滑掉单日上报波动带来的噪声让模型看到一个更稳定的近期水平。代码实现也很简单df[t] np.arange(len(df)) df[lag1] df[new_cases].shift(1) df[lag7] df[new_cases].shift(7) df[rolling7] df[new_cases].shift(1).rolling(7).mean()这里最需要注意的是滚动均值的 shift(1)。如果直接用 rolling(7).mean()那么第 t 天的滚动均值里包含了第 t 天自己的新增病例数。训练时模型会偷看当天的真实数值指标虚高部署到真正预测未来时才露馅。这个细节我后面第 5 章还会专门讲它是时序预测里最常见也最隐蔽的特征泄漏来源。特征构造完成后把缺失值处理掉。lag1 第一行是 NaNlag7 前 7 行是 NaNrolling7 前 8 行是 NaN。最简单的做法是立刻丢弃这些行df df.dropna().reset_index(dropTrue)如果数据更长、缺失更多也可以考虑用中位数填充。但丢行在序列较长时通常更干净不会引入人为编造的数值。2.2 训练集与测试集划分按时间切不能随机洗牌这部分是所有刚接触时序预测的人最容易犯的错误。普通回归任务里我们习惯用 train_test_split(X, y, test_size0.2, random_state42) 随机打乱样本但在时间序列里随机打乱等于让模型偷看未来。举个例子如果某一天的样本被划分到训练集而它后面一周的样本被划分到测试集那么训练时模型已经通过 lag7 这一特征学到了未来信息。测试集的作用是模拟真实预测场景真实预测时我们只有过去的数据没有未来的数据。随机打乱的测试集给不出可信的误差估计。正确做法是按时间顺序切割split_idx int(len(df) * 0.8) train df.iloc[:split_idx].copy() test df.iloc[split_idx:].copy() feature_cols [t, lag1, lag7, rolling7] X_train train[feature_cols] y_train train[new_cases] X_test test[feature_cols] y_test test[new_cases]用前 80% 的数据训练后 20% 的数据做测试模拟用过去预测未来的真实场景。这样做出来的评估指标才有参考价值。3. 建模与训练从最小二乘解到 sklearn 落地3.1 最小二乘法与正规方程原理线性回归的数学形式很简洁。假设特征矩阵为 X每一行是一个样本的特征模型要学一组权重 w 和偏置 b使得 y ≈ Xw b。把偏置合并进权重后目标就变成求解L(w) ||Xw - y||²也就是残差平方和。为什么用平方误差而不是绝对误差两个原因。第一平方误差是连续可导的凸函数能直接通过求导得到全局最优解第二它对大误差的惩罚更重模型会更努力去避免出现严重偏离的预测。当然这也是它的缺点后面讲异常值影响时会提到。求 L(w) 对 w 的梯度并令梯度为 0能得到传说中的正规方程w (XᵀX)⁻¹Xᵀy这就是热词里经常提到的线性回归正规矩阵方法。它给出了线性回归的闭式解不需要迭代训练就能直接算出来。但需要注意的是实际工程中 sklearn 并不会真的用这个公式而是用奇异值分解SVD来求解。原因是直接计算 XᵀX 的逆矩阵在特征维度较高或特征相关性较强时数值不稳定SVD 的数值稳定性好得多。理解正规方程的意义在于明白线性回归的本质是一个凸优化问题而不是真的手写矩阵求逆。3.2 训练代码与关键参数sklearn 的用法非常简单但有几个参数值得说清楚from sklearn.linear_model import LinearRegression model LinearRegression(fit_interceptTrue) model.fit(X_train, y_train) print(特征系数, dict(zip(feature_cols, model.coef_))) print(截距, model.intercept_)fit_interceptTrue 表示模型自己学习一个偏置项。如果你的特征做过标准化视觉上截距会接近训练集标签的均值这里特征不标准化截距也会被直接拟合出来。看系数的作用更有意思。系数表示在其他特征不变时该特征每增加 1 个单位预测值平均变化多少。如果 lag7 的系数明显大于 lag1说明模型认为一周前同期水平对当天的预测贡献更大这符合疫情数据里存在周内节律的特征。如果 t 的系数是负的说明模型整体学到的是下降趋势。训练完成后不着急看测试集先在训练集上画一条拟合曲线确认模型没有在代码层面出问题。我训练完会立刻打印前几个样本的真实值和预测值肉眼看一下量级是否一致。这一步花不了几秒钟却能排查掉大量类似单位不一致特征顺序搞错的低级bug。4. 结果评估用 RMSE、残差图判断模型到底行不行4.1 回归预测的评估指标选哪个不要只盯着一个指标看。我用三个指标一起评估它们各自反映不同侧面指标全称公式特点MAE平均绝对误差mean(|y_pred - y_true|)直观单位与原始数据相同对大误差不敏感RMSE均方根误差sqrt(mean((y_pred - y_true)²))对大误差更敏感模型出现严重偏离时会明显变大R²决定系数1 - SS_res / SS_tot表示模型解释了目标变量多少方差但时间序列里容易虚高代码就三行from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred model.predict(X_test) 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}, R²{r2:.4f})经验上如果 RMSE 比 MAE 大很多说明模型在个别日子上出现了大偏差如果两者接近说明误差分布相对均匀。对我这份模拟数据典型结果大致是 MAE 在 10 左右、RMSE 在 12 左右。这些数字要和原始数据的量级一起看如果每日新增病例在 50-200 之间波动MAE 是 10说明平均每天偏差十几例对趋势预测来说已经凑合如果原始数据只有 30 例MAE 是 10那模型基本没什么用。R² 在时间序列预测里要格外小心。当目标变量本身有明显上升趋势时哪怕模型只是大概沿着趋势走R² 也可能高达 0.9 以上。这并不代表预测精准只是说明跟着趋势走已经赢过了直接用均值预测。所以我的习惯是主要看 MAE 和 RMSE 的实际量级R² 只做参考。4.2 画预测曲线和残差图比指标数字更诚实指标是评估的骨架可视化才是评估的血肉。至少画两张图一张是测试集的真实曲线与预测曲线对比另一张是残差图。第一张图能直接看出模型在哪里飘了import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) plt.plot(test[date], y_test.values, label真实值) plt.plot(test[date], y_pred, label预测值) plt.legend() plt.xticks(rotation45) plt.title(测试集上的线性回归预测) plt.tight_layout() plt.show()第二张图更有诊断价值。把残差y_true - y_pred按时间顺序画出来如果残差像白噪声一样在 0 上下随机波动说明模型已经捕获了数据里最主要的模式如果残差出现明显的周期性或趋势说明数据里还有模型没学到的东西——通常是非线性关系。residual y_test.values - y_pred plt.figure(figsize(10, 3)) plt.plot(test[date], residual) plt.axhline(0, colorgray, linestyle--) plt.xticks(rotation45) plt.title(残差随时间变化) plt.tight_layout() plt.show()我在真实数据上做这个练习时残差图几乎总是一个形状前期残差是正的或负的后期变成相反的符号。这说明线性模型拟合的直线只是穿越了真实的曲线在早期和后期都会系统性偏移。这就是模型在向你说这里有一条弯曲的趋势我这条直线装不下。5. 真实预测中避不开的坑拐点、泄漏与误差累积5.1 拐点问题线性模型反应迟钝疫情走势里最要命的是拐点。真实新增病例不会一直线性涨干预措施生效后增速会放缓、到达峰值、然后回落。而线性模型只有一个全局斜率它为了拟合整个历史窗口会把上升期和回落期平均成一条较平缓的直线。结果就是上升期它系统性低估回落期它系统性高估拐点附近简直是在凭感觉瞎猜。这几乎是所有线性模型做疫情预测的通病。缓解办法只有一个缩小训练窗口。不要拿全部 120 天去训练而是只取最近 14 天或 21 天。模型不需要学会整个历史的平均趋势它只需要学会最近的走势方向。在快速变化的数据里近期窗口往往比长期窗口更有价值。缺点是窗口太短会导致样本太少特征稍微多一点就容易过拟合所以每加一个特征就要权衡一次样本量是否扛得住。5.2 特征泄漏滚动均值的那个 shift(1)第 2 章提过的滚动均值 shift(1) 在这里必须展开讲。下面两种写法看起来几乎一样效果天差地别# 错误滚动均值包含当天真实值 df[rolling7_bad] df[new_cases].rolling(7).mean() # 正确滚动均值只用当天之前的数据 df[rolling7_ok] df[new_cases].shift(1).rolling(7).mean()错误写法里第 t 天的滚动均值实际是第 t-6 天到第 t 天的平均值里面包含了模型要预测的当天真实值。训练时模型会学到只要滚动均值很大当天值就很大这种近乎作弊的映射测试集指标好看到飞起。但真正部署到未来预测时未来日期的滚动均值只能用已有历史值计算它不再包含当天真实值这一信息预测能力立刻大幅缩水。这就是标准的特征泄漏。不只是滚动均值任何涉及当前样本之后的信息的特征都会造成这种假象。排查方法很简单构造完每个特征后检查构造该特征时是否用到了当天的目标变量值。如果用到了就必须 shift。5.3 多步预测的误差累积很多教程做完单步预测就收工了用前 N 天预测第 N1 天评估完结束。但实际需求往往是预测未来 7 天预测未来 14 天。多步预测时如果第 N1 天用真实历史值预测第 N2 天继续用真实历史值预测这不叫多步预测这叫多次单步预测误差不会累积。真实的多步预测是先用已知数据预测第 N1 天然后把第 N1 天的预测值当作已知数据去预测第 N2 天如此滚动下去。这样每一步的误差都会传给下一步几步之后预测值会逐渐漂移离真实曲线越来越远。我实测的体会是线性回归滚动预测 3 步之内还在可控范围超过 5 步基本只能看趋势方向数值已经不值得细究。要缓解误差累积有两条路一是每隔几步重新用真实观测值校准一次现实中就是等新一天的数据出来就重新预测一遍二是直接为每个预测步数训练独立模型比如专门训练一个用过去 7 天预测未来第 7 天的模型。后者样本利用率差一些但能绕开滚动递推带来的漂移问题。5.4 指标虚高与单日上报噪声还有一个体验很微妙的坑R² 虚高。在趋势明显的序列上哪怕预测天天偏 20 例R² 也可能显示 0.95。初学者容易因此误以为模型很好。解决方法是同时打印 MAE 和 RMSE用预测值和真实值平均差了多少例来替代抽象的百分比解释力。单日噪声也要特别注意。新增病例数据经常有上报积压、修正、节假日检测量骤降等影响单日值可能比前后几天低一半或者高一半。这种尖峰对线性回归影响很大因为平方误差会把异常值的权重放到极大。一个折中的做法是不要把预测目标直接定为单日新增病例数而是定为7 日均值。7 日均值平滑了噪声模型学起来更容易预测曲线也更贴合公共卫生决策里真正关心的趋势概念。常见坑根本原因有效对策拐点后预测失灵线性模型只有一个全局斜率缩小训练窗口只学近期走势指标虚高滚动均值/滞后特征泄漏未来信息检查特征是否包含当天真实值该 shift 就 shift多步预测漂移预测误差不断累积滚动递推预测或按预测步数单独训练单日噪声干扰上报波动被平方误差放大预测 7 日均值而不是单日值R² 误导趋势项让 R² 虚高同时看 RMSE/MAE 实际量级6. 线性回归之外多项式回归与更专业的模型6.1 多项式回归给直线加一点弯曲当残差图显示明显的曲线趋势时最自然的升级方向是多项式回归。思路是在特征矩阵里加入 t²、t³ 甚至更高次项让模型拟合一条曲线而不是直线。sklearn 里直接用 PolynomialFeatures 完成特征扩展from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import make_pipeline poly_model make_pipeline( PolynomialFeatures(degree3, include_biasFalse), LinearRegression() ) poly_model.fit(X_train[[t]], y_train)多项式回归能拟合出上升、峰值、回落的形态在训练窗口内看起来比线性回归聪明很多。但代价有两个一是模型可解释性变差系数不再有每单位 x 变化带来多少 y 变化的直白含义二是过拟合风险急剧升高阶数越高曲线在数据边界外就越放飞自我一旦外推几个时间步预测值可能以三次方的速度冲到完全离谱的数值。我的建议是多项式回归适用于拟合历史趋势、做较短步数的外推阶数不要超过 3。如果阶数到 4、5 才能拟合好训练集几乎可以肯定是在过拟合噪声而不是在学真实趋势。6.2 更接近真实传播过程的模型SIR 与后续方向如果目标是纯粹预测每天新增多少例线性回归和多项式回归终究只是数学拟合工具它们不懂传染病传播机制。更贴近问题本质的方法是 SIR/SEIR 这类仓室模型把人群分为易感者Susceptible、感染者Infectious、康复者Recovered等仓室用常微分方程描述人群在这些状态之间的流转。SIR 模型的一个核心优势是它能自然刻画感染人数上升导致易感人群减少、传播速度随之放缓的过程也就是拐点是从机制里内生出来的而不是靠一条多项式曲线硬拗出来的。对学习机器学习的同学来说从线性回归走向 SIR 模型是一条很顺畅的进阶路径。你不需要马上学会解偏微分方程只需要理解它的建模思想把对数字曲线的拟合升级成对传播机制的建模。后续还可以尝试 Facebook Prophet 这类专门为时间序列设计的工具或者 LSTM 这类深度模型。但说句实在话我做了不少类似的数据实验后最大的体会是在拿到干净、可靠的数据之前讨论模型优劣没有意义。模型再先进也扛不住脏数据、延迟上报和变化的外部环境因素。回到线性回归本身它就像一把最朴素的尺子。尺子量不出人体温度但任何人做测量之前都需要一把尺子来定标。我的习惯始终是面对一份陌生的时序数据第一件事永远是用线性回归快速建立基线看它的误差模式看残差图再决定要不要上更复杂的工具。等哪天你的线性回归基线已经稳定复现了再打开 Prophet 或者 LSTM你会发现自己对数据的理解比一上来就跑复杂模型的那批人要扎实得多。