2020年那个冬天,几乎所有做传染病建模的人都在面对同一个问题:病毒传播的速度到底能不能被提前算出来?机器学习从业者第一次感受到,流行病学预测模型不是教科书里那种“跑通一个模型就算交差”的练习,而是要在每一天变化的数据、不断调整的防控手段、滞后而残缺的报告口径里,用尽可能多的信号拼出一张未来几周的图景。这篇文章我想把这套东西拆开讲清楚——从问题定义、数据处理、模型选型到部署和踩坑,基于我个人做类似项目的完整思路来复盘,适合正在入门智慧医疗方向、或者想把时序预测项目落到真实场景中的同学参考。题目虽然是“第1章”,但内容足够支撑你从零搭建一个可用的预测闭环。
1. 先把问题定义清楚:这不是普通的时序预测
1.1 流行病学预测到底在预测什么
很多人一上来就急着选模型,这是最大的误区。做普通的时间序列预测,比如电商销量、服务器负载,目标通常是“未来的数值是多少”。但流行病学预测面对的是传染病在人群中的传播过程,它输出的远不止一条病例曲线,还应当包括几个关键问题:未来一周或两周内,每日新发病例会落在什么区间;当前传播是否在加速还是减速——这对应有效再生数的变化趋势;医疗系统在未来会承受多大的住院和重症压力。
不同的目标对应完全不同的建模方案。如果只需要预测病例数,可以用纯粹的机器学习时序模型;如果需要解释传播动力学、估计干预措施的效果,就必须引入仓室模型的结构化假设。开篇第一件事,是明确你的服务对象是谁。如果模型要服务公共卫生决策,需要的不是黑盒的精确数字,而是“在乐观/悲观情景下的范围”;如果模型要服务于医院内部的床位和物资调度,需要的又是另一种粒度的住院率预测。定义不清楚,后面全部白做。
1.2 传统仓室模型和机器学习各自的边界
传统流行病学建模的主角是SIR、SEIR这类仓室模型。把人群划分成易感者(S)、潜伏者(E)、感染者(I)、康复者(R),用常微分方程描述状态之间的流转。这类模型的最大优势是“可解释”——每个参数都有生物学或流行病学含义,模型的曲线可以直接对应到传播过程。但它的痛点也恰恰在此:假设传播率β、康复率γ在模拟周期内是常数,这在真实疫情中几乎不成立。人群防护意识在变化、病毒变异导致传播力改变、不同阶段检测能力也在变,所有这些都会导致参数漂移。
机器学习模型的优势正好互补。它不要求你事先假设传播机制,而是从大量历史特征中自动学习“哪些因素会推高病例数”。但纯机器学习也有盲区:它很难外推到训练数据中没有见过的疫情阶段。比如一个城市此前每日新增从未超过一千,模型预测两万例时的行为就会非常不稳定。
所以早期我调研这类项目时得到的重要结论是:不要站队,做混合。用SEIR的框架提供动力学约束,用机器学习拟合时变参数或直接做残差修正。这是后面整个方案的核心基调,也建议大家把思路从“哪个模型更强”切换成“如何把两种建模哲学组合起来”。
2. 数据准备:脏活儿累活儿都在这一层
2.1 需要哪些数据源,以及从哪里找
流行病学预测的第一步永远是数据盘点。我从实际项目里总结出的数据分层大概是这样的:
- 核心流行病学数据:每日新增本地确诊、无症状感染者、现有住院人数、重症人数、累计治愈和死亡。这些是模型的主序列,通常来自各地卫健委发布的通报,再按天归一成结构化表格。
- 人口和社会特征:地区常住人口、人口密度、年龄结构(因为不同年龄段的感染风险和重症率差异很大)、城市化率。
- 人群移动数据:跨城市迁徙强度、市内公共交通客流量、地铁拥挤度、热点区域人流指数。在传染病模型里,人口流动直接影响传播范围的扩散速度,是特征体系中非常重要的一类。
- 防疫干预数据:核酸检测范围与频次、隔离政策的起止时间、公共场所关闭状态、口罩佩戴率。这些可以用哑变量、等级评分或者“距离政策开始的天数”来刻画。
- 外部环境数据:气温、相对湿度、日照时长。很多呼吸道病毒的传播效率有季节性,把温度湿度放进特征工程往往能明显提升模型在跨季节预测时的表现。
需要说明的是,部分数据存在隐私合规和可获得性问题,比如手机信令原始数据就不是普通团队能直接拿到的。实际落地时常用公开的迁徙指数替代,或者用流量占比这类聚合脱敏数据。这并不影响思路,关键是清楚数据背后的含义——指数上升30%到底对应多少人员流动,需要在特征编码时备注清楚。
2.2 病例数落后于感染数的“报告延迟”陷阱
做流行病学数据清洗时,最容易踩的一个坑是“报告的今天”不等于“发生的今天”。每天公布的病例数,实际上是在一个时间跨度内陆续完成检测并确诊的个案。潜伏期平均三到七天的特性决定了:今天的新增确诊,很大概率是三到五天前被感染的人群。真正的感染时间曲线和报告曲线之间存在一个系统性平移,而报告本身还会因为检测能力、送检流程甚至节假日而抖动。
处理办法有几类。最朴素的是在特征里加入滞后窗口——用过去三到七天的病例数预测未来,模型自己会学到潜伏期造成的时间平移。更严格的做法是用“发病日期”而不是“报告日期”来重构序列,但这需要病例级别的流调数据,很多场景拿不到。还有一种方法是做延迟分布反卷积:假设从感染到确诊的延迟服从某个分布,用当前报告数据反推真实的感染曲线。这个方法学术上很成熟,但实现起来要对边界和尾部做很多稳健性处理,我当时是在第二阶段才引入的,初期没必要做这么重。
2.3 特征工程的几个关键操作
特征工程是这类项目性价比最高的一环。我把自己的通用做法按优先级排序给你:
第一优先级是滞后特征和目标统计。预测未来七天的每日新增病例,至少要构造:过去1/3/7/14天的病例数,过去7天病例数的移动均值、标准差、峰值,过去7天环比变化率,以及“最近一天较七天平均值”的偏差。这些特征本质上是在告诉模型当前的传播处在什么趋势阶段。
第二优先级是比例特征。单纯看病例绝对值,容易被检测量影响。比如一个城市突然开展大规模核酸筛查,一天查出几千存量病例,这不代表疫情在加速,只是“把账算清了”。所以建议构造阳性率(阳性检出数/检测人数)和检测量本身,让模型能区分“传播加速”与“检测加强”两种情况。另外,无症状感染者与确诊的比例在不同阶段也很有信息量。
第三优先级是外部特征。把移动指数、政策等级、气象变量直接作为普通特征输入,或者构造一些交叉特征,比如“病例上升期叠加管控收紧”这样的交互项。树模型对这类交叉特征很敏感,值得多尝试。
2.4 数据清洗中的常见脏数据问题
实际清洗时的情况比论文里描述的复杂得多。比如历史数据被修订——某天公布的累计数,后面又被修正,我们到底该用哪个版本作为训练标签?我的建议是:记录每一次数据修订,训练和评估使用同一套“数据发布当时可见”的快照,而不是事后修正版。否则模型在回测时会看到未来信息,上线后的表现会比回测差很多。这就是“前视偏差”的典型例子,很多人一开始没在意,结果模型上线第一周就翻车。
另一个典型问题是节假日效应。春节期间检测点放假、人员流动结构完全改变,每日新增病例数会短暂失真是很正常的事。如果不加处理,模型会把这几天当成异常样本,硬学出一个错误的模式。经验做法是引入节假日哑变量,或者把节假日当天的目标值做一个“不做特别学习”的标记,让模型保持对常态分布的拟合。缺失值处理也要按时间序列的方式——前向填充优先于全局均值填充,因为后者会抹掉趋势信息。
3. 模型选型与核心原理:从SEIR到深度学习
3.1 SEIR仓室模型的核心公式与参数含义
SEIR四个字母对应易感者(Susceptible)、潜伏者(Exposed)、感染者(Infectious)、康复者(Removed或Recovered)。整个模型可以写成一组离散递推方程。离散形式在编程时更直观,我习惯把时间步设为一天,每天做一次状态转移:
$$S_{t+1} = S_t - \frac{\beta S_t I_t}{N_c}$$
$$E_{t+1} = E_t + \frac{\beta S_t I_t}{N_c} - \sigma E_t$$
$$I_{t+1} = I_t + \sigma E_t - \gamma I_t$$
$$R_{t+1} = R_t + \gamma I_t$$
这里的β是每日每个感染者的有效接触传染率,σ是潜伏期倒数的进展速率,γ是康复速率。基本再生数R0由β/γ近似得到,代表一个感染者平均传染多少人。实际应用中市民不会把N_c理解成全部人口,而应该理解成“当前有效易感人群规模”,尤其是疫苗接种和既往感染后,易感池会变小。把这几个状态变量映射到卫健委的公开数字时,I对应“目前在院治疗”,R对应“累计治愈+死亡”,E没有直接观测值,只能间接估计,这正是后来用机器学习去拟合的切入点。
3.2 为什么纯SEIR不够用:参数是时变的
如果你用真实疫情数据去拟合一个恒定参数的SEIR,几乎一定失败。因为β本质上不是常数,而是和人群戴口罩比例、聚集性活动强度、封控时间、病毒变异情况都相关的时变函数。政策一调整,β立刻变化,而模型的常微分方程不会自己知道这件事。
解决办法有两种路线。经典路线是“分段估计”——划分时间段,在每个阶段重新拟合参数,这是一些研究团队的常用做法,简单易行,但分段边界怎么定有很多主观性。机器学习路线是“用神经网络表达β(t)”。具体思路是:构造一个小的神经网络,把当前各项干预指标、移动指数、季节因素作为输入,输出每天的β值,再把这个β代入SEIR递推方程,用真实病例序列作为标签,端到端反传训练。这样既保留了SEIR的动力学结构,又能让参数随外部环境变化。这也是我见过最成功的混合建模方式之一,目前已经有若干开源库支持这类“神经网络增强仓室模型”,降低了自造轮子的难度。
3.3 循序渐进:Prophet变体 → 树模型 → LSTM
如果从零开始落地,不建议直接挑战神经微分方程。我建议的推进路线是按模型复杂度递增,每一步都做好评估再往前走。
第一步用Prophet做基线和数据诊断。Prophet的本质是加法趋势分解,把序列拆成趋势项、季节项、节假日项。它最大的价值不在于预测精度,而在于快速帮你看清数据里有哪些异常点、趋势拐点在哪、星期效应有多强。这一步通常两天就能跑完,足以评估数据有没有硬伤。
第二步用XGBoost/LightGBM加滞后特征。树模型在结构化特征场景下表现稳健,不需要归一化,能自动处理特征交互,训练过程也容易调试。把滞后病例数、外部特征、日历特征全部堆进去,用时间序列交叉验证评估,通常能拿到一个不错的baseline。
第三步才是LSTM或Transformer类模型。循环网络可以同时拿“病例序列”和“外部特征序列”作为输入,输出未来七天的预测。它的优势是能捕捉长距离依赖,但也更容易在小样本场景下过拟合。CNN或Transformer模型如Informer也可以做序列预测,但在历史数据长度有限(比如只有一两年)时,规模收敛反而很慢。训深度学习模型必须配足验证滚动机制,绝不能只看训练集损失。
3.4 集成与混合建模是提升精度的关键
我自己最终落地的版本是“三层结构”。第一层是SEIR动力学模型,输出纯粹的机理预测;第二层是LightGBM和LSTM,分别从表格特征和序列特征出发做统计预测;第三层是Stacking,把前面所有模型的预测结果和近期误差作为特征,再训练一个小模型做权重分配。这样的好处是:如果某个阶段政策导致SEIR突然偏离,统计模型还能兜底;如果数据变化异常导致LSTM发散,树模型的稳定输出也能让结果不至于整体崩溃。
集成还有一个作用——把多个模型的分位点结果汇总。比如分别用分位数损失训练五个模型,再取它们在30%和70%分位点的平均值,合成一个“不确定性区间”。这个方法比单独用一个模型预测方差稳健得多,尤其是在数据不稳定的场景里。
4. 模型评估与验证:别被常规交叉验证坑了
4.1 时序泄漏:随机划分是灾难
很多机器学习教程都会教你用train_test_split随机划分训练集测试集,但这个操作放在时间序列预测上会直接导致数据泄漏。前一天的数据和后一天的数据在统计学上严重相关——今天大概率会影响明天。随机划分等于把“未来”的信息放进了训练集,回测指标会虚高到匪夷所思,上线后立刻归零。
正确做法是使用时间序列交叉验证,比如滚动窗口验证:设定训练集为前180天,验证集为紧接着的14天,然后每7天向前滚动一次,重复多次。每一步训练集都会“吸收”之前验证过的数据,模拟模型上线后不断用新数据更新的真实过程。观察各次回测结果的误差分布,才能准确估计模型在未来的真实表现。
4.2 评价指标的选择:MAPE会“爆炸”的坑
评价指标方面,MAE和RMSE是最常见的。但面对流行病学数据,MAPE有个很头疼的问题:当真实值很小(比如只有个位数)时,任何微小的绝对偏差都会被放大成巨大的百分比误差。疫情早期的几例和十几例之间差出100%的百分比变化,这对评估毫无意义。我的建议是优先用WAPE(加权绝对百分比误差)或SMAPE,既保留了百分比口径的可解释性,又不会被零值附近的噪声主导。
医疗场景还需要关注“预测区间覆盖率”。每天的预测应当给出一个区间,比如“下周每日新增预计在1200到1800之间”,如果实际值落在区间外的频率超过了设定概率,说明模型的高斯假设或分位数估计有问题。公共卫生决策者宁可要一个略宽但可靠的区间,也不要一个精准但经常脱靶的点预测。这也是我在汇报口径上做了一次重要转变:从报“点值”改成报“区间+置信度”。
4.3 多步前向预测的评估策略
预测未来1天容易,预测未来7天或14天才是真实需求。这里有个细节:不要只评估“第7天的预测值”和真实值的差距,而要评估整个轨迹的开销。我通常的做法是:从第T天出发,预测T+1到T+7共7个点,然后逐天统计误差随提前期的变化曲线。通常第1天误差最小,第5到第7天误差迅速膨胀——这是正常的,因为不确定性随提前期递增。如果模型到第5天后误差突然爆炸而非渐进上升,往往说明训练数据中存在前视泄漏或预测被推到训练集分布之外。
另外,这类任务适合用“分阶段评估”,把疫情划分为上升期、平台期、回落期三个不同阶段分别评估。一个在上升期表现好的模型,在平台期可能完全失效,因为平台期的信号更弱、信噪比更低。把模型的表现拆细到这个程度,才能准确判断它到底还能不能上线。
5. 实操过程:从特征库到部署的完整链路
5.1 特征库与训练数据的标准结构
数据工程层面,建议把所有原始数据先整理成统一列名:日期、地区、指标名、指标值。然后再通过pivot转成一个“日期×地区×特征”的宽表。我通常把特征分成三层前缀:epi_、mob_、env_,代表流行病学、移动性、环境三类。这样看一眼列名就知道特征来源,模型调试时定位问题会很快。
下面是一个简化的训练样本构建示意(伪代码):
import pandas as pd # 假设 daily_wide 结构: [date, region, new_confirmed, mobility_index, temperature, policy_level, ...] df = daily_wide.sort_values(["region", "date"]).copy() # 基础滞后特征 for lag in [1, 3, 7, 14]: df[f"epi_lag_{lag}"] = df.groupby("region")["new_confirmed"].shift(lag) # 移动平均比率特征 df["epi_ma7"] = df.groupby("region")["new_confirmed"].transform(lambda x: x.rolling(7, min_periods=1).mean()) df["epi_ratio_1_7"] = df["epi_lag_1"] / (df["epi_ma7"] + 1) # 日历特征 df["weekday"] = df["date"].dt.weekday df["is_holiday"] = df["date"].dt.date.isin(holiday_dates).astype(int) # 预测目标:未来7天累计新增或逐日新增 for h in range(1, 8): df[f"target_{h}"] = df.groupby("region")["new_confirmed"].transform(lambda x: x.shift(-h))这段代码里有几个细节值得注意。滞后和移动平均的分组必须按地区独立计算,不能跨地区滑动。预测目标使用shift(-h),所以最终训练每一行的时候要drop掉包含NaN的行。如果想做“未来七天总和”预测,可以再加一列把未来七天累加起来。这个数据结构的最大好处是模型无关——同一份宽表可以同时喂给LightGBM和LSTM,只是后者要reshape成序列格式。
5.2 LightGBM的时序交叉验证与调参
时序场景下,我一般不用LightGBM默认的EarlyStopping策略,因为默认是随机划分验证集,依然会泄漏。正确做法是把验证集按时间切在训练集之后,再用早停控制树的数量。
import lightgbm as lgb train_df = df[df["date"] < "2022-06-01"] valid_df = df[(df["date"] >= "2022-06-01") & (df["date"] < "2022-06-15")] feats = [c for c in df.columns if c.startswith("epi_") or c.startswith("mob_") or c.startswith("env_")] model = lgb.LGBMRegressor( objective="mae", n_estimators=2000, learning_rate=0.03, num_leaves=64, subsample=0.8, colsample_bytree=0.8, ) model.fit(train_df[feats], train_df["target_1"], eval_set=[(valid_df[feats], valid_df["target_1"])], callbacks=[lgb.early_stopping(50, verbose=True)])参数上,objective选“mae”比默认的“mse”更稳健,因为流行病学数据里偶尔会有极端值,MAE对长尾不敏感。学习率设小一点,配套更大的树数量,是最能稳定出效果的组合。我试过不少数据集后,感觉这个项目的调参不需要太花哨,按时序验证、按早停、特征工程做扎实,效果基本就能稳定在行业可用的水平。
5.3 LSTM的输入结构与训练细节
LSTM的输入是三维张量(样本数, 时间步长, 特征数)。假设我们用过去14天的数据预测未来7天,构造方法如下:
import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout SEQ_LEN = 14 HORIZON = 7 N_FEATURES = len(feats) def make_sequences(df, seq_len=SEQ_LEN, horizon=HORIZON): xs, ys = [], [] data = df[feats].values targets = df["target_7"].values for i in range(len(df) - seq_len - horizon + 1): xs.append(data[i : i + seq_len]) ys.append(targets[i + seq_len + horizon - 1]) return np.array(xs), np.array(ys) # 按时间顺序划分 train_seq, train_t = make_sequences(df[df["date"] < "2022-06-01"]) valid_seq, valid_t = make_sequences(df[(df["date"] >= "2022-06-01") & (df["date"] < "2022-06-15")]) model = Sequential([ LSTM(64, return_sequences=True, input_shape=(SEQ_LEN, N_FEATURES)), Dropout(0.2), LSTM(32), Dense(16, activation="relu"), Dense(1), ]) model.compile(optimizer="adam", loss="mae") model.fit(train_seq, train_t, validation_data=(valid_seq, valid_t), epochs=50, batch_size=32, verbose=0)训练LSTM时最需要注意的还是过拟合。流行病学数据一般也就是几百天,样本量远比不上自然语言或图像数据,所以Dropout一定要用,epochs也不宜过多,配合早停把验证损失守住。如果验证损失连续几十个epoch不下降就停。归一化方面,建议对每个特征分别做MinMax缩放,但要注意缩放器的统计量只能从训练集计算,不能在整个数据集上计算,否则又有前视泄漏。
5.4 部署到智慧医疗系统的接口设计
模型训好之后,工程上要做的是把它包成一个标准的预测服务。我在项目里用的是一个简单但实用的三层接口:
- 输入层:接收地区编码、当前日期、最近N天历史序列(或直接从统一数据平台拉取)。
- 模型层:加载训练好的LightGBM和LSTM权重,从特征库自动构建特征,产出多模型预测。
- 输出层:返回一个JSON,内容包含未来7天每日新增的点预测、80%置信区间、以及一条趋势判断(上升/平稳/下降)。
这块服务不直接参与诊疗决策,而是作为公共卫生态势感知模块嵌入到更宽的智慧医疗平台里,供医院床位调度、防疫物资储备和疾控分析做参考。从我的经验看,输出格式的设计和“给谁看”强相关:给决策者看,要用摘要和区间图;给数据分析师看,要给原始数值和置信度;给系统对接看,要定义清晰的错误码和重试机制。
6. 常见问题与排查技巧
6.1 预测突然“失灵”:先怀疑数据口径,再怀疑模型
这类项目上线后最常见的故障是连续几天预测值显著偏差。我的排查顺序是固定的:先看当天预测输入用的历史数据有没有被修正,甚至有没有数据缺失;再看是不是有新政策上线导致传播状态突变;最后才怀疑模型参数。有一次线上模型某天输出突然翻倍,查到最后发现是上游数据同步脚本把另一个城市的数据混进来了。这类问题从特征分布图上往往一眼就能看出来,但如果没做监控图,排查耗时就会非常长。所以特征分布漂移监控和预测误差监控要一起部署。
6.2 节假日导致的预测暴跌/暴涨怎么处理
节假日期间新增病例数下降,很多时候不是疫情好转,而是检测量下降了。模型如果学到“这个日期附近病例下降”,等节假日结束后会把反弹预测得过于缓慢或过快。我在实践中是用“节假日检测调整系数”来修正的:当检测量低于平时60%时,把当天的新增病例按检测缺失比例做一次粗略放大,作为特征输入而不是直接改标签。同时在训练时把这几天样本的权重调低,不让它们主导模型的趋势判断。
6.3 为什么模型在平台期误差特别大
平台期的特点是没有强趋势,每天病例数在小范围内随机波动。模型如果学习到的是一种趋势外推逻辑,在平台期就不知道该往哪个方向拐,预测结果容易反复震荡。这类问题的本质不是模型不够强,而是信噪比低。应对方法有两个:一是降低预测输出的更新频率,比如在平台期只做“未来三天稳定”的判断,不做逐日精细预测;二是在集成模型里增加一个“均值回归”分支,专门预测序列会围绕当前水平波动,避免整体模型在无趋势阶段乱动。
6.4 算出来的有效再生数忽高忽低怎么办
有效再生数(Rt)是衡量当前传播能力的重要指标,通常用近几日的病例时间序列反推。如果直接用每日病例数做窗口除,算出来的Rt噪声会非常大。更稳的做法是先用7日移动平均平滑序列,再去做滑动窗口估计,或者采用贝叶斯平滑方法。Rt的用途不是看单日数值,而是看趋势——连续多日在1以下和连续多日在1以上,对应的态势完全不同。当Rt在1附近反复穿越时,要提醒使用者“当前处于临界状态,任何短期波动都不具备决定性意义”。
6.5 需求方觉得预测“不够准”,怎么沟通
这是比算法更难的问题。非技术决策者经常拿模型和“天气预报”类比,觉得不准就没有价值。但流行病学预测的核心价值不在于“算准每一天”,而在于“给出不同情景下的准备边界”。在和决策者沟通时,我通常把输出改成三个情景:低情景(感染人数在预测区间下沿,可以维持现有资源)、中情景(在区间中部,需要准备一定余量)、高情景(在上沿,需要启动应急储备)。把决策动作绑定到情景而不是绑定到具体的数字上,模型的实际作用会显著提升。
写在最后的经验沉淀
做这类跨学科项目,我最深的体会是:机器学习模型的精度,往往取决于你愿不愿意花三倍时间在数据清洗和特征构造上,而不是花三倍时间换一个更复杂的神经网络。流行病学预测尤其如此——数据发布的延迟、口径的变化、政策干预带来的结构突变,都是模型要吃的“噪音”,预处理阶段是否考虑周全,直接决定模型上线后的真实表现。如果你打算在这个方向深耕,建议从Prophet当基线、树模型做主力、深度学习做辅助的组合入手,把监控和评估机制先立起来,再逐步把混合建模加进去。这套思路不只适用于病例预测,对住院率、重症率、医疗资源占用量的预测同样成立。技术永远是工具,真正有价值的,是你对问题本身的理解有多深。