☰
回归项目实战:用Python预测每日新增病例数
2026/10/21 6:21:29 网站建设 项目流程

这几天在整理回归相关的复盘笔记时,我给自己定了一个稍微“有手感”的实战题:回归项目实战,预测新冠人数。通俗点说,就是拿到一份公开时间序列数据,用前面几天的每日新增报告数,预测下一天的新增量。很多人第一反应是把“预测感染人数”当成玄学,其实落到代码上就是最典型的回归问题:连续型预测、时间序列特征、回归模型,再配上验证和误差分析。我选这道题一方面是因为它不需要大数据平台,不用GPU,只要会 pandas、sklearn 就能复现;另一方面是因为复试前能把回归流程走通,比背一百遍损失函数更有说服力。

这个项目也特别适合准备复试的人用来“自测状态”:如果你能解释清楚为什么不能用随机切分、为什么要给特征加 shift、为什么预测新增比预测累计更合理,那说明回归这一章基本过关了。下面我就按 DAY4 实际整理的项目笔记,把整个流程、踩坑点和最后的复盘结果写清楚。

1. 项目整体设计与思路拆解

1.1 为什么选择“每日新增数”作为预测目标

一开始我盯着数据表想了很久:表里既有累计报告数,又有每日新增数,到底拿哪一列做回归目标更合理?

经验告诉我,累计数列的“自相关性”太强了。今天的累计几乎等于昨天的累计加上今天的少量新增,回归模型只要学到“预测值约等于上一个值”就可以获得很低的误差,但这根本不是学到了什么规律,顶多是“抄作业”。这种问题叫非平稳序列,直接回归看起来很美,实际上毫无解释能力。

所以我选择预测每日新增数。新增数列才有真正的随机波动,也正因为有波动,回归模型才有发挥空间:哪几天高、哪几天低、滞后多少天会影响今天的值,这些都是模型需要去学的东西。

预测目标定了之后,再思考累计值怎么用:可以单独对每日新增建立回归模型,预测出下一天新增后,再把这个预测值累加到累计列上,用来还原累计曲线。这样既避免了“预测累计就好似在抄前一格”,又能让最终展示结果自然一些。

1.2 怎样把“日期序列”改造成“回归问题”

很多新手卡在第一步,总觉得时间序列很特殊,模型不能直接吃日期。其实只要把问题转换成监督学习就好:构造特征时,拿前一天的数值、前两天数值、前七天数值、星期几等作为 X,拿当天的每日新增数作为 y。

我这里先提供一个核心伪代码思路,后面会给出直接能跑的实现:

df["lag_1"] = df["new_cases"].shift(1) df["lag_2"] = df["new_cases"].shift(2) df["lag_7"] = df["new_cases"].shift(7) df["roll_7"] = df["new_cases"].rolling(7).mean().shift(1) df["dayofweek"] = df.index.dayofweek X = df[["lag_1", "lag_2", "lag_7", "roll_7", "dayofweek"]] y = df["new_cases"]

如果你对这段代码没有感觉,可以这样理解:模型每预测一次,不是去看“整个疫情曲线”,而是看一张“最近七天的行情表”。这张表格的每一行,代表某一天的信息,目标值则是后一天的价格。回归模型就是一个根据历史行情表来推测第二天的助手。

这个转换之所以关键,是因为它把时间顺序问题硬生生变成了普通的回归表格问题。后面做训练、测试、交叉验证时,我们只需小心时间顺序,不再需要特殊处理。

1.3 项目的安排与工作量预估

我实际把这个项目压缩在一个下午完成,大概三个小时。时间分配大概是:数据清洗和缺失值处理 40 分钟,特征工程 40 分钟,模型训练 60 分钟,评估和对比 40 分钟。中间穿插各种报错排查,比如 shift 忘记加一、索引错位、验证集划分顺序搞反等,预计还要再留半小时。如果是基础较好的读者,两小时跑通完全有可能。

我选择的数据是某公开数据汇总站导出的 284 条每日记录,包含日期、累计报告数、每日新增数三个字段。没有做什么特殊筛选,意在让项目尽可能接近真实工作流。

2. 核心细节解析与实操要点

2.1 缺失日期不能无脑 drop

我之前做过一个错误示范:拿到数据直接dropna(),结果发现预测曲线出现很多“断崖式”空隙。原因很简单:数据处理时,原始表格可能缺了某些日期,尤其早期数据不完整,经常有整行缺失。

一旦直接丢弃缺失行,时间序列就出现了空洞,滞后特征也会错位。比如今天明明是 3 月 5 日,但因为 3 月 4 日缺失,lag_1实际取到的是 3 月 3 日的值,信息就歪了。

正确的做法是先补全日期,再决定缺失值怎么填:

df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() full_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq="D") df = df.reindex(full_index) df["new_cases"] = df["new_cases"].fillna(0)

这里把缺失日期的每日新增填为 0,是一种短期可用方案。如果缺失的是累计报告数,则不能用 0,因为累计值不会倒退;更好的做法是插值。对每日新增,如果一天缺失且前后数值都正常,填 0 会让 7 日均值略微拉低;如果连续缺多天,我会考虑先差分再插值。

注意:一旦有缺失日期,之后所有窗口特征都要重新生成。最稳妥的方式是先补全日期,再统一用shift和rolling生成特征。

2.2 shift 方向是很多人最容易错的地方

生成滞后特征,shift(1)代表“取前一天的数值”。如果错用了shift(-1),你就把未来数据拽进了特征里,模型会在训练时偷看到当天的实际值。这样做训练集误差会小得惊人,但一到测试阶段立刻崩掉。

我每次都会做一个简单的检查:把特征构造好的表格打印出来,看同一行里的日期、目标值、滞后值是否满足时间逻辑。比如某一行日期是 3 月 8 日,目标值是 3 月 8 日的新增数,lag_1就必须是 3 月 7 日的新增数,lag_2就必须是 3 月 6 日。只要打印几十行,就能轻易发现错位。

2.3 滚动窗口特征要记得再次 shift

不仅滞后特征要 shift,滚动均值也要 shift。原因很简单:如果用当天的 7 日均值来预测当天新增,模型就把未来信息“偷看”了。

举个例子,如果我要预测 3 月 8 日的新增数,那 3 月 8 日当天的滚动均值里包含了 3 月 8 日自己,这就是泄漏。所以构造特征时一定要先算 7 日窗口,再整体往后挪一天:

df["roll_7"] = df["new_cases"].rolling(7).mean().shift(1)

这一步的 shift(1) 是习惯性动作,也是排查数据泄漏时的第一个重点。

2.4 “星期几”这类日历特征也是有用信息

很多人做时间序列预测,只关心数值型滞后特征,忽略了星期几。但在每日新增数这类序列里,周内周期性明显。比如有些发布渠道周初补齐,有些周末偏低,趋势往往不是完全平滑的。

我加入dayofweek列之后,模型预测的周内尖峰明显更贴合真实序列。这个特征对树模型尤其有用,因为树模型可以做分段,比如周六周日走一个专门的分支规则,周一再走到另一个分支。

你还可以再扩展月内日序、是否为节假日等特征。但刚开始做,我建议只加dayofweek和简单的节假日标记,不要一上来堆几十个特征,容易把模型拖进过拟合。

3. 回归模型实操:从线性模型到树模型

3.1 基线模型:昨天的值就是最朴素的基准

在训练任何复杂模型前,我先建立了一个基线:直接用前一天的新增数当作今天的预测值。也就是pred_t = real_{t-1}。这个模型几乎不花成本,但它非常重要,因为后续所有复杂模型的误差都要跟这个基线做对比。如果连 LR 都比不过这个“无脑复制”的基线,说明特征工程有问题,或者模型选择不合理。

实际比较下来,基线表现的 RMSE 通常不低,因为新增数日波动较大,连续两天完全相同或接近的场景很少。任何稍微像样的回归模型,都应该明显优于这个基线。

3.2 岭回归:处理滞后特征之间的严重共线性

我首选岭回归,而不是普通线性回归,原因是滞后特征高度相关。比如lag_1和lag_2本身就存在高度相关性,普通最小二乘回归的系数会很不稳定,稍微换一段验证数据,系数就剧烈摆动。

岭回归通过 L2 正则化把系数往零收缩,牺牲一点训练误差,换取泛化能力。实操中,我用RidgeCV在一个小范围内搜索 alpha:

from sklearn.linear_model import RidgeCV ridge = RidgeCV(alphas=[0.01, 0.1, 1.0, 10.0]) ridge.fit(X_train, y_train) y_pred_ridge = ridge.predict(X_test)

这里不需要太精细地调参,因为回归模型的核心竞争力不在超参数,而在于特征结构是否合理。如果特征都构造好了,默认 alpha 附近也会取得较好的效果。

3.3 树模型:抓住非线性变化和周期性

