☰
基于MATLAB的SEIR流感传播动力学仿真系统搭建
2026/10/12 4:03:38 网站建设 项目流程

前阵子做课题的时候,临时接到一个需求:搭一个“基于MATLAB的模拟病毒以及相关流感传播的动力学仿真系统”。听起来像正规科研项目,我理解下来就一句话——用MATLAB把流感在虚拟人群里“养”出来,再通过调整参数看它怎么传播、什么时候达到峰值、打疫苗或减少接触后能不能被压下去。这套东西对我最大的吸引力在于,不用等真实疫情数据,所有“如果”都能在几分钟内跑出结果。如果你是做课程设计、毕设,或者单纯对流行病学建模感兴趣,这篇完全可以照着搭。

1. 项目整体设计:仿真系统不只是解方程

1.1 先想清楚系统要解决什么问题

很多人一听到“动力学仿真”,第一反应是去找微分方程、调求解器,然后跑出一条曲线就觉得自己做完了。但实际做下来你会发现,仿真系统的价值不在“能跑”,而在“能回答业务问题”。我这个项目的目标很聚焦:给定一个人群规模、初始感染者数量、接触习惯和医疗介入时间,预测未来几个月每天有多少潜伏者、多少有症状感染者、多少人已经恢复。

需求拆解其实是三个层面。第一,参数要能任意改,不能每次改数字都去翻代码;第二,模型本身要符合流感传播的常识,至少得有潜伏期这个环节,不能一感染就马上有传染性;第三,结果必须能直观对比,比如“第30天开始封控”和“不封控”的峰值差异。围绕这三个层面,系统才逐步成型。

我建议你也按这种思路来:先别急着写代码,把问题框成“输入-模型-输出”三个盒子。输入是人口学参数和干预策略,模型是动力学方程组,输出是感染曲线、峰值时间、累计病例数这些可量化指标。这样不管后面怎么加功能,结构都不会乱。

1.2 为什么选MATLAB而不是其他工具

选MATLAB做这个项目,不是因为它流行,而是因为它确实贴合仿真场景。Python在国内学生里用得多,但做常微分方程求解和即时交互绘图,MATLAB的开箱即用程度更高。尤其ode45这种变步长求解器,底层实现非常成熟,你只需要把方程写成函数,它自动处理步长和误差,不需要自己造轮子。

我整理过两者在早期原型阶段的差异,给你一个参考:

维度MATLABPython
微分方程求解内置ode45、ode15s,调用简单需要Scipy的solve_ivp,参数略繁琐
交互式绘图拖拽、缩放、图例自动更新需要用Matplotlib反复刷新
数值稳定性处理自带NonNegative、Mass选项需要手动设置事件或添加辅助函数
学习成本矩阵思维直接看个人基础

当然Python也有优势,比如免费、生态广、后续对接机器学习容易。但如果你做的就是一个教学演示或科研预演系统,MATLAB的交互性会让人特别舒服。“改一个参数→重新运行→看曲线变化”,这一套流程在MATLAB里几乎是顺手就来的事。

1.3 系统整体架构怎么搭

整个系统的运行流程我把它拆成五段:参数配置、模型方程、数值求解、结果存储、可视化输出。前面两步是核心,后面三步基本是套路。具体来说,参数配置集中在一个struct里,包括总人口数、初始感染人数、潜伏期倒数、恢复率倒数、模拟天数、接触率等;模型方程是一个独立的函数文件,输入当前时间t和状态向量y,输出各仓室的导数;数值求解统一用ode45,有时候遇到刚性再切ode15s。

结果存储我用一个矩阵y保存,每一行对应一个时间点,每一列对应S、E、I、R的人数。可视化部分单独写一个脚本,从矩阵中拿数据画图。这样模块之间只通过接口通信,想换模型或者调整干预策略时,改动范围非常小。

我在项目早期犯过一个错误:把所有代码写在一个几百行的脚本里,结果每次想改参数都要往下翻很久。后来把所有可调数字集中到params结构体,模型函数只认结构体,整个清爽了很多。这也是我给所有做仿真的人第一个建议:一开始就把参数当输入,别把数字硬编码在方程里。

