Last active
March 27, 2020 13:56
-
-
Save globalpolicy/46b98b197aa01f4ddec9e70701aa7de0 to your computer and use it in GitHub Desktop.
Covid19 simulation using realtime (daily) data - SIR model
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Chart.min.js"></script> | |
<style> | |
#overlay { | |
position: fixed; | |
/* Sit on top of the page content */ | |
display: block; | |
/* Hidden by default */ | |
width: 100%; | |
/* Full width (cover the whole page) */ | |
height: 100%; | |
/* Full height (cover the whole page) */ | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(0, 0, 0, 0.5); | |
/* Black background with opacity */ | |
z-index: 2; | |
/* Specify a stack order in case you're using a different order for other elements */ | |
cursor: pointer; | |
/* Add a pointer on hover */ | |
} | |
#overlayText { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
font-size: 50px; | |
color: white; | |
transform: translate(-50%, -50%); | |
-ms-transform: translate(-50%, -50%); | |
} | |
</style> | |
<div id="overlay"> | |
<div id="overlayText"> | |
Fetching data... | |
</div> | |
</div> | |
<div id="chartDiv"> | |
<canvas id="chart" width="1200" height="600" style="border:1px solid"> | |
</canvas> | |
</div> | |
<div> | |
Data used for parameter estimation: | |
<label for="latest">Latest</label> | |
<input type="radio" name="EstimationMode" id="opt_latest" class="inputs" checked=true> | |
<label for="min">Last 5</label> | |
<input type="radio" name="EstimationMode" class="inputs" id="opt_last5"> | |
<label for="max">Last 14</label> | |
<input type="radio" name="EstimationMode" class="inputs" id="opt_last14"> | |
<label for="mean">Mean</label> | |
<input type="radio" name="EstimationMode" class="inputs" id="opt_mean"> | |
</div> | |
<br /> | |
Estimated parameters: | |
<div title="Contact rate (Infection constant)"> | |
<b>β</b> = <span id="betaText">-</span> | |
</div> | |
<div title="Recovery constant"> | |
<b>α</b> = <span id="alphaText">-</span> | |
</div> | |
<div title="Basic Reproduction Number, beta/alpha"> | |
<a href="https://en.wikipedia.org/wiki/Basic_reproduction_number" | |
style="text-decoration:none;"><b>R</b><sub>0</sub></a> = | |
<span id="R0Text">-</span> | |
</div> | |
<br /> | |
Inputs: | |
<div> | |
<label for="I0">I<sub>0</sub></label> | |
<input type="number" class="inputs" id="I0" value=510 min=1 title="Initial infected population"> | |
</div> | |
<div> | |
<label for="N">N</label> | |
<input type="number" id="N" class="inputs" value=7e9 min=10 title="Total population"> | |
</div> | |
<div> | |
<label for="maxTime">Max. Time</label> | |
<input type="number" id="maxTime" class="inputs" value=365 title="Duration to simulate"> | |
</div> | |
<script src="main.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let chart = null; | |
let chartState = [];//stores visibility of graphs | |
let fetchedData; | |
let todaysData, yesterdaysData; | |
let N = Number(document.getElementById("N").value); | |
let delT = 0.1; | |
let alpha, beta; | |
attachEventHandlers(); | |
fetchLatestData(finishedFetchingData); | |
function attachEventHandlers() { | |
let inputs = document.getElementsByClassName("inputs"); | |
for (let i = 0; i < inputs.length; i++) { | |
inputs[i].addEventListener("change", inputsChangedEventHandler); | |
} | |
function inputsChangedEventHandler(event) { | |
showBanner("Fetching data..."); | |
N = Number(document.getElementById("N").value); | |
fetchLatestData(finishedFetchingData); | |
//save current state of the graphs' visibility | |
chartState = []; | |
for (let i = 0; i < chart.data.datasets.length; i++) { | |
chartState.push(chart.getDatasetMeta(i).hidden); | |
} | |
} | |
} | |
function showBanner(text) { | |
document.getElementById("overlay").style.display = "block"; | |
document.getElementById("overlayText").innerHTML = text; | |
} | |
function hideBanner() { | |
document.getElementById("overlay").style.display = "none"; | |
} | |
function fetchLatestData(finishedFetchingData) { | |
let xhr = new XMLHttpRequest(); | |
xhr.addEventListener("readystatechange", function () { | |
if (this.readyState === this.DONE) { | |
let JSONResponse = JSON.parse(this.response); | |
let countries = Object.keys(JSONResponse); | |
let days = JSONResponse[countries[0]].length; | |
let timeSeries = []; | |
for (let i = 0; i < days; i++) { | |
let globalConfirmed = 0, globalDeaths = 0, globalRecovered = 0, date; | |
for (let j = 0; j < countries.length; j++) { | |
let countryData = JSONResponse[countries[j]][i]; | |
date = countryData.date; | |
globalConfirmed += countryData.confirmed; | |
globalDeaths += countryData.deaths; | |
globalRecovered += countryData.recovered; | |
} | |
timeSeries.push({ | |
date: date, | |
confirmed: globalConfirmed, | |
deaths: globalDeaths, | |
recovered: globalRecovered, | |
infective_: globalConfirmed - globalDeaths - globalRecovered, | |
susceptible_: N - globalConfirmed, | |
removed_: globalRecovered + globalDeaths | |
}); | |
} | |
fetchedData = timeSeries; | |
finishedFetchingData(); | |
} | |
}); | |
xhr.open("GET", "https://pomber.github.io/covid19/timeseries.json"); | |
xhr.send(); | |
} | |
function finishedFetchingData() { | |
let mean_alpha = 0, mean_beta = 0; | |
for (let i = 1; i < fetchedData.length; i++) {//start from second | |
let todayActiveCases = fetchedData[i].infective_; | |
let yesterdayActiveCases = fetchedData[i - 1].infective_; | |
let susceptibleToday = fetchedData[i].susceptible_; | |
let susceptibleYesterday = fetchedData[i - 1].susceptible_; | |
let delS = susceptibleToday - susceptibleYesterday; | |
let I = 0.5 * (todayActiveCases + yesterdayActiveCases); | |
let S = 0.5 * (susceptibleToday + susceptibleToday); | |
beta = Math.abs(delS) / (I * S / N); | |
let cumulativeRemovedToday = fetchedData[i].removed_; | |
let cumulativeRemovedYesterday = fetchedData[i - 1].removed_; | |
let delR = cumulativeRemovedToday - cumulativeRemovedYesterday; | |
alpha = delR / I; | |
fetchedData[i].beta = beta; | |
fetchedData[i].alpha = alpha; | |
mean_alpha += alpha; | |
mean_beta += beta; | |
} | |
mean_alpha /= (fetchedData.length - 1); | |
mean_beta /= (fetchedData.length - 1); | |
if (document.getElementById("opt_latest").checked) { | |
//do nothing. default behavior | |
} | |
else if (document.getElementById("opt_last5").checked) { | |
alpha = fetchedData.slice(fetchedData.length - 5).reduce((ret, element) => ret + element.alpha, 0) / 5; | |
beta = fetchedData.slice(fetchedData.length - 5).reduce((ret, element) => ret + element.beta, 0) / 5; | |
} | |
else if (document.getElementById("opt_last14").checked) { | |
alpha = fetchedData.slice(fetchedData.length - 14).reduce((ret, element) => ret + element.alpha, 0) / 14; | |
beta = fetchedData.slice(fetchedData.length - 14).reduce((ret, element) => ret + element.beta, 0) / 14; | |
} | |
else if (document.getElementById("opt_mean").checked) { | |
alpha = mean_alpha; | |
beta = mean_beta; | |
} | |
document.getElementById("betaText").innerHTML = beta.toFixed(3); | |
document.getElementById("alphaText").innerHTML = alpha.toFixed(3); | |
document.getElementById("R0Text").innerHTML = (beta / alpha).toFixed(2); | |
computeAndGraph((beta / alpha).toFixed(2)); | |
} | |
function computeAndGraph(R0) { | |
showBanner("Computing..."); | |
let I0 = Number(document.getElementById("I0").value); | |
let maxTime = Number(document.getElementById("maxTime").value); | |
let I_ar = []; | |
let S_ar = []; | |
let R_ar = []; | |
let t_ar = []; | |
let I_plus_R_ar = []; | |
let realI_ar = []; | |
let realS_ar = []; | |
let realR_ar = []; | |
let realI_plus_R_ar = []; | |
let I = I0; | |
let S = N; | |
let R = 0; | |
let counter = 0; | |
//let skipEvery = Math.round((maxTime / delT) / 100);//so that the final number of data points is always a hundred | |
for (let t = 0; t <= maxTime; t += delT) { | |
let delS = -beta * S * I / N * delT; | |
let delI = beta * S * I / N * delT - alpha * I * delT; | |
let delR = alpha * I * delT; | |
S += delS; | |
I += delI; | |
R += delR; | |
/* | |
if (counter % skipEvery == 0) { | |
I_ar.push(I); | |
S_ar.push(S); | |
R_ar.push(R); | |
t_ar.push(Number(t.toFixed(2))); | |
I_plus_R_ar.push(I + R); | |
} | |
counter++; | |
*/ | |
let t_rounded = Number(t.toFixed(2)); | |
if (Number.isInteger(t_rounded)) { | |
//only plot daily data | |
I_ar.push(I); | |
S_ar.push(S); | |
R_ar.push(R); | |
t_ar.push(t_rounded); | |
I_plus_R_ar.push(I + R); | |
if (t_rounded < fetchedData.length) { | |
realI_ar.push(fetchedData[t_rounded].infective_); | |
realS_ar.push(fetchedData[t_rounded].susceptible_); | |
realR_ar.push(fetchedData[t_rounded].removed_); | |
realI_plus_R_ar.push(fetchedData[t_rounded].confirmed); | |
} | |
} | |
} | |
hideBanner(); | |
drawChart({ | |
t: t_ar, | |
I: I_ar, | |
S: S_ar, | |
R: R_ar, | |
IplusR: I_plus_R_ar, | |
realI: realI_ar, | |
realS: realS_ar, | |
realR: realR_ar, | |
realIplusR: realI_plus_R_ar | |
}, N, R0); | |
} | |
function drawChart(output, y0, R0) { | |
var ctx = document.getElementById('chart').getContext('2d'); | |
if (chart != undefined) { | |
chart.destroy(); | |
} | |
chart = new Chart(ctx, { | |
type: 'line', | |
data: { | |
labels: output.t, | |
datasets: [{ | |
label: 'Est. Infected', | |
data: output.I, | |
borderColor: ['rgba(255, 99, 132, 1)'], | |
borderWidth: 1, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Est. Susceptible', | |
data: output.S, | |
borderColor: ['rgba(99, 255, 132, 1)'], | |
borderWidth: 1, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Est. Recovered', | |
data: output.R, | |
borderColor: ['rgba(99, 132, 255, 1)'], | |
borderWidth: 1, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Est. Cumulative infected', | |
data: output.IplusR, | |
borderColor: ['rgba(150, 132, 55, 1)'], | |
borderWidth: 1, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Obs. Infected', | |
data: output.realI, | |
borderColor: ['rgba(205, 49, 82, 1)'], | |
borderWidth: 2, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Obs. Susceptible', | |
data: output.realS, | |
borderColor: ['rgba(49, 205, 82, 1)'], | |
borderWidth: 2, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Obs. Recovered', | |
data: output.realR, | |
borderColor: ['rgba(49, 82, 205, 1)'], | |
borderWidth: 2, | |
fill: false, | |
pointRadius: 1 | |
}, | |
{ | |
label: 'Obs. Cumulative infected', | |
data: output.realIplusR, | |
borderColor: ['rgba(100, 82, 5, 1)'], | |
borderWidth: 2, | |
fill: false, | |
pointRadius: 1 | |
} | |
] | |
}, | |
options: { | |
animation: { | |
duration: 1 | |
}, | |
scales: { | |
yAxes: [{ | |
ticks: { | |
beginAtZero: true, | |
//max: y0 | |
}, | |
scaleLabel: { | |
display: true, | |
labelString: 'Population' | |
} | |
}], | |
xAxes: [{ | |
ticks: { | |
autoSkip: true, | |
maxTicksLimit: 30 | |
}, | |
scaleLabel: { | |
display: true, | |
labelString: 'Time (days)' | |
} | |
}] | |
}, | |
title: { | |
display: true, | |
text: "Covid-19 over time | R0 = " + R0 | |
}, | |
maintainAspectRatio: false, | |
responsive: false | |
} | |
}); | |
//restore saved graphs' visibility states | |
if (chartState.length > 0) { | |
for (let i = 0; i < chart.data.datasets.length; i++) { | |
chart.getDatasetMeta(i).hidden = chartState[i]; | |
} | |
} | |
chart.update(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment