ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SEIR模型的疫情传播建模实战:从数据清洗到干预评估

基于SEIR模型的疫情传播建模实战:从数据清洗到干预评估

1. 从标题出发:一个建模项目到底要解决什么

“建立模型尝试解决新型冠状肺炎的一些问题”——这个标题看起来很大,但真正落到实操层面,它其实是一个典型的数据建模与仿真项目。核心思路是:把疫情传播过程中涉及的关键因素抽象成变量,用数学或统计模型去拟合、预测、评估不同干预手段的效果。能做的事情包括但不限于:预测感染人数趋势、评估隔离措施对传播速度的影响、分析医疗资源缺口、模拟不同复工节奏下的风险变化。适合有一定编程基础、对数据分析感兴趣、或者正在做相关课题的学生和从业者参考。

我最早接触这类建模任务是在一个跨学科数据分析项目里,当时团队需要快速搭建一个能跑通“数据输入—参数估计—趋势输出”全流程的原型。踩过的坑不少,后面会逐一拆开讲。这篇文章不会给你一个“万能模型”,而是把建模的完整思路、关键参数怎么定、代码怎么写、结果怎么解读、哪里容易翻车,全部摊开来说。你照着做,至少能少走两三个星期的弯路。

2. 建模前的整体设计与思路拆解

2.1 为什么选传染病动力学模型而不是纯统计外推

很多人第一反应是用时间序列做拟合,比如ARIMA或者简单的指数平滑。这类方法在短期预测上确实快,但它有个致命问题:无法解释干预措施带来的拐点。纯统计模型看到的是“数字在涨”,它不知道是因为病毒本身传播力强,还是因为检测量上去了,还是因为人们开始戴口罩了。一旦外部条件变化,模型立刻失效。

传染病动力学模型(最经典的是SIR及其变种SEIR)的核心优势在于:它把人群分成几个状态——易感者、暴露者、感染者、康复者——然后描述状态之间的转移速率。这个转移速率直接对应现实中的传播概率、潜伏期、恢复时间。你调整一个参数,比如把接触率降下来,模型立刻能告诉你感染峰值会推迟多久、峰值高度会降多少。这就是“可解释性”带来的价值。

注意:动力学模型不是越复杂越好。我见过有人一上来就搭十几维的方程组,结果参数根本估不出来,跑出来的曲线比股票还随机。新手建议从SEIR四仓模型起步,跑通了再考虑加年龄分层、空间扩散、疫苗接种等模块。

2.2 数据源的选择与预处理逻辑

建模最耗时间的不是写方程,而是找数据、洗数据。常见的数据来源包括:公开的每日新增病例数、治愈数、死亡数,以及人口密度、流动数据、检测能力等辅助信息。这里有个关键原则:数据的时间粒度和空间粒度必须和模型匹配。如果你用的是省级每日数据,模型就不该细化到市级小时级,否则参数估计会极度不稳定。

预处理阶段我通常会做四件事:第一,对齐时间轴,把不同来源的数据统一到同一日期序列;第二,处理缺失值,短缺口用线性插值,长缺口直接截断,不要硬填;第三,平滑异常值,比如某天因为检测积压导致数字暴增,可以用前后三天的移动平均做修正;第四,归一化,把绝对人数转成占总人口的比例,方便不同地区对比。

2.3 模型选型的权衡:SEIR、Agent-Based还是回归

模型类型优势劣势适用场景
SEIR仓室模型结构清晰、参数少、易解释假设均匀混合,忽略个体差异区域级趋势预测、干预效果评估
Agent-Based模型可模拟个体行为、空间接触计算量大、参数极多、难校准小范围精细模拟、政策微观评估
回归/时间序列上手快、无需动力学假设无法外推、拐点解释弱短期数据补全、趋势平滑

我的建议是:先用SEIR跑通基线,再用回归做对比验证。如果两者趋势差异很大,说明数据质量或参数假设有问题,这时候再去考虑更复杂的模型才有意义。一上来就搞Agent-Based,大概率会陷在调参里出不来。

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

3.1 SEIR模型的数学结构与参数含义

SEIR模型把人群分成四类:S(易感者)、E(暴露者/潜伏期)、I(感染者)、R(康复或移除)。微分方程组长这样:

# 简化版SEIR微分方程组 # beta: 传播率, sigma: 潜伏期转为感染期的速率, gamma: 恢复率 dS_dt = -beta * S * I / N dE_dt = beta * S * I / N - sigma * E dI_dt = sigma * E - gamma * I dR_dt = gamma * I

这里每个参数都有现实对应:beta反映的是“一个感染者每天能传染多少人”,sigma是潜伏期倒数(比如潜伏期5天,sigma=0.2),gamma是感染期倒数(比如病程10天,gamma=0.1)。基本再生数R0 = beta / gamma,这个值决定了疫情是否会爆发。R0大于1,感染人数会增长;小于1,疫情会逐渐消退。

