新冠疫情预测建模记录

持续到现在的新冠疫情,已经改变了千万人的命运,其中也包括我

面对这么残暴的天灾,我做了一个预测模型,希望能贡献自己一点微薄的力量,让天灾早点过去

有首歌唱得好:

让我悲也好,让我悔也好,让疫情过去没烦恼;让我苦也好,让我累也好,让我看见大家的笑……

好了,废话结束,进入正题:

在这之前,github上已经有一个预测项目了:

https://github.com/YiranJing/Coronavirus-Epidemic-2019-nCov

于是我将他的源码down下来仔细看了看

该项目作者实现了自己的SIR和SEIR模型
(两种经典的传染病模型:https://baike.baidu.com/item/%E4%BC%A0%E6%9F%93%E7%97%85%E6%A8%A1%E5%9E%8B/5130035?fr=aladdin

而模型的关键部分是定义了一个分布函数:

def Binomial_log_like(n: int, p: float, k: int):
    loglik = lgamma(n+k+1) - lgamma(k+1) - lgamma(n+1) + k*np.log(p) + n*np.log(1-p)

上述公式定义了对疫情传染人数趋势的一个假设公式

顾名思义,他将疫情的传染人数曲线看作二元对数分布

使用SIR和SEIR构建模型是否恰当呢?

这两种传染病动力学模型,其实都未考虑隔离措施的因素

都是建立在自然传播和自然康复的基础上,所以如果要做更客观的预测,需要考虑人类隔离活动的影响

而疫情数据实际的分布是否满足这种分布呢?我们取出历史数据看看:

新冠疫情预测建模记录
国内每天新增病例趋势

从上图可以发现,新增病例曲线总体上更有点类似于伽马分布

而曲线中残缺的部分,猜测则是由于*相关部门采取了强有力的隔离措施,导致了病毒传播的中断

所以,从宏观上看,新增病例曲线就是两种力量不断对抗的结果:

新增感染数量 = 病毒自然传播力 - 人类阻断摩擦力

如果非要用一个物理现象做类比,就好像给往下滚动的雪球踩刹车

假设人类阻断力在23号封城以后是一个常量(已达到人类能实现的*别阻断)

那么其实我们就可以用伽马曲线近似的逼近23号之后的曲线

所以,这里我自定义了一个控制函数:

def control_curve(x, i0, missing, latency=7, infection_num=2):
    """
    传染曲线,假设过了潜伏期即被隔离
    今天传染人数 = (昨天实际传染人数 - 昨天隔离人数) * 平均传染人数
    昨天隔离人数 = 七天前实际传染人数
    今天传染人数 = (昨天实际传染人数 - 七天前实际传染人数) * 平均传染人数
    
    :param x: 传染时间
    :param i0: 初始传染人数
    :param missing: 遗漏率
    :param latency: 潜伏期(确切的说,这里代表平均发病时间,假设发病时间内每天都具有传染性)
    :param infection_num: 平均传染人数
    """
    if x >= latency:
        isolated = i0 * (infection_num ** (x - latency))
    else:
        isolated = 0
    day_num = i0 * missing * (infection_num ** x) - isolated
    if day_num < 0:
        day_num = 0
    return day_num

该函数中存在4个未知参数,我们可以利用scipy的优化器自动求解最优值:

from scipy.optimize import curve_fit

def control_curve_list(x, i0, missing, latency, infection_num):
    y = list(map(control_curve, x, [i0] * len(x), [missing] * len(x), [latency] * len(x), [infection_num] * len(x)))
    return y

popt,pcov = curve_fit(f=control_curve_list, xdata=x_data[:-1], ydata=y_data[:-1])

通过 curve_fit 曲线拟合函数,我们就可以快速的得到一组近似最优解

拟合出来后,看看效果:
新冠疫情预测建模记录
最优参数得到的模型预测120天结果

看起来很光滑呀,实际数据恐怕会比这个粗糙很多

那这个预测值,与实际是否匹配呢?

看起来,方差还是比较大的,有没有办法得到一个更精确的预测呢?

我们可以大胆假设,人类的隔离力度,在每个阶段、每个区域可能都不尽相同

每个时期的参数值可能会略有不同

所以接下来就用贝叶斯优化器对最优参数继续做更深入的探索:

这里先引入JS散度,方便贝叶斯优化器对结果进行比较

from bayes_opt import BayesianOptimization

def JS_divergence(p,q):
    M=(p+q)/2
    return 0.5*scipy.stats.entropy(p, M)+0.5*scipy.stats.entropy(q, M)

再构建一个目标函数,返回可比较的分值

def result_function(nu, close_contacts, k, I0, E0, T, death_rate):
    N = 14 * (10 ** 8)
    econ = len(t)
    R0 = I0 * (death_rate) * 2
    try:
        Est = Estimate_parameter(nu=nu, k=close_contacts, t=t, I=I)
        eso = Estimate_Wuhan_Outbreak(Est, k=k, N=N, E0=E0, I0=I0, R0=R0, T=T, econ=econ)
        result = eso._run_SIER('Forecat', 'China', 'Days after 2019-12-1', death_rate=death_rate, show_Plot=False)
        score = JS_divergence(I,result['Infected'])
    except Exception as e:
        score = 999
        print(e)
    try:
        score = float(score)
    except:
        score = 999
    if score > 999:
        score = 999
    return -score

然后,根据前面的最优参数,手工设置一些搜寻区间:

rf_bo = BayesianOptimization(
        result_function,
        {'nu': (1/20, 1/10),
        'close_contacts': (5, 15),
        'k': (1, 3),
        'I0': (1, 10000),
        'E0': (0,10000),
         'T':(3,14),
         'death_rate':(0.2,0.3)
        }
    )

最后,把我们的优化器跑起来,让它自己找找

rf_bo.maximize(init_points=5,
    n_iter=1000,)

最终,我们就得到了一个相对靠谱的结果:
新冠疫情预测建模记录

使用这个参数进行预测,与实际结果的差距就不大了

整套逻辑写好后,也可以把流程写成一个pipline,每天自动拟合

这样,就能不断的逼近最客观的结果!

以上就是我分享的思路,有不正确的地方还请大家多多批评!

上一篇:ant 使用指南---java模块化编译【转】


下一篇:Docker 容器初体验