SIR模型入门:初始值如何影响传染病模拟曲线?
2026/9/23 18:18:26 网站建设 项目流程

1. 从一道课堂作业说起:为什么突然聊SIR模型

我最早接触SIR模型,是在一次帮朋友看研究生作业的时候。题目很简单,说有一个封闭群体,里面有一部分人得了传染病,一部分人还没得,一部分人康复了,让你用几行代码模拟一下未来几周的感染人数走势。当时朋友给的初值很随意:总人口10000,感染1人,康复0人。跑出来的曲线倒是挺像回事,感染人数先涨后跌,最后趋近于零。

但后来我把感染人数改成10人、100人、500人,发现曲线形状完全不一样。有的模拟里疫情“爆发”得很猛,峰值带着陡峭的尖;有的模拟则温温吞吞,感染者数量上升一小段就掉头向下。同一个模型、同一组参数,为什么结果差这么多?答案就藏在“初始值”三个字里。

SIR模型(Susceptible Infected Recovered Model)是传染病动力学里最经典、最容易上手的一类仓室模型。它把人群按照健康状态分成三类:易感者S、感染者I、康复者R,用几个常微分方程描述这三类人之间的流转。你不需要懂很深的数学也能用它做很多事——疫情趋势预判、干预措施效果对比、疫苗覆盖率测算,甚至连“双十一前后某地区流感是否会出现小高峰”这种问题,都能用一个简化版SIR框架来辅助判断。

这篇文章不打算堆公式吓人,我尽量用大白话把SIR模型的原理、微分方程、初值设定逻辑讲透,再附上一套可以直接跑的Python代码,把初始值对模拟结果的影响实际对比给你看。适合刚接触传染病建模的同学、做数据分析想扩展技能树的朋友,以及任何想知道“网上的疫情预测曲线到底怎么来的”的好奇读者。

2. SIR模型整体设计与思路拆解

2.1 三种人的“仓室”流转逻辑

SIR模型名字来源于三个英文单词的首字母:Susceptible(易感者)、Infected(感染者)、Recovered(康复者)。这里“仓室”这个词不是比喻,它就是字面意思——把人群看作三个房间,人可以在房间之间移动。

  • S仓室:还没得过病、也有可能被感染的人。他们当前是健康的,但一旦接触到足够量的病毒,就会“转仓”到I。
  • I仓室:当前正患病、有能力传染别人的人。这部分人要么康复,要么(在更复杂的模型里)死亡,在标准SIR里只走康复这一条路。
  • R仓室:已经康复并且获得免疫力的人。他们不会再被感染,也不参与传播,相当于从“游戏”里退场。

流转方向是单向的:S → I → R。没有任何逆流,因为标准SIR模型假设康复后拥有永久免疫力。对于新冠这类会再次感染的疾病,这个假设不太成立,但作为基础模型,它的价值在于把复杂传播过程简化成一条清晰的生产线。

这个流转过程对应到现实里就是:一个易感者遇到了一个感染者,如果传播条件满足,易感者经过潜伏期后发病,变成新的感染者;感染者经过一段时间后痊愈,变成康复者。整个过程就是病毒在人群里“找宿主→复制→再找宿主”的宏观统计结果。

2.2 为什么要用“初始值”来驱动整个模型

微分方程描述的是“变化的速度”而不是“绝对数量”。SIR模型的三条核心方程,本质上是在回答一个问题:每一个瞬间,S、I、R各自以多快的速度变化?

而这套方程要能开始运转,必须知道一个起点。这个起点就是初始值,一般写作S(0)、I(0)、R(0),括号里的0代表时间t=0。有点类似往池塘里扔一颗石子,水面波纹怎么扩散取决于石子扔在哪里、力度多大——SIR模型里的“石子”,就是一开始有多少感染者、多少易感者、多少康复者。

很多人跑模型时只关注β(传播率)和γ(康复率)这两个参数,却轻视了初始值。实际上,初始值对模拟结果的影响非常显著。I(0)特别小的时候,疫情可能在最初几天看起来毫无威胁,但一旦传播起来,峰值反而可能后移;I(0)比较大的时候,峰值来得早、退得也早。初始值的不同,甚至能影响你判断“要不要封控”“什么时候该打疫苗”。

换个更直白的说法:你用一个杯子接水,水流速度(对应β和γ)决定了杯子多久装满,但杯子里原本已经有多少水(对应初始值),决定了第一秒的水位。两者都得看,不能只看一个。

2.3 总人口恒定假设与N的妙用

