前言
最近看了幾篇關於傳染病模型的科普文章覺得很有趣,於是自己動手擼了一遍。雖然貌似傳染病模型和運籌學和控制論好像沒有關係,實際上傳染病模型很多都是動力學模型(常微分方程),這些模型我們在Control theory裏邊並不陌生哈。有了動力學模型也就必然會有模型參數的辨識,而模型參數的辨識往往可以被建模爲一個優化問題,而優化問題的求解也是我的老本行運籌學了。
在此聲明,本文的模型還是比較toy的,能有多少準確性很難保證,不過我覺得是一個很好的練手的project,畢竟作爲一個科研工作者我們以我們的方式來做點什麼。
1 SIR模型簡介
SIR模型是常見的一種描述傳染病傳播的數學模型,其基本假設是將人羣分爲以下三類:
1 易感人羣(Susceptible):指未得病者,但缺乏免疫能力,與感病者接觸後容易受到感染。
2 感染人羣(Infective):指染上傳染病的人,他可以傳播給易感人羣。
3 移除人羣(Removed):被移出系統的人。因病癒(具有免疫力)或死亡的人。這部分人不再參與感染和被感染過程。
如下圖所示,在SIR 模型中以上三類人羣之間存在兩個轉換的關係:
- 易感人羣與感染人員接觸時被傳染,傳染率爲 。傳染率反映了疾病傳播的強度,傳染率越大則易感人羣和感染人員接觸後被傳染的可能性越大。
- 感染人羣以固定平均速率 恢復或死亡。恢復率 ,取決於感染的平均持續時間 。
簡化起見分別用三類人羣的首字母 表示三類人羣的數量, 表示人工總數。那麼三類人羣數量隨時間的動態變化的規則可用以下常微分方程組表示:
(1)
(2)
(3)
2 採用優化算法對傳染率進行參數辨識
通過上面的介紹我們知道SIR模型實際上是採用動力學模型(三個常微分方程)對三類人羣隨時間變化的過程進行建模,採用傳染率和恢復率來量化描述疾病傳染和疾病被治癒等行爲。很重要的一點是隻有獲得準確的動力學模型參數纔有可能建立一個相對精確的模型。那麼要採用SIR模型對武漢新型肺炎傳播進行建模其主要問題就是確定出以下參數
1 傳染率 和 恢復率
2 易感人羣(Susceptible)初值,感染人羣(Infective)初值,移除人羣(Removed)初值
由於第一例病例是在12月8日被確診,因此選擇初始時間在12月8日。感染人羣初值爲1,易感人羣初值爲 ,移除人羣初值爲0,其中 爲武漢市總人口數。 爲恢復率,因爲新型冠狀病毒肺炎的恢復期大約是14天,因此取 。因此下面我們主要是圍繞如何辨識出準確的傳染率
爲了方便進行參數辨識,我們對上述SIR模型進行一個簡化,我們認爲在疾病傳播的早期有 (傳播早期患病人數較少,所以可以近似認爲所有人都是易感人羣),將這個條件帶入到式(2)中可得
(4)
易知該微分方程的通解爲:
(5)
由 可得 ,帶入式(5)中可得
(6)
由此可以構建如下參數辨識問題:
決策變量:傳染率
目標函數: (7)
其中 爲實際的患病人數(從實際數據來), 爲時間集合,以天爲單位。
通過求解上述優化問題即可得到武漢新型肺炎的傳染率 ,易知該優化問題是一個非線性非凸的優化問題。
3 參數辨識所需數據獲得與選取
從上面的參數辨識問題可以看出,我們需要武漢市新型肺炎患病的歷史數據(主要是每天的患病人數)。我們從github上下載了全國主要城市1月20日至2月1日患病人數的數據(數據來源:839Studio/Novel-Coronavirus-Updates),其數據格式如下:
我們從中提取武漢市1月18日至1月22日的數據來進行參數辨識
爲何選取18日至22日的數據?因爲武漢封城是在23日,在封城之前疾病的傳播受到人爲因素影響較小,所以採用封城前的數據來做參數辨識。同時由於18日之前武漢患病數據可能並不真實(有瞞報情況),所以18日之前的數據也不採用。
通過數據預處理後,得到用來辨識的數據格式如下所示:
4 Python代碼實現
數據處理要用到Numpy和Pandas,解微分方程和求解辨識問題需要Scipy,畫圖需要Matplotlib
import numpy as np
import pandas as pd
import math
import datetime
from scipy.integrate import odeint
from scipy.optimize import minimize
import matplotlib.pyplot as plt
主幹代碼分爲三大塊,1是數據預處理部分,主要功能是從原始數據中提取出我們辨識問題所需的數據,2是辨識問題部分,主要功能是求解優化問題,得到準確的傳染率,3是SIR模型,主要是動力學模型的求解。
1 數據預處理部分,原始數據存儲在Updates_NC.csv文件中,主要是提取出武漢市的數據,然後計算出每天累計患病人數,最後提取1月18日之後的數據。
Updates_NC = pd.read_csv('Updates_NC.csv')
class preProcess():
def __init__(self):
self.wuHan = Updates_NC[Updates_NC['城市'] == '武漢市']
wuHanInfection = self.wuHan.groupby('報道時間')['新增確診'].sum()
wuHanRecovered = self.wuHan.groupby('報道時間')['新增出院'].sum()
wuHanDead = self.wuHan.groupby('報道時間')['新增死亡'].sum()
self.wuHan = {'報道時間':wuHanInfection.index, '新增確診':wuHanInfection.values, '新增出院': wuHanRecovered.values, '新增死亡':wuHanDead.values}
self.wuHan = pd.DataFrame(self.wuHan, index = [i for i in range(wuHanInfection.shape[0])])
def getTotal(self):
wuHanTotalInfection = [self.wuHan.loc[0:i,'新增確診'].sum() for i in range(self.wuHan.shape[0])]
wuHanTotalRecovered = [self.wuHan.loc[0:i,'新增出院'].sum() for i in range(self.wuHan.shape[0])]
wuHanTotalDead = [self.wuHan.loc[0:i,'新增死亡'].sum() for i in range(self.wuHan.shape[0])]
self.wuHan = self.wuHan.join(pd.DataFrame([wuHanTotalInfection,wuHanTotalRecovered ,wuHanTotalDead], index = ['累計確診', '累計出院','累計死亡']).T)
print(self.wuHan)
def removeNoisyData(self):
self.wuHan = self.wuHan[self.wuHan['報道時間'] >= '1月18日']
self.wuHan.index = [i for i in range(self.wuHan.shape[0])]
print(self.wuHan)
def report(self):
plt.plot(self.wuHan.index, self.wuHan['累計確診'])
plt.xlabel('Day')
plt.ylabel('Number of people(Wu Han)')
plt.show()</code></pre></div><p>2 定義出參數辨識問題,主要是定義出目標函數(costfunction)即可調用Scipy來幫助我們求解優化問題。實際上在代碼中我們在求解辨識問題的時候,將傳染率拆成了2項相乘的形式 <img src="https://www.zhihu.com/equation?tex=%5Cbeta%3DnContact+%5Ctimes+infectionProb+" alt="[公式]" eeimg="1" data-formula="\beta=nContact \times infectionProb "> ,其中 <img src="https://www.zhihu.com/equation?tex=nContact" alt="[公式]" eeimg="1" data-formula="nContact"> 爲感染人員每天接觸的正常人的數量(假設爲5人), <img src="https://www.zhihu.com/equation?tex=infectionProb" alt="[公式]" eeimg="1" data-formula="infectionProb"> 爲感染概率</p><div class="highlight"><pre><code class="language-text">class estimationInfectionProb():
def __init__(self, estUsedTimeIndexBox, nContact, gamma):
self.timeRange = np.array([i for i in range(estUsedTimeIndexBox[0],estUsedTimeIndexBox[1] + 1)])
self.nContact, self.gamma = nContact, gamma
self.dataStartTimeStep = 41
def setInitSolution(self, x0):
self.x0 = 0.04
def costFunction(self, infectionProb):
#print(infectionData.wuHan.loc[self.timeRange - self.dataStartTimeStep,'累計確診'])
#print(np.exp((infectionProb * self.nContact - self.gamma) * self.timeRange))
res = np.array(np.exp((infectionProb * self.nContact - self.gamma) * self.timeRange) - \
infectionData.wuHan.loc[self.timeRange - self.dataStartTimeStep,'累計確診'])
return (res**2).sum() / self.timeRange.size
def optimize(self):
self.solution = minimize(self.costFunction, self.x0, method='nelder-mead', options={'xtol': 1e-8, 'disp': True})
print('infection probaility: ', self.solution.x)
return self.getSolution()
def getSolution(self):
return self.solution.x
def getBasicReproductionNumber(self):
self.basicReproductionNumber = self.nContact * self.solution.x[0] / (self.gamma)
print("basic reproduction number:", self.basicReproductionNumber)
return self.basicReproductionNumber</code></pre></div><p>定義出SIR 模型的類,代碼寫得比較直觀和簡單,此處就不多解釋了,相信很容易看懂。主要是用scipy來求解常微分方程,即式(1-3)。需要注意的是此處的傳染率 <img src="https://www.zhihu.com/equation?tex=%5Cbeta" alt="[公式]" eeimg="1" data-formula="\beta"> 要用上面參數辨識得到的值。</p><div class="highlight"><pre><code class="language-qvto">class wuHanSIRModel():
def __init__(self, N, beta, gamma):
self.beta, self.gamma, self.N = beta, gamma, N
self.t = np.linspace(0, 360, 361)
self.setInitCondition()
def odeModel(self, population, t):
diff = np.zeros(3)
s,i,r = population
diff[0] = - self.beta * s * i / self.N
diff[1] = self.beta * s * i / self.N - self.gamma * i
diff[2] = self.gamma * i
return diff
def setInitCondition(self):
self.populationInit = [self.N - 1, 1, 0]
def solve(self):
self.solution = odeint(self.odeModel,self.populationInit,self.t)
def report(self):
#plt.plot(self.solution[:,0],color = 'darkblue',label = 'Susceptible',marker = '.')
plt.plot(self.solution[:,1],color = 'orange',label = 'Infection',marker = '.')
plt.plot(self.solution[:,2],color = 'green',label = 'Recovery',marker = '.')
plt.title('SIR Model' + ' infectionProb = '+ str(infectionProb))
plt.legend()
plt.xlabel('Day')
plt.ylabel('Number of people')
plt.show()
最近在家上github很慢,我就把全套代碼放到網盤上吧。網盤鏈接如下: https://pan.baidu.com/s/1X4oiPhU5d8CQX01DOg6zPA 提取碼: abqa
5 結果分析
1 武漢市患病人數分析
如下圖所示是武漢市患病人數數據,用紅色框圈出的是累計患病人數,可以很明顯地看到從1月11日至1月18日的累計患病人數基本保持不變,這部分數據的可信度還需要進一步考量。
2 傳染率辨識結果分析
我們得到的辨識結果爲 infection probaility: 0.03926041,傳染率 = 0.19630
同時可以採用如下公式估算出 武漢新型肺炎病毒基本傳染數(basic reproduction number)
基本傳染數是指在沒有外力介入,同時所有人都沒有免疫力的情況下,一個感染某種傳染病的人,會把疾病傳染給其他多少個人的平均數,基本傳染數是衡量疾病傳染性的一個重要指標。若 則傳染病將會逐漸消失,若 傳染病會以指數方式散佈,成爲流行病。非典的基本傳染數約爲0.85-3,埃博拉基本傳染數約1.5-2.5。
3 用SIR模型預測武漢市患病人數
定義nContact爲 每個患病人員每天會接觸nContact個正常人,nContact越大表明隔離和管制措施越弱,同時接觸後正常人的患病概率爲0.03926的情況下SIR模型結果如下
即若不採取任何管制和防控措施的條件下,假設nContact=5,武漢市患病人數隨時間變化的曲線如下所示:
當nContact=4,武漢市患病人數隨時間變化的曲線如下所示:
當nContact=3,武漢市患病人數隨時間變化的曲線如下所示:
當nContact=2 (表明已經採用了比較嚴厲的隔離和管制措施),武漢市患病人數隨時間變化的曲線如下所示:
從上面的結果可以非常清晰得看出隨着nContact的減少,患病人數急劇減少,當nContact=2時 患病人數甚至不超過100人,可見採取嚴厲的管制和防控隔離措施對武漢市新型冠狀病毒患病人數的減少有着非常明顯的效果。
6 總結
1 採用動力學模型對疾病傳播建模並不難,難點在於如何利用真實數據辨識動力學模型中的參數。
2 對真實數據的收集,預處理等工作看起來不起眼,實際上在我們的工作中是一個非常重要的部分。
3 SIR模型還是比較粗糙的一個模型,主要是沒有考慮到潛伏期,那麼SEIR模型會更加精確一些,後期有興趣可以再做一做。
4 還可以考慮兩階段的模型,例如武漢市在1月23日採取了封城措施,那麼可以將封城前後分別進行建模,構成一個兩階段模型會更加準確一些。
參考文獻:
小烎模:武漢肺炎傳播的多種數學模型張戎:傳染病的數學模型【1】新型冠狀病毒的疫情評估與預測報告,北京航空航天大學計算機學院智慧城市(BIGSCity)課題組和經管學院數據智能(DIG)課題組