线性模型适合平滑、线性关系较强的序列,但每日新增数经常出现“脉冲式”抬升。比如某些时段报告规则变化,导致单日新增突然跳高,线性模型很难描述这种分段式变化。这种情况下我习惯用梯度提升回归树。

我当时用的是 sklearn 自带的GradientBoostingRegressor,参数不复杂:

from sklearn.ensemble import GradientBoostingRegressor gbm = GradientBoostingRegressor( n_estimators=120, learning_rate=0.05, max_depth=3, random_state=42 ) gbm.fit(X_train, y_train) y_pred_gbm = gbm.predict(X_test)

树模型可以对不同区间做切分,比如“如果滞后 7 天值高,且昨天值偏低,则今天预测值中等”,这类规则性判断比单个线性公式要灵活。代价是需要稍微控制深度,我用max_depth=3防止过拟合。

当然,这个项目也可以考虑 LSTM,但 284 条每日数据对神经网络来说太短,不容易学出稳定规律。我建议先把传统回归吃透,再决定是否上时序模型,不要为了炫技硬上深度学习。

3.4 把训练流程写成统一的模板

为了后面对比模型,我把流程封装成一个简单函数,输入不同的模型对象,都走同一套训练和预测过程:

def train_evaluate(model, X_train, y_train, X_test): model.fit(X_train, y_train) pred = model.predict(X_test) return pred

再配合一个函数计算指标,方便一次性比较 RMSE、MAE、MAPE。不用做复杂封装,只要别让代码重复粘贴三次就行。实际项目里,我强烈建议把数据处理、特征工程、模型训练、评估四个阶段分开写,每个阶段用少量注释标清楚,否则后面排查问题会很痛苦。

4. 评估指标与时间序列验证

4.1 时间序列数据严禁随机切分

普通回归项目里,随机划分训练集和测试集没问题,因为样本之间互相独立。但时间序列数据完全不能这么做,一旦随机打乱,某一天的测试数据可能出现在训练数据之前,模型就等于拿着“未来”去预测“过去”,验证结果虚高。

正确的做法是按时间顺序切分。我当时的做法是前 70% 做训练,后 15% 做验证,最后 15% 做测试。代码实现很简单:

train_size = int(len(df) * 0.7) val_size = int(len(df) * 0.15) test_size = len(df) - train_size - val_size X_train = X.iloc[:train_size] y_train = y.iloc[:train_size] X_val = X.iloc[train_size:train_size+val_size] y_val = y.iloc[train_size:train_size+val_size] X_test = X.iloc[train_size+val_size:] y_test = y.iloc[train_size+val_size:]

关于第一段时间要足够长的原因,我这里不展开太多,关键是如果你只用最后 14 天做测试,那么测试采样窗口太短,模型评估结果会很不稳定。

4.2 滚动时间序列交叉验证:只看一次测试结果不够

在复试问答里,如果面试官问“你怎么评估时间序列模型”,只回答“按时间切分一次”其实不够,更严谨的回答是“滚动时间序列交叉验证”。

sklearn 直接提供了TimeSeriesSplit,每次切分都用更早的数据训练、更近的数据验证,逐步提升起点:

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_index, val_index in tscv.split(X): X_train_cv, X_val_cv = X.iloc[train_index], X.iloc[val_index] y_train_cv, y_val_cv = y.iloc[train_index], y.iloc[val_index] # 在这里训练并记录指标

这样得到的指标平均分,比单次切分更能反映模型稳定性。我当时用这种办法对比了岭回归和梯度提升,最后发现梯度提升虽然在训练集上更精确,但在滚动交叉验证里优势并不明显,说明它有一点过拟合,需要控制树的深度。

4.3 指标并非越“大”越说明模型好,要结合数据尺度看

常用回归指标有三个:

指标含义适用场景
RMSE误差平方后开根号,放大较大误差新增数有异常尖峰时,能反映模型是否被极端值牵着走
MAE绝对误差的平均更接近普通人理解的“平均差多少”
MAPE百分比误差数值较平稳阶段好用,但新增数接近 0 时不稳定

我实践后发现,只看 RMSE 不够,还需要结合每一天新增数的波动范围。比如评估集平均每日新增约 500 例,RMSE 在 130-160 之间还是可以接受的;如果 RMSE 高于 300,那基本相当于完全没用。另外,预测值偶尔会跑到负数,这时不能直接拿去计算 MAPE,要先做clip(lower=0)。

4.4 对比基线才有说服力

我总结出了一个很实用的评价原则:任何一个模型,先跟“昨天值就是今天预测值”的基线比。如果模型的 RMSE 没有显著低于基线,你就要重新审视特征工程,而不是继续调参。