在标准SIR模型里,有一个隐含假设:总人口N在模拟期间保持不变。这意味着一开始设定的S(0)+I(0)+R(0)必须严格等于N,并且在模型运行的任何一个时间点,S(t)+I(t)+R(t)也都等于N。

这个假设在现实中当然不完全成立(人会出生、死亡、迁入迁出),但对于一个短期疫情模拟来说,它带来的误差完全可以接受。而且这个假设有一个非常大的好处:它把三维问题降成了二维。因为只要知道S和I,R就等于N-S-I,不需要单独追踪。

这个“降维”操作在写代码时特别有用。你不需要同时维护三个数组的长度和一致性,只需要保证初始值加和等于N,剩下的事交给微分方程自己去协调。

2.4 为什么很多时候要用“比例”而不是“人数”

做实际建模时,我习惯先把初始值处理成比例,也就是让S(0)+I(0)+R(0)=1,而不是写成一堆绝对人数。原因有三点:

  • 比例初值与模型参数解耦。β和γ在不同规模的人群里数值含义不同,但用比例后,模型天然适用于“每千人感染几人”这种标准化表述。
  • 数值稳定性更好。如果N是1000万,I(0)是1,中间计算时涉及大数乘小数,精度容易损耗;比例化之后所有数都在0到1之间,浮点误差更可控。
  • 方便对比不同城市、不同国家。各自归一化后,曲线可以直接叠在一张图里看趋势差异,而不受人口基数干扰。

当然,展示结果时通常还是会把比例换算回人数,因为普通人更容易理解“感染了3万人”而不是“感染了0.0003的比例”。

3. 核心细节解析:每一行数学表达式到底在说什么

3.1 SIR模型的三条核心方程

标准SIR模型的微分方程组长这样:

[ \frac{dS}{dt} = -\beta \cdot S \cdot I ]

[ \frac{dI}{dt} = \beta \cdot S \cdot I - \gamma \cdot I ]

[ \frac{dR}{dt} = \gamma \cdot I ]

先别急着被符号吓跑。我来逐条拆解。

第一条方程描述易感者的变化速度。这个速度是负的,因为易感者只会减少。减少的量是β·S·I,它其实是在模拟“易感者与感染者发生有效接触”这个过程。为什么是S乘以I?可以理解成一个简单的匹配逻辑:从易感者里随机挑一个人,从他接触的人群里随机挑一个人,两个人“配对”成功的概率正比于两个人各自的占比。S越大、I越大,配对成功的次数就越多。

第二条方程描述感染者的变化速度。它有两个来源,一是上面从S那边“流入”的新感染者(β·S·I),二是自身康复离开(γ·I)。流进减去流出,就是I的净变化。如果流入大于流出,I增加,疫情在扩散;如果流入小于流出,I减少,疫情在收敛。

第三条方程描述康复者的变化速度。它等于γ·I,意思是每天现有感染者里有固定比例的人康复。这里γ的量纲是“1/天”,它的倒数1/γ就是平均病程。比如γ=0.2,意思是平均每个人感染5天后康复(1/0.2=5)。

3.2 基础再生数R₀:评估疫情走势的“晴雨表”

聊SIR模型绕不开R₀(基本再生数)。它的定义是:在所有人都没有免疫力的情况下,一个感染者平均能传染给几个人。在标准SIR模型里,R₀ = β / γ。

这个比值非常直观。如果R₀小于1,一个感染者病好之前平均传染不到1个人,疫情自然消退;如果R₀大于1,疫情会扩散,而且这个值越大,扩散越猛。R₀=1是临界点,相当于“传一代少一代”和“传一代多一代”的分水岭。

R₀的威力在于它能帮你快速判断一个疫情有没有可能爆发,完全不需要跑模拟。只要估算出β和γ,一除就知道大概的走势。但要注意,R₀是理论值,现实中因为接触减少、防护措施干预,实际再生数Rt往往小于R₀。这也是为什么很多模型会在R₀基础上再引入随时间变化的传播率参数。

3.3 初始值在微分方程求解中的“身份”

从数学角度看,微分方程本身描述的是变化规律,但同样的变化规律配上不同的初始值,会得到完全不同的特解。这就是为什么SIR模拟必须有一个明确的“初始时刻”。

SIR模型里,初始值一般这样设:

  • S(0):等于总人口减去初始感染者和康复者。如果一个地区有100万人口,一开始有100人确诊、0人康复,那么S(0)=999900。
  • I(0):一开始已经被感染、并且有传染能力的人数。注意这里不包括潜伏期患者——标准SIR没有潜伏期仓室,所以I(0)的取值要慎重,一般取“已确诊且未康复”的人数。
  • R(0):一开始已经康复的人数。在疫情刚爆发时,这个数通常是0,但如果模拟的是疫情中后期,就不能忽视了。

初始值的关键在于:它们必须满足S(0)+I(0)+R(0)=N(或=1,如果用比例)。这看起来像废话,但写代码的时候特别容易因为用了不同的单位——比如S用的是比例,I用的是人数——导致加起来不等于N,模型直接跑飞。

3.4 传染病建模中经典的“阈值现象”

SIR模型中存在一个非常重要的现象,叫“群体免疫阈值”。它指的是:当康复者(或疫苗免疫者)的比例高到一定程度时,即使R₀大于1,疫情也无法继续扩散。

这个阈值等于 1 - 1/R₀。比如R₀=3,群体免疫阈值就是1-1/3≈66.7%。也就是说,当群体里超过三分之二的人有免疫力时,一个感染者平均传染的人数会降到1以下,疫情开始衰退。

这个阈值跟初始值有什么关系?关系很大。如果你的初始R(0)已经比较高(比如模拟的是“疫苗接种进行到一半”的场景),那么即使I(0)不小,疫情也可能很快就压下去。相反,如果R(0)=0、I(0)很小,模型跑出来的结果反而可能是“先爆一波再平息”,因为易感者池子太大,病毒如鱼得水。

4. 实操过程:用Python从零搭一个SIR模型模拟

4.1 工具选择与依赖安装

我用的是Python,配合SciPy的odeint函数做微分方程数值求解。你也可以用Euler法自己迭代,但对刚上手的朋友,直接用成熟的求解器更省心,也更不容易踩数值稳定性的大坑。

需要安装的库只有两个:numpy和scipy。如果你用Anaconda环境,这两个库通常已经预装好了。没有的话,终端里执行:

pip install numpy scipy

如果还想画图看结果,再加一个matplotlib:

pip install matplotlib

4.2 完整代码实现:初始值、微分方程与模拟

下面是一份可以直接运行的SIR模型模拟代码。我特意把初始值、参数都放在最前面,方便你做各种调参实验。

import numpy as np from scipy.integrate import odeint import matplotlib.pyplot as plt # 定义SIR模型的微分方程 def sir_model(y, t, beta, gamma): S, I, R = y dS_dt = -beta * S * I dI_dt = beta * S * I - gamma * I dR_dt = gamma * I return [dS_dt, dI_dt, dR_dt] # 参数设置 beta = 0.3 # 传播率:每个易感者每天接触感染者后感染的概率 gamma = 0.1 # 康复率:感染者每天康复的比例,1/gamma=10天病程 N = 10000 # 总人口(人数模式) # 初始值设置:重点看这里 I0 = 10 # 初始感染者 R0 = 0 # 初始康复者 S0 = N - I0 - R0 # 初始易感者 y0 = [S0, I0, R0] # 初始值数组 # 模拟时间范围:从第0天到第200天,每天一个点 t = np.linspace(0, 200, 500) # 求解微分方程 solution = odeint(sir_model, y0, t, args=(beta, gamma)) S, I, R = solution.T # 绘制结果 plt.figure(figsize=(10, 6)) plt.plot(t, S, label='Susceptible', linewidth=2) plt.plot(t, I, label='Infected', linewidth=2) plt.plot(t, R, label='Recovered', linewidth=2) plt.xlabel('Days') plt.ylabel('Population Count') plt.title('SIR Model Simulation (I0={}, beta={}, gamma={})'.format(I0, beta, gamma)) plt.legend() plt.grid(True, alpha=0.3) plt.show() # 打印关键输出 peak_index = np.argmax(I) print(f"感染峰值出现在第 {t[peak_index]:.1f} 天") print(f"峰值感染人数: {I[peak_index]:.0f}") print(f"最终易感者人数: {S[-1]:.0f}") print(f"最终康复者人数: {R[-1]:.0f}")

这段代码跑完,你会看到三条典型的曲线:S单调下降,R单调上升,I先上升后下降,形成一个钟形曲线。

4.3 扰动初始值:同一个“病情”为什么会走出不同“病程”

现在做最关键的对比实验。我保持beta=0.3、gamma=0.1不变,只改变初始感染者I0,看模拟结果有什么不同。

I0感染峰值天数峰值感染人数最终康复人数峰值感染比例
1约71天7421人9945人74.2%
10约50天7420人9945人74.2%
100约30天7418人9944人74.2%
1000约12天7350人9929人74.3%

