knitr::opts_chunk$set(echo = TRUE)
This vignette provides detailed code to reproduce the main analysis the paper
. Briefly, we quantify the benefits of extending COVID-19 vaccination beyond adults by simulating the model (described in the Quick Start Guide
) forward in time under different vaccination strategies. Specifically, we compare daily cases, hospital admissions, and intensive care (IC) admissions for vaccination in adults only, those 12 years and above, and those 5 years and above. All of the functions used here are documented, so we encourage users to refer to the helps files.
This vignette demonstrates only how to reproduce the forward simulations. Code to reproduce figures from the main text in the accompanying paper can be found in the inst/extdata/scripts/manuscript
folder of the package.
vacamole
may be installed from github using the devtools
package. There are a number of additional packages that we need for this analysis.
# Required to run vacamole devtools::install_github("kylieainslie/vacamole") library(vacamole) ## Required for this analysis library(deSolve) library(reshape2) library(ggplot2) library(dplyr) library(stringr) library(tidyr) library(readxl) library(rARPACK) library(readr) library(lubridate) library(kableExtra)
A synthetic vaccination s
First we need to load the vaccination schedules for the different vaccination strategies: adults only (18+), extension to adolescents (12+), and extension to adolescents and children (5+). These files are located in the inst/extdata/inputs
folder of the package. Each file includes a column for vaccine product and dose for each age group (see Quick Start Guide
). Rows indicate the proportion of that age group who received that dose and product on that day.
basis_5plus <- read_csv("../inst/extdata/inputs/vac_schedule_5plus.csv") %>% select(-starts_with("...1")) basis_12plus <- read_csv("../inst/extdata/inputs/vac_schedule_12plus.csv") %>% select(-starts_with("...1")) basis_18plus <- read_csv("../inst/extdata/inputs/vac_schedule_18plus.csv") %>% select(-starts_with("...1"))
Next, the vaccination schedules needs to be converted before input into the model. The vaccination schedule data frame is converted into a list with weighted averages of the following quantities: vaccination rate, vaccine effectiveness, and delay to protection based on the number of people in the population who receive each dose and type of vaccine at each time point. The model is designed to incorporate a single vaccine product with a 2-dose regimen that 1) reduces susceptibility to infection, 2) reduces risk of hospitalization if a vaccinated individual is infected, and 3) reduces risk of infecting others (transmission) if a vaccinated person is infected. Therefore we need to provide estimates for vaccine effectiveness against infection, hospitalisation, and transmission (if infected). Since there is more than one vaccine product currently licensed for use against COVID-19 in The Netherlands (the vaccines made by Pfizer/BioNTech, Moderna, AstraZeneca, and Janssen), we incorporate different vaccine products by taking the daily weighted average of the number of people with each vaccine product (and dose), the corresponding delay to protection of each vaccine product, and the vaccine effectiveness against each outcome (Table S1). Janssen is incorporated by using zero for the number of second doses at all time points.
# read in vaccine effectiveness parameters ------------------- # these will be used when converting the vaccine schedule into # weighted vaccine effectiveness, delay to protection, and # vaccination rate ve_info <- readRDS("../inst/extdata/inputs/ve_params.rds") # convert vaccination schedules ------------------------------ # the vaccination schedule is assumed to be cumulative over # time # 5+ -------------------------------------------------------- converted_5plus <- convert_vac_schedule( vac_schedule = basis_5plus, ve = ve_info[[1]], delay = ve_info[[2]], hosp_multiplier = ve_info[[3]], ve_trans = ve_info[[4]], add_child_vac = TRUE ) # 12+ ------------------------------------------------------- converted_12plus <- convert_vac_schedule( vac_schedule = basis_12plus, ve = ve_info[[1]], delay = ve_info[[2]], hosp_multiplier = ve_info[[3]], ve_trans = ve_info[[4]] ) # 18+ ------------------------------------------------------- converted_18plus <- convert_vac_schedule( vac_schedule = basis_18plus, ve = ve_info[[1]], delay = ve_info[[2]], hosp_multiplier = ve_info[[3]], ve_trans = ve_info[[4]] )
The model expects a named list of parameter values (Table 1) describing the rates of transitioning between compartments, the converted vaccine schedule, and contact patterns. The model allows the user to specify a threshold of either cases (use_cases = TRUE
) or IC admissions to determine which contact patterns to use. There are four different patterns the user can specify: normal (pre-pandemic), very relaxed, relaxed, and lockdown. These are specified using the c_normal
, c_very_relaxed
, c_relaxed
, and c_lockdown
arguments. The thresholds for changing the contact patterns are specified using thresh_n
, thresh_l
, thresh_m
, and thresh_u
arguments. If the user does not want the contact patterns to change, they can specify keep_cm_fixed = TRUE
and specify the contact patterns to use with the c_start
argument. Many of the parameter values used for this analysis are located in the inst/extdata/inputs
folder of the package.
params_table <- read_xlsx("parameter_description_table.xlsx") %>% rename(Parameter = parameter, Type = type, Description = description) params_table %>% kbl() %>% kable_styling()
# read in transition rates ----------------------------------- transition_rates <- readRDS("../inst/extdata/inputs/transition_rates.rds") # read in transmission matrices ------------------------------ april_2017 <- readRDS("../inst/extdata/inputs/contact_matrices/converted/transmission_matrix_april_2017.rds") april_2020 <- readRDS("../inst/extdata/inputs/contact_matrices/converted/transmission_matrix_april_2020.rds") june_2020 <- readRDS("../inst/extdata/inputs/contact_matrices/converted/transmission_matrix_june_2020.rds") september_2020 <- readRDS("../inst/extdata/inputs/contact_matrices/converted/transmission_matrix_september_2020.rds") february_2021 <- readRDS("../inst/extdata/inputs/contact_matrices/converted/transmission_matrix_february_2021.rds") june_2021 <- readRDS("../inst/extdata/inputs/contact_matrices/converted/transmission_matrix_june_2021.rds") # create list of transmission matrices ----------------------- cm <- list(april_2017 = april_2017, april_2020 = april_2020, june_2020 = june_2020, september_2020 = september_2020, february_2021 = february_2021, june_2021 = june_2021) # define population size (by age group) ---------------------- age_dist <- c(0.10319920, 0.11620856, 0.12740219, 0.12198707, 0.13083463,0.14514332, 0.12092904, 0.08807406, 0.04622194) # source: CBS n <- 17407585 # Dutch population size n_vec <- n * age_dist # create named list of parameter ----------------------------- params <- list(beta = 0.0004 , # transmission rate beta1 = 0.14, # amplitude of seasonal forcing gamma = 0.5, # 1/gamma = infectious period sigma = 0.5, # 1/sigma = latent period epsilon = 0.01, # case importation rate N = n_vec, # population size vector (by age group) h = transition_rates$h, # rate from infection to hospital admission i1 = transition_rates$i1, # rate from hospital to IC i2 = transition_rates$i2, # rate from IC back to hospital d = transition_rates$d, # rate from hospital to death d_ic = transition_rates$d_ic, # rate from IC to death d_hic = transition_rates$d_hic, # rate from hospital (after returning from IC) to death r = transition_rates$r, # rate of recovery from hospital r_ic = transition_rates$r_ic, # rate of recovery from hospital (after IC) p_report = 1/3, # case ascertainment rate c_start = june_2021, c_lockdown = february_2021, c_relaxed = june_2020, c_very_relaxed = june_2021, c_normal = april_2017, keep_cm_fixed = FALSE, # if true c_start is used for entire simulation, else not vac_inputs = basis_18plus, # converted vaccine schedule - rates per day for doses 1,2 and VE for each day (infection, transmission, hospitalization), delay to protection use_cases = TRUE, # if true threshold is for cases, if false threshold is for ic thresh_n = 0.5/100000 * sum(n_vec), # number of cases per 100000: threshold for normal contact matrix thresh_l = 5/100000 * sum(n_vec), # government threshold thresh_m = 14.3/100000 * sum(n_vec), # government threshold thresh_u = 100000/100000 * sum(n_vec), # no lockdown unless everyone infected no_vac = FALSE, # if TRUE, no vaccination is assumed t_calendar_start = yday(as.Date("2020-01-01")), # calendar start date (ex: if model starts on 31 Jan, then t_calendar_start = 31) beta_change = NULL # value to change beta to after normal contact patterns are initiated )
The model is partitioned into nine age groups, so users need to supply the initial number of individuals in each age group for each compartment. The initial conditions should be a named list. For the forward simulations we use the number of individuals in each compartment on the last day of the model fit as the initial conditions. Here, we read in previously performed model fits. A separate vignette details the model fit procedure.
# specify file path where model fit outputs are stored -------- file_path <- "../inst/extdata/results/model_fits/" date_of_fit <- "2021-10-01" # this specifies the suffix on the model fit file output_from_model_fit <- readRDS(paste0(file_path,"output_from_fits_", date_of_fit, ".rds")) # get the number of individuals in each state on the last day # of model fit (22 June 2021) --------------------------------- # these will be used as the initial conditions init_cond_22june2021 <- unlist(lapply(unname(output_from_model_fit$`end_date_2021-06-22`), tail, 1))
For the beginning of the forward simulations we use the transmission rate from the last time window of the model fit. However, once non-pharmaceutical interventions (NPIs) are relaxed we need a transmission rate that was not estimated during a time period in which NPIs were in use. Thus, for forward simulations, we use the transmission rate that corresponds to the basic reproduction number of the Delta variant ($R_0 = 4.6$). To incorporate uncertainty, we draw random samples of the reproduction number from a normal distribution with mean 4.6 and standard deviation equal to the square root of the variance from the model fits of the last time window before the forward simulations.
# read in MLE of transmission rate from model fit ------------- beta_mles <- data.frame(beta = readRDS(paste0(file_path, "mles_from_fits_", date_of_fit, ".rds"))) %>% mutate(end_date = names(output_from_model_fit)) beta_draws <- readRDS(paste0(file_path, "beta_draws_from_fits_", date_of_fit, ".rds")) index <- which(beta_mles$end_date == "end_date_2021-06-22") # determine Rt and beta for forward simulations --------------- my_sd <- sqrt(0.0000945) # sqrt(var) of Rt from fits to last time window S_diag <- diag(init_cond_22june2021[c(2:10)]) # diagonal matrix of susceptibles rho <- as.numeric(eigs(S_diag %*% params$c_normal, 1)$values) # parameter to convert Rt to beta # R draws ----------------------------------------------------- rt_for_delta_period <- rnorm(n = 200, mean = 4.6, sd = my_sd) # R_0=2.3 => beta = 0.0003934816 * 2 = 0.0007869632 (follows from different analysis) # convert to beta --------------------------------------------- betas <- c(0.0007869632, (rt_for_delta_period / rho) * params$gamma)
Now that we have prepared all of the model inputs, we can now run the model for each vaccination scenario for different values of the transmission rate (beta) and the contact patterns using forward_sim_wrap_func()
. Results are saved to the current working directory with the file name specified by the tag
argument.
# ------------------------------------------------------------- # run models -------------------------------------------------- # ------------------------------------------------------------- todays_date <- Sys.Date() # 5+ ---------------------------------------------------------- forward_sim_func_wrap(params = params, start_date = "2021-06-22", end_date = "2022-03-31", init_cond = init_cond_22june2021, beta_m = beta_mles[index, 1], vac_inputs = converted_5plus, beta_c = betas, beta_d = beta_draws[[index]][, 1], t_normal = yday(as.Date("2021-11-01")) + 365, # simulation is in days since start contact_matrices = cm, tag = paste0("results_5plus_", todays_date)) # 12+ -------------------------------------------------------- forward_sim_func_wrap(params = params, start_date = "2021-06-22", end_date = "2022-03-31", init_cond = init_cond_22june2021, beta_m = beta_mles[index, 1], vac_inputs = converted_12plus, beta_c = betas, beta_d = beta_draws[[index]][, 1], t_normal = yday(as.Date("2021-11-01")) + 365, contact_matrices = cm, tag = paste0("results_12plus_", todays_date)) # 18+ ------------------------------------------------------- forward_sim_func_wrap(params = params, start_date = "2021-06-22", end_date = "2022-03-31", init_cond = init_cond_22june2021, beta_m = beta_mles[index,1], vac_inputs = converted_18plus, beta_c = betas, beta_d = beta_draws[[index]][,1], t_normal = yday(as.Date("2021-11-01")) + 365, contact_matrices = cm, tag = paste0("results_18plus_",todays_date))
The output from the model runs needs quite a bit of data wrangling to get it into a form that's easy to plot. For simplicity, the code to perform the data wrangling is located in inst/extdata/scripts/manuscript
. Here, we will just load the already wrangled results and show the code to make figure 1 from the paper. The scripts to make the other figures are located in inst/extdata/scripts/manuscript
.
```r
all_res_for_plot <- readRDS("../inst/extdata/results/all_res_for_plot.rds")
fig1a <- ggplot(data = all_res_for_plot %>% filter(outcome == "Daily Cases", date >= as.Date("2021-11-01"), Immunity == "No Waning") %>% group_by(Scenario, age_group2, date, outcome) %>% summarise_at(.vars = c("mle", "lower", "upper"), .funs = "sum"), aes(x = date, y = mle, fill = age_group2,linetype = Scenario)) + geom_ribbon(aes(ymin = lower, ymax = upper, fill = age_group2), alpha = 0.3) + geom_line(aes(color = age_group2), size = 1) + scale_linetype_manual(values = c("dotted", "dashed", "solid")) + labs(y = "Cases per day", x = "Date of infection") + ylim(0,NA) + scale_x_date(date_breaks = "2 weeks", date_labels = "%d %b %Y") + theme(legend.position = "bottom", panel.background = element_blank(), axis.text.x = element_text(angle = 45, hjust = 1, size = 14), axis.text.y = element_text(size = 14), strip.text.x = element_text(size = 14), legend.text = element_text(size = 14), legend.title = element_text(size = 14), legend.box="vertical", axis.title=element_text(size=14,face="bold")) + guides(fill=guide_legend("Age Group"), colour = guide_legend("Age Group"), linetype = guide_legend("Strategy"))
table1 <- all_res_for_plot %>% filter(outcome != "Daily Deaths", Immunity == "No Waning") %>% group_by(Scenario, age_group2, outcome) %>% summarise_at(.vars = c("mle", "lower", "upper"), .funs = "sum")
table1a <- table1 %>% group_by(age_group2, outcome) %>% mutate(abs_diff = mle - mle[Scenario == "Vaccination of 18+"], abs_diff_lower = lower - lower[Scenario == "Vaccination of 18+"], abs_diff_upper = upper - upper[Scenario == "Vaccination of 18+"], perc_diff = (mle * 100)/mle[Scenario == "Vaccination of 18+"] - 100, perc_diff_lower = (lower * 100)/lower[Scenario == "Vaccination of 18+"] - 100, perc_diff_upper = (upper * 100)/upper[Scenario == "Vaccination of 18+"] - 100) %>% mutate_if(is.numeric, round, 1) %>% as.data.frame()
fig1_inset <- ggplot(data = table1a %>% filter(Scenario != "Vaccination of 18+"), aes(x = outcome, y = abs(perc_diff), fill = age_group2)) + geom_bar(stat = "Identity", position = position_dodge()) + geom_errorbar(aes(ymin = abs(perc_diff_upper), ymax = abs(perc_diff_lower), width = 0.2), position = position_dodge(0.9)) + labs(x = "", y = "Percent Reduction (%)", fill = "Age Group") + scale_x_discrete(labels = c("Daily\nCases", "Hospital\nAdmissions", "IC\n Admissions")) + facet_wrap(~Scenario, nrow = 2) + theme(legend.position = "bottom", panel.background = element_blank(), axis.text.x = element_text(size = 12), #angle = 45, hjust = 1, axis.text.y = element_text(size = 14), strip.text.x = element_text(size = 14), legend.text = element_text(size = 14), legend.title = element_text(size = 14), axis.title=element_text(size=14,face = "bold")) + guides(fill=guide_legend(nrow=2,byrow=TRUE))
fig1 <- fig1a + annotation_custom(ggplotGrob(fig1_inset), xmin = as.Date("2022-02-01"), xmax = as.Date("2022-04-04"), ymin = 15000, ymax = 100000) fig1
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.