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. 回归模型怎么选:从线性回归到随机森林、XGBoost
3.1 线性回归:先跑通,看系数和残差
我一直主张任何回归项目先跑一个最简单的线性回归,不是因为它效果最好,而是因为它能给你一个基线,并且让你看到系数和残差。很多新手一上来就是 XGBoost,结果模型调了一堆参数,却说不清楚数据里到底有什么信号。
线性回归的假设是特征和目标之间存在线性关系。放在疫情数据上,滞后项其实就是过去的观测值,相当于一个自回归模型。你会发现线性回归在这个问题上并不差,因为相邻几天的新增人数本身高度相关,仅靠滞后 1 天和滞后 7 天的特征,就能解释一部分方差。
跑完线性回归,重点看两个东西:
- 系数的符号和大小:滞后 1 天的系数通常最大,接近 1。这说明当日新增和前一天高度相关,属于正常现象。但如果所有滞后项的系数都乱糟糟,说明特征之间存在多重共线性,可以考虑用岭回归。
- 残差是否随机:把预测值和真实值画出来,再画残差图。如果残差呈现明显的波浪形或趋势,说明模型没有捕捉到周期性和突变信息,需要加特征或换模型。
线性回归的另一个作用是检查是否有明显的“滞后复制”现象。如果预测序列比真实序列晚一天,说明模型几乎是在搬运前一天的数值,这是自回归模型常见的病态。
3.2 正则化与岭回归:处理多重共线性
滞后特征之间天然高度相关,比如第 5 天和第 6 天的新增人数相关性可以达到 0.9 以上。这种情况下,普通最小二乘回归的系数估计方差会变得很大,训练集拟合好,测试集一塌糊涂。
解决多重共线性有两个常用思路:一是做特征筛选,只留少量滞后项;二是用正则化方法,比如岭回归(L2 正则)和 Lasso(L1 正则)。
我当时把线性回归替换成岭回归,参数alpha取 1.0,效果比普通线性回归稳很多。岭回归会让系数的绝对值变小,不会像普通回归那样出现正负交替的大系数。如果你用的是 Python,sklearn.linear_model.Ridge里有一个RidgeCV,可以交叉验证自动选 alpha,非常方便。
还有人会想到逻辑回归。这里提醒一下:逻辑回归本身是做分类的,输出的是概率,不适用于连续数值的预测。除非你把新增人数分成高、中、低三档,否则逻辑回归在这个项目里用不上。
3.3 树模型:随机森林回归与 XGBoost 回归的对比
树模型能拟合非线性关系,也能捕捉特征之间的交互,比如“如果前一天新增很高但滞后 7 天在下降,则可能出现拐点”这种规则。所以我会在基线之后跑随机森林回归和 XGBoost 回归。
随机森林的优点是参数少,不容易过拟合,开箱即用。它对特征缩放不敏感,也不需要像线性回归那样严格处理共线性。缺点是外推能力弱,如果训练集中的最大值是 10 万,测试集突然出现 20 万,随机森林最多预测到接近训练集的最大值,无法外推得更高。疫情数据经常会出现新高峰,这会导致随机森林在突增阶段预测严重偏低。
XGBoost 回归在带正则的梯度提升框架下表现更灵活,而且可以通过树的深度和叶子节点权重控制复杂度。它的缺点是超参数多,调参需要时间。我常用的参数组合是:learning_rate=0.05,max_depth=4,n_estimators=300,subsample=0.8,colsample_bytree=0.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(drop=True) # 生成滞后1-21天的特征 for lag in range(1, 22): df[f'lag_{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(drop=True) # 定义特征和标签 features = [f'lag_{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里的shuffle=False,那是可以的,但千万不要设置shuffle=True。如果使用TimeSeriesSplit做交叉验证,会更严谨,因为疫情数据的趋势是不断变化的,单一时间点切分只能验证某一阶段的预测能力。
我第一次测试时用train_test_split(test_size=0.2, shuffle=True),模型得分虚高到可以骗过自己。后来学乖了,所有时间序列任务一律用顺序切分。
4.3 模型训练与超参数设定
线性模型直接调用即可:
from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor models = { 'ridge': Ridge(alpha=1.0), 'rf': RandomForestRegressor(n_estimators=300, max_depth=5, random_state=42), 'xgb': XGBRegressor( learning_rate=0.05, max_depth=4, n_estimators=300, subsample=0.8, colsample_bytree=0.8, random_state=42 ) } 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(alpha=1.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 记录每次实验的配置和结果,这个习惯帮我省了大量时间。