1. 从数据到决策:传染病建模到底在算什么
1.1 一个建模项目该从哪里下手
传染病建模这件事,听起来像是流行病学家的专属领域,但实际上它离我们并不远。任何一个做数据分析、做系统仿真、甚至做游戏数值策划的人,都能在这套方法论里找到熟悉的味道。我最初接触这类项目的时候,第一反应是:这不就是一个带状态转移的动力学系统吗?把人群分成几个仓室,设定转移规则,跑时间步,看曲线怎么走。后来发现,事情比想象中复杂得多,但也比想象中有趣得多。
这个项目的核心目标很明确:用数学模型去回答关于新型冠状肺炎的几个关键问题。比如,病毒的基本传播能力有多强?如果不管控,感染曲线会飙到什么程度?如果采取不同强度的干预措施,效果差多少?医疗资源在什么时间点会被击穿?这些问题,靠直觉回答不了,靠拍脑袋更不行,必须用模型去推演。
适合谁来参考这篇内容?我认为有三类人。第一类是有一定编程基础、想入门传染病建模的数据从业者,你不需要有医学背景,但需要理解微分方程或者离散仿真的基本逻辑。第二类是公共卫生或政策研究方向的从业者,你可能不写代码,但需要理解模型输出的含义和局限,才能做出合理判断。第三类是对复杂系统仿真感兴趣的工程师,传染病模型本质上是一个典型的复杂系统问题,涉及非线性动力学、参数敏感性、不确定性量化,这些技能在别的领域同样通用。
1.2 为什么选仓室模型而不是纯统计拟合
建模路线有很多条,最主流的两条路是:一条是仓室模型(比如SIR、SEIR及其变体),另一条是纯统计拟合(比如用时间序列模型去拟合确诊数据)。这两条路各有优劣,我最终选了仓室模型为主、统计方法为辅的混合路线,原因如下。
纯统计拟合的优势是简单直接,你有确诊数据,直接上ARIMA或者LSTM去拟合预测就行了。但它有个致命问题:它只能告诉你“数据长什么样”,不能告诉你“为什么长这样”。一旦干预措施发生变化,历史数据的模式就被打破了,纯统计模型的预测会严重失准。而仓室模型的优势恰恰在于,它把传播机制显式地写进了方程里,你可以通过调整参数来模拟不同干预场景,这是纯统计方法做不到的。
当然,仓室模型也有它的代价。它对数据的要求更高,你需要估计很多参数,而这些参数往往没有直接观测值。比如传播率、潜伏期、无症状比例,这些量在早期数据里很难准确估计。所以我的做法是:用仓室模型搭建仿真框架,用统计方法去估计和校准参数,两者结合。
注意:不要一上来就追求最复杂的模型。我见过不少人直接上SEIR+年龄分层+空间扩散+时变参数,结果参数多到根本标定不了,最后跑出来的结果还不如最简单的SIR靠谱。建模的第一原则是:从简单开始,逐步增加复杂度,每增加一个参数都要问自己——这个参数我能估出来吗?
1.3 核心思路的拆解
整个项目的思路可以拆成四步:
- 定义状态变量:把人群划分为若干仓室,比如易感者、暴露者、感染者、康复者、死亡者。每个仓室的人数随时间变化。
- 建立转移方程:定义仓室之间的转移规则和速率。比如易感者以某个速率变成暴露者,暴露者经过潜伏期变成感染者,感染者以一定概率康复或死亡。
- 参数估计与校准:用实际数据去反推模型参数,使得模型输出与观测数据尽可能吻合。
- 情景模拟与敏感性分析:调整关键参数,模拟不同干预策略下的结果,分析哪些参数对结果影响最大。
这四步看起来简单,但每一步都有大量的细节和坑。接下来我会逐一展开,把每个环节的关键技术点、实操方法和踩坑经验都讲清楚。
2. 模型搭建的核心细节与参数体系
2.1 仓室划分:SEIR还是更复杂的变体
最基础的传染病模型是SIR,只有三个仓室:易感者、感染者、康复者。这个模型适合那些感染后立刻具有传染性、且康复后获得免疫力的疾病。但新型冠状肺炎有一个显著特点:存在潜伏期,且潜伏期内可能具有传染性。这就需要用SEIR模型,多一个暴露者仓室。
SEIR的基本结构是这样的:
- S(Susceptible):易感人群,还没被感染,也没有免疫力。
- E(Exposed):暴露者,已经接触了病毒但还没出现症状,处于潜伏期。
- I(Infectious):感染者,有症状且具有传染性。
- R(Recovered):康复者,获得免疫力(或死亡,取决于是否单独建模)。
但实际项目中,我很快发现基础SEIR不够用。原因有几个:第一,无症状感染者也有传染性,需要单独建模;第二,确诊和实际感染之间存在时间差,检测能力会影响数据;第三,死亡和康复需要分开追踪。所以我最终用的是一个扩展版SEIR,增加了无症状感染者仓室和死亡仓室。
具体来说,我的仓室划分是:
| 仓室 | 含义 | 关键参数 |
|---|---|---|
| S | 易感者 | 初始值、总人口 |
| E | 暴露者(潜伏期) | 潜伏期时长 |
| I_s | 有症状感染者 | 传染率、症状期时长 |
| I_a | 无症状感染者 | 传染率(通常低于有症状者) |
| R | 康复者 | 康复率 |
| D | 死亡者 | 病死率 |
这个划分比基础SEIR复杂,但比那些动辄十几个仓室的模型简单得多。我觉得这是一个比较好的平衡点:既能捕捉关键特征,又不至于参数多到标定不了。
实操心得:仓室不是越多越好。每增加一个仓室,你就多了一组需要估计的参数。如果你的数据只有每日确诊数,那你最多能可靠估计两三个参数。仓室多了,参数就不可辨识了,模型跑出来的结果看着漂亮,其实全是过拟合。
2.2 转移方程:连续还是离散
仓室之间的转移可以用连续时间的微分方程来描述,也可以用离散时间的差分方程来描述。两者在数学上是等价的(当时间步足够小的时候),但在实现上有很大区别。
连续时间微分方程的形式是这样的:
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是潜伏期倒数,gamma是康复率倒数。
离散时间差分方程则是:
S(t+1) = S(t) - beta * S(t) * I(t) / N E(t+1) = E(t) + beta * S(t) * I(t) / N - sigma * E(t) I(t+1) = I(t) + sigma * E(t) - gamma * I(t) R(t+1) = R(t) + gamma * I(t)我最终选了离散时间实现,原因很实际:数据是按天报告的。连续时间模型虽然数学上更优雅,但你需要用数值积分去求解,而且和按天报告的数据对齐时反而更麻烦。离散模型直接以天为步长,和数据结构天然匹配,实现起来也简单得多。
当然,离散模型有个前提:时间步要足够小,使得每个步长内状态变化不会太大。对于日粒度数据来说,这个条件基本满足。但如果你的数据是周粒度或者月粒度,那离散模型的误差就会比较大,这时候还是用连续模型更稳妥。
2.3 关键参数的含义与估算方法
模型里最关键的几个参数,我逐个说一下含义和估算方法。
传播率beta:这是最重要的参数,表示一个感染者在单位时间内传染给易感者的概率乘以接触人数。beta越大,传播越快。估算方法通常有两种:一种是从早期指数增长阶段的增长率反推,另一种是用优化算法去拟合确诊数据。我用的是后者,因为早期数据噪声太大,直接反推误差很高。
潜伏期倒数sigma:sigma等于1除以平均潜伏期。比如平均潜伏期是5天,那sigma就是0.2。这个参数通常可以从文献里查到,不需要自己估计。但要注意,潜伏期的定义在不同研究里可能不一样,有的指从暴露到出现症状,有的指从暴露到具有传染性,用的时候要统一。
康复率倒数gamma:gamma等于1除以平均感染期。这个参数也是从文献里查比较靠谱。但有个坑:感染期的定义也有歧义,有的指从出现症状到康复,有的指从具有传染性到不再具有传染性。我建议在项目文档里明确写清楚每个参数的定义,不然后面自己都会搞混。
基本再生数R0:R0 = beta / gamma,表示在完全没有免疫力的群体中,一个感染者平均能传染多少人。R0是模型输出的一个综合指标,不是直接输入的参数。很多人会把R0和beta搞混,其实R0是beta和gamma的组合。
病死率CFR:死亡人数除以确诊人数。这个参数在模型中通常作为一个比例,决定感染者中有多少会死亡。但要注意,CFR的计算受检测能力影响很大,检测不足的时候CFR会被高估。
注意:参数估计不是一锤子买卖。随着数据积累,你需要不断更新参数估计。我建议把参数估计做成一个可重复的流程,每次有新数据就重新跑一遍,而不是手动改参数。
2.4 初始条件:被低估的关键因素
很多人把注意力全放在参数上,却忽略了初始条件的重要性。初始条件包括:初始感染人数、初始暴露人数、总人口、初始免疫比例等。这些值如果设得不对,模型输出会差很多。
初始感染人数尤其关键。在疫情早期,实际感染人数往往远高于报告确诊人数,因为检测能力有限。如果你直接用报告确诊数作为初始感染人数,模型会严重低估传播速度。我的做法是:用报告确诊数乘以一个检测覆盖率倒数来估算实际感染人数。比如你估计检测覆盖率是20%,那实际感染人数大约是报告数的5倍。
总人口这个参数看起来简单,但也有讲究。如果你建模的是一个城市,那总人口就是城市常住人口。但如果你建模的是一个国家,那就要考虑人口流动的影响,可能需要引入空间分层。我一开始做的是单城市模型,总人口设为常数,后来发现人口流动对结果影响很大,才加了流入流出项。
初始免疫比例在疫情早期通常设为0,因为新病毒大家都没有免疫力。但如果你的模型是从疫情中期开始跑的,那就要考虑已经感染过的人群,初始免疫比例不能忽略。
3. 实操过程:从数据到仿真到结果解读
3.1 数据准备与清洗
数据是建模的燃料。没有好的数据,再精巧的模型也是空中楼阁。我用的数据主要是每日新增确诊数、每日新增死亡数、每日康复数。这些数据通常来自公开的疫情通报,但直接拿来用会有很多问题。
第一个问题是报告延迟。确诊数据往往有延迟,今天确诊的人可能是几天前就感染的。这个延迟会使得模型拟合出现偏差。我的处理方法是:在模型里加一个观测延迟项,把模型预测的感染人数经过一个延迟函数映射到报告确诊数,再去拟合。
第二个问题是周末效应。很多地方的检测和报告在周末会减少,导致数据出现周期性波动。如果不处理,模型会把这个波动当成真实的传播变化。我的做法是对数据做7天滑动平均,平滑掉周末效应。但滑动平均会引入滞后,所以只用来做参数估计,不用来做实时预测。
第三个问题是数据修正。疫情数据经常会被回溯修正,今天看到的数字和一周前看到的可能不一样。我建议在项目里保存数据快照,每次拉取数据都记录时间戳,这样后面回溯的时候知道用的是哪个版本的数据。
数据清洗的代码大概长这样:
import pandas as pd import numpy as np # 读取原始数据 raw = pd.read_csv('daily_cases.csv', parse_dates=['date']) # 按日期排序 raw = raw.sort_values('date').reset_index(drop=True) # 计算7天滑动平均 raw['new_cases_smooth'] = raw['new_cases'].rolling(window=7, center=True).mean() # 处理缺失值:用前向填充 raw['new_cases_smooth'] = raw['new_cases_smooth'].fillna(method='ffill') # 计算累计确诊 raw['cum_cases'] = raw['new_cases'].cumsum() # 剔除异常值:单日新增超过均值5倍标准差的点 mean = raw['new_cases'].mean() std = raw['new_cases'].std() raw = raw[np.abs(raw['new_cases'] - mean) < 5 * std] print(f"清洗后数据量: {len(raw)}") print(raw.head(10))这段代码看起来简单,但每一步都有讲究。滑动平均的窗口选7天是因为一周有7天,能完整覆盖一个周期。异常值剔除用5倍标准差是因为疫情数据本身波动就大,阈值设太小会误删真实信号。
3.2 模型实现:用Python搭建SEIR仿真
模型实现我选的是Python,主要因为生态好,numpy做数值计算、scipy做优化、matplotlib做可视化,一条龙。下面是我用的核心仿真代码。
import numpy as np from scipy.integrate import odeint def seir_model(y, t, N, beta, sigma, gamma): """ 基础SEIR模型的微分方程 y: [S, E, I, R] t: 时间 N: 总人口 beta: 传播率 sigma: 潜伏期倒数 gamma: 康复率倒数 """ S, E, I, R = y dSdt = -beta * S * I / N dEdt = beta * S * I / N - sigma * E dIdt = sigma * E - gamma * I dRdt = gamma * I return [dSdt, dEdt, dIdt, dRdt] # 参数设置 N = 1000000 # 总人口 beta = 0.5 # 传播率 sigma = 0.2 # 潜伏期倒数(平均潜伏期5天) gamma = 0.1 # 康复率倒数(平均感染期10天) # 初始条件 I0 = 10 # 初始感染者 E0 = 20 # 初始暴露者 R0 = 0 # 初始康复者 S0 = N - I0 - E0 - R0 # 时间范围 t = np.linspace(0, 200, 200) # 求解 y0 = [S0, E0, I0, R0] solution = odeint(seir_model, y0, t, args=(N, beta, sigma, gamma)) # 提取结果 S = solution[:, 0] E = solution[:, 1] I = solution[:, 2] R = solution[:, 3] # 计算R0 R0 = beta / gamma print(f"基本再生数 R0 = {R0:.2f}") print(f"峰值感染人数 = {I.max():.0f}") print(f"峰值出现时间 = 第{t[I.argmax()]:.0f}天")这段代码跑出来的结果,R0等于5,峰值感染人数大约占总人口的20%左右,峰值出现在第60天前后。这个结果和很多早期研究的估计是吻合的。
但实际项目中,我用的是扩展版SEIR,增加了无症状感染者和死亡仓室。代码会更长,但结构是一样的。关键是要把每个仓室的转移关系理清楚,不要搞混。
实操心得:写仿真代码的时候,一定要加单元测试。比如你可以手动算一个简单场景的结果,然后和代码输出对比。我一开始没做测试,结果有个仓室的转移方向写反了,跑出来的曲线看着挺合理,实际上完全错了。后来加了测试才发现。
3.3 参数校准:用优化算法拟合实际数据
参数校准是整个项目里最耗时的环节。我的做法是:定义一个损失函数,衡量模型输出和实际数据的差距,然后用优化算法去最小化这个损失函数。
损失函数我选的是负对数似然,而不是简单的均方误差。原因是疫情数据通常服从泊松分布或负二项分布,用似然函数更合理。具体来说,如果每日新增确诊数服从泊松分布,那对数似然就是:
log L = sum(y_obs * log(y_pred) - y_pred - log(y_obs!))其中y_obs是实际观测值,y_pred是模型预测值。
优化算法我用的是L-BFGS-B,因为它支持边界约束,可以限制参数在合理范围内。比如beta必须在0到1之间,sigma必须在0到1之间。
from scipy.optimize import minimize from scipy.stats import poisson def loss_function(params, data, N): """ 负对数似然损失函数 params: [beta, sigma, gamma] data: 实际观测的每日新增确诊数 N: 总人口 """ beta, sigma, gamma = params # 参数边界检查 if beta <= 0 or sigma <= 0 or gamma <= 0: return 1e10 # 运行模型 I0 = data[0] if data[0] > 0 else 1 E0 = I0 * 2 S0 = N - I0 - E0 y0 = [S0, E0, I0, 0] t = np.arange(len(data)) try: solution = odeint(seir_model, y0, t, args=(N, beta, sigma, gamma)) except: return 1e10 I_pred = solution[:, 2] # 计算每日新增(用感染者的变化率近似) new_infections = np.diff(I_pred, prepend=I_pred[0]) new_infections = np.maximum(new_infections, 1e-6) # 避免log(0) # 负对数似然 nll = -np.sum(poisson.logpmf(data, new_infections)) return nll # 初始参数猜测 initial_params = [0.3, 0.2, 0.1] # 参数边界 bounds = [(0.01, 2.0), (0.05, 1.0), (0.01, 1.0)] # 优化 result = minimize( loss_function, initial_params, args=(observed_data, N), method='L-BFGS-B', bounds=bounds ) print(f"最优参数: beta={result.x[0]:.4f}, sigma={result.x[1]:.4f}, gamma={result.x[2]:.4f}") print(f"R0 = {result.x[0]/result.x[2]:.2f}")这段代码有几个关键点。第一,初始感染人数不能设为0,否则模型永远跑不起来,我用的是数据第一天的值。第二,预测的每日新增用感染者变化率来近似,这在离散数据上是合理的。第三,参数边界要设得合理,太宽会导致优化跑飞,太窄会错过最优解。
参数校准的结果通常不是唯一的。不同的参数组合可能给出相似的拟合效果,这就是所谓的参数不可辨识性。解决方法是:固定一些从文献中能查到的参数(比如潜伏期),只估计最关键的几个参数(比如beta和gamma)。或者用贝叶斯方法,给出参数的后验分布,而不是单点估计。
3.4 情景模拟:不同干预策略的效果对比
模型校准好之后,就可以做情景模拟了。这是整个项目最有价值的部分,因为你可以回答“如果……会怎样”的问题。
我设计了四种情景:
| 情景 | 干预措施 | 参数调整 |
|---|---|---|
| 无干预 | 什么都不做 | beta保持不变 |
| 轻度干预 | 减少50%接触 | beta乘以0.5 |
| 中度干预 | 减少70%接触 | beta乘以0.3 |
| 强力干预 | 减少90%接触 | beta乘以0.1 |
模拟结果非常直观:无干预情况下,峰值感染人数占总人口的20%以上,医疗系统必然被击穿。轻度干预能把峰值压到10%左右,但仍然很危险。中度干预能把峰值压到3%以下,基本可控。强力干预能把峰值压到1%以下,但需要维持很长时间。
这里有个关键概念叫干预时机。同样的干预强度,早做和晚做效果差很多。我做了对比模拟,发现如果在感染人数还很少的时候就采取强力干预,可以把疫情控制在很小的规模。但如果等到感染人数已经很多了再干预,效果会大打折扣,因为传播已经形成了惯性。
注意:情景模拟的结果高度依赖模型假设。如果你的模型没有考虑超级传播事件、没有考虑人口异质性、没有考虑行为反馈,那模拟结果只能作为参考,不能作为决策的唯一依据。我建议在报告结果的时候,一定要附上敏感性分析,说明哪些假设对结果影响最大。
3.5 敏感性分析:哪些参数最关键
敏感性分析是检验模型稳健性的重要手段。我用的方法是拉丁超立方采样,在参数空间里均匀采样,然后看每个参数对输出的影响。
具体做法是:对每个参数,在其合理范围内采样100个值,其他参数固定在中位数,然后跑模型,记录峰值感染人数和峰值时间。最后计算每个参数的偏相关系数,相关系数越大,说明该参数对结果影响越大。
我的分析结果是:传播率beta对峰值感染人数的影响最大,相关系数超过0.9。潜伏期sigma的影响次之,康复率gamma的影响相对较小。这个结果符合直觉:传播越快,峰值越高。
但有个反直觉的发现:潜伏期越长,峰值反而可能越高。原因是潜伏期长意味着感染者被发现和隔离的时间更晚,传播窗口更长。这个发现对防控策略有重要启示:快速检测和隔离比单纯减少接触更重要。
敏感性分析的代码大概长这样:
from scipy.stats import spearmanr import itertools # 参数范围 beta_range = np.linspace(0.1, 1.0, 20) sigma_range = np.linspace(0.1, 0.5, 20) gamma_range = np.linspace(0.05, 0.3, 20) # 存储结果 results = [] for beta, sigma, gamma in itertools.product(beta_range, sigma_range, gamma_range): # 运行模型 y0 = [N - 100, 50, 50, 0] t = np.linspace(0, 300, 300) solution = odeint(seir_model, y0, t, args=(N, beta, sigma, gamma)) I = solution[:, 2] peak_infection = I.max() peak_time = t[I.argmax()] results.append({ 'beta': beta, 'sigma': sigma, 'gamma': gamma, 'peak_infection': peak_infection, 'peak_time': peak_time }) df = pd.DataFrame(results) # 计算Spearman相关系数 for param in ['beta', 'sigma', 'gamma']: corr, p_value = spearmanr(df[param], df['peak_infection']) print(f"{param} vs 峰值感染人数: 相关系数={corr:.3f}, p值={p_value:.4f}")这段代码跑完,你会得到一张相关系数表,清楚地看到每个参数的影响力。这个表在写报告的时候非常有用,可以让读者快速理解模型的敏感点在哪里。
4. 常见问题与排查技巧实录
4.1 模型跑出来的曲线不对劲怎么办
这是最常见的问题。模型跑出来的曲线要么是直线,要么是爆炸式增长,要么是负数,总之就是不像真实的疫情曲线。我总结了几种典型情况和排查方法。
情况一:感染人数一直不增长。这通常是因为初始感染人数设得太小,或者beta设得太小。排查方法:检查初始感染人数是否大于0,检查beta是否大于gamma(如果beta小于gamma,R0小于1,疫情会自动消退)。
情况二:感染人数瞬间爆炸,一天之内全感染。这通常是因为beta设得太大,或者时间步长太大。排查方法:减小beta,或者减小时间步长。离散模型的时间步长建议不要超过1天。
情况三:感染人数出现负数。这是数值计算的误差,通常发生在感染人数接近0的时候。解决方法:在每步更新后,把负数截断为0。
情况四:曲线形状不对,没有峰值或者峰值太早太晚。这通常是参数组合的问题。排查方法:画相图,看S、E、I、R四个变量的关系是否合理。或者手动算几个时间点的值,和代码输出对比。
实操心得:调试模型的时候,先用最简单的SIR模型跑通,确认基本逻辑没问题,再逐步增加复杂度。不要一上来就上最复杂的模型,出了问题你都不知道是哪个环节的错。
4.2 参数估计不收敛的排查思路
参数估计不收敛是另一个高频问题。优化算法跑了几千次迭代,损失函数还是很大,或者参数跑到了边界上。我遇到过以下几种情况。
情况一:初始参数猜得太离谱。优化算法对初始值敏感,如果初始值离最优解太远,可能会陷入局部最优。解决方法:用网格搜索先粗筛一遍,找到损失函数较小的区域,再用优化算法精细搜索。
情况二:参数不可辨识。不同的参数组合给出相似的拟合效果,优化算法在参数空间里来回震荡。解决方法:固定一些参数,只估计最关键的几个。或者用正则化,给参数加先验约束。
情况三:数据质量太差。如果数据噪声太大,或者有系统性偏差,优化算法会去拟合噪声,而不是真实信号。解决方法:对数据做平滑处理,或者用稳健的损失函数(比如Huber损失)。
情况四:模型结构不对。如果模型本身就不能描述数据的生成过程,那再怎么优化参数也没用。解决方法:检查模型假设是否合理,比如是否应该用SEIR而不是SIR,是否需要加无症状仓室。
4.3 预测结果和实际不符的原因分析
模型预测和实际不符,这是常态,不是异常。关键是要理解为什么不符,而不是简单地否定模型。我总结了几种常见原因。
原因一:干预措施发生了变化。模型假设beta是常数,但实际上干预措施会改变beta。如果实际干预加强了,但模型不知道,预测就会偏高。解决方法:用时变参数模型,让beta随时间变化。
原因二:病毒变异。病毒变异会改变传播率和致病性,模型如果用的是旧参数,预测就会失准。解决方法:定期更新参数,或者用多个变异株分别建模。
原因三:数据报告延迟。实际感染人数总是先于报告确诊人数,模型如果拟合的是报告数据,预测就会滞后。解决方法:在模型里加观测延迟项。
原因四:人群行为反馈。当疫情严重时,人们会自发减少接触,这相当于beta自动下降。模型如果没有考虑这个反馈,预测就会偏高。解决方法:引入行为反馈项,让beta随感染人数变化。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 感染人数不增长 | 初始值太小或beta太小 | 检查初始值和R0 | 增大初始值或beta |
| 感染人数爆炸 | beta太大或步长太大 | 检查beta和步长 | 减小beta或步长 |
| 出现负数 | 数值误差 | 检查接近0的值 | 截断为0 |
| 参数不收敛 | 初始值差或不可辨识 | 检查损失函数曲面 | 网格搜索或固定参数 |
| 预测偏高 | 干预加强或行为反馈 | 对比实际干预措施 | 用时变参数 |
| 预测偏低 | 干预放松或病毒变异 | 对比病毒特性 | 更新参数 |
| 拟合很好但预测差 | 过拟合 | 检查参数数量 | 简化模型或加正则化 |
4.5 独家避坑技巧
最后分享几个我在项目中踩过的坑,以及总结出来的技巧。
技巧一:保存每次实验的配置和结果。建模是一个反复迭代的过程,你会跑很多次实验。如果不记录每次的参数和结果,后面根本分不清哪个结果是哪次跑的。我建议用配置文件管理参数,每次实验自动保存到一个带时间戳的文件夹里。
技巧二:可视化要趁早。不要等模型完全跑通了再画图。每改一次代码,就画一次图,看看曲线形状对不对。可视化是最快的调试手段。
技巧三:用合成数据验证模型。在真实数据上跑之前,先用合成数据验证模型。合成数据的生成过程你完全知道,如果模型能从合成数据里恢复出真实参数,说明模型实现没问题。
技巧四:不要迷信单点预测。模型给出的预测是一个点估计,但实际的不确定性很大。我建议用集成方法,跑多个模型或多个参数组合,给出预测区间,而不是单一数字。
技巧五:和领域专家保持沟通。模型是工具,不是目的。模型的假设是否合理,参数是否可信,结果是否有意义,这些都需要领域专家来判断。我见过不少建模项目,技术做得很漂亮,但假设完全脱离实际,结果毫无参考价值。
这个项目后续还可以这样扩展:加入年龄分层,因为不同年龄段的接触模式和重症率差异很大;加入空间维度,因为不同地区的传播条件不同;加入疫苗接种模块,模拟不同接种策略的效果。每增加一个维度,模型的复杂度都会上升,但能回答的问题也更精细。关键是找到那个平衡点:模型足够复杂以捕捉关键特征,又足够简单以可靠标定。