2. 动力学模型选型:用哪一层模型取决于你的问题

2.1 从SIR模型说起

先从一个最基础的模型入手。SIR模型把人群分成三类:易感者S、感染者I、恢复者R。感染者通过与易感者接触,以一定速率把病毒传出去,同时感染者自身也会恢复。写成微分方程就是下面这样:

dS/dt = -β * S * I / N dI/dt = β * S * I / N - γ * I dR/dt = γ * I

这里的β是有效接触率,可以理解为“一个感染者每天能导致多少新感染”;γ是恢复率,等于“1 / 平均感染期”。分母上的N是对接触概率做归一化,保证接触次数不随人口规模无限变大。

SIR告诉你一个基本结论:疫情能不能暴发,关键看有效再生数R0 = β / γ。如果R0 > 1,感染者人数会先升后降形成一个峰;如果R0 < 1,疫情基本起不来。这个模型非常经典,但它有个明显缺陷:它假设人感染后立刻具备传染性,而现实中流感存在潜伏期。于是我们需要把潜伏者单独分出来。

2.2 加入潜伏期:SEIR模型

SEIR模型在SIR基础上增加了一个暴露者仓室E,指那些已经感染了病毒、但暂时没有传染性或传染性很低的人。方程组变成:

dS/dt = -β * S * I / N dE/dt = β * S * I / N - σ * E dI/dt = σ * E - γ * I dR/dt = γ * I

新增的σ是潜伏期转阳速率,等于“1 / 平均潜伏期”。比如潜伏期平均3天,那σ ≈ 1/3。这里要注意一个细节:E仓室的人一般不计入确诊或报告病例,但在传播链条里他们至关重要。如果你用SIR模型去拟合真实曲线,常常会发现模型预测的峰值比实际来得早、来得猛,原因就是忽略了潜伏期带来的延迟。

我在系统里选了SEIR作为默认模型,因为它足够简单,又能解释流感传播中“接触感染者→过几天才发病”的中间过程。如果你还想把无症状感染者、住院隔离等状态加进去,可以把仓室继续细分,但每多一个仓室,参数就多一份不确定性。我的经验是:先用SEIR把基线跑稳,再慢慢往里面加细节,不要一上来就搞十个仓室的“豪华模型”。

2.3 参数怎么定:从经验值到可调范围

参数确定是仿真中最容易“自欺欺人”的环节。很多同学从某篇论文里抄一个β=0.5,跑出来曲线挺好看,但完全不知道这个值符不符合自己设定的人群。标准做法是先定γ和σ,这两个值有明确的流行病学含义,通常可以从文献或公共卫生报告里找到参考范围。

以常见的流感型传播为例,恢复期按5到7天算,γ大约在1/5到1/7,也就是每天0.14到0.2;潜伏期按2到4天算,σ大约在0.25到0.5。β则根据你要模拟的基本再生数反推。如果你想模拟一个R0≈2的疫情,设定γ=1/5.2≈0.192,那么β = R0 * γ ≈ 0.385。这里不需要精确,但要有一个闭环的验证逻辑:先给R0,反推β,跑完仿真后看峰值时间是否和直觉一致。

另外别忘了,β不是一个物理常数,它会随口罩佩戴率、室内通风条件、人群密度变化。所以我在系统里把β做成默认可调参数,并且支持在模拟中途改变它的值,用来表达干预措施生效。

2.4 要不要把模型变得更复杂

流感传播的真实因素很多:不同年龄人群接触模式不同、城市和农村人口密度不同、季节温度也会影响病毒存活。要不要把这些都塞进模型,取决于你的“解释目标”。如果是教学演示,SEIR已经完全够用;如果要做区域公共卫生决策支持,可能还要引入年龄分层、空间迁徙、随机噪声等。

我在系统里预留了一个扩展位:把β从固定值改成函数,比如beta(t)在模拟时间推进时可以按预定时间表变化。这样既不用重写方程,又能模拟多阶段干预。遇到刚性方程组时,把求解器切成ode15s,代码也不需要大改。