在实际复现中,我得到的是梯度提升模型略优于基线约 15%-25% 的相对误差改善,岭回归略微差一点,但整体流程是完整的。这个结果并不夸张,因为这类短时序数据的可预测性有限,预测模型能带来的提升幅度本来就不大。这也提醒我,不要迷信复杂模型,短序列数据里,简单模型加上合理特征往往已经够用。

5. 常见问题与排查技巧实录

5.1 预测曲线整体“延迟一天”怎么办

我最早用岭回归时,预测曲线看起来总是比真实曲线慢一天,像把真实曲线右移了一样。原因是模型过于依赖lag_1,当 lag_1 的系数最大时,预测结果本质上就是前一天的数值稍微衰减一点,自然存在一天延迟。

解决办法是加入更长周期的特征,比如lag_7和 7 日均值,并适当减少lag_1的权重。我调完之后,曲线明显没那么“滞后感”了,但也不会完全消除延迟,因为时间序列预测本身就存在滞后性,完全消除反而可能是泄漏。

5.2 训练误差很低,但测试误差爆表,优先检查数据泄漏

排查这问题有两个关键点:第一,确认没有在算滚动窗口时忘记右边 shift;第二,确认没有用全数据集均值或全数据集标准差做特征标准化。我举一个典型错例:如果先对整个new_cases列计算全局均值,再把这个值作为特征放进去,测试阶段的未来信息就已经混入训练集。正确做法是只对训练集计算统计量,再应用到测试集。

我建议你构造特征时,把最终生成的 DataFrame 多打印几行,日期从前往后看,人为检查每行列特征是否都来自更早时间点。这个动作虽然笨,但最有保障。

5.3 新增数序列中包含大量“尖峰”,要不要平滑

疫情数据的每日新增数经常出现单日暴增,原因是统计口径或补报错位,这类尖峰对模型影响很大。我试过把原始新增列先做 3 日移动平均再进入模型,模型指标会好看,但有一个副作用:预测结果也会被平滑,丢失单周内的真实波动。

我的建议是:先做一次实验,对原始列直接建模;如果尖峰导致模型训练不稳,再考虑 3 日移动平均的版本,并把两个结果都保留下来,复盘时解释清楚。这种方法也能在面试时体现你理解数据结构,而不是只会套公式。

5.4 常见问题排查表

现象可能原因处理方式
预测曲线滞后真实曲线一天模型过度依赖 lag_1,或窗口特征不足增加 lag_7、roll_7,减少单滞后特征权重
训练误差极低但测试误差高数据泄漏检查 shift 方向、全局统计量、rolling 窗口是否用了未来数据
验证集评估优于测试集切分时没按时间顺序,随机抽样了改用严格时间顺序切分或 TimeSeriesSplit
预测值出现负数模型输出超出物理下限np.clip(pred, 0, None)
加入更多特征后几乎没有提升特征本身与目标相关性低做相关性分析,只保留相关性较高的特征
预测结果过于平滑,尖峰完全丢失平滑过多或模型太简单尝试树模型,或减少平滑窗口

5.5 复试复盘时最值得讲给面试官的两个细节

准备复试时,我不会去背整个项目的代码,而是把两个最容易暴露水平的点讲清楚。

第一个是“为什么用新增数而不是累计数训练”。这个问题的答案体现了对时间序列数据的理解:累计序列近乎随机游走,模型容易退化成复制上一天的结果,没有研究价值;预测新增数能验证模型学到的是真实规律,而不是惯性。

第二个是“为什么特征工程里 shift 如此关键”。我会直接说明 shift 是为了避免“未来数据泄漏”,如果只调参数不看 shift,模型一切顺利都可能是虚假繁荣。这个细节看似普通,但能把做过项目的人和不经大脑复制代码的人迅速区分开。

这次 DAY4 最大的收获不是把某个模型调得多好,而是真正理解了一条铁律:时间序列回归项目里,数据构造的严谨性永远优先于模型的复杂度。你先保证每一步的历史信息没有污染未来信息,再去比较线性模型、树模型和时序模型才有意义。如果一开始顺序就错了,后面每一步都可能白做。

如果你也想拿同类型题目练手,我强烈建议先不要急着选新的算法,而是把这条完整流程跑三遍:第一遍用随机数据试通代码,第二遍换成真实公开数据,第三遍尝试加入一个额外特征并观察效果变化。三遍之后,所谓“实战能力”会明显不一样。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询