实操心得:beta不是固定值。它会随着人们戴口罩、减少聚集而下降。所以我在代码里通常把beta设成时间的分段函数,比如第0-20天是0.5,第21天开始因为管控降到0.2。这样模型才能反映干预效果。

3.2 参数估计的三种实用方法

参数估计是建模中最容易翻车的地方。我试过三种方法,各有适用场景:

第一种:文献参考法。直接从已发表的同类研究里找参数范围。比如潜伏期取5-7天,感染期取7-14天。这种方法快,但问题是不同地区、不同毒株的参数差异很大,直接套用容易偏差。

第二种:最小二乘法拟合。把模型输出的累计感染人数和实际数据做对比,用优化算法(如scipy.optimize.minimize)找使误差最小的参数组合。这种方法数据驱动,但需要足够长的数据序列,而且容易陷入局部最优。

第三种:贝叶斯推断。给每个参数一个先验分布,用MCMC采样得到后验分布。这种方法能给出参数的不确定性范围,但计算量大,代码复杂度高。新手建议先用前两种,等模型稳定了再上贝叶斯。

# 最小二乘拟合示例 from scipy.optimize import minimize import numpy as np def loss(params, data): beta, sigma, gamma = params # 运行SEIR模型得到预测值 pred = run_seir(beta, sigma, gamma) return np.sum((pred - data) ** 2) result = minimize(loss, x0=[0.5, 0.2, 0.1], args=(actual_data,), bounds=[(0.1, 1.0), (0.1, 0.5), (0.05, 0.3)])

3.3 干预措施的建模方式

干预措施在模型里通常体现为参数的变化。具体来说:

  • 减少接触:降低beta值。比如封控期间beta可能降到原来的30%。
  • 缩短发现时间:提高sigma值,让潜伏期更快转为隔离状态。
  • 提升治愈能力:提高gamma值,缩短感染期。
  • 疫苗接种:把部分S直接移到R,或者降低S对beta的敏感度。

我一般会设计多个情景:基线情景(无干预)、温和干预(beta降30%)、强力干预(beta降70%),然后对比不同情景下的峰值时间和峰值高度。这样输出的结果对决策参考价值最大。

注意:干预措施的效果有滞后性。今天开始戴口罩,不会明天就见效,通常要等一个潜伏期(5-7天)才能看到新增数字下降。建模时要把这个延迟考虑进去,否则会高估干预的即时效果。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装

整个项目用Python就能跑通,核心依赖四个库:numpy做数值计算,scipy做优化和积分,pandas做数据处理,matplotlib做可视化。安装命令如下:

pip install numpy scipy pandas matplotlib

如果你需要做贝叶斯推断,再加一个pymc或者emcee。我建议先用前四个把基线跑通,不要一开始就装一堆库,环境越复杂越容易出问题。

4.2 数据读取与清洗的完整代码

假设你手头有一份CSV文件,包含日期、累计确诊、累计治愈、累计死亡四列。清洗流程如下:

import pandas as pd import numpy as np # 读取数据 df = pd.read_csv('epidemic_data.csv', parse_dates=['date']) df = df.sort_values('date').reset_index(drop=True) # 计算每日新增 df['new_cases'] = df['cumulative_confirmed'].diff().fillna(0) df['new_cases'] = df['new_cases'].clip(lower=0) # 去除负数 # 平滑异常值:用7天移动平均 df['new_cases_smooth'] = df['new_cases'].rolling(window=7, center=True).mean() df['new_cases_smooth'] = df['new_cases_smooth'].fillna(method='bfill').fillna(method='ffill') # 计算活跃感染者(近似) df['active'] = df['cumulative_confirmed'] - df['cumulative_recovered'] - df['cumulative_deaths'] df['active'] = df['active'].clip(lower=0) print(df.head(10))

这段代码里有两个细节值得说:第一,clip(lower=0)是为了防止数据修正导致的负数;第二,移动平均用了center=True,这样平滑后的曲线不会整体偏移。如果你不做平滑,某天的检测积压会让模型误以为传播率突然飙升。

4.3 SEIR模型的完整实现与求解

下面是一个可直接运行的SEIR模型实现,包含参数分段和结果输出:

import numpy as np from scipy.integrate import odeint import matplotlib.pyplot as plt def seir_model(y, t, N, beta, sigma, gamma): S, E, I, R = y dS = -beta * S * I / N dE = beta * S * I / N - sigma * E dI = sigma * E - gamma * I dR = gamma * I return [dS, dE, dI, dR] def run_seir(N, I0, E0, days, beta_func, sigma, gamma): t = np.linspace(0, days, days) y0 = [N - I0 - E0, E0, I0, 0] result = odeint(seir_model, y0, t, args=(N, beta_func, sigma, gamma)) return t, result # 参数设置 N = 1000000 # 总人口 I0 = 10 # 初始感染者 E0 = 20 # 初始暴露者 sigma = 0.2 # 潜伏期5天 gamma = 0.1 # 感染期10天 # beta随时间变化:前20天0.5,之后0.2 def beta_func(t): return 0.5 if t < 20 else 0.2 t, result = run_seir(N, I0, E0, 160, beta_func, sigma, gamma) S, E, I, R = result.T plt.figure(figsize=(10, 6)) plt.plot(t, I, label='Infected') plt.plot(t, E, label='Exposed') plt.plot(t, R, label='Recovered') plt.xlabel('Days') plt.ylabel('Population') plt.legend() plt.title('SEIR Model with Intervention at Day 20') plt.grid(True) plt.show()

