|
| 1 | +%% Modelling COVID-19 with a General Branching Process (GBP), also called Crump-Mode-Jagers branching process |
| 2 | +% The example is built for the specifics of the data and the quarantine in Bulgaria. |
| 3 | +% It includes 3 scenarios for the future R0 |
| 4 | + |
| 5 | +%% load the branching process simulator |
| 6 | +addpath('../../') % adds the function BranchingProcessSimulator to the path |
| 7 | +addpath('../') % adds the functions confInterval and readCSVData to the path |
| 8 | + |
| 9 | +%% read and plot the data for the chosen country |
| 10 | +% save the data from: https://opendata.ecdc.europa.eu/covid19/casedistribution/csv |
| 11 | +% then write in the file data.csv |
| 12 | +[dates, newcases_hist, totalcases_hist] = readCSVData('../data.csv', 'France'); |
| 13 | + |
| 14 | +% the first sevral cases seem controlled and should be excluded as a beginning of the epidemics |
| 15 | +% the epidemics starts on : |
| 16 | +ep_start = find(newcases_hist==0, 1, 'last')+1; |
| 17 | +dates=dates(ep_start:end); |
| 18 | +newcases_hist=newcases_hist(ep_start:end); |
| 19 | +totalcases_hist=totalcases_hist(ep_start:end); |
| 20 | +%% Build a branching model |
| 21 | +detection_time = 8; % assumes 2 days after the symptoms develop (on average) the person is tested for corona |
| 22 | +num_days_passed = dates(end)-dates(1) + 1 + detection_time; % days passed since the infection began = days since first case + detection time needed |
| 23 | + |
| 24 | +sim_num=1000; % number of simulations to perform |
| 25 | +horizon=90; % horizon in days, after the last available data |
| 26 | +T=num_days_passed+horizon; % the simulation period for the process, from the first infected to horizon |
| 27 | +h=0.5; % the time step is 0.5 day |
| 28 | +omega=60; % no one is infected for more than omega days, technical parameter that does not need to be accurate, smaller -> faster speed, |
| 29 | + % but it needs to be at least as large that the P(being infected after omega) is very close to zero. |
| 30 | + |
| 31 | +%% model of S, survivability function, NOT to be mistaken with the percentage of people who survive the virus! |
| 32 | +% S is virus specific and represents the survivability function of the virus inside the body of a person |
| 33 | +% Here it is assumed normally distributed where the values mu and sigma are taken from the article: |
| 34 | +% "Feasability of controlling 2019-nCoV outbreaks by isolation of cases and contacts", J. Hellewell, S. Abbot, et al. |
| 35 | +% https://doi.org/10.1101/2020.02.08.20021162 |
| 36 | +% |
| 37 | +% average time for recovery in the article is 5.8 + 9.1 = 14.9, std = sqrt(2.6^2 + 19.53) = 5.1274 |
| 38 | +% however the data in bulgaria (recovery rates and active cases) seems to show that people need more time to clear the virus out of their body |
| 39 | +% even after they are already healthy and recovered. Here a value of 25 days is used for the average days to clear the virus. |
| 40 | +% As worldometers.info writes, the active cases and recovories depend highly on the way we count a recovery. |
| 41 | +% In Bulgaria it is counted after a person tests negative twice for the virus. |
| 42 | +S=(1-normcdf(0:h:omega, 30, 5.1274)')./(1-normcdf(0,30, 5.1274)); % trimmed normal life length with average of 14 days until clearance |
| 43 | +S(end)=0; % the chance of surviving after age omega is zero |
| 44 | +% If you want to try exponential distribution instead, use S=(1-expcdf(0:h:omega, 14.9)')./normcdf(0,omega, 14.9); % exponential with average of 14 days |
| 45 | + |
| 46 | +%% other parameters for the simulator |
| 47 | +U=1; % no types - no mutations, there is a possibility to include a second, quarantined type here with a probability to become quarantined |
| 48 | + |
| 49 | +H=[0, 1]'; % people infect only 1 other person when infection happens, no multiple infections |
| 50 | + |
| 51 | +% Im=[]; |
| 52 | +% there is no immigration in the example, but it could be included as a random variable representing the number of individuals returning to Bulgaria |
| 53 | +% which are infected with the virus |
| 54 | + |
| 55 | +% the initial number of infections is uniformly distributed with respect to "days passed since infection" (i.e. "age" of the infection) |
| 56 | +% those infections have uniformly distributed "ages" on every call of the Z_0 function |
| 57 | +age_struct_pdf=unifpdf(0:h:omega, 0, 5.8)'; |
| 58 | +age_struct_prob=age_struct_pdf./(sum(age_struct_pdf)); |
| 59 | + |
| 60 | +Z_0=@()(mnrnd(2, age_struct_prob)'); |
| 61 | + |
| 62 | +%% MAIN SCENARIO, model of the Point process mu |
| 63 | +% values are taken from the article https://doi.org/10.1101/2020.02.08.20021162 |
| 64 | +% from onset to isolation 3.83 (var = 5.99) |
| 65 | +% incubation 5.8 (std = 2.6) |
| 66 | +% average time of infecting other people is 5.8 + 3.83 = 9.63, std = sqrt(2.6^2 + 5.99) = 3.5707 |
| 67 | + |
| 68 | +% R0 - average number of infected people from a single person, conditional on that person staying infected |
| 69 | +% distribution of the infection days is taken to be gamma-like and the point process integrates to R0: |
| 70 | +% Expectation = k * theta = 9.63, Variance = k * theta^2 = 3.5707^2 => theta = 3.5707^2 / 9.63 = 1.3240, k = 9.63/1.3240 = 7.2734 |
| 71 | +% Here we assume we have 2 periods of the epidemic - before the people started distancing themselves and after. |
| 72 | +% R0 is chosen to fit what happened already |
| 73 | +%R0 = [ ones(1,T/h+1-(7+days_before_quarantine+horizon-detection_time)/h)*0.85 ones(1,(horizon-detection_time)/h)*0.85]; |
| 74 | +% T/7 % number of weeks since the beginning |
| 75 | + |
| 76 | +R0 = [linspace(12, 12, 7/h) linspace(12, 12, 7/h) linspace(12, 12, 7/h) linspace(12, 7.5, 7/h) ... |
| 77 | + linspace(7.5, 4, 7/h) linspace(4, 2, 7/h) linspace(2, 1, 7/h) linspace(1, 0.6, 7/h)... |
| 78 | + linspace(0.6, 0.5, T/h+1-(8*7+horizon-detection_time)/h) linspace(0.5, 0.5, (horizon-detection_time)/h)]; |
| 79 | +mu_covid_pdf=gampdf(0:h:omega, 7.2734, 1.3240)'; |
| 80 | +mu_matrix=zeros(size(mu_covid_pdf,1), 1, T/h+1); |
| 81 | +mu_matrix(:,1,:)=R0.*repmat(mu_covid_pdf/(sum(mu_covid_pdf)*h),1, T/h+1); |
| 82 | +mu=@()(mu_matrix); % the input format for mu is described in the BranchingProcessSimulator.m |
| 83 | + |
| 84 | +% we can also calculate the age structure of the population which is of interest in this case |
| 85 | +% we can use it to calculate the percentage of people on working age, for example |
| 86 | +% to get the age structure use: [ActiveCases, ActiveCasesByType, ActiveCasesByAge, TotalCases, TotalCasesByTypes] = BranchingProcessSimulator(sim_num, T, h, S, H, U, Z_0, mu, 'GetAgeStructure', true); |
| 87 | +[ActiveCases, ~, ~, TotalCases, ~] = BranchingProcessSimulator(sim_num, T, h, S, H, U, Z_0, mu, 'GetAgeStructure', false, 'approx_limit', 5); |
| 88 | +NewCases = diff(TotalCases'); |
| 89 | +% CuredCases = TotalCases - ActiveCases; |
| 90 | + |
| 91 | +% converts from h period of time to daily period of time |
| 92 | +NewCasesDaily = squeeze(sum(reshape(NewCases, [2, size(NewCases,1)*h, size(NewCases,2)])))'; |
| 93 | +TotalCasesDaily= TotalCases(:, 1:(1/h):T/h); |
| 94 | +ActiveCasesDaily= ActiveCases(:, 1:(1/h):T/h); |
| 95 | + |
| 96 | +% build and save plots |
| 97 | +buildPlots(NewCasesDaily, TotalCasesDaily, ActiveCasesDaily, newcases_hist, dates, horizon, detection_time, ... |
| 98 | + 'MainScenario', 'No change in R_0 (no change in measures)'); |
| 99 | + |
| 100 | +%% OPTIMISTIC SCENARIO model of the Point process mu, Optimistic Scenario, R0 changes from 0.5 to 0.3, from now on |
| 101 | +R0 = [linspace(12, 12, 7/h) linspace(12, 12, 7/h) linspace(12, 12, 7/h) linspace(12, 7.5, 7/h) ... |
| 102 | + linspace(7.5, 4, 7/h) linspace(4, 2, 7/h) linspace(2, 1, 7/h) linspace(1, 0.6, 7/h)... |
| 103 | + linspace(0.6, 0.5, T/h+1-(8*7+horizon-detection_time)/h) linspace(0.3, 0.3, (horizon-detection_time)/h)]; |
| 104 | +mu_covid_pdf=gampdf(0:h:omega, 7.2734, 1.3240)'; |
| 105 | +mu_matrix=zeros(size(mu_covid_pdf,1), 1, T/h+1); |
| 106 | +mu_matrix(:,1,:)=R0.*repmat(mu_covid_pdf/(sum(mu_covid_pdf)*h),1, T/h+1); |
| 107 | +mu_optimistic=@()(mu_matrix); % the input format for mu is described in the BranchingProcessSimulator.m |
| 108 | + |
| 109 | +[ActiveCases_optimistic, ~, ~, TotalCases_optimistic, ~] = BranchingProcessSimulator(sim_num, T, h, S, H, U, Z_0, mu_optimistic, 'GetAgeStructure', false); |
| 110 | +NewCases_optimistic = diff(TotalCases_optimistic'); |
| 111 | +% CuredCases = TotalCases - ActiveCases; |
| 112 | + |
| 113 | +% converts from h period of time to daily period of time |
| 114 | +NewCasesDaily_optimistic = squeeze(sum(reshape(NewCases_optimistic, [2, size(NewCases_optimistic,1)*h, size(NewCases_optimistic,2)])))'; |
| 115 | +TotalCasesDaily_optimistic= TotalCases(:, 1:(1/h):T/h); |
| 116 | +ActiveCasesDaily_optimistic= ActiveCases(:, 1:(1/h):T/h); |
| 117 | + |
| 118 | +% build and save plots |
| 119 | +buildPlots(NewCasesDaily_optimistic, TotalCasesDaily_optimistic, ActiveCasesDaily_optimistic, newcases_hist, dates, horizon, detection_time, ... |
| 120 | + 'OptimisticScenario', 'Decline in R_0 (better results from measures)'); |
| 121 | + |
| 122 | +%% PESSIMISTIC SCENARIO model of the Point process mu, Pessimistic Scenario, R0 changes from 0.5 to 0.8, from now on |
| 123 | +R0 = [linspace(12, 12, 7/h) linspace(12, 12, 7/h) linspace(12, 12, 7/h) linspace(12, 7.5, 7/h) ... |
| 124 | + linspace(7.5, 4, 7/h) linspace(4, 2, 7/h) linspace(2, 1, 7/h) linspace(1, 0.6, 7/h)... |
| 125 | + linspace(0.6, 0.5, T/h+1-(8*7+horizon-detection_time)/h) linspace(0.8, 0.8, (horizon-detection_time)/h)]; |
| 126 | +mu_covid_pdf=gampdf(0:h:omega, 7.2734, 1.3240)'; |
| 127 | +mu_matrix=zeros(size(mu_covid_pdf,1), 1, T/h+1); |
| 128 | +mu_matrix(:,1,:)=R0.*repmat(mu_covid_pdf/(sum(mu_covid_pdf)*h),1, T/h+1); |
| 129 | +mu_pessimistic=@()(mu_matrix); % the input format for mu is described in the BranchingProcessSimulator.m |
| 130 | + |
| 131 | +[ActiveCases_pessimistic, ~, ~, TotalCases_pessimistic, ~] = BranchingProcessSimulator(sim_num, T, h, S, H, U, Z_0, mu_pessimistic, 'GetAgeStructure', false); |
| 132 | +NewCases_pessimistic = diff(TotalCases_pessimistic'); |
| 133 | +% CuredCases = TotalCases - ActiveCases; |
| 134 | + |
| 135 | +% converts from h period of time to daily period of time |
| 136 | +NewCasesDaily_pessimistic = squeeze(sum(reshape(NewCases_pessimistic, [2, size(NewCases_pessimistic,1)*h, size(NewCases_pessimistic,2)])))'; |
| 137 | +TotalCasesDaily_pessimistic= TotalCases(:, 1:(1/h):T/h); |
| 138 | +ActiveCasesDaily_pessimistic= ActiveCases(:, 1:(1/h):T/h); |
| 139 | + |
| 140 | +% build and save plots |
| 141 | +buildPlots(NewCasesDaily_pessimistic, TotalCasesDaily_pessimistic, ActiveCasesDaily_pessimistic, newcases_hist, dates, horizon, detection_time, ... |
| 142 | + 'PessimisticScenario', 'Increase in R_0 (worse results from measures)'); |
| 143 | + |
| 144 | +%% Comparison of scenarios |
| 145 | +[NewCasesDaily_mean, NewCasesDaily_lower, NewCasesDaily_upper, NewCasesDaily_median]=confInterval(NewCasesDaily, 0.10); |
| 146 | +[NewCasesDaily_optimisic_mean, NewCasesDaily_optimisic_lower, NewCasesDaily_optimisic_upper, NewCasesDaily_optimisic_median]=confInterval(NewCasesDaily_optimistic, 0.10); |
| 147 | +[NewCasesDaily_pessimistic_mean, NewCasesDaily_pessimistic_lower, NewCasesDaily_pessimistic_upper, NewCasesDaily_pessimistic_median]=confInterval(NewCasesDaily_pessimistic, 0.10); |
| 148 | + |
| 149 | +line_wd=2.5; |
| 150 | +figure('visible','on', 'Units','pixels','OuterPosition',[0 0 1280 1024]); |
| 151 | +set(gca,'FontSize',16) |
| 152 | +hold on |
| 153 | +h_hist=plot(dates, newcases_hist, 'Color', [0, 0, 0, 0.7],'LineWidth', 1.5); |
| 154 | +h_main=plot(dates(1):dates(end)+horizon, NewCasesDaily_median(detection_time:end-1), '-', 'Color', [0, 0, 0.5, 0.5], 'LineWidth', line_wd); |
| 155 | +h_optimistic=plot(dates(end):dates(end)+horizon, NewCasesDaily_optimisic_median(end-horizon-1:end-1), '-', 'Color', [0, 0.5, 0, 0.5], 'LineWidth', line_wd); |
| 156 | +h_pessimistic=plot(dates(end):dates(end)+horizon, NewCasesDaily_pessimistic_median(end-horizon-1:end-1), '-', 'Color', [0.5, 0, 0, 0.5], 'LineWidth', line_wd); |
| 157 | +h_CI=plot(dates(1):dates(end)+horizon, NewCasesDaily_lower(detection_time:end-1), '--', 'Color', [0,155/255,1,1], 'LineWidth', line_wd); |
| 158 | +plot(dates(1):dates(end)+horizon, NewCasesDaily_upper(detection_time:end-1), '--', 'Color', [0,155/255,1,1], 'LineWidth', line_wd); |
| 159 | +legend([h_main, h_optimistic, h_pessimistic, h_hist, h_CI], 'Main Scenario', 'Optimistic Scenario', 'Pessimistic Scenario', 'Observed new daily cases', '90% conf. interval', 'Location', 'NorthWest') |
| 160 | +xtickangle(90) |
| 161 | +x_ticks = xticks; |
| 162 | +xticks(x_ticks(1):7:x_ticks(end)) |
| 163 | +dateaxis('X',2) |
| 164 | +ylabel('\bf{New Daily Cases (Observed)}') |
| 165 | +xlabel('\bf{Date}') |
| 166 | +title('Forecasts of observed New Daily Cases (by scenario)') |
| 167 | +print(strcat('./Figures/forecast_newcases_by_scenario'), '-dpng', '-r0') |
| 168 | + |
| 169 | +%% Shows the input to the branching process |
| 170 | +%%% MU, POINT PROCESS |
| 171 | +line_wd=2.5; |
| 172 | +figure('visible','on', 'Units','pixels','OuterPosition',[0 0 1280 1024]); |
| 173 | +set(gca,'FontSize',16) |
| 174 | +hold on |
| 175 | +mu_forecast = squeeze(mu()); |
| 176 | +plot(0:h:omega, mu_forecast(:, end), 'Color', [0.5,0,0,0.5], 'LineWidth', line_wd); |
| 177 | +ylabel('Point process density') |
| 178 | +xlabel('Days') |
| 179 | +title('Probability for spreading the infection, by days') |
| 180 | +print('./Figures/PointProcessDensity', '-dpng', '-r0') |
| 181 | + |
| 182 | +%%% Infected period distribution |
| 183 | +line_wd=2.5; |
| 184 | +figure('visible','on', 'Units','pixels','OuterPosition',[0 0 1280 1024]); |
| 185 | +set(gca,'FontSize',16) |
| 186 | +hold on |
| 187 | +plot(0:h:omega, [diff(1-S); 0]./h, 'Color', [0,0,0.5,0.5], 'LineWidth', line_wd); |
| 188 | +ylabel('Infected period PDF') |
| 189 | +xlabel('Days') |
| 190 | +title('Probability for getting cleared of the virus, by days') |
| 191 | +print('./Figures/InfectedPeriodPDF', '-dpng', '-r0') |
| 192 | + |
| 193 | +%% save the results |
| 194 | +% save(strcat('results_', num2str(sim_num))) |
| 195 | + |
0 commit comments