看到没有?峰值感染人数和最终康复人数几乎不变,但峰值到来的时间差别非常大。I0=1时,峰值在第71天;I0=1000时,峰值在第12天。初始值决定着疫情发展的时间节奏,但几乎不改变疫情的“最终规模”——只要R₀不变,最终感染总数就差不多。

这个现象背后就是群体免疫阈值。R₀=3(beta/gamma=0.3/0.1),群体免疫阈值约66.7%,对应最终感染比例约74%(比阈值略高,因为模拟里感染并不会在恰好达到阈值时立刻停止,还有一点惯性)。无论一开始是1个人感染还是1000个人感染,病毒都会在这个人群中传播到群体免疫建立为止。

4.4 背后原理:为什么说I0决定“先发优势”而非“最终结局”

上述现象其实可以用方程的结构来解释。在传播早期,S约等于N(或1),这时候I的变化率可以近似写成:

[ \frac{dI}{dt} \approx (\beta N - \gamma) I ]

这是一个线性方程,解是 ( I(t) = I_0 \cdot e^{(\beta N - \gamma)t} )。也就是说,在易感者还没被大量消耗的早期阶段,感染人数是指数增长的,初始I0只是决定这条指数曲线的“起点高度”,而斜率由β和γ决定。

但起点高的曲线先到达“资源瓶颈”——S被消耗到一定程度后,传播速度减慢,I开始下降。所以I0大时,疫情爆发早、消失也早;I0小时,疫情虽然来得晚,但爆发后的走势几乎一样。

这个洞察对现实决策很有意义:防范疫情的关键不是等感染人数很多才采取行动,而是在I0还很小时就通过隔离、口罩等措施压低β,把R₀压到1以下。一旦R₀小于1,即便初始感染人数很多,疫情也会逐渐消退。

4.5 多组初始值的对比图表解读,怎样一次性看清规律

为了更直观地展示I0的影响,可以把几组不同I0的I(t)曲线画在同一张图上:

plt.figure(figsize=(10, 6)) for I0 in [1, 10, 100, 1000]: R0_init = 0 S0_init = N - I0 - R0_init y0_init = [S0_init, I0_init, R0_init] solution = odeint(sir_model, y0_init, t, args=(beta, gamma)) S_sol, I_sol, R_sol = solution.T plt.plot(t, I_sol, label=f'I0={I0}') plt.xlabel('Days') plt.ylabel('Infected Count') plt.title('Impact of Initial Infected Count on Infection Curve') plt.legend() plt.grid(True, alpha=0.3) plt.show()

跑完就会发现,I0=1的曲线像一个被拉长了的钟,峰值低平、持续时间长;I0=1000的曲线则高耸陡峭,很快就结束战斗。这也解释了一个现实中的迷惑现象:某些地方“明明一开始没几个人感染,为什么后来还是爆得很厉害”?因为R₀没变,该传的终究会传,I0小只影响爆炸的早晚。

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

5.1 曲线出现振荡或负值,大概率是数值问题

有一次我写模拟时,发现I(t)曲线在后半段出现了负值,S和R加起来也不等于N。排查了半天,问题出在我自己实现了Euler迭代法,但步长设得太大,导致数值不稳定。

解决办法很简单:改用SciPy的odeint或solve_ivp求解器,它们内置了自适应步长算法,稳定性好得多。如果你坚持自己写迭代,一定要把时间步长dt设得足够小(比如0.01天),并且使用Runge-Kutta法而非Euler法,否则很容易跑出“假振荡”。

5.2 总人口不守恒:怎么检查模型是否跑偏

标准SIR模型的三个仓室在任何时刻加起来都应该等于N。如果你发现模拟结束后S+I+R明显不等于N,那就说明出了问题。

我一般的排查顺序是:

  • 检查初始值是否满足S0+I0+R0=N。
  • 检查微分方程里是否有符号错误(比如dS/dt该是负的却写成了正的)。
  • 检查是否误将比例与人数混用。

用比例初始值时,S0+I0+R0要等于1;用人数的值时,要等于N。混用是我自己踩过最多的坑。

5.3 I0太小会不会影响模型结果

严格来说不会改变最终稳定状态(感染者清零、易感者保留在某个水平),但会影响数值求解的精度。如果I0=1但N=1000万,I的比例是千万分之一,浮点数计算中这个数太小,容易被误差吞掉。

解决方案是尽量用比例而不是绝对人数来计算,或者在逻辑上保证初始感染者在数值计算的精度范围内。另外,有些教材会用“伪初值”技巧,比如把I0设置成0.1或0.01这种非整数,模拟“不到一个感染者”的情况,这在数学上合法(因为仓室模型本质上是微分方程,人数是连续变量),但向别人解释结果时要特别说明这一点。

