用疫情数据实战回归预测:从滞后特征到数据泄漏避坑指南
1. 为什么拿疫情数据练回归模型1.1 这不是蹭热点是一个回归问题最好的入门样本在机器学习的各类任务里回归是最基础、也最容易被低估的一种。很多人习惯用房价预测、波士顿房价、加利福尼亚房价做演示但这些数据集已经被写烂了缺少真实场景的脏乱差和波动性。相比之下疫情人数预测这个选题天然具备回归问题的所有要素数值型目标、时序相关特征、外部扰动、周期性变化以及一个很重要但常被忽视的杀手——数据不干净。当时我的想法很简单与其空谈理论不如拿真实世界里大家都有感知的数据建一个回归模型预测未来几天的新增感染人数。利益相关不大但数据公开、可复现而且这个场景里的坑特别多踩坑的过程比结果有价值得多。这篇文章适合什么人如果你刚刚学完线性回归想找一个比教材案例更“真实”的练习如果你已经会用 sklearn 跑回归但对时间序列数据做回归时容易掉进数据泄漏、自相关陷阱那么这篇文章正好对路。你不需要有深厚的数学功底但最好会一点 Python 和 pandas。1.2 建模目标预测新增人数而不是累计人数很多初学者第一次拿到疫情数据第一反应是预测累计确诊人数。这个选择在回归问题上是一个典型的坑累计序列是一条单调递增的曲线无论用什么模型只要把昨天的累计值搬过来就能得到非常漂亮的 R2 0.99 以上看起来厉害得不行实际上毫无意义。我在实际建模时把目标定为“每日新增确诊人数”也就是当日新增值。这个指标波动更大、更贴近“回归预测”的本质——模型需要从历史特征里学到变化规律而不是靠惯性复制前一天的累计值。预测新增人数的问题也更现实因为它直接反映了传播速度的变化而不是存量规模。所以先立个规矩永远不要预测累计值。累计值天然是上一期的函数模型很容易作弊。预测增量、预测变化率、预测差分值这类目标才有挑战性。2. 数据准备从拿到原始序列到能喂给模型2.1 数据源选择与字段说明疫情数据有很多公开渠道GitHub 上也有很多历史存档。我用的是一份按天记录的确诊数据字段非常干净主要包括日期、地区、当日新增、当日治愈、当日死亡、累计确诊。为了演示我只保留日期和当日新增两个字段先做一个单变量回归。有两点要提醒新手。第一从原始网页或 CSV 读入数据后一定要把日期列解析成 datetime 类型并且用pd.to_datetime强制转换否则后续排序、做滞后特征时到处报错。第二观察数据是否存在缺失日期。部分数据源在早期会跳过某些天或者某几天集中在同一天上报这些需要显式处理。我当时拿到数据后先做了三件事检查日期是否连续缺的天数用前一天的值填充或者直接删掉视缺失比例而定。把新增值中的负数和异常大值标记出来比如某天突然几万需要核对是不是重复统计。做一个初步的可视化折线图看趋势、是否有明显的尖峰和周期性。这一步看着琐碎但后续模型效果好不好一半取决于这里的数据清洗。2.2 基础特征滞后项、滑动平均、星期指数拿到每天的“当日新增”序列之后接下来要把一维序列变成回归模型可以吃的 tabular 数据。最核心的思路是用过去若干天的值作为特征预测当天的值。举例来说如果你用前 7 天的新增人数来预测第 8 天那么对于某条样本特征就是第 1 到第 7 天的值标签就是第 8 天的值。这种特征叫滞后特征lag features在时间序列回归里是最常用的做法。我实际用的特征组合包括滞后 1 天到滞后 21 天的当日新增值覆盖 3 周的传播周期。滞后 3 天和 7 天的滑动平均用来平滑突发波动。星期因子疫情数据经常有“星期日低、星期二高”的人工报告周期引入星期几作为类别特征可以让模型学到这种偏置。滞后 7 天与滞后 14 天的比值用来表达增长趋势的方向。不要全部堆上去特征太多在小样本下会过拟合。我当时的数据一共只有 500 多天样本量并不大所以保留 7 到 15 个特征是比较安全的区间。2.3 数据切分与时间序列陷阱这里的坑比模型本身更值得讲。普通回归做训练测试切分是随机打乱数据但时间序列数据绝对不能随机切。你今天的数据和三个月前的数据之间存在时间依赖随机切会把未来的信息放进训练集导致模型在测试集上得分虚高上线后发现模型“吃未来”。正确的做法是按时间顺序切分。比如前 80% 作为训练集后 20% 作为测试集。更严格一点的做法是使用时间序列交叉验证比如扩大窗口方式先用前 100 天训练预测第 101-110 天再用前 110 天训练预测第 111-120 天以此类推。我在第一次实验时因为贪方便用了train_test_split默认的随机切分结果在测试集上 R2 高达 0.95当时还很高兴。后来发现训练集中混入了测试集后面的未来数据等于提前告诉模型答案。换成按时间顺序切分后R2 立刻掉到 0.6 以下这才是正常水平。3. 回归模型怎么选从线性回归到随机森林、XGBoost3.1 线性回归先跑通看系数和残差我一直主张任何回归项目先跑一个最简单的线性回归不是因为它效果最好而是因为它能给你一个基线并且让你看到系数和残差。很多新手一上来就是 XGBoost结果模型调了一堆参数却说不清楚数据里到底有什么信号。线性回归的假设是特征和目标之间存在线性关系。放在疫情数据上滞后项其实就是过去的观测值相当于一个自回归模型。你会发现线性回归在这个问题上并不差因为相邻几天的新增人数本身高度相关仅靠滞后 1 天和滞后 7 天的特征就能解释一部分方差。跑完线性回归重点看两个东西系数的符号和大小滞后 1 天的系数通常最大接近 1。这说明当日新增和前一天高度相关属于正常现象。但如果所有滞后项的系数都乱糟糟说明特征之间存在多重共线性可以考虑用岭回归。残差是否随机把预测值和真实值画出来再画残差图。如果残差呈现明显的波浪形或趋势说明模型没有捕捉到周期性和突变信息需要加特征或换模型。线性回归的另一个作用是检查是否有明显的“滞后复制”现象。如果预测序列比真实序列晚一天说明模型几乎是在搬运前一天的数值这是自回归模型常见的病态。3.2 正则化与岭回归处理多重共线性滞后特征之间天然高度相关比如第 5 天和第 6 天的新增人数相关性可以达到 0.9 以上。这种情况下普通最小二乘回归的系数估计方差会变得很大训练集拟合好测试集一塌糊涂。解决多重共线性有两个常用思路一是做特征筛选只留少量滞后项二是用正则化方法比如岭回归L2 正则和 LassoL1 正则。我当时把线性回归替换成岭回归参数alpha取 1.0效果比普通线性回归稳很多。岭回归会让系数的绝对值变小不会像普通回归那样出现正负交替的大系数。如果你用的是 Pythonsklearn.linear_model.Ridge里有一个RidgeCV可以交叉验证自动选 alpha非常方便。还有人会想到逻辑回归。这里提醒一下逻辑回归本身是做分类的输出的是概率不适用于连续数值的预测。除非你把新增人数分成高、中、低三档否则逻辑回归在这个项目里用不上。3.3 树模型随机森林回归与 XGBoost 回归的对比树模型能拟合非线性关系也能捕捉特征之间的交互比如“如果前一天新增很高但滞后 7 天在下降则可能出现拐点”这种规则。所以我会在基线之后跑随机森林回归和 XGBoost 回归。随机森林的优点是参数少不容易过拟合开箱即用。它对特征缩放不敏感也不需要像线性回归那样严格处理共线性。缺点是外推能力弱如果训练集中的最大值是 10 万测试集突然出现 20 万随机森林最多预测到接近训练集的最大值无法外推得更高。疫情数据经常会出现新高峰这会导致随机森林在突增阶段预测严重偏低。XGBoost 回归在带正则的梯度提升框架下表现更灵活而且可以通过树的深度和叶子节点权重控制复杂度。它的缺点是超参数多调参需要时间。我常用的参数组合是learning_rate0.05max_depth4n_estimators300subsample0.8colsample_bytree0.8。如果数据量不大一定要开 early stopping不然会过拟合训练集。树模型和线性模型在这个项目上的表现差异很有意思。在平稳期线性回归和树模型差距不大因为相邻天数的高度相关性已经提供了主要信号。但在疫情快速上升或下降阶段树模型对突变点的反应往往更迟钝线性模型对幅度的外推反而稍好。这也是为什么一个成熟的建模流程里应该同时跑多类模型而不是把宝押在某个热门模型上。3.4 为什么不做时序专用模型本文以回归视角做对照提到时序预测很多人会想到 LSTM、Prophet、ARIMA。这些确实是时间序列领域常用的方法但回归模型的优势在于可解释性、门槛低、调试方便以及可以灵活加入外部特征。本文选择用回归模型来做不是否定时序模型而是希望提供一个更容易理解的基线。ARIMA 适合平稳序列疫情数据在爆发期的非平稳性非常强直接用 ARIMA 需要反复差分和调参。Prophet 对假期、趋势和季节性的处理很优雅但它的拟合过程像一个黑盒出了问题不好排查。LSTM 需要的数据量更大在几百个样本上的表现可能不如树模型稳定。所以我建议把回归模型作为基准如果回归效果已经够用就不必急着上复杂模型。4. 特征工程与实操代码把回归跑起来4.1 生成滞后特征的代码下面是我实际用来生成滞后特征和训练集的代码基于 pandas 和 sklearn。这段代码会读取一份包含 date 和 new_case 两列的 CSV 文件生成滞后特征。import pandas as pd import numpy as np df pd.read_csv(covid_data.csv) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 生成滞后1-21天的特征 for lag in range(1, 22): df[flag_{lag}] df[new_case].shift(lag) # 生成滑动平均特征 df[ma_3] df[new_case].rolling(3).mean().shift(1) df[ma_7] df[new_case].rolling(7).mean().shift(1) # 星期因子 df[weekday] df[date].dt.weekday # 删除前21行因为那些行的滞后特征为空 df df.iloc[21:].reset_index(dropTrue) # 定义特征和标签 features [flag_{i} for i in range(1, 22)] [ma_3, ma_7, weekday] X df[features] y df[new_case] # 按时间顺序切分 train_size int(len(df) * 0.8) X_train, X_test X.iloc[:train_size], X.iloc[train_size:] y_train, y_test y.iloc[:train_size], y.iloc[train_size:]这段代码里有几个细节值得说明。shift(lag)会把当天之前第 lag 天的值放到当前行如果数据已经按日期排序这个操作就是正确的。滑动平均必须用.shift(1)否则未来数据会泄漏进来也就是用当天及当天的前 3 天均值去预测当天这等于把标签信息带进了特征。4.2 训练集/验证集切分注意不要 shuffle上面代码里用的是顺序切分没有打乱数据。这里再强调一次如果有人改了参数train_test_split里的shuffleFalse那是可以的但千万不要设置shuffleTrue。如果使用TimeSeriesSplit做交叉验证会更严谨因为疫情数据的趋势是不断变化的单一时间点切分只能验证某一阶段的预测能力。我第一次测试时用train_test_split(test_size0.2, shuffleTrue)模型得分虚高到可以骗过自己。后来学乖了所有时间序列任务一律用顺序切分。4.3 模型训练与超参数设定线性模型直接调用即可from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor models { ridge: Ridge(alpha1.0), rf: RandomForestRegressor(n_estimators300, max_depth5, random_state42), xgb: XGBRegressor( learning_rate0.05, max_depth4, n_estimators300, subsample0.8, colsample_bytree0.8, random_state42 ) } for name, model in models.items(): model.fit(X_train, y_train) score model.score(X_test, y_test) # R2 print(f{name} R2: {score:.4f})Ridge(alpha1.0)是一个比较保守的正则强度如果训练集很大alpha 可以调小一点。RandomForestRegressor的max_depth不要设置得太深疫情数据本身只有几百个样本如果让树无限生长很容易把所有训练样本都记住。XGBoost 的核心是learning_rate和n_estimators的配合学习率越小需要的树越多但不代表越多越好。4.4 评估指标回归任务最常用的三个指标是均方误差MSE、均方根误差RMSE和平均绝对误差MAE。我一般会同时看 RMSE 和 MAE因为这两个指标的含义不同。RMSE 对大误差特别敏感如果某一天模型预测值和真实值相差 5000这个误差会在 RMSE 里贡献很大。MAE 反映的是平均误差水平不容易被极端值带偏。比如两个模型模型 A每天都在真实值附近波动偶尔某一天误差巨大。模型 B每天误差都在 1000 左右但没有极大值。模型 A 的 RMSE 可能很高MAE 可能略低模型 B 的 RMSE 适中MAE 较高。放在疫情预测场景里我更关注 MAE因为真实使用者不会只关心某一两天的极端误差而是整体偏差水平。多步预测时还可以计算 MAPE平均绝对百分比误差但要注意新增人数在低位时MAPE会剧烈膨胀比如真实值 10预测值 20误差是 100%看着吓人但实际偏差只有 10 人。所以百分比误差在数值较小的场景里要慎用。5. 常见问题排查与避坑实录5.1 预测值变成前一天的水平——“伪预测”这是我调试阶段遇到的问题中最常见的一个。模型跑完画图预测曲线和真实曲线高度重合但仔细看预测曲线整体向右平移了一天。换言之模型并没有预测明天它只是把今天的值复制到明天而因为第二天真实值和第一天往往相差不大所以误差看起来也很小。解决思路有两步。第一步是使用多步滞后特征而不仅是滞后 1 天。第二步是计算“一步延迟相关”对比预测序列与真实序列错位一天后的相关系数如果相关性明显高于对应当天的相关系数就说明模型在偷懒。增加滞后 7 天的特征可以缓解一部分这类问题但根本上模型确实缺少对趋势的捕捉。如果你想让模型准确预测拐点还需要加入外部变量比如政策变化、出行指数等。5.2 数据泄漏用未来信息预测过去这个坑在特征工程里特别隐蔽。我见过有人把当天的“治愈人数”也作为特征这看起来合理因为医院忙不忙可能和感染人数有关。但问题是治愈人数在当天公布之前是拿不到的它本身就是一个未来值。类似地滑动平均如果没有 shift也属于泄漏。还有一个容易被忽略的场景在数据清洗阶段用整段数据的均值或中位数填充缺失值。如果你在训练前用全量的均值填充了测试集中的缺失值那么这个均值包含了未来信息。正确的做法是只用训练部分的统计量来填充或者用前向填充。我建议每次生成特征后都从训练集和测试集中随机抽样几行手动检查这些特征的值在样本当天是否真的“已知”。比如预测 3 月 10 日时特征里是否出现了 3 月 10 日当天的数据如果有就是泄漏。5.3 疫情数据的特殊波动节假日、变异株爆发真实疫情数据不像教科书数据集那么干净。我遇到的第一个问题是明显的星期周期性——周一的新增数通常比周中低原因是周末检测量减少、报告延迟。这种周期性不是疫情本身的传播规律而是报告流程造成的模型如果看到了星期特征能学到一个固定的星期偏移但无法理解为什么某个假期后数据突然翻倍。更麻烦的是突发事件。比如某段时间出现了新的传播毒株感染人数快速上升模型训练集中从来没有见过这种形态。这种情况下基于回归的模型普遍会低估新增数因为它的天性是向历史均值回归而不是猜测“这是一个新世界”。面对这种数据漂移一个有效的手段是给训练样本加上时间衰减权重距离当前时间越近的样本权重越高让模型更关注最近的变化。XGBoost 里可以直接用sample_weight传递权重我试过之后对近期突变的拟合确实更敏感。5.4 评估指标选择的坑RMSE 容易被大值绑架有一段时间我觉得 RMSE 变小了很多心里还挺高兴。后来我看了残差分布发现原因是模型在大多数平稳日子的预测变得保守把误差控制在几百以内但在某几个爆发日误差超过一万导致 RMSE 下降不明显甚至不降反升。如果只看 RMSE会以为模型没有进步其实 MAE 已经改善了很多。后来我改用“分位数评估法”把真实值和预测值分别按天排序看模型在高值区间前 10% 的天的误差也看低值区间的误差。对于传染病预测高值区间才是需要警惕的因为高值意味着快速增长政策响应往往依赖这些数值。如果你只关注全局平均误差模型可能为了“平均”牺牲掉对高峰的敏锐度。6. 值得再深挖的方向6.1 多步预测递归预测与直接预测上面的模型本质上是一个“单步预测器”预测明天需要用到今天的真实值。但在实际使用中我们往往需要预测未来 7 天甚至 14 天这时候两个选择递归预测和直接预测。递归预测的思路是先用模型预测明天然后把预测结果作为特征输入到模型里继续预测后天如此滚动。这种方式的优点是只需训练一个模型缺点是一旦前面预测偏了误差会不断累积预测到第 7 天时可能已经完全偏离。直接预测的思路是为每一天训练一个独立的模型比如模型 1 预测明天模型 2 预测明天加后天依此类推。每个模型根据自己的滞后特征预测固定天数后的值不会把中间预测误差带进来。缺点是训练成本高而且多个模型之间可能不一致。从稳定性的角度在样本量有限时我倾向于递归预测但要对误差累积有心理准备。如果你想做得更细可以结合两种方式用直接预测模型的结果对递归预测做校正。6.2 加入外部变量天气、出行、疫苗数据只靠历史新增人数做回归模型能学到的基本就是一个平滑器。真正想在预测上提升必须加入外部变量。比如可以把每日新增特征换成 7 天移动平均再加入一个“出行强度指数”因为人流量下降会直接影响传播速度。还有疫苗接种率、重点区域新增占比、检测数量等都能提供额外信息。不过外部变量也有风险。一方面很多数据的统计口径经常调整特征前后不一致模型会学到假规律。另一方面外部变量本身也有发布时间延迟比如今天的出行指数可能要第二天才能拿到如果用它预测今天等于用了未来数据。所以加外部变量前一定要确认这些特征在预测时点已经可知。6.3 从不完美预测中得到的教训我做这件事最大的收获不是某个模型拟合得多好而是接受一个事实传染病传播无法用简单回归模型准确预测。真实世界中人的行为变化、政策调整、病毒变异这些都是模型看不到的变量。回归模型的价值不在于它给出了一个“准确答案”而在于它提供了一个可量化的基线。如果你把这个项目当作学习素材建议不要只盯着 R2 和 RMSE。多画几条曲线多看看模型在哪些阶段失效多想想失效的原因这些比调参更有意义。当我看到预测曲线在高峰期远远低于真实值时我理解了为什么机器学习里的“外推”困难当我看到平稳期的预测几乎完美时我理解了自相关性强大的威力。这些体会比任何教材都能更深刻地带你进入数据分析的实战状态。最后再分享一个小技巧做这类预测项目时尽量保留每一次实验的参数和结果写成一个实验日志。我当时没有记录后来想复现一个几周前效果不错的模型却怎么也想不起来当时用了哪些特征、 alpha 是多少。从那天起我老老实实用 CSV 记录每次实验的配置和结果这个习惯帮我省了大量时间。