跑完这段代码,你会看到一条先上升后下降的感染曲线,峰值大概在第35-40天出现。如果把beta_func改成全程0.5,峰值会更高、来得更早。这就是干预的效果。

4.4 结果解读与可视化技巧

模型跑出来只是第一步,怎么解读才是关键。我通常关注四个指标:峰值时间(疫情什么时候最严重)、峰值高度(医疗系统会不会被击穿)、累计感染比例(最终多少人被感染)、干预敏感度(beta降10%能带来多大改善)。

可视化方面,我建议至少画三张图:第一张是S/E/I/R四条曲线随时间变化;第二张是不同干预情景下的感染曲线对比;第三张是参数敏感度热力图,横轴是beta,纵轴是gamma,颜色表示峰值高度。第三张图特别有用,它能告诉你哪个参数对结果影响最大,从而指导防控重点。

实操心得:画图时把“实际数据点”也叠加上去。如果模型曲线和实际数据偏差很大,不要急着调参,先检查数据本身有没有问题。我遇到过好几次是数据源把“累计”和“新增”搞混了,导致模型怎么调都对不上。

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

5.1 模型曲线和实际数据对不上怎么办

这是最常见的问题,排查顺序如下:

现象可能原因排查方法
模型峰值远高于实际beta估计过高检查是否用了早期未管控数据拟合
模型峰值远低于实际初始感染者设置过少调整I0,用早期数据反推
曲线形状完全不对数据粒度或定义错误确认累计/新增是否混用
后期下降太快gamma估计过高用康复数据重新估计病程

我的经验是:先信数据,再信模型。如果数据本身有缺失或定义混乱,再好的模型也救不回来。另外,不要追求完美拟合,传染病数据本身噪声就大,R平方能到0.8以上就算不错了。

5.2 参数估计不收敛的解决思路

最小二乘拟合时,优化算法可能报“不收敛”或者跑出离谱的参数值。原因通常有三个:第一,初始值设得太远,比如beta初始值设成5,而实际可能在0.3左右;第二,参数边界没设,优化器跑到负值去了;第三,数据太少,欠定问题。

解决办法:给每个参数设合理的上下界,比如beta在0.1到1.0之间,sigma在0.1到0.5之间,gamma在0.05到0.3之间。初始值用文献参考值。如果还是不行,改用网格搜索先粗筛一遍,再用优化算法精调。

5.3 干预措施效果被高估的坑

我踩过最大的坑是:模型显示“强力干预后两周疫情就控制住了”,但实际数据要一个月才见效。后来发现原因是没有考虑检测延迟和报告延迟。从感染到确诊再到上报,中间可能隔了5-10天。模型里如果直接用“干预开始时间”作为beta下降的起点,就会高估效果。

修正方法:在模型里加一个“报告延迟”参数,把实际干预时间往后推7-10天再输入模型。或者更简单粗暴:把干预后的beta下降幅度打个七折。这个经验值是我对比了多个地区数据后总结的,不一定精确,但比不修正强得多。

5.4 模型外推的风险与边界

任何模型都只能在一定范围内外推。SEIR模型假设人群均匀混合,如果实际存在超级传播事件或者空间聚集,外推就会失效。我的做法是:预测不超过当前数据长度的三分之一。比如你有60天数据,最多预测未来20天。超过这个范围,误差会指数级放大。

另外,模型输出一定要给区间,不要只给一条线。用参数的后验分布或者敏感度分析,给出“乐观/中性/悲观”三条曲线。这样读者才知道不确定性有多大,不会把模型当成算命。

6. 建模之外的几点个人体会

这个项目做下来,我最大的感受是:模型的价值不在于预测得多准,而在于帮你理清变量之间的关系。哪怕预测偏差20%,只要你能说清楚“如果beta降一半,峰值会推迟两周”,这个结论就有参考价值。反过来,如果只是跑出一条曲线而不解释参数含义,那和画图工具没什么区别。

另外,数据清洗的时间通常占整个项目的60%以上。我见过太多人花三天调模型,却只花三小时洗数据,最后结果一塌糊涂。建议你把数据质量检查做成一个清单:时间连续性、定义一致性、异常值比例、缺失值比例,每项都过了再开始建模。

最后分享一个小技巧:把模型代码封装成函数,参数用字典传入。这样你可以快速切换情景,不用每次改代码。比如run_scenario({'beta': 0.3, 'sigma': 0.2, 'gamma': 0.1}),跑十个情景也就几行代码。这个习惯让我在对比分析时效率提高了至少三倍。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进