3. 系统实操:MATLAB代码一步步写出来

3.1 参数配置集中管理

项目一开始,我把参数全部定义在脚本头部,像下面这样:

% params_seir.m params.N = 100000; % 总人口数 params.beta = 0.385; % 有效接触率 params.sigma = 1/3; % 潜伏期倒数 params.gamma = 1/5.2; % 恢复率 params.E0 = 0; % 初始潜伏者 params.I0 = 10; % 初始确诊感染者 params.tSpan = [0 180]; % 模拟180天

这些数字不是拍脑袋。N=100000方便后续算比例;I0=10模拟的是境外输入或者局部暴发的最初几个病例;tSpan设到180天,足够看完整波峰和回落。用结构体存储的好处是,函数调用时直接传params,不会出现参数顺序搞错的问题。

3.2 SEIR方程与ODE45求解

模型方程我单独写成一个函数文件seir_rhs.m。它接收当前时间t、状态向量y和参数结构体p,返回导数向量。

function dydt = seir_rhs(t, y, p) % 状态变量拆分:S E I R S = y(1); E = y(2); I = y(3); R = y(4); % SEIR微分方程 dS = -p.beta * S * I / p.N; dE = p.beta * S * I / p.N - p.sigma * E; dI = p.sigma * E - p.gamma * I; dR = p.gamma * I; dydt = [dS; dE; dI; dR]; end

主脚本里这样调用:

% 初始状态:N人里减去初始E和I,剩余为S,R初始为0 y0 = [params.N - params.E0 - params.I0; params.E0; params.I0; 0]; % 使用变步长求解器 [t, y] = ode45(@(t, y) seir_rhs(t, y, params), params.tSpan, y0);

为什么用ode45而不是自己写四步龙格库塔?因为ode45会自动调整步长,感染人数快速攀升时步长变小,曲线趋于平缓时步长变大。自己写定步长要么算得慢,要么可能错漏陡峭段。

3.3 干预措施怎么模拟

模拟疫苗和管控,核心思路就是让β随时间变化。第30天开始执行保持社交距离,可以等效为把β乘以一个0.4的系数。修改方程函数如下:

function dydt = seir_rhs(t, y, p) S = y(1); E = y(2); I = y(3); R = y(4); % 根据时间调整感染率,模拟干预 beta = p.beta; if t >= p.interventionDay beta = beta * p.interventionFactor; end dS = -beta * S * I / p.N; dE = beta * S * I / p.N - p.sigma * E; dI = p.sigma * E - p.gamma * I; dR = p.gamma * I; dydt = [dS; dE; dI; dR]; end

这样改的好处是不用把方程分两段求解。虽然数学上在干预日那天方程发生了突变,但只要ode45的容差设置合适,结果仍然稳定。如果想模拟更平滑的干预生效过程,可以把beta写成关于t的连续函数,比如用tanh过渡。

疫苗接种在SEIR框架里可以简化成“以每天一定比例把S直接转为R”,因为接种后的人不会再感染,也不能传播。但要注意,接种需要形成一定覆盖面才开始显著影响传播,所以接种率设置比数字大小更重要。

3.4 可视化与结果输出

仿真跑出来的矩阵只是数字,真正有价值的是一目了然的图。我用最朴素的方式画曲线:

figure; plot(t, y(:,2), 'LineWidth', 1.8); hold on; plot(t, y(:,3), 'LineWidth', 1.8); plot(t, y(:,4), 'LineWidth', 1.8); legend('潜伏者E', '有症状感染者I', '恢复者R'); xlabel('时间(天)'); ylabel('人数'); grid on;

如果要做多情景对比,就在循环里改动params.beta,把每种情景的y(:,3)保存到矩阵中,最后统一画出来。对比图上一定要标注峰值坐标,用[maxVal, idx] = max(y(:,3))配合text标注,否则多条曲线堆在一起,讲了半天也分不清哪根是哪根。

4. 关键参数与动力学行为分析

4.1 R0对爆发曲线的影响