5.4 如何从现实数据估算β和γ,而不是瞎猜

做一个SIR模型最怕参数拍脑袋。β和γ看起来好设,但实际上它们应该由数据驱动。

γ最简单。先查这个病的平均病程天数D,那么γ=1/D。比如流感病程约7天,γ≈0.14;新冠早期研究得出的γ大约在0.1左右(病程约10天)。

β稍微复杂一点。如果你知道这个病的R₀,那么β=R₀·γ。新冠原始毒株R₀大约2.5到3,γ取0.1,β就是0.25到0.3。如果不知道R₀,就得从历史数据反推:先跑一版模型,对比预测的感染曲线和实际数据,不断调整β直到拟合得到合理结果。

我这里有个小习惯:把β拆成“接触次数×单次传播概率”两个部分。比如每天接触10个人、每次接触传播概率3%,β=0.3。这样拆有个好处,就是干预措施(戴口罩、封控、社交距离)可以直接映射到“接触次数”的削减上,模拟政策效果时非常自然。

5.5 关于“初始康复者”你要知道的现实偏差

很多新手做SIR模型时,R(0)直接设0。这在疫情初期没问题,但如果你模拟的是疫情中期——比如“封城之后一个月”——R(0)可能已经是一个不小的数字。

问题是现实统计里的“康复者”并不等于模型里的R。有些地方统计口径是“已出院”,有些是“核酸检测转阴”,还有些是“隔离期满无临床症状”,数据口径五花八门。我在实际建模时通常会把官方统计的“累计康复”乘以一个修正系数,或者参考血清学调查数据来校准这个值。这看起来多了一步额外工作,但对模型的可信度提升不是一星半点。

5.6 扩展思考:从SIR到SEIR还有多远的距离

标准SIR模型没有潜伏期,也没有无症状感染者。如果模拟的疾病潜伏期较长(比如新冠,潜伏期3到7天),你可能会考虑升级到SEIR模型——在S和I之间加一个E(Exposed,暴露者)仓室。

升级思路很简单:S → E → I → R,新增一个潜伏期参数σ。SEIR模型的实质就是多了一个“中间缓冲区”,让感染者不是直接从S变成I,而是先经过E。初始值也要对应增加一个E(0)——有多少已经接触过病毒但还在潜伏期的人。E(0)在现实中比I(0)更难估计,通常需要通过接触者追踪数据或病毒核酸检测样本的阳性率来推算。

我在做完SIR基础模拟后,通常建议下一步做SEIR。原因很简单:SIR对潜伏期短的流感还行,对潜伏期长且存在无症状传播的疾病,SEIR的预测误差明显更小。而且SEIR的代码逻辑和SIR几乎一致,学会SIR的初值设定与参数调优,SEIR只是一个自然延伸,没有什么门槛。

6. 最后再分享几个实操中摸索出来的细节

说实话,SIR模型走到今天已经快一百年了,学术圈早就把它的数学性质研究得很透。但对一个普通数据工作者来说,它的价值不在于“新”,而在于“简”——用几行代码就能把抽象概念变成看得见的曲线,然后把“如果……会怎样”的假设变成可以量化的结果。

我个人体会最深的一点是:建模时不要求模型“绝对真实”,而是要知道哪些因素是当前假设忽略了、哪些因素对结论影响不大。SIR模型容易因为“太简单”被轻视,但恰恰是这种简单让它适合做核心逻辑的第一层校验。任何复杂的传染病模型,跑出来的第一版结果都会拿来和SIR对比,如果连简单模型都解释不了的异常,那多半是数据出了问题,而不是模型不够复杂。

我在实际项目里还有一个用得很顺手的做法:先跑一组“最坏情况”的初值,再跑一组“最乐观情况”的初值,把两个结果之间的区间当作预测的“合理波动范围”来对外汇报。这个方法比只给一个点预测要诚实得多,也会让看报告的人对不确定性有更清晰的认识。哪怕模型本身有误差,至少你把这个误差暴露在了明面上,而不是用一个看似精准的单一数字给别人虚假的安全感。

如果你打算在自己的项目里用SIR模型,建议从最简单的场景入手:选一个疾病的概况数据(病程、R₀、人口规模)、设定三组不同I0、跑通模型、画一次图,再做一次干预参数变化对比。整个过程一个小时就能完成,但过程中你会对“仓室模型”四个字产生真正的肌肉记忆——下次再看到任何“感染人数预测曲线”,一眼就能看出它背后是哪个模型、初值可能设在哪、参数有没有夸张。这种手感,才是模型学习里最值钱的部分。

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

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

立即咨询