我做的第一个对比实验就是固定γ和σ,只改β。因为γ=1/5.2≈0.192,所以当β分别取0.2、0.35、0.5时,对应的R0约为1.04、1.82、2.6。结果很有意思:

β取值估算R0感染者峰值时间(近似)峰值规模(近似)
0.21.04不形成明显峰极低
0.351.82第80天前后约2.5万人
0.52.60第45天前后约4.3万人

这组结果是典型的动力学行为:R0越大,爆发越快,峰值越高,出现得越早。如果你的系统画出来的曲线不符合这个趋势,多半是初始感染者设得太多,或者β和γ的组合本身就不合理。我每次修改参数后都会先看R0,再看峰值时间,这是一条快速自查路径。

4.2 潜伏期与隔离启动时间的影响

把潜伏期从3天改到5天,σ从1/3变成1/5,会发现E的峰值更平坦,I的峰值稍微右移,但总感染人数变化不剧烈。这说明潜伏期主要影响“延迟”,而不是“总量”。但隔离启动时间就完全不一样了。

我做过一组模拟:别的参数不变,每次把干预日从第20天开始,每次往后推10天,一直推到第70天。结果第20天开始干预时,总感染人数控制在很少范围;第40天再干预,曲线已经冲起来了;第60天之后干预,峰值几乎和不干预一样。这背后逻辑很简单:干预太晚时,病毒已经在大量易感人群中扩散开来,临时降低β也只能延缓峰值,无法改变总体规模。这个结论对系统演示特别有价值。

4.3 群体免疫门槛的估算

只要模型是均质混合的SEIR,就可以估算群体免疫门槛H = 1 - 1/R0。如果R0=2.5,粗略计算需要约60%的人群通过感染或接种获得免疫,传播才会进入下降通道。当然现实世界有年龄结构、空间聚集,这个数字只做方向性参考。

我在系统里加了一个输出项:感染比例最终收敛值。把R矩阵最后一列除以N,就能看到“自然感染”最终覆盖了多少人。配合不同R0参数形成对照表格,这个玩法在答辩和汇报中也很容易出彩。

4.4 灵敏度分析怎么做

灵敏度分析帮助你回答一个问题:参数不确定时,结果到底有多不稳。我常用的方法是在基准值附近做±20%的单因素扰动,每次只改一个参数,记录目标指标(比如峰值人数、累计感染数),最后画一张散点图。

MATLAB里可以这样实现:

params.beta0 = 0.385; factors = 0.8:0.05:1.2; for k = 1:length(factors) p = params; p.beta = params.beta0 * factors(k); [t, y] = ode45(@(t,y) seir_rhs(t,y,p), p.tSpan, y0); peakI(k) = max(y(:,3)); end plot(100*factors, peakI, '-o');

如果有多个参数要做全局灵敏度,可以用parfor并行循环,一次性跑几百组参数也不会耗费太多时间。关键是记录好实验数据的来源,避免分析到一半忘了哪条曲线对应什么参数组合。

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

5.1 曲线出现振荡或“锯齿”

ode45通常很稳,但如果方程里干预措施写成了硬切换(比如if t > day直接跳变),求解器可能会在切换点附近做小步长震荡。解决方法是把切换条件写得平滑一点,或者在odeset里设置MaxStep上限。另一个人为原因是用一个有噪声的β序列作为输入,这时不应该让beta随每个时间步剧烈波动,而应通过插值或滤波。

5.2 仿真出现负人数

SEIR模型里人数代表群体数量,理论上不会出现负值。如果出现负值,一般是数值误差积累导致。最简单的方法是让求解器结果非负:

opts = odeset('NonNegative', ones(1,4)); [t, y] = ode45(@(t,y) seir_rhs(t,y,params), params.tSpan, y0, opts);

还有一个常见原因:初始状态S、E、I、R之和没有严格等于N,导致微分方程里的归一化分母和实际总人口不一致。每次运行前打印sum(y0)检查一下,能避免很多奇怪问题。

5.3 结果与直觉不符:先检查R0

如果你设置了一个很高的β,预期会暴发,但曲线却平平稳稳,先别怀疑代码,先算一下R0 = beta/gamma。很多初学者把γ设成1,同时把β设成0.3,最后R0<1,当然跑不出暴发。看到曲线不符合预期时,我的排查顺序永远相同:先看R0,再看初始感染者数量,最后看模拟时长。

5.4 多情景对比时的坑

有时候你需要对比“不干预”“第30天干预”“第50天干预”三条曲线。最容易犯的错误是在同一个函数里反复改全局变量,结果后跑的情景覆盖了前面的结果。我的处理方式是把params深拷贝到p,每个情景独立使用一个副本。循环里给不同颜色和线型,图例命名里带上关键参数值,这样看对比图时不用回去翻代码。

5.5 常见问题速查表

问题现象可能原因解决办法
曲线不上升R0小于或等于1增大β或减小γ
峰值出现时间很早初始感染者太多降低I0
出现负人数数值误差使用NonNegative选项
干预后曲线仍然陡峭干预起效太慢或太晚提前干预日,加大β削减幅度
多次运行结果不同代码里用了随机数但没固定种子用rng(0)固定随机种子
求解器运行很慢方程存在刚性改用ode15s

这些坑我基本都踩过。最让我印象深刻的一次是,做对比实验时忘记给每个情景复制参数结构体,结果三条曲线长得一模一样,排查了快半小时才发现变量被覆盖了。从那以后,我习惯在每个脚本开头用clear all,每个循环体内部用p = params,这种习惯能省很多时间。

6. 扩展方向与个人经验总结

6.1 从课程设计到教学演示

这套系统的第一用途是教学。如果你在课堂上给学生演示“为什么流感难以清零”,可以用SEIR模型快速展示:只要仍有易感人群输入,第二次接触率上升就会引发新的小波峰。这种动态过程靠嘴讲很难讲清楚,但仿真曲线一放出来,所有逻辑都变得具体了。

我在某个模拟项目里经常用这个系统做情景推演。先设置一个基线情景,也就是没有干预的“自然传播曲线”;再设置一个干预情景,比如从第X天开始把接触率下降50%。然后把两条曲线并排对比,输出“累计感染人数减少了百分之多少”这一个关键数字。这种表达比任何专业术语都直观。

6.2 还能加哪些功能

如果时间充裕,我建议往三个方向扩展。第一是随机性:把确定性的SEIR改成随机微分方程,模拟“偶然事件”对传播过程的影响,这需要引入Stochastic Differential Equations工具箱或自己写粒子滤波。第二是网络结构:把均质人群改成人群间的接触矩阵,不同年龄组之间的传播速率不同,这样能更精细地模拟校园、办公场所等场景。第三是参数反演:用真实的时间序列数据去拟合β和γ,让系统从“推演工具”升级为“数据分析工具”。

第三个方向尤其有意思,它把仿真和优化拉到了一起。你可以用lsqcurvefit去匹配目标曲线,虽然收敛不一定顺利,但整个分析框架会提升一个段位。想做扩展的话,建议先把现有的SEIR封装成函数接口,输入输出保持稳定,再往外部叠加算法。

6.3 动手之前先问“为什么”

最后分享一点个人体会。我做了几个仿真项目后最大的感受是,真正的难点从来不是写微分方程,而是搞清楚“我要从模型里得到什么决策信息”。如果只是要一个漂亮的S形曲线,任何工具都能做到;如果你想知道“隔离措施晚一周启动会导致多少人受到感染”,你就必须把时间轴、干预参数、输出指标全部设计清楚。

另外,参数命名和注释一定要规范。三个月后回看你写的代码,如果beta、gamma满天飞,自己也要先查半天。我习惯在每个参数后面标注单位或含义,比如beta注明“每天有效接触次数”,gamma注明“恢复率=1/感染期”。这种小细节不会让结果更准确,但能让整个项目的可维护性上一个台阶。

如果你也想动手搭一套类似系统,建议从最简单的SEIR开始,先跑通、再调参、后扩展。等你能在十分钟内改出一组新情景,并且自信地解释曲线变化的背后原因,这套仿真系统就算真正掌握在你手里了。

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

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

立即咨询