License
+ +YEAR: 2024 +COPYRIGHT HOLDER: epidemics authors ++ +
diff --git a/dev/.nojekyll b/dev/.nojekyll new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dev/.nojekyll @@ -0,0 +1 @@ + diff --git a/dev/LICENSE-text.html b/dev/LICENSE-text.html new file mode 100644 index 00000000..26505018 --- /dev/null +++ b/dev/LICENSE-text.html @@ -0,0 +1,105 @@ + +
YEAR: 2024 +COPYRIGHT HOLDER: epidemics authors ++ +
LICENSE.md
+ Copyright (c) 2024 epidemics authors
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+vignettes/design-principles.Rmd
+ design-principles.Rmd
This vignette outlines the design decisions that have been taken during the development of the epidemics R package, and provides some of the reasoning, and possible pros and cons of each decision.
+This document is primarily intended to be read by those interested in understanding the code within the package and for potential package contributors.
+epidemics aims to help public health practitioners - rather than research-focussed modellers - to rapidly simulate disease outbreak scenarios, and aims to balance flexibility, performance, user-friendliness, and maintainability.
+epidemics trades away some flexibility in defining model structures for a gain in the ease of defining epidemic scenario components such as affected populations and model events.
epidemics attempts to balance performance and maintainability through minimum sufficient use of C++, and not attempting to write a domain-specific language (such as odin).
To be more broadly applicable, epidemics provides a ‘library’ of compartmental models which are adapted from the published literature. These models focus on broad types of diseases or settings, rather than specific diseases or scenarios. Thus all models are intended to be applicable to a range of diseases.
All function outputs are expected to return a structure that inherits from <data.frame>
, to make further processing easier for users.
+The exact structure of the output - whether a list of <data.frame>
s, a <data.table>
or <tibble>
, or a nested version of these tabular data classes - is yet to be fixed.
+The eventual stable output type must allow users to conveniently access the epidemic trajectory data, identify and filter intervention scenarios for comparisons among them, and allow interoperability with data science tools such as the Tidyverse.
Fig. 1: epidemics is designed to allow easy combination of composable elements with a model structure taken from a library of published models, with sensible default parameters, to allow public health practitioners to conveniently model epidemic scenarios and the efficacy of response strategies.
+ +Fig. 2: epidemics package architecture, and the ODE model stack. epidemics includes multiple internal functions in both R and C++ which check that inputs are suitable for each model, and to cross-check that inputs are compatible with each other. Function names indicate their behaviour (e.g. assert_*()
), but not all similarly named functions are called at similar places in model function bodies. Partly, this reflects the privileged positions of some model components (such as the population
, against which other components are checked), but also that some composable elements (such as time-dependence) may not be vectorised.
The notes here refer to broad decisions and not to individual models – see individual model vignettes and published sources for modelling decisions.
+There are two broad types of models:
+Deterministic models implemented as solutions to systems of ordinary different equations (ODEs),
Stochastic models with discrete timesteps where individuals move between compartments probabilistically.
epidemics models’ compartmental transitions are fixed. Users cannot create new links between compartments, although links between compartments can be disallowed by modifying model parameters.
The models’ epidemiological parameter sets are unique, although some parameters are shared among models. These can be changed by users although reasonable default values are provided.
The model components - such as the population affected by the epidemic, or any policy responses in the form of interventions - are called composable elements. Composable elements can be and are expected to be modified by users to match their situation. All model components except the population
are optional.
Each model allows a unique sets of composable elements considered suitable for the expected use case. E.g. the Ebola model does not allow modelling a vaccination regime, as this is considered unlikely for most Ebola outbreaks. The allowed set of composable elements is subject to change, and is open to user requests.
The effect of interventions is modelled as being additive, not multiplicative. When an intervention with an X% reduction in contacts overlaps with an intervention with a Y% reduction, the cumulative effect on contacts \(C\) is \(C \times 1 - (X + Y)\), rather than \(C \times (1 - X)(1 - Y)\). Additive effects were considered easier for users to understand than multiplicative effects.
A common method for defining and solving ODEs in R is to write the ODE system in R and pass it to solvers such as deSolve::lsoda()
from the deSolve package. We have opted against this approach (referred to as ‘R-deSolve’).
It is possible to write ODE compartmental transitions in C++, and expose the C++ code to R, eventually passing this ODE system to deSolve solvers (referred to as ‘Rcpp-deSolve’). We have also opted against this approach, as it involves substantial interconversion of more complex objects such as structured lists (the model composable elements) from R to C++ in each solver timestep, reducing performance.
ODE systems are written in C++, and solved using the Boost odeint solvers with fixed step sizes. This means that within each call to model_*()
, R objects are converted for use by C++ only once (in the scalar arguments case), and are not passed back and forth multiple times.
odeint is provided by the package BH (Boost Headers), making it convenient to use in Rcpp packages. The r2sundials package provides an Rcpp-friendly interface to the SUNDIALS ODE solvers, but the documentation is less clear overall, and seems to recommend the use of RcppArmadillo and RcppXPtrUtils; these have not been evaluated for use.
odeint imposes certain constraints on ODE systems. There is no functionality to easily define ‘events’ (also called ‘callbacks’) and combine them with the ODE system. This means that the ODE systems have to encapsulate information on the timing of events (such as interventions) and the effect.
Equations describing the right-hand side of model ODE systems \(x' = f(x)\) are thus written as structs
with an overloaded function call operator that makes them FunctionObject
types. The struct
thus holds epidemiological parameters and composable elements, which are passed by reference in any operations, reducing copying. See this simple example from the Boost documentation. It can be passed as a function to a solver, and the epidemiological parameters are calculated in each solver timestep as some function of time and the composable elements (as applicable).
epidemics relies on the Eigen C++ library provided by RcppEigen to represent matrices of initial conditions. Eigen matrices are among the types accepted as initial states by odeint solvers. Alternatives include Boost Basic Linear Algebra Library (uBLAS) and standard library arrays and vectors; a partial list - which does not include Eigen - is provided by Boost, and Armadillo matrices may also be supported. We use Eigen matrices as Eigen offers broad and well documented support for matrix operations, easy conversion between Rcpp types, and also conforms to previous use of Eigen in finalsize.
The models’ C++ code is in two levels that underlie the R source code - the C++ source code and the C++ headers. This is to make the header code (the model ODEs and helper functions) shareable so that they can be re-used in other Rcpp packages and this is explored more in this blog post.
epidemics defines C++ namespaces in the package headers to more clearly indicate where certain functionalities sit. All model structures are in the namespace epidemics
, while the namespaces vaccination
, intervention
, and time_dependence
contain C++ code to handle these composable elements (such as calculating the effect of cumulative interventions). The namespace odetools
defines the initial state type, which is an Eigen::MatrixXd
.
epidemics currently includes a single discrete-time stochastic model that implements a sub-compartment system taken from @getz2018, and initially adapted in Epirecipes. +Some modelling decisions are explained here, while recognising that adding more models with different implementations could see them being revisited.
+Stochastic models currently target, and are expected to target, diseases for which outbreak sizes are initially small and thus where stochasticity matters more; the typical example is Ebola virus disease (simply, Ebola), and this is reflected in the model name.
The Ebola model is also expected to be suitable for diseases with a high fatality risk that are detected when outbreak sizes are small, and with an isolation and treatment response comparable to Ebola, e.g. Marburg virus disease.
Note that very different alternative implementations of stochastic models exist.
The current implementation was chosen due to the use of Erlang sub-compartments that were felt to better represent the long-tailed distributions of infection times seen in haemorrhagic fevers like Ebola.
The model implements a two-level structure similar to the ODE models, with .model_ebola_internal()
the function implementing the simulation, while model_ebola()
is the user-facing wrapper with input checks and parameter and scenario combination handling.
The model allows vectors for the infection parameters and the duration (called time_end
); however the number of replicates for each parameter-scenario combination is not allowed to vary. A default of 100 was chosen as a reasonable middle ground between efficiency and realism.
The model allows lists of intervention sets and time-dependence functions to be passed.
+Only interventions on model parameters are allowed, and interventions on social contacts are not allowed, as the model does not include age-stratification in transmission (or otherwise).
The parameters infectiousness_rate
(often denoted \(\sigma\)) and removal_rate
(recovery or death with safe burial, \(\gamma\)) cannot be targeted by rate interventions or time-dependence as these values determine the number of sub-compartments in the exposed and infectious (and hospitalised) compartments, respectively. These are fixed at the start of the simulation, and changing them partway is challenging to support as it would involve redistributing individuals currently in any sub-compartment into more or fewer sub-compartments.
The model supports and defaults to multiple replicates or runs for each parameter-scenario combination.
+For any \(N\) runs with a single parameter set and a single intervention set, runs will differ due to stochasticity (essentially, as different random numbers are drawn for each run).
For any \(N\) runs with multiple parameter sets and a single intervention set, runs will differ due to stochasticity within parameter sets, but any differences between runs across parameter sets (e.g. two different transmission rates) will be due to differences in parameter values alone.
For multiple runs of multiple parameter-scenario combinations, differences among runs will be due to differences in parameters and composable elements (such as interventions) alone.
The withr package is used to ensure that seeds are preserved across parameter sets and interventions.
The input and output types correspond to the ODE model types.
The model is written in R although this may change in future. The main reason for keeping the implementation in R is that the stats::rmultinom()
function is a very efficient way of drawing from a categorical distribution (often called a discrete distribution) with heterogeneous probabilities. This StackOverflow question from 2014 has more details including benchmarks against other implementations using Rcpp; though old, we have benchmarked a minimal example using Boost and found that this information is still valid.
The major composable elements are bundled into custom S3 classes that inherit from lists, and which are expected to be understandable elements of epidemic response: <population>
, the <intervention>
superclass, and <vaccination>
. All other composable elements take the form of named, structured lists. These are not defined as classes because they are expected to be less used, or used by more advanced modellers who are comfortable working with lists directly.
A key element of composable elements being or inheriting from lists is that they can be easily handled within the C++ code as they are interpreted as Rcpp lists.
All matrix objects referring to demography-group coefficients follow the ‘demography-in-rows’ pattern from finalsize, where rows represent demography groups, and columns represent coefficients. This includes the initial conditions matrix in <population>
s, where columns are epidemiological compartments, but also vaccination rates in <vaccination>
, and contacts reductions in <contacts_intervention>
s.
Interventions may be of the <contacts_intervention>
or <rate_intervention>
class, which inherit from the abstract super-class <intervention>
. This inheritance structure was chosen to maintain coherence between the intervention types, and to keep the option open of unifying these classes into a single concrete type in the future.
All composable elements except the population
are optional. Model functions internally generate dummy values for the other composable elements allowed for each model, as these are required for the C++ code.
<intervention>
and <vaccination>
objects may be combined with objects of the same (sub)-class using the c()
method for each (sub)-class, and resulting in an object of the same (sub)-class. The interpretation for each class is however slightly different:
<intervention>
: Combining two interventions of the same sub-class is understood to represent the combined application of multiple epidemic response strategies, e.g. closing schools and also closing workplaces, or introducing mask mandates over different intervals.
<vaccination>
: Combining two vaccination objects results in a two-dose vaccination regime, rather than two separate vaccination campaigns (each delivering a single dose). This is because epidemics focuses on the initial pandemic response phase where vaccination is not expected to be available at scale, rather than long-term vaccination campaigns against endemic infections.
epidemics is moving towards allowing vectors or lists to be passed as model function arguments, to easily allow the incorporation of parameter uncertainty and comparisons of multiple scenarios (while maintaining comparability across scenarios in each function call). Consequently, each function call may consist of many 1000s of model runs (see PR #176 and the vignette on scenario modelling).
+All model epidemiological parameters are allowed to be passed as numeric vectors (currently only ODE models). This includes time_end
, which allows runs of different durations within a single function call. This use case was taken from this report which aimed to calculate the epidemic size over a given period with uncertainty in the start time (and hence the duration). epidemics follows the Tidyverse rules on vector recycling for epidemiological parameters.
Arguments intervention
and vaccination
accepting composable elements are allowed to be passed as lists of inputs. In the case of intervention
, this is a list of lists, with each element a list of <intervention>
objects (typically one <contacts_intervention>
and a variable number of <rate_intervention>
s): this is referred to as an intervention set. All other composable elements may currently only be scalar values, but this may change in future (see issue #181 for passing lists of populations).
Model functions internally create combinations of composable elements, and each such combination is referred to as a scenario, and also combine each scenario with each set of epidemiological parameters. This is to ensure comparability across scenarios.
epidemics follows the finalsize example in following Google’s C++ code style, and in using Cpplint and Cppcheck for static code analysis as options such as Clang-tidy do not work well with Rcpp code; see this blog post for more.
Function naming: Function names aim to contain verbs that indicate the function behaviour. Internal input checking functions follow the checkmate naming style (e.g. assert_*()
).
Function naming: Internal functions aim to begin with a .
(e.g. .model_default_cpp()
) to more clearly indicate that they are not for direct use.
The aim is to restrict the number of hard dependencies while maintaining user-friendliness.
+A wider range of packages are taken on as soft dependencies to make the vignettes more user-friendly.
+There are no special requirements to contributing to epidemics, but contributions of new models should be clearly motivated, fall within the scope of the package, target a disease type or situation that is not already covered by existing models, and ideally should already be peer-reviewed. +In general, please follow the package contributing guide.
+vignettes/epidemics.Rmd
+ epidemics.Rmd
This initial vignette shows to get started with using the epidemics package.
+Further vignettes include guidance on “Modelling the implementation of vaccination regimes”, as well as on“Modelling non-pharmaceutical interventions (NPIs) to reduce social contacts” and “Modelling multiple overlapping NPIs”.
+There is also guidance available on specific models in the model library, such as the Vacamole model developed by RIVM, the Dutch Institute for Public Health.
+Prepare population and contact data.
+Note that the social contacts matrices provided by the socialmixr package follow a format wherein the matrix \(M_{ij}\) represents contacts from group \(i\) to group \(j\).
+However, epidemic models traditionally adopt the notation that \(M_{ij}\) defines contacts to \(i\) from \(j\) (Wallinga, Teunis, and Kretzschmar 2006).
+\(q M_{ij} / n_i\) then defines the probability of infection, where \(q\) is a scaling factor dependent on \(R_0\) (or another measure of infection transmissibility), and \(n_i\) is the population proportion of group \(i\). +The ODEs in epidemics also follow this convention.
+For consistency with this notation, social contact matrices from socialmixr need to be transposed (using t()
) before they are used with epidemics.
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 40),
+ symmetric = TRUE
+)
+#> Removing participants that have contacts without age information. To change this behaviour, set the 'missing.contact.age' option
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
+
+# view contact matrix and demography
+contact_matrix
+#>
+#> contact.age.group [,1] [,2] [,3]
+#> [0,20) 7.883663 2.794154 1.565665
+#> [20,40) 3.120220 4.854839 2.624868
+#> 40+ 3.063895 4.599893 5.005571
+
+demography_vector
+#> [0,20) [20,40) 40+
+#> 14799290 16526302 28961159
Prepare initial conditions for each age group.
+
+# initial conditions
+initial_i <- 1e-6
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
+
+# view initial conditions
+initial_conditions
+#> S E I R V
+#> [0,20) 0.999999 0 1e-06 0 0
+#> [20,40) 0.999999 0 1e-06 0 0
+#> 40+ 0.999999 0 1e-06 0 0
Prepare a population as a population
class object.
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
+
+uk_population
+#> <population> object
+#>
+#> Population name:
+#> "UK"
+#>
+#> Demography
+#> [0,20): 14,799,290 (20%)
+#> [20,40): 16,526,302 (30%)
+#> 40+: 28,961,159 (50%)
+#>
+#> Contact matrix
+#>
+#> contact.age.group [0,20) [20,40) 40+
+#> [0,20) 7.883663 2.794154 1.565665
+#> [20,40) 3.120220 4.854839 2.624868
+#> 40+ 3.063895 4.599893 5.005571
+# run an epidemic model using `epidemic`
+output <- model_default(
+ population = uk_population,
+ time_end = 600, increment = 1.0
+)
Plot epidemic over time, showing only the number of individuals in the exposed and infected compartments.
+
+# plot figure of epidemic curve
+filter(output, compartment %in% c("exposed", "infectious")) %>%
+ ggplot(
+ aes(
+ x = time,
+ y = value,
+ col = demography_group,
+ linetype = compartment
+ )
+ ) +
+ geom_line() +
+ scale_y_continuous(
+ labels = scales::comma
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ expand_limits(
+ y = c(0, 500e3)
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )
vignettes/finalsize_comparison.Rmd
+ finalsize_comparison.Rmd
New to epidemics? It may help to read the “Get started” vignette first!
+Additionally, read how to quickly calculate the final size of an epidemic - with fewer epidemiological inputs than epidemics required - the finalsize package, which uses an analytical solution to avoid simulating the full epidemic dynamics.
+The total number of individuals expected to have been infected over an epidemic is called the epidemic final size (Miller 2012). +The R package finalsize can help to calculate the epidemic final sizes using the final size equation (Miller 2012), while accounting for heterogeneity in population contacts as well as susceptibility to infection, such as due to vaccination.
+epidemics can also be used to calculate the final size of an outbreak while accounting for heterogeneity in contacts and the rollout of vaccination campaigns.
+This vignette lays out which of the two is more suitable for specific tasks, and how to convert between when seeking more insight into the effect of policy decisions made during epidemic response.
+While both finalsize and epidemics can be used to calculate the epidemic final size, they are aimed at different use cases.
+finalsize assumes that both infection and population characteristics are fixed to their initial conditions. +For the infection, this includes properties such as the transmission rate, while for the population, it includes social contacts between demographic groups, and the proportion of demographic groups that have specific levels of susceptibility to infection.
+An advantage is that finalsize only requires that we define the initial transmission rate of the infection and susceptibility of the population, rather than all the time-dependent processes that drive the epidemic shape, such as the duration of infectiousness or latent period of the infection. For questions relating to epidemic shape, rather than shape, this results in a much simpler set of inputs.
+However, it also means that finalsize cannot be used to model temporal dynamics of epidemic response, or be used to answer policy questions with a temporal component, such as when to implement interventions.
+epidemics includes a number of scenario models, each with its own assumptions. Most models allow for some modification of the initial characteristics of the outbreak due to a range of events. For the infection, this includes interventions (such as masking or treatments) that reduce the number of forward transmissions or deaths, but also seasonal effects which may increase or decrease the transmission rate. +For the population, initial conditions of social contacts can be influenced by interventions as well.
+This makes it much easier to model the temporal dynamics of public-health policy decisions which are taken during epidemic response as epidemics has many more features that allow for such modelling.
+However, it requires more inputs to be defined, including time-dependent infections processes. It can also be more difficult to model scenarios in which more complicated susceptibility structure is required, such as when some demographic groups have underlying immunity to infection due to past exposure or vaccination. +Thus epidemics is likely to be especially useful for outbreaks of novel pathogens (such as the Covid-19 pandemic) where there is little population immunity to infection.
+It is easier to configure finalsize out-of-the-box for scenarios with complex demographic patterns of underlying susceptibility to (or immunity against) infection, potentially due to a history of previous outbreaks and the policy responses (such as vaccination). Thus, while it cannot model temporal dynamics, it can quickly provide useful initial estimates of the final size of outbreaks, without having to write compartmental models which implement multiple policy decisions.
+Here, we show an example in which we show how to model a similar scenario using both finalsize and epidemics. For example, we might want to study the full epidemic dynamics in future, but start with a simpler final size estimate in the meantime. +Here we show how to build equivalent models, while allowing extensions to model epidemic temporal dynamics (using epidemics) later.
+As an illustration, we use epidemics to model the effect of vaccination on the trajectory of an epidemic. +While finalsize does not allow for the implementation of a dynamic vaccination calendar, we can model reduced susceptibility to infection in the population, such as due to prior vaccination.
+The two methods are comparable if we model vaccination in epidemics as occurring before the main wave of the epidemic, as this sets up underlying susceptibility.
+We first prepare the population (modelled on the U.K.), initial conditions, and model parameters, before passing them to epidemics::model_default()
.
+This example does not include any interventions or vaccination regimes.
Code to prepare model inputs is folded below for brevity. See the “Get started” vignette for an explanation of how to prepare these inputs.
+
+# load contact and population data from socialmixr::polymod
+polymod <- polymod
+contact_data <- contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 40),
+ symmetric = TRUE
+)
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
+
+# initial conditions: one in every 1 million is infected
+initial_i <- 1e-6
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+rownames(initial_conditions) <- rownames(contact_matrix)
+# prepare the population to model as affected by the epidemic
+# the contact_matrix, demography_vector, and initial_conditions
+# have been prepared in the folded code above
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
+
+# view the population
+uk_population
+#>
+#> Population name:
+#> Demography
+#> [0,20): 14,799,290 (20%)
+#> [20,40): 16,526,302 (30%)
+#> 40+: 28,961,159 (50%)
+#>
+#> Contact matrix
+#>
+#> contact.age.group [0,20) [20,40) 40+
+#> [0,20) 7.883663 2.794154 1.565665
+#> [20,40) 3.120220 4.854839 2.624868
+#> 40+ 3.063895 4.599893 5.005571
We model the spread of influenza with pandemic potential, assuming $R_0 = $ 1.5, a pre-infectious period of 3 days, and an infectious period of 7 days.
+This leads to the following model parameters for transmission rate \(\beta\), the rate of transition from exposed to infectious \(\sigma\), and the recovery rate \(\gamma\).
+
+# simulate pandemic parameters
+transmission_rate <- 1.5 / 7
+infectiousness_rate <- 1 / 3
+recovery_rate <- 1 / 7
For simplicity, we implement a vaccination regime in which vaccines are delivered to individuals over the age of 40 over the course of roughly six months (150 days) before the main epidemic peak. +We assume that 1 in every 1000 individuals in this age group is vaccinated every day; this translates to approximately 28,900 individuals per day.
+We assume that the vaccination is non-leaky, protecting vaccinated individuals from infection, and this due to the model structure of the ‘default’ SEIR-V model in epidemics that we use here.
+New to modelling vaccination using epidemics? It may help to read the “Modelling a vaccination campaign” vignette first.
+We first create a <vaccination>
object to represent this vaccination regime.
We then pass this vaccination regime to the epidemic function in the optional vaccination
argument, and model 600 days of the epidemic.
+We obtain the ‘final’ epidemic size using epidemics::epidemic_size()
.
+# model epidemic with vaccination prior to the main epidemic wave
+output <- model_default(
+ population = uk_population,
+ transmission_rate = transmission_rate,
+ infectiousness_rate = infectiousness_rate,
+ recovery_rate = recovery_rate,
+ vaccination = vaccinate_elders,
+ time_end = 600, increment = 1.0
+)
+
+# Calculate the epidemic size using the helper function
+finalsize_dat <- tibble(
+ demography_group = names(demography_vector),
+ value = epidemic_size(output) / demography_vector
+)
+
+# View the data
+finalsize_dat
+#> # A tibble: 3 × 2
+#> demography_group value
+#> <chr> <dbl>
+#> 1 [0,20) 0.617
+#> 2 [20,40) 0.525
+#> 3 40+ 0.343
To compare the results of the ODE model against the analytical method, it is necessary to set up the population’s susceptibility and demography-in-susceptibility matrices to reflect the proportion of individuals in each age group vaccinated before the outbreak.
+To do this, we need to calculate the total proportion of individuals in each age group vaccinated at the end of the epidemic model run.
+ +We use the proportion of individuals vaccinated in each age group to set up matrices to pass to the susceptibility
and p_susceptibility
arguments of finalsize::final_size()
.
New to final size estimation with heterogeneity in susceptibility to infection? It may help to read the “Modelling heterogeneous susceptibility” vignette, and the “Guide to constructing susceptibility matrices” for finalsize first.
+We create the susceptibility matrix to have two groups, ‘unvaccinated’, with full susceptibility, and ‘vaccinated’, who are immune to infection.
+We then create the demography-in-susceptibility matrix to reflect that only some individuals in the >40 age group are vaccinated. +Since vaccination has not been implemented for other age groups, we assume that all individuals in those groups are fully susceptible.
+
+# Second column holds the vaccinated (who are protected fully)
+susceptibility[, "vaccinated"] <- 0
+
+# Assume susceptibility varies within age groups
+p_susceptibility <- matrix(
+ data = 1.0,
+ nrow = length(demography_vector),
+ ncol = 2,
+ dimnames = list(
+ names(demography_vector),
+ c("unvaccinated", "vaccinated")
+ )
+)
+p_susceptibility[, "vaccinated"] <- p_vacc
+p_susceptibility[, "unvaccinated"] <- 1 - p_vacc
We then calculate the expected final size using the modified susceptibility matrices, and combine this data with the output of the ODE model as in the previous example.
+We need to scale the contact matrix appropriately.
+
+# Calculate the proportion of individuals infected in each age group
+dat1 <- final_size(
+ r0 = 1.5,
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ susceptibility = susceptibility,
+ p_susceptibility = p_susceptibility
+)
+
+# Final size returns the proportion infected in each susceptibility group
+# (i.e. non vaccinated and vaccinated)
+# here we calculate the proportion of infected for the age group as a whole
+dat3 <- dat1 %>%
+ select(
+ -susceptibility,
+ final_size = p_infected
+ ) %>%
+ filter(susc_grp == "unvaccinated") %>%
+ select(-susc_grp)
+
+fs <- dat3$unvaccinated * p_susceptibility[, "unvaccinated"]
+
+finalsize_dat <- finalsize_dat %>%
+ select(demo_grp = demography_group, seir_v = value) %>%
+ left_join(dat3)
We can print the data to compare values, which are similar.
+
+finalsize_dat
+#> # A tibble: 3 × 3
+#> demo_grp seir_v final_size
+#> <chr> <dbl> <dbl>
+#> 1 [0,20) 0.617 0.660
+#> 2 [20,40) 0.525 0.542
+#> 3 40+ 0.343 0.272
However, note that while epidemics::epidemic_size()
can be used for any time point in the epidemic, finalsize::final_size()
only returns the size at the end of the epidemic, showing how epidemics is more suitable to examine temporal dynamics.
+# note that epidemic_size() returns the absolute values
+# epidemic size after 10% of the epidemic model run
+epidemic_size(data = output, stage = 0.1)
+#> [1] 574.4348 488.3563 585.5760
+
+# epidemic size at 50%
+epidemic_size(data = output, stage = 0.5)
+#> [1] 5565772 4890178 5254726
An important reason to use finalsize for final size calculations may be speed — finalsize::final_size()
is much faster than epidemics::epidemic_size()
applied to epidemics::model_default()
.
+This makes it much more suitable for high-performance applications such as fitting to data.
The benchmarking shows that the total time taken for 1,000 runs using both methods is very low, and both methods could be used to scan across multiple values of the input parameters, such as when dealing with parameter uncertainty.
+
+# run benchmarks using {bench}
+benchmark <- mark(
+ analytical_method = final_size(
+ r0 = 1.5,
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ susceptibility = susceptibility,
+ p_susceptibility = p_susceptibility
+ ),
+ ode_model = epidemic_size(
+ model_default(
+ population = uk_population,
+ transmission_rate = transmission_rate,
+ infectiousness_rate = infectiousness_rate,
+ recovery_rate = recovery_rate,
+ vaccination = vaccinate_elders,
+ time_end = 600, increment = 1.0
+ )
+ ),
+ iterations = 1000,
+ time_unit = "s",
+ check = FALSE
+)
+
+# view the total time for 1000 runs in seconds
+select(as_tibble(benchmark), expression, total_time)
+#> # A tibble: 2 × 2
+#> expression total_time
+#> <bch:expr> <dbl>
+#> 1 analytical_method 0.473
+#> 2 ode_model 11.0
Note that some model runs using epidemics that implement more complex compartmental structure, or multiple interventions and vaccination regimes are likely to be slower than the example shown here.
+However, users should choose finalsize only when the assumptions underlying a final size calculation are met. +For example, in cases where vaccinations are concurrent with a large number of infections, or when interventions are applied to reduce transmission, the final size assumptions are not met. +In these cases, users are advised to use a dynamical model such as those in epidemics instead.
+vignettes/model_diphtheria.Rmd
+ model_diphtheria.Rmd
New to epidemics? It may help to read the “Get started” vignette first!
+This vignette shows how to model an outbreak of diphtheria (or a similar acute directly-transmitted infectious disease) within the setting of a humanitarian aid camp, where the camp population may fluctuate due to external factors, including influxes from crisis affected areas or evacuations to other camps or areas. +In such situations, implementing large-scale public health measures such contact tracing and quarantine, or introducing reactive mass vaccination may be challenging.
+The vignette on modelling a vaccination campaign shows how to model the introduction of a mass vaccination campaign for a fixed, stable population.
+epidemics provides a simple SEIHR compartmental model based on Finger et al. (2019), in which it is possible to vary the population of each demographic group throughout the model’s simulation time and explore the resulting epidemic dynamics. +This baseline model only tracks infections by demographic groups, and does not include variation in contacts between demographic groups (e.g. by age or occupation), as contacts are likely to be less clearly stratified in a camp setting. +The baseline model also does not allow interventions that target social contacts (such as social distancing or quarantine), and does not include a ‘vaccinated’ compartment. It is therefore suited to analysis of a rapidly spreading infection prior to the introduction of any reactive vaccine campaign.
+However, the model does allow for seasonality in model parameters and interventions on model parameters. +Similarly, the model allows for a proportion of the initial camp population to be considered vaccinated and thus immune from infection.
+We create a population object corresponding to the Kutupalong camp in Cox’s Bazar, Bangladesh in 2017-18, rounded to the nearest 100, as described in Additional file 1 provided with Finger et al. (2019). +This population has three age groups, < 5 years, 5 – 14 years, \(\geq\) 15 years. +We assume that only one individual is infectious in each age group.
+
+# three age groups with five compartments SEIHR
+n_age_groups <- 3
+demography_vector <- c(83000, 108200, 224600)
+initial_conditions <- matrix(0, nrow = n_age_groups, ncol = 5)
+
+# 1 individual in each group is infectious
+initial_conditions[, 1] <- demography_vector - 1
+initial_conditions[, 3] <- rep(1, n_age_groups)
+
+# camp social contact rates are assumed to be uniform within and between
+# age groups
+camp_pop <- population(
+ contact_matrix = matrix(1, nrow = n_age_groups, ncol = n_age_groups),
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions / demography_vector
+)
+
+camp_pop
+#>
+#> Population name:
+#> Demography
+#> Dem. grp. 1: 83,000 (20%)
+#> Dem. grp. 2: 108,200 (30%)
+#> Dem. grp. 3: 224,600 (50%)
+#>
+#> Contact matrix
+#> Dem. grp. 1: Dem. grp. 2: Dem. grp. 3:
+#> Dem. grp. 1: 1 1 1
+#> Dem. grp. 2: 1 1 1
+#> Dem. grp. 3: 1 1 1
We assume, following Finger et al. (2019), that 20% of the 5 – 14 year-olds were vaccinated against (and immune to) diphtheria prior to the start of the outbreak, but that coverage is much lower among other age groups.
+
+# 20% of 5-14 year-olds are vaccinated
+prop_vaccinated <- c(0.05, 0.2, 0.05)
We run the model with its default parameters, assuming that:
+We also make several assumptions regarding clinical progression and reporting. Specifically: case reporting (that about 3% of infections are reported as cases), the proportion of reported cases needing hospitalisation (1%), the time taken by cases needing hospitalisation to seek and be admitted to hospital (5 days, giving a daily hospitalisation rate of 0.2 among cases), and time spent in hospital (5 days, giving a daily hospitalisation recovery rate of 0.2).
+Finally, we assume there are no interventions or seasonal effects that affect the dynamics of transmission during the outbreak. We then run the model and plot the outcomes.
+
+data <- model_diphtheria(
+ population = camp_pop,
+ prop_vaccinated = prop_vaccinated
+)
+filter(data, compartment == "infectious") %>%
+ ggplot() +
+ geom_line(
+ aes(time, value, colour = demography_group)
+ ) +
+ scale_y_continuous(
+ labels = scales::comma
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group",
+ labels = c("<5", "5-15", ">15")
+ ) +
+ expand_limits(
+ x = c(0, 101)
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ y = "Individuals infectious"
+ )
We now model the same outbreak, but an increase in susceptible individuals towards the end of the outbreak, to illustrate the effect that an influx of non-immune individuals could could have on outbreak dynamics. +In this example, we do not assume any prior immunity among new arrivals into the population.
+We prepare a population change schedule as a named list giving the times of each change, and the corresponding changes to each demographic group.
+Note that the model assumes that these changes apply only to the susceptible compartment, as we assume that the wider population entering the camp is not yet affected by diphtheria (i.e. no infected or recovered arrival), and that already infected or hospitalised individuals do not leave the camp.
+Here, the population size of the camp increases by about 75% overall, which is similar to reported values in the Kutupalong camp scenario (Finger et al. 2019).
+
+ggplot() +
+ geom_area(
+ data = data_pop_size,
+ aes(time, total_susceptibles / 10),
+ fill = "steelblue", alpha = 0.5
+ ) +
+ geom_line(
+ data = filter(data, compartment == "infectious"),
+ aes(time, value, colour = demography_group)
+ ) +
+ geom_vline(
+ xintercept = pop_change$time,
+ colour = "red",
+ linetype = "dashed",
+ linewidth = 0.2
+ ) +
+ annotate(
+ geom = "text",
+ x = pop_change$time,
+ y = 20e3,
+ label = "Population increase",
+ angle = 90,
+ vjust = "inward",
+ colour = "red"
+ ) +
+ scale_y_continuous(
+ labels = scales::comma,
+ sec.axis = dup_axis(
+ trans = function(x) x * 10,
+ name = "Individuals susceptible"
+ )
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group (individuals infectious)",
+ labels = c("<5", "5-15", ">15")
+ ) +
+ expand_limits(
+ x = c(0, 101)
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ y = "Individuals infectious"
+ )
This example shows how an increase in the number of susceptibles in the population can lead to a rise in the transmission potential of the infection and therefore extend the duration of an outbreak.
+vignettes/model_ebola.Rmd
+ model_ebola.Rmd
New to epidemics? It may help to read the “Get started” vignette first!
+This vignette shows how to model an epidemic in which stochasticity is expected to play an important role. +This is often the case with outbreaks of infections such as Ebola virus disease (EVD, or simply, ebola).
+epidemics includes a discrete time, stochastic compartmental model of ebola based on a consensus model presented in Li et al. (2019), with six compartments — susceptible, exposed, infectious, hospitalised, funeral, and removed. +The transitions between compartments are based on an Erlang model developed by Getz and Dougherty (2018) for the 2014 West African EVD outbreak, with the code adapted from Epirecipes.
+The model allows easily running multiple replicates, accepts infection parameter vectors to help model parameter uncertainty, and can accept lists of intervention sets to model response scenarios.
+Prepare population data — for this model, we only need the total population size and the initial conditions.
+We can create a <population>
object with dummy values for the contact matrix, and assign the total size to the demography vector.
We prepare a population with a total size of 14 million, corresponding to the population of Guinea, a West African country where the 2014 Ebola virus epidemic originated.
+We assume that 1 person is initially infected and infectious, and 10 other people have been exposed and is yet to become infectious.
+
+population_size <- 14e6
+
+# prepare initial conditions as proportions
+initial_conditions <- c(
+ S = population_size - 11, E = 10, I = 1, H = 0, F = 0, R = 0
+) / population_size
+# prepare a <population> object
+guinea_population <- population(
+ name = "Guinea",
+ contact_matrix = matrix(1), # note dummy value
+ demography_vector = 14e6, # 14 million, no age groups
+ initial_conditions = matrix(
+ initial_conditions,
+ nrow = 1
+ )
+)
+
+guinea_population
+#>
+#> Population name:
+#> Demography
+#> Dem. grp. 1: 14,000,000 (100%)
+#>
+#> Contact matrix
+#> Dem. grp. 1:
+#> Dem. grp. 1: 1
We use the default model parameters, beginning with an \(R_0\) taken from the value estimated for ebola in Guinea (Althaus 2014). +We assume that ebola has a mean infectious period of 12 days, and that the time between exposure and symptom onset — the pre-infectious period — is 5 days.
+Together, these give the following values:
+Transmission rate (\(\beta\)): 0.125,
Infectiousness rate (\(\gamma^E\) in Getz and Dougherty (2018)): 0.4,
Removal rate (\(\gamma^I\) in Getz and Dougherty (2018)): 0.1667
This model does not yet have an explicit “deaths” compartment, but rather, the “removed” compartment holds all individuals who have recovered and who have died and been buried safely. +Functionally, they are similar as they are not part of the model dynamics.
+We assume that only 10% of infectious individuals are hospitalised, and that hospitalisation achieves a modest 30% reduction in transmission between hospitalised individuals and those still susceptible.
+We assume also that any deaths in hospital lead to ebola-safe funerals, such that no further infections result from them.
+We assume that infectious individuals who are not hospitalised die in the community, and that their funerals are potentially not ebola-safe. +We assume that the transmission rate of ebola at funerals is 50% of the baseline transmission rate. +This can also be interpreted as the proportion of funerals that are ebola-safe.
+We run the model using the function model_ebola()
.
The model is run for 100 days (the default), with data reported on each day. +This is an appropriate period over which to obtain initial predictions for the outbreak, and after which response efforts such as non-pharmaceutical interventions and vaccination campaigns are likely to be launched to bring the outbreak under control.
+The function automatically runs 100 replicates (stochastic realisations) as a default, and this can be changed using the replicates
argument.
+# run with a pre-set seed for consisten results
+# run the epidemic model using `epidemic`
+# we call the model "ebola" from the library
+data <- withr::with_seed(
+ 1,
+ model_ebola(
+ population = guinea_population
+ )
+)
+
+# view the head of the output
+head(data)
+#> time demography_group compartment value replicate
+#> <int> <char> <char> <num> <int>
+#> 1: 0 demo_group_1 susceptible 13999989 1
+#> 2: 0 demo_group_1 exposed 10 1
+#> 3: 0 demo_group_1 infectious 1 1
+#> 4: 0 demo_group_1 hospitalised 0 1
+#> 5: 0 demo_group_1 funeral 0 1
+#> 6: 0 demo_group_1 removed 0 1
We plot the size of the ebola epidemic over time; the epidemic size is taken to be the number of individuals considered ‘removed’.
+
+# plot figure of epidemic curve
+filter(data, compartment == "removed") %>%
+ ggplot(
+ aes(
+ x = time, y = value
+ )
+ ) +
+ geom_line(aes(group = replicate), linewidth = 0.2, colour = "grey") +
+ stat_summary(
+ fun.min = function(x) quantile(x, 0.025),
+ fun.max = function(x) quantile(x, 0.975),
+ geom = "ribbon", fill = "red", alpha = 0.2
+ ) +
+ stat_summary(geom = "line") +
+ scale_y_continuous(
+ labels = scales::comma
+ ) +
+ expand_limits(
+ x = c(0, 101),
+ y = c(-10, NA)
+ ) +
+ coord_cartesian(expand = FALSE) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ y = "Outbreak size"
+ )
We observe that model stochasticity leads to wide variation in model outcomes within the first 100 days: some outbreaks may be much more severe than the mean outbreak. +Note how in nearly all replicates, the epidemic size is increasing at near exponential rates.
+We can find the size of each replicate using the epidemic_size()
function on the original simulation data.
+Note that the final size here refers to the final size after 100 days, which is the duration of our simulation.
Looking to model parameter uncertainty? The vignette on modelling parameter uncertainty has helpful information that is also applicable to the Ebola model.
+Interventions that affect model parameters (called rate interventions) can be simulated for an Ebola outbreak in the same way as for other models; creating and passing rate interventions is covered in the vignette on modelling rate interventions.
+Note that the model does not include demographic variation in contacts, as EVD spreads primarily among contacts caring for infected individuals, making age or demographic structure less important. Thus this model allows interventions on model rates only (as there are no demographic groups for contacts interventions).
+Here, we compare the effect of an intervention that begins 15 days into the outbreak, and reduces transmission by 20%, against the counterfactual, baseline scenario of no specific outbreak response. +We simulate 100 days as in the previous examples.
+
+# create an intervention on the transmission rate
+reduce_transmission <- intervention(
+ type = "rate",
+ time_begin = 15, time_end = 100, reduction = 0.2
+)
+
+# create a list of intervention scenarios
+intervention_scenarios <- list(
+ baseline = NULL,
+ response = list(
+ transmission_rate = reduce_transmission
+ )
+)
+# run the epidemic model and save data as the baseline, using a fixed seed
+data_scenarios <- withr::with_seed(
+ 1,
+ model_ebola(
+ population = guinea_population,
+ intervention = intervention_scenarios
+ )
+)
+
+# assign scenario names
+data_scenarios$scenario <- c("baseline", "response")
+
+# unnest the data, preserving scenario identifiers
+data_scenarios <- select(data_scenarios, data, scenario) %>%
+ unnest(data)
+filter(data_scenarios, compartment == "removed") %>%
+ ggplot(aes(time, value, colour = scenario)) +
+ geom_vline(
+ xintercept = 15,
+ linetype = "dotted"
+ ) +
+ geom_line(
+ aes(group = interaction(scenario, replicate)),
+ linewidth = 0.2, alpha = 0.5
+ ) +
+ stat_summary(
+ geom = "ribbon",
+ fun.min = function(x) quantile(x, 0.025),
+ fun.max = function(x) quantile(x, 0.975),
+ alpha = 0.3, colour = NA,
+ aes(fill = scenario)
+ ) +
+ stat_summary(
+ geom = "line", linewidth = 1
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Scenario",
+ labels = c("Baseline", "Intervention")
+ ) +
+ scale_fill_brewer(
+ palette = "Dark2",
+ name = "Scenario",
+ labels = c("Baseline", "Intervention")
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Days",
+ y = "Outbreak size"
+ )
We see that applying an intervention that reduces transmission may be effective in reducing outbreak sizes.
+We can also model the rollout of vaccination against EVD, but because this model is structured differently from other models in epidemics, vaccination must be modelled differently too.
+model_ebola()
does not accept a vaccination regime as a <vaccination>
object, but we can still model the effect of vaccination as a gradual decrease in the transmission rate \(\beta\) over time.
This is done by using the time dependence functionality of epidemics.
+New to implementing parameter time dependence in epidemics? It may help to read the vignette on time dependence functionality first!
+Note that the time-dependence composable element cannot be passed as a set of scenarios.
+We first define a function suitable for the time_dependence
argument.
+This function assumes that the baseline transmission rate of ebola decreases over time, with a 0.5% reduction each day due to vaccination.
+# we assume a 0.5% daily reduction
+# we assume that this vaccination begins on the 15th day
+time_dep_vax <- function(
+ time, x, time_start = 15, reduction = 0.005) {
+ if (time < time_start) {
+ x
+ } else {
+ x * (1.0 - reduction)^time
+ }
+}
+data_baseline <- with_seed(
+ 1,
+ model_ebola(
+ population = guinea_population
+ )
+)
+data_vax_scenario <- with_seed(
+ 1,
+ model_ebola(
+ population = guinea_population,
+ time_dependence = list(
+ transmission_rate = time_dep_vax
+ )
+ )
+)
+
+data_baseline$scenario <- "baseline"
+data_vax_scenario$scenario <- "scenario"
+
+# bind the data together
+data_scenarios <- bind_rows(data_baseline, data_vax_scenario)
+filter(data_scenarios, compartment == "removed") %>%
+ ggplot(aes(time, value, colour = scenario)) +
+ geom_vline(
+ xintercept = 15,
+ linetype = "dotted"
+ ) +
+ geom_line(
+ aes(group = interaction(scenario, replicate)),
+ linewidth = 0.2, alpha = 0.5
+ ) +
+ stat_summary(
+ geom = "ribbon",
+ fun.min = function(x) quantile(x, 0.025),
+ fun.max = function(x) quantile(x, 0.975),
+ alpha = 0.3, colour = NA,
+ aes(fill = scenario)
+ ) +
+ stat_summary(
+ geom = "line", linewidth = 1
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Scenario",
+ labels = c("Baseline", "Vaccination campaign")
+ ) +
+ scale_fill_brewer(
+ palette = "Dark2",
+ name = "Scenario",
+ labels = c("Baseline", "Vaccination campaign")
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Days",
+ y = "Outbreak size"
+ )
Similar functionality can be used to model other parameters more flexibly than allowed by the <rate_intervention>
class.
Since EVD is a severe disease with a high case fatality risk, responses to an outbreak may require the simultaneous implementation of a number of measures to reduce transmission to end the outbreak quickly.
+We can use the EVD model and existing functionality in epidemics to model the implementation of a multi-pronged response to ebola that aims to improve:
+detection and treatment of cases,
safety of hospitals and the risk of community transmission, and
safety of funeral practices to reduce the risk of transmission from dead bodies.
Here, we show the combined effects of these interventions separately.
+We can pass the interventions to the model function’s intervention
argument as a named list, with the names indicating the model parameters to target.
+# create an intervention on the transmission rate
+improve_hospitalisation <- intervention(
+ type = "rate",
+ time_begin = 15, time_end = 100, reduction = 0.3
+)
+
+# create an intervention on ETU risk
+improve_etu_safety <- intervention(
+ type = "rate",
+ time_begin = 15, time_end = 100, reduction = 0.7
+)
+
+# create an intervention on the transmission rate
+reduce_funeral_risk <- intervention(
+ type = "rate",
+ time_begin = 15, time_end = 100, reduction = 0.5
+)
+# run the epidemic model and save data as the baseline, using a fixed seed
+data_scenarios <- withr::with_seed(
+ 1,
+ model_ebola(
+ population = guinea_population,
+ intervention = intervention_scenarios
+ )
+)
+filter(data_scenarios, compartment == "removed") %>%
+ ggplot(aes(time, value, colour = scenario)) +
+ geom_vline(
+ xintercept = 15,
+ linetype = "dotted"
+ ) +
+ geom_line(
+ aes(group = interaction(scenario, replicate)),
+ linewidth = 0.2, alpha = 0.5
+ ) +
+ stat_summary(
+ geom = "ribbon",
+ fun.min = function(x) quantile(x, 0.025),
+ fun.max = function(x) quantile(x, 0.975),
+ alpha = 0.3, colour = NA,
+ aes(fill = scenario)
+ ) +
+ stat_summary(
+ geom = "line", linewidth = 1
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Scenario",
+ labels = c("Baseline", "Combined interventions")
+ ) +
+ scale_fill_brewer(
+ palette = "Dark2",
+ name = "Scenario",
+ labels = c("Baseline", "Combined interventions")
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Days",
+ y = "Outbreak size"
+ )
This model has compartments adopted from the consensus model for Ebola virus disease presented in Li et al. (2019), and with transitions between epidemiological compartments modelled using Erlang sub-compartments adapted from Getz and Dougherty (2018); see References.
+The R code for this model is adapted from code by Ha Minh Lam and initially made available on Epirecipes under the MIT license.
+The transition rates between the exposed and infectious, and infectious and funeral compartments (and also hospitalised to removed), \(\gamma^E\) and \(\gamma^I\) in Getz and Dougherty’s notation, are passed by the user as the infectiousness_rate
and removal_rate
respectively.
The shape of the Erlang distributions of passage times through the exposed and infectious compartments (\(k^E\) and \(k^I\)) are recommended to be set to 2 as a sensible choice, which is the default value for the erlang_sbubcompartments
argument, but can be allowed to vary (but not independently).
Getz and Dougherty’s equation (6) gives the relationship between these parameters and the mean pre-infectious \(\rho^E\) and infectious \(\rho^I\) periods.
+\[\gamma^E = \dfrac{k^E}{\rho^E} = \dfrac{2}{\rho^E} ~\text{and}~ +\gamma^I = \dfrac{k^I}{\rho^I} = \dfrac{2}{\rho^I}\]
+In this discrete time model, \(\gamma^E\) and \(\gamma^I\) are used to determine the passage times of newly exposed or infectious individuals through their respective compartments (thus allowing for variation in passage times).
+Li et al. (2019) present a consensus model for EVD in which individuals can follow multiple pathways from the infectious compartment — to hospitalisation, funeral transmission, and safe burial or recovery (removed), with the possibility of skipping some compartments in this sequence (e.g. directly from infectious to removed, infectious to hospitalisation to removed etc.). +This model simplifies these transitions to only two pathways:
+Infectious → funeral → removed (safe burial)
Infectious → hospitalised → removed (recovery or safe burial)
A proportion, 1.0 - prop_community
, of infectious individuals are transferred to the hospitalised compartment in each timestep, This compartment represents Ebola Treatment Units (ETUs), and individuals in the hospitalised compartment are considered to be infectious but no longer in the community.
The passage time of individuals in the hospitalised compartment is similar to that of individuals in the infectious compartment (i.e., infectious in the community), which means that an infectious individual with \(N\) timesteps before exiting the infectious compartment will exit the hospitalised compartment in the same time.
+Hospitalised individuals can contribute to transmission of Ebola to susceptibles depending on the value of \(p_\text{ETU}\), which is the probability (or proportion) of hospitalised cases contributing to the infection of susceptibles.
+This is interpreted as the relative risk of Ebola virus treatment units (ETUs), with a range of 0.0 – 1.0, where 0.0 indicates that hospitalisation prevents onward transmission entirely, while 1.0 indicates that hospitalisation does not reduce transmission at all.
+This is passed as the argument etu_risk
.
We assume that deaths in hospital lead to Ebola-safe funerals, and individuals exiting the hospitalised compartment move to the ‘removed’ compartment, which holds both recoveries and deaths.
+We assume that infectious individuals who are not hospitalised die, and that deaths outside of hospital lead to funerals that are potentially not Ebola-safe, and the contribution of funerals to Ebola transmission is determined by \(p_\text{funeral}\), which can be interpreted as the relative risk of funerals, and is passed as the funeral_risk
argument.
+Values closer to 0.0 indicate that there is low to no risk of Ebola transmission at funerals, while values closer to 1.0 indicate that the risk is similar to that of transmission in the community.
Individuals are assumed to spend only a single timestep in the funeral transmission compartment, before they move into the ‘removed’ compartment. Individuals in the ‘removed’ compartment do not affect model dynamics.
+vignettes/model_vacamole.Rmd
+ model_vacamole.Rmd
New to epidemics? It may help to read the “Get started” and “Modelling a vaccination campaign” vignettes first!
+Vacamole is a deterministic, compartmental epidemic model built by Kylie Ainslie and others at RIVM, the Dutch Public Health Institute for the Covid-19 pandemic, with a focus on scenario modelling for hospitalisation and vaccination (Ainslie et al. 2022).
+Vacamole was implemented as an R package, and the source code can be found on GitHub. Some versions have been used to generate scenarios for the ECDC Covid-19 Scenario Hub.
+The original model has 8 conceptual compartments - four epidemiological compartments: susceptible, exposed, infectious, and recovered (S, E, I, R respectively), three hospitalisation compartments: hospitalised, in intensive care, and returning from intensive care to regular hospital care (H, ICU, ICU2H), and death. Only infected individuals can enter the hospitalisation or death compartments.
+Individuals from the susceptible compartment may be vaccinated partially (\(V_1\)) or fully (\(V_2\); assuming a two dose regimen), with each dose reducing their probability of being infected, and of being hospitalised or dying.
+We have made some modifications to the ODE model of Vacamole in order to make it more general and thus potentially more applicable to a wider range of settings.
+Specifically,
+We have dropped the ICU and ICU2H compartment as this is potentially less relevant to a context in which intensive care capacity is low.
We have added transitions from the infectious (I) and hospitalised (H) compartments to death (D), as this may be a more general assumption when hospital capacity is low (relatively more I → D) or when treatments are poor (relatively more H → D).
We assume, in a first pass implementation that vaccination primarily reduces adverse outcomes, by modification to the transition rates (\(\beta_V,\eta_{V},\omega_V\)):
+\(\beta_V\): The transmission rate \(\beta\) for individuals in the fully vaccinated compartment \(V_2\);
\(\eta_{V}\): The hospitalisation rate \(\eta\) for fully vaccinated, infected individuals (\(I_V\) → \(H_V\));
\(\omega_V\): The mortality rate for all fully vaccinated individuals at any stage in or post infection (I, or H).
The details of the ODE system for the version of Vacamole included in epidemics can be found at the end of this page.
+Prepare population and contact data.
+
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 65),
+ symmetric = TRUE
+)
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
Prepare initial conditions for each age group. The Vacamole model has 11 compartments and therefore requires a matrix with 11 columns.
+
+# initial conditions
+initial_i <- 1e-6
+
+# // 0| 1| 2|3| 4|5| 6|7| 8|9|10
+# // S|V1|V2|E|EV|I|IV|H|HV|D|R
+
+# make initial conditions - order is important
+initial_conditions <- c(
+ S = 1 - initial_i,
+ V1 = 0, V2 = 0,
+ E = 0, EV = 0,
+ I = initial_i, IV = 0,
+ H = 0, HV = 0, D = 0, R = 0
+)
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
Prepare the time in days over which to model the epidemic, with the outbreak beginning at day zero.
+
+epidemic_days <- 300
Prepare a population as a population
class object.
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
We prepare a two-dose vaccination campaign by concatenating two single dose vaccination regimes, i.e., applying the function c()
to two vaccination
objects.
It is possible to combine multiple vaccination objects together using c()
— the only limitation is that all vaccination regimes combined in this way must have the same number of demographic groups. It is possible to add a single dose to a two-vaccination regime by using c()
on two vaccination
objects with two and one dose, respectively.
+# prepare a two dose vaccination regime for a single age group
+# prepare the first dose
+dose_1 <- vaccination(
+ name = "two-dose vaccination", # name given to first dose
+ nu = matrix(1e-2, nrow = 3),
+ time_begin = matrix(30, nrow = 3),
+ time_end = matrix(epidemic_days, nrow = 3)
+)
+
+# prepare the second dose with a 30 day interval in start date
+dose_2 <- vaccination(
+ name = "two-dose vaccination", # name given to first dose
+ nu = matrix(1e-2, nrow = 3),
+ time_begin = matrix(60, nrow = 3),
+ time_end = matrix(epidemic_days, nrow = 3)
+)
+
+# use `c()` to combine the two doses
+double_vaccination <- c(dose_1, dose_2)
+
+# print to visualise
+double_vaccination
+#>
+#> Vaccination name:
+#> Begins at:
+#> dose_1 dose_2
+#> [1,] 30 60
+#> [2,] 30 60
+#> [3,] 30 60
+#>
+#> Ends at:
+#> dose_1 dose_2
+#> [1,] 300 300
+#> [2,] 300 300
+#> [3,] 300 300
+#>
+#> Vaccination rate:
+#> dose_1 dose_2
+#> [1,] 0.01 0.01
+#> [2,] 0.01 0.01
+#> [3,] 0.01 0.01
First, we run the model with no vaccination. This is the default behaviour, and a vaccination regime only need be specified if it is to be included. The model’s default parameters are:
+Transmission rate (\(\beta\), transmission_rate
): 0.186, resulting from an \(R_0\) = 1.3 and an infectious period of 7 days, given that \(\beta = R_0 / \text{infectious period}\).
Infectiousness rate (\(\sigma\), infectiousness_rate
): 0.5, assuming a pre-infectious period of 2 days, given that \(\sigma = 1 / \text{pre-infectious period}\).
Hospitalisation rate (\(\eta\), hospitalistion_rate
): 1.0 / 1000, assuming that one in every thousand infectious individuals is hospitalised.
Mortality rate (\(\omega\), mortality_rate
): 1.0 / 1000, assuming that one in every thousand infectious and hospitalised individuals dies.
Recovery rate (\(\gamma\), recovery_rate
): 0.143, assuming an infectious period of 7 days, given \(\gamma = 1 / \text{infectious period}\).
Susceptibility reduction from vaccination (susc_reduction_vax
): 0.2, assuming a 20% reduction in susceptibility for individuals who are doubly vaccinated.
Hospitalisation reduction from vaccination (hosp_reduction_vax
): 0.2, assuming a 20% reduction in hospitalisation for individuals who are doubly vaccinated.
Mortality reduction from vaccination (mort_reduction_vax
): 0.2, assuming a 20% reduction in mortality for individuals who are doubly vaccinated.
+# note model will automatically account for no vaccination
+data <- model_vacamole(
+ population = uk_population,
+ time_end = epidemic_days
+)
Next we run the model with a two dose vaccination regime.
+
+data_vaccination <- model_vacamole(
+ population = uk_population,
+ vaccination = double_vaccination, # note vaccination object for two doses
+ time_end = epidemic_days
+)
First, we calculate the total number of infections resulting in recoveries and deaths over the course of the simulation; this is the epidemic’s final size.
+
+# collect data from the two scenarios
+data_scenarios <- list(data, data_vaccination)
+
+# get deaths and recoveries from infection
+data_scenarios <- Map(
+ data_scenarios, c("no_vax", "vax"),
+ f = function(df, sc) {
+ df_ <- distinct(df, demography_group)
+
+ # get total deaths per group
+ df_$total_deaths <- filter(
+ df, time == max(time), compartment == "dead"
+ ) %>% pull(value)
+
+ # get total recoveries per group using helper function `epidemic_size()`
+ # do not count dead
+ df_$total_recovered <- epidemic_size(df, include_deaths = FALSE)
+
+ # add scenario information
+ df_$scenario <- sc
+
+ # return data
+ df_
+ }
+)
+
+# collect data
+data_scenarios <- bind_rows(data_scenarios)
+
+# transform to long format
+data_scenarios <- pivot_longer(
+ data_scenarios,
+ cols = c("total_deaths", "total_recovered"),
+ names_to = "measure"
+)
+ggplot(data_scenarios) +
+ geom_col(
+ aes(demography_group, value, fill = scenario),
+ position = "dodge",
+ colour = "black"
+ ) +
+ facet_wrap(
+ facets = vars(measure),
+ scales = "free_y",
+ labeller = labeller(
+ measure = c(
+ total_deaths = "Total deaths",
+ total_recovered = "Epidemic size (infections without deaths)"
+ )
+ )
+ ) +
+ scale_fill_discrete_qualitative(
+ palette = "Dynamic",
+ labels = c("No vaccination", "Vaccination"),
+ name = "Scenario",
+ na.value = "lightgrey"
+ ) +
+ scale_y_continuous(
+ labels = label_comma(
+ scale = 1e-6, suffix = "M"
+ )
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "bottom",
+ legend.key.height = unit(2, "mm"),
+ strip.background = element_blank(),
+ strip.text = element_text(
+ face = "bold",
+ size = 11
+ )
+ ) +
+ expand_limits(
+ x = c(0.5, length(unique(data_scenarios$demography_group)) + 0.5)
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ labs(
+ x = "Age group",
+ y = "No. of individuals",
+ title = "Effect of vaccination on deaths and total infections"
+ )
Finally, we can compare the peak of hospital bed occupancy in each scenario — this can be a rough indication of how much hospital capacity would be required if a pandemic of these characteristics were to occur, as well as another way to examine the effect of vaccination in reducing this requirement.
+
+# collect data from the two scenarios
+data_scenarios <- list(data, data_vaccination)
+
+peak_hospital_occupancy <- vapply(data_scenarios, function(df) {
+ # get highest hospital occupancy
+ # first get total hospitalisations among vaccinated and un- or part-vacc.
+ df <- filter(
+ df,
+ grepl(
+ pattern = "hospitalised", x = compartment,
+ fixed = TRUE
+ )
+ ) %>%
+ # summarise all hospitalised over time, aggregating away age groups
+ summarise(
+ total_hosp = sum(value), .by = "time"
+ ) %>%
+ # filter for the time point with highest hospital occupancy
+ filter(total_hosp == max(total_hosp)) %>%
+ # get the value
+ pull(total_hosp)
+}, FUN.VALUE = numeric(1))
+
+# set names for comprehensibility
+names(peak_hospital_occupancy) <- c("No vaccination", "Vaccination")
+
+# show peak hospital occupancy in a readable format
+format(peak_hospital_occupancy, big.mark = ",", digits = 1)
+#> No vaccination Vaccination
+#> "1,813" " 8"
This example demonstrates that implementing vaccination can substantially reduce peak hospital occupancy (by about 100%) compared to a scenario in which no vaccines are deployed.
+The Vacamole ODE system adapted for {epidemics} is:
+Susceptibles who are not vaccinated, or only partially vaccinated (considered unprotected) can transition to exposed and vaccinated:
+\[dS = -\beta S(I+I_V) - \nu_1 S\]
+Two sequential vaccination compartments, with a lower conversion rate from two-dose vaccinated individuals (considered to be protected) to exposed:
+\[dV_1 = \nu_1S - \beta V_1(I+I_V) - \nu_2V_1\]
+\[dV_2 = \nu_2V_1 - \beta_VV_2(I+I_V)\]
+Two parallel exposed compartments, with similar conversion to infectious:
+\[dE = \beta (S+V_1)(I+I_V) - \sigma E\]
+\[dE_V = \beta_VV_2(I+I_V) - \sigma E_V\]
+Two parallel infectious compartments, with lower hospitalisation and mortality rate for vaccinated:
+\[dI = \sigma E - \gamma I - \eta I - \omega I\]
+\[dI_V = \sigma E_V - \gamma I_V - \eta_{V} I_V - \omega_V I_V\]
+Two parallel hospitalisation compartments, with a lower mortality rate for vaccinated:
+\[dH = \eta I - \gamma H - \eta_2 H - \omega H\]
+\[dH_V = \eta_{V} I - \gamma H_V - \omega_V H_V\]
+Single recovered compartment:
+\[dR = \gamma(I + H + I_V + H_V)\]
+Single mortality compartment:
+\[dD = \omega(I + H) + \omega_V(I_V + H_V)\]
+vignettes/modelling_interventions.Rmd
+ modelling_interventions.Rmd
Prepare population and contact data.
+epidemics expects social contacts matrices \(M_{ij}\) to represent contacts to \(i\) from \(j\) (Wallinga, Teunis, and Kretzschmar 2006), such that \(q M_{ij} / n_i\) is the probability of infection, where \(q\) is a scaling factor dependent on infection transmissibility, and \(n_i\) is the population proportion of group \(i\).
+Social contacts matrices provided by the socialmixr package follow the opposite convention, where \(M_{ij}\) represents contacts from group \(i\) to group \(j\).
+Thus social contact matrices from socialmixr need to be transposed (using t()
) before they are used with epidemics.
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 40),
+ symmetric = TRUE
+)
+#> Removing participants that have contacts without age information. To change this behaviour, set the 'missing.contact.age' option
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
Prepare initial conditions for each age group.
+
+# initial conditions
+initial_i <- 1e-6
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
Prepare a population as a population
class object.
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
Prepare an intervention to simulate school closures.
+
+# prepare an intervention with a differential effect on age groups
+close_schools <- intervention(
+ name = "School closure",
+ type = "contacts",
+ time_begin = 200,
+ time_end = 300,
+ reduction = matrix(c(0.5, 0.001, 0.001))
+)
+
+# examine the intervention object
+close_schools
+#> <contacts_intervention> object
+#>
+#> Intervention name:
+#> "School closure"
+#>
+#> Begins at:
+#> [1] 200
+#>
+#> Ends at:
+#> [1] 300
+#>
+#> Reduction:
+#> Interv. 1
+#> Demo. grp. 1 0.500
+#> Demo. grp. 2 0.001
+#> Demo. grp. 3 0.001
+# run an epidemic model using `epidemic`
+output <- model_default(
+ population = uk_population,
+ intervention = list(contacts = close_schools),
+ time_end = 600, increment = 1.0
+)
Plot epidemic over time, showing only the number of individuals in the exposed and infected compartments.
+
+# plot figure of epidemic curve
+filter(output, compartment %in% c("exposed", "infectious")) %>%
+ ggplot(
+ aes(
+ x = time,
+ y = value,
+ col = demography_group,
+ linetype = compartment
+ )
+ ) +
+ geom_line() +
+ annotate(
+ geom = "rect",
+ xmin = close_schools$time_begin,
+ xmax = close_schools$time_end,
+ ymin = 0, ymax = 500e3,
+ fill = alpha("red", alpha = 0.2),
+ lty = "dashed"
+ ) +
+ annotate(
+ geom = "text",
+ x = mean(c(close_schools$time_begin, close_schools$time_end)),
+ y = 400e3,
+ angle = 90,
+ label = "School closure"
+ ) +
+ scale_y_continuous(
+ labels = scales::comma
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ expand_limits(
+ y = c(0, 500e3)
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )
vignettes/modelling_multiple_interventions.Rmd
+ modelling_multiple_interventions.Rmd
New to epidemics, or to modelling non-pharmaceutical interventions? It may help to read the “Get started” and “Modelling a non-pharmaceutical intervention targeting social contacts” vignettes first!
+We prepare population and contact data from the U.K., with epidemiological compartments matching the default epidemic model (SEIR-V).
+We assume that one in every million people has been infected and is infectious, translating to about 67 total infections for a U.K. population of 67 million.
+The code for these steps is similar to that in the “Getting started vignette” and is hidden here, although it can be expanded for reference.
+
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 65),
+ symmetric = TRUE
+)
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
+# initial conditions
+initial_i <- 1e-4
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
We first examine the baseline scenario — no interventions are implemented to slow the spread of the epidemic — and visualise the outcomes in terms of daily new infections.
+We simulate an epidemic using model_default()
, calling the default model outlined in the “Get started vignette”.
+# no intervention baseline scenario
+data_baseline <- model_default(
+ population = uk_population,
+ time_end = 400, increment = 1.0
+)
+
+# get new infections
+data_baseline_infections <- new_infections(data_baseline, by_group = TRUE)
We show one instance of the plotting code using the ggplot2 package here, with further instances hidden to keep the vignette short.
+
+# visualise the spread of the epidemic in terms of new infections
+# plot figure of epidemic curve
+plot_baseline <- ggplot() +
+ geom_line(
+ data = data_baseline_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "dashed"
+ ) +
+ scale_y_sqrt(
+ labels = scales::comma,
+ breaks = c(10^seq(3, 5), 5e4)
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )
+
+plot_baseline
In the baseline scenario, the epidemic would continue for approximately 400 days, or more than one year, with a peak in new infections between the 150th and 180th day for the different age groups.
+In this scenario, the epidemic is expected to have a final size of around 23 million individuals infected overall.
+In the following sections we shall iteratively model the effects of applying non-pharmaceutical interventions that reduce contacts to examine whether the epidemic’s final size could be reduced, and whether the peak of infections can be spread out over time.
+We shall prepare multiple interventions that could plausibly implemented over the course of an epidemic.
+In the first example, we shall model school closures, which are primarily aimed at reducing infections among younger individuals who could then transmit them to their families.
+We first prepare an intervention that simulates the effect of closing schools to reduce the contacts of younger people. +We assume that this reduces the contacts of the age group 0 – 19 by 30%, and the contacts of all other age groups by only 1% (as most adults meet individuals their own age).
+This intervention is assumed to last 6 months or approximately 180 days, beginning from the 100th day of the outbreak — this is similar to the duration of initial school closures during the Covid-19 pandemic in 2020 in the U.K.
+
+# prepare an intervention that models school closures for ~3 months (100 days)
+close_schools <- intervention(
+ name = "School closure",
+ type = "contacts",
+ time_begin = 60,
+ time_end = 60 + 180,
+ reduction = matrix(c(0.3, 0.01, 0.01))
+)
+
+# examine the intervention object
+close_schools
+#>
+#> Intervention name:
+#> Begins at:
+#> [1] 60
+#>
+#> Ends at:
+#> [1] 240
+#>
+#> Reduction:
+#> Interv. 1
+#> Demo. grp. 1 0.30
+#> Demo. grp. 2 0.01
+#> Demo. grp. 3 0.01
We simulate the epidemic for 600 days as we expect the intervention to spread disease incidence out over a longer period.
+
+# no intervention baseline scenario
+data_schools_closed <- model_default(
+ population = uk_population,
+ intervention = list(contacts = close_schools),
+ time_end = 600, increment = 1.0
+)
+
+# get new infections
+data_noschool_infections <- new_infections(data_schools_closed, by_group = TRUE)
+# visualise the spread of the epidemic in terms of new infections when
+# schools are closed
+plot_noschool <-
+ ggplot() +
+ geom_vline(
+ xintercept = c(
+ close_schools$time_begin,
+ close_schools$time_end
+ ),
+ linetype = "dotted"
+ ) +
+ annotate(
+ geom = "text",
+ x = mean(c(close_schools$time_begin, close_schools$time_end)),
+ y = 1000,
+ label = "Schools closed"
+ ) +
+ geom_line(
+ data = data_baseline_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "dashed"
+ ) +
+ geom_line(
+ data = data_noschool_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "solid"
+ ) +
+ scale_y_sqrt(
+ labels = scales::comma,
+ breaks = c(10^seq(3, 5), 5e4)
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )
+
+plot_noschool
In the school closures only scenario, the epidemic would continue for approximately 600 days, or more than one and a half years, with more than 10,000 daily new infections for over one year.
+In this scenario, the epidemic is expected to have a final size of around 22 million individuals infected overall.
+General workplace closures are a broad-based measure aimed at reducing infections among all working age adults.
+We shall prepare an intervention that simulates the effect of closing workplaces in addition to closing schools, and we assume that this reduces the contacts of the age group 20 – 65 by 30%, and the contacts of all other age groups by only 1% (we assume that individuals in other age groups are not meeting many individuals in workplaces).
+This intervention is assumed to last 60 days, beginning from the 80th day of the outbreak — this simulates a situation in which policymakers decide to close workplaces three weeks after deciding to close schools, and in which they choose not to keep workplaces closed for as long (2 months or 60 days vs. 6 months or 180 days for school closures).
+
+# prepare an intervention which mostly affects adults 20 -- 65
+intervention_duration <- 60
+close_workplaces <- intervention(
+ name = "Workplace closure",
+ type = "contacts",
+ time_begin = 80,
+ time_end = 80 + intervention_duration,
+ reduction = matrix(c(0.01, 0.3, 0.01))
+)
+
+# examine the intervention object
+close_workplaces
+#>
+#> Intervention name:
+#> Begins at:
+#> [1] 80
+#>
+#> Ends at:
+#> [1] 140
+#>
+#> Reduction:
+#> Interv. 1
+#> Demo. grp. 1 0.01
+#> Demo. grp. 2 0.30
+#> Demo. grp. 3 0.01
We can use the c()
function to combine two intervention
objects into a single intervention
that accommodates the effect of both interventions.
+This multiple intervention can then be passed — as a single intervention
object — to an epidemic model in model_default()
.
When an intervention
object is made up of multiple interventions the first intervention to be specified is treated as the baseline intervention, with an age-specific effect on reducing contacts during its duration of effect.
All further interventions are assumed to be additional percentage point increases on the effect of any active interventions.
+We can use the c()
function to combine two intervention
objects into a single intervention
that accommodates the effect of both interventions.
+This multiple intervention can then be passed — as a single intervention
object — to an epidemic model in model_default()
.
+# combine interventions using `c()`
+combined_interventions <- c(
+ close_schools, close_workplaces
+)
+
+# visualise the combined intervention object
+combined_interventions
+#>
+#> Intervention name:
+#> Begins at:
+#> npi_1 npi_2
+#> [1,] 60 80
+#>
+#> Ends at:
+#> npi_1 npi_2
+#> [1,] 240 140
+#>
+#> Reduction:
+#> Interv. 1 Interv. 2
+#> Demo. grp. 1 0.30 0.01
+#> Demo. grp. 2 0.01 0.30
+#> Demo. grp. 3 0.01 0.01
We can combine any number of intervention
objects into a single intervention
object to be passed to model_default()
.
+The cumulative effect of the interventions is handled automatically by the C++ code underlying epidemics.
Remember that two multiple interventions can also be combined using c()
.
+It is important to be careful when doing this and make sure that only the interventions you intend to combine are concatenated together.
+For example, c(combined_interventions, combined_interventions)
is valid, and would effectively double the effect of school and workplace closures in the example.
We simulate an epidemic using the model_default()
function, calling the default model outlined in the “Get started vignette”.
We simulate the epidemic for 600 days.
+
+# get data from an epidemic model with both interventions
+data_combined <- model_default(
+ population = uk_population,
+ intervention = list(contacts = combined_interventions),
+ time_end = 600, increment = 1.0
+)
+
+# get data on new infections
+data_infections <- new_infections(data_combined, by_group = TRUE)
+plot_intervention_cases <-
+ ggplot() +
+ geom_vline(
+ xintercept = c(
+ close_schools$time_begin,
+ close_schools$time_end
+ ),
+ linetype = "dotted"
+ ) +
+ geom_vline(
+ xintercept = c(
+ close_workplaces$time_begin,
+ close_workplaces$time_end
+ ),
+ colour = "red",
+ linetype = "dotted"
+ ) +
+ annotate(
+ geom = "text",
+ x = mean(c(close_schools$time_begin, close_schools$time_end)),
+ y = 50000,
+ label = "Schools closed"
+ ) +
+ annotate(
+ geom = "text",
+ x = mean(c(
+ close_workplaces$time_begin,
+ close_workplaces$time_end
+ )),
+ y = 30000,
+ colour = "red",
+ label = "Workplaces\nclosed"
+ ) +
+ geom_line(
+ data = data_baseline_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "dashed"
+ ) +
+ geom_line(
+ data = data_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "solid"
+ ) +
+ scale_y_sqrt(
+ labels = scales::comma,
+ breaks = c(10^seq(3, 5), 5e4)
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )
+
+plot_intervention_cases
In this scenario with school closures for 6 months as well as workplace closures for 2 months, the epidemic would still continue for approximately 600 days, or more than one and a half years, but the number of days with 10,000 or more daily cases is reduced, and these typically occur once the intervention on workplaces is lifted.
+In this scenario, the epidemic is expected to have a final size of around 22 million individuals infected overall — this is similar to the school closures only intervention.
+There is a distinct peak new infections once schools are simulated to re-open, with nearly 100,000 new infections on some days.
+In the scenario with combined interventions, daily new infections are forecast to exceed 50,000 on the 397th day of the epidemic.
+In this final example, we simulate re-implementing two months of workplace closures, but not school closures, to reduce infections.
+
+# log the date that cases exceed 50000 daily
+start_date <- min(
+ which(
+ new_infections(data_combined, by_group = FALSE)[, new_infections > 50000]
+ )
+)
+
+# create a new workplace closures object
+workplace_closures_2 <- intervention(
+ type = "contacts",
+ time_begin = start_date,
+ time_end = start_date + 60,
+ reduction = matrix(c(0.01, 0.3, 0.01))
+)
Combine the multiple interventions object with two interventions with the third intervention.
+
+combined_interventions <- c(combined_interventions, workplace_closures_2)
We simulate an epidemic using the model_default()
function for 600 days as before.
+# get data from an epidemic model with both interventions
+data_combined <- model_default(
+ population = uk_population,
+ intervention = list(contacts = combined_interventions),
+ time_end = 600, increment = 1.0
+)
+
+# get data on new infections
+data_infections <- new_infections(data_combined, by_group = TRUE)
+plot_three_interventions <-
+ ggplot() +
+ geom_vline(
+ xintercept = c(
+ close_schools$time_begin,
+ close_schools$time_end
+ ),
+ linetype = "dotted"
+ ) +
+ geom_vline(
+ xintercept = c(
+ close_workplaces$time_begin,
+ close_workplaces$time_end,
+ workplace_closures_2$time_begin,
+ workplace_closures_2$time_end
+ ),
+ colour = "red",
+ linetype = "dotted"
+ ) +
+ annotate(
+ geom = "text",
+ x = mean(c(close_schools$time_begin, close_schools$time_end)),
+ y = 50000,
+ label = "Schools closed"
+ ) +
+ annotate(
+ geom = "text",
+ x = c(
+ mean(
+ c(close_workplaces$time_begin, close_workplaces$time_end)
+ ),
+ mean(c(workplace_closures_2$time_begin, workplace_closures_2$time_end))
+ ),
+ y = 30000,
+ colour = "red",
+ label = c("Workplaces\nclosed", "Workplaces\nclosed")
+ ) +
+ geom_line(
+ data = data_baseline_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "dashed"
+ ) +
+ geom_line(
+ data = data_infections,
+ aes(time, new_infections, colour = demography_group),
+ linetype = "solid"
+ ) +
+ scale_y_sqrt(
+ labels = scales::comma,
+ breaks = c(10^seq(3, 5), 5e4)
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )
+
+plot_three_interventions
In this scenario with school closures for 6 months as well as two phases of workplace closures for 2 months each, the epidemic would continue for over 600 days, or more than one and a half years, but the number of days with 10,000 or more daily cases would be reduced, and these typically occur once the interventions on workplaces are lifted — this is similar to the previous example.
+In this scenario, the epidemic is expected to have a final size of around 15 million individuals infected overall — this is lower than the previous examples.
+vignettes/modelling_rate_interventions.Rmd
+ modelling_rate_interventions.Rmd
New to epidemics, or to modelling interventions? It may help to read the “Get started” first! See the “Modelling a non-pharmaceutical intervention” vignettes for a guide to modelling interventions on social contacts instead.
+We prepare population and contact data from the U.K., with epidemiological compartments matching the default epidemic model (SEIR-V).
+We assume that one in every million people has been infected and is infectious, translating to about 67 total infections for a U.K. population of 67 million.
+The code for these steps is similar to that in the “Getting started vignette” and is hidden here, although it can be expanded for reference.
+
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 65),
+ symmetric = TRUE
+)
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
+# initial conditions
+initial_i <- 1e-4
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
We model an intervention on the transmission rate \(\beta\) that reduces it by 10%. +This could represent interventions such as requiring people to wear masks that reduce transmission.
+
+# prepare an intervention that models mask mandates for ~3 months (100 days)
+mask_mandate <- intervention(
+ name = "mask mandate",
+ type = "rate",
+ time_begin = 60,
+ time_end = 60 + 100,
+ reduction = 0.1
+)
+
+# examine the intervention object
+mask_mandate
+#>
+#> Intervention name:
+#> Begins at:
+#> [,1]
+#> [1,] 60
+#>
+#> Ends at:
+#> [,1]
+#> [1,] 160
+#>
+#> Reduction:
+#> Interv. 1
+#> 0.1
+
+# check the object
+is_intervention(mask_mandate)
+#> [1] TRUE
+
+is_contacts_intervention(mask_mandate)
+#> [1] FALSE
+
+is_rate_intervention(mask_mandate)
+#> [1] TRUE
We first run a baseline scenario — no interventions are implemented to slow the spread of the epidemic — and visualise the outcomes in terms of daily new infections.
+We simulate an epidemic using model_default()
, calling the default model outlined in the “Get started vignette”.
To examine the effect of a mask mandate, we simulate the epidemic for 200 days as we expect the intervention to spread disease incidence out over a longer period.
+
+# no intervention baseline scenario
+data <- model_default(
+ population = uk_population,
+ time_end = 200, increment = 1.0
+)
+
+# with a mask mandate
+data_masks <- model_default(
+ population = uk_population,
+ intervention = list(transmission_rate = mask_mandate),
+ time_end = 200, increment = 1.0
+)
+# get new infections in each scenario
+data <- new_infections(data, by_group = TRUE)
+data_masks <- new_infections(data_masks, by_group = TRUE)
+
+# assign a scenario name to each scenario
+data$scenario <- "baseline"
+data_masks$scenario <- "masks"
+
+# bind data together
+data_combined <- bind_rows(data, data_masks)
We plot the data to examine the effect that implementing a mask mandate has on the daily number of new infections.
+
+ggplot(data_combined) +
+ geom_line(
+ aes(time, new_infections, col = demography_group, linetype = scenario)
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ annotate(
+ geom = "rect",
+ xmin = mask_mandate[["time_begin"]],
+ xmax = mask_mandate[["time_end"]],
+ ymin = 0, ymax = 150e3,
+ fill = alpha("red", alpha = 0.2),
+ lty = "dashed"
+ ) +
+ scale_y_continuous(
+ labels = scales::comma
+ ) +
+ scale_linetype_manual(
+ name = "Scenario",
+ values = c(
+ baseline = "dashed",
+ masks = "solid"
+ )
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ expand_limits(
+ y = c(0, 100e3)
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "New infections"
+ )
vignettes/modelling_scenarios.Rmd
+ modelling_scenarios.Rmd
New to epidemics? It may help to read the “Get started” vignette first!
+This vignette shows how epidemics can conveniently be used to:
+NOTE: This vignette applies only to the deterministic ODE models — the default, Vacamole, and diphtheria models. +The Ebola virus disease model included in the package is expected to receive this functionality in the near future.
+Uncertainty in the characteristics of an epidemic is a key element and roadblock in epidemic response (Shea et al. 2020). Epidemic dynamics can be influenced by two main sources of uncertainty: intrinsic randomness in the transmission process, and uncertain knowledge of the parameters underlying the transmission process. Here we focus on the latter, i.e. uncertainty in the input parameters of an epidemic model.
+epidemics model functions can accept numeric vectors for all infection parameters; the model is run for each parameter combination using the same population and other model components (interventions, vaccinations) etc.
+This allows users to quickly obtain results for a range of parameter values without having to repeatedly call model_*()
in a loop or similar; this iteration is performed internally.
Some benefits of vectorising inputs:
+Inputs are checked all at once, rather than \(N\) times for each element of the parameter vector — this improves performance over manual iteration;
Model output is organised to make filtering by parameter values and scenarios easier (more on this below).
Note that it is always possible to pass a single value of any infection parameter. Single values may be referred to as “scalar” values or “scalars”. Passing scalar infection parameters will yield a simple table of the model “time”, “demography group”, “compartment”, and “value”, for the number of individuals in each demographic group in each compartment at each model time point.
+Click on “Code” below to see the hidden code used to set up a population in this vignette. For more details on how to define populations and initial model conditions please see the “Getting started with epidemic scenario modelling components” vignette. In brief, we model the U.K. population with three age groups, 0 – 19, 20 – 39, > 40, and social contacts stratified by age.
+
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 40),
+ symmetric = TRUE
+)
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
+
+# initial conditions
+initial_i <- 1e-6
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
+# UK population created from hidden code
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
For this example, we consider influenza with pandemic potential (Ghani et al. 2010), and prepare multiple samples of the estimated \(R\). This reflects pandemic response scenarios in which \(R\) estimates always come with some uncertainty (due to limitations in the data and estimation methods). Sampling from a distribution that \(R\) is expected to follow allows us to better understand the extent of variance in possible epidemic outcomes.
+We obtain the probability distribution function (PDF) from the distribution of the serial intervals; this is a Gamma distribution with shape \(k\) = 2.622 and scale \(\theta\) = 0.957 (Ghani et al. 2010).
+The forthcoming Epiverse package epiparameter is expected to make it substantially easier to access and use epidemiological parameters, such as the serial interval, reported in the literature, making it easier to model scenarios differing in the intrinsic characteristics of the pathogen causing the outbreak.
+We use this PDF to estimate the \(R\) of the 2009 influenza pandemic in the U.K., using the EpiEstim package. +We use the \(R\) estimate (the mean and standard deviation) from EpiEstim to generate 100 samples of \(R\), assuming that it is normally distributed. +Users who are drawing parameters with greater variance may wish to draw a larger number of samples.
+
+# Get 2009 influenza data for school in Pennsylvania
+data(Flu2009)
+flu_early_data <- filter(Flu2009$incidence, dates < "2009-05-10")
+
+# get the PDF of the distribution of serial intervals
+serial_pdf <- dgamma(seq(0, 25), shape = 2.622, scale = 0.957)
+# ensure probabilities add up to 1 by normalising them by the sum
+serial_pdf <- serial_pdf / sum(serial_pdf)
+
+# Use EpiEstim to estimate R with uncertainty
+# Uses Gamma distribution by default
+output_R <- estimate_R(
+ incid = flu_early_data,
+ method = "non_parametric_si",
+ config = make_config(list(si_distr = serial_pdf))
+)
+
+# Plot output to visualise
+plot(output_R, "R")
Here, we generate 100 samples of \(R\), and convert to the transmission rate (often denoted \(\beta\)) by dividing by the infectious period of 7 days.
+Since EpiEstim estimates \(Rt\), the instantaneous \(R\), we shall use the mean of the estimates over the time period, and the mean of the standard deviation, as parameters for a distribution from which to draw \(R\) samples for the model.
+
+# get mean mean and sd over time
+r_estimate_mean <- mean(output_R$R$`Mean(R)`)
+r_estimate_sd <- mean(output_R$R$`Std(R)`)
+
+# Generate 100 R samples
+r_samples <- with_seed(
+ seed = 1,
+ rnorm(
+ n = 100, mean = r_estimate_mean, sd = r_estimate_sd
+ )
+)
+
+infectious_period <- 7
+beta <- r_samples / infectious_period
+# pass the vector of transmissibilities to the argument `transmission_rate`
+output <- model_default(
+ population = uk_population,
+ transmission_rate = beta,
+ time_end = 600
+)
+
+# view the output
+head(output)
+#> transmission_rate infectiousness_rate recovery_rate time_end param_set
+#> <num> <num> <num> <num> <int>
+#> 1: 0.1977482 0.5 0.1428571 600 1
+#> 2: 0.2333557 0.5 0.1428571 600 2
+#> 3: 0.1885540 0.5 0.1428571 600 3
+#> 4: 0.2954036 0.5 0.1428571 600 4
+#> 5: 0.2397671 0.5 0.1428571 600 5
+#> 6: 0.1892204 0.5 0.1428571 600 6
+#> population intervention vaccination time_dependence increment scenario
+#> <list> <list> <list> <list> <num> <int>
+#> 1: <population[4]> <list[1]> 1 1
+#> 2: <population[4]> <list[1]> 1 1
+#> 3: <population[4]> <list[1]> 1 1
+#> 4: <population[4]> <list[1]> 1 1
+#> 5: <population[4]> <list[1]> 1 1
+#> 6: <population[4]> <list[1]> 1 1
+#> data
+#> <list>
+#> 1: <data.table[9015x4]>
+#> 2: <data.table[9015x4]>
+#> 3: <data.table[9015x4]>
+#> 4: <data.table[9015x4]>
+#> 5: <data.table[9015x4]>
+#> 6: <data.table[9015x4]>
The output is a nested <data.table>
, with the output of each run of the model for each unique transmission_rate
contained as a <data.frame>
in the list column "data"
.
The output of model_*()
when an infection parameter is passed as a vector is a nested <data.table>
. This is similar to a nested <tibble>
, and can be handled by popular data science packages, such as from the Tidyverse.
More on handling nested data can be found in this section on list-columns in R for Data Science and in the documentation for nested data in the tidyr package. Equivalent operations are possible on <data.table>
s directly; see this R Bloggers post on unnesting data.
We unnest the output’s “data” column in order to plot incidence curves for each transmission rate value.
+
+# select the parameter set and data columns with dplyr::select()
+# add the R value for visualisation
+# calculate new infections, and use tidyr to unnest the data column
+data <- select(output, param_set, transmission_rate, data) %>%
+ mutate(
+ r_value = r_samples,
+ new_infections = map(data, new_infections)
+ ) %>%
+ select(-data) %>%
+ unnest(new_infections)
+# plot the data
+filter(data) %>%
+ ggplot() +
+ geom_line(
+ aes(time, new_infections, col = r_value, group = param_set),
+ alpha = 0.3
+ ) +
+ # use qualitative scale to emphasize differences
+ scale_colour_fermenter(
+ palette = "Dark2",
+ name = "R",
+ breaks = c(0, 1, 1.5, 2.0, 3.0),
+ limits = c(0, 3)
+ ) +
+ scale_y_continuous(
+ name = "New infections",
+ labels = scales::label_comma(scale = 1e-3, suffix = "K")
+ ) +
+ labs(
+ x = "Time (days since start of epidemic)"
+ ) +
+ facet_grid(
+ cols = vars(demography_group)
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top",
+ legend.key.height = unit(2, "mm")
+ )
epidemics model functions can accept multiple infection parameters as vectors, so long as any vectors are all of the same length, or of length 1 (scalar values) as shown below.
+
+beta <- rnorm(n = 100, mean, sd)
+gamma <- rnorm(n = 100, mean, sd) # the recovery rate
+
+model_default(
+ population,
+ transmission_rate = beta, # same length as gamma
+ infectiousness_rate = 0.5, # length 1
+ recovery_rate = gamma
+)
epidemics allows the duration of an model run to be varied, as this may be useful when examining how variation in the start time of an epidemic affects outcomes by a fixed end point. This example shows how to estimate potential variation in the final epidemic size over a range of epidemic start times (and hence durations, assuming a fixed end).
+
+# view durations
+ggplot() +
+ geom_histogram(aes(duration)) +
+ theme_bw() +
+ labs(
+ x = "Epidemic duration",
+ y = "Count"
+ )
+# pass the vector of durations to `time_end`
+output <- model_default(
+ population = uk_population,
+ time_end = duration
+)
+
+# view the output
+head(output)
+#> transmission_rate infectiousness_rate recovery_rate time_end param_set
+#> <num> <num> <num> <num> <int>
+#> 1: 0.1857143 0.5 0.1428571 589 1
+#> 2: 0.1857143 0.5 0.1428571 595 2
+#> 3: 0.1857143 0.5 0.1428571 568 3
+#> 4: 0.1857143 0.5 0.1428571 583 4
+#> 5: 0.1857143 0.5 0.1428571 589 5
+#> 6: 0.1857143 0.5 0.1428571 593 6
+#> population intervention vaccination time_dependence increment scenario
+#> <list> <list> <list> <list> <num> <int>
+#> 1: <population[4]> <list[1]> 1 1
+#> 2: <population[4]> <list[1]> 1 1
+#> 3: <population[4]> <list[1]> 1 1
+#> 4: <population[4]> <list[1]> 1 1
+#> 5: <population[4]> <list[1]> 1 1
+#> 6: <population[4]> <list[1]> 1 1
+#> data
+#> <list>
+#> 1: <data.table[8850x4]>
+#> 2: <data.table[8940x4]>
+#> 3: <data.table[8535x4]>
+#> 4: <data.table[8760x4]>
+#> 5: <data.table[8850x4]>
+#> 6: <data.table[8910x4]>
NOTE: When the duration of the model runs is varied, each model output will have a potentially distinct number of rows.
+
+# calculate the epidemic size to view the mean and SD of sizes
+epidemic_size_estimates <- select(output, param_set, data) %>%
+ mutate(
+ size = map(data, function(x) {
+ tibble(
+ demography_group = unique(x$demography_group),
+ size = epidemic_size(x)
+ )
+ })
+ ) %>%
+ select(size) %>%
+ unnest(size) %>%
+ summarise(
+ across(size, .fns = c(mean = mean, sd = sd)),
+ .by = "demography_group"
+ )
+# view the range of epidemic sizes
+range(epidemic_size_estimates$size)
+#> [1] Inf -Inf
Users may wish to model epidemic trajectories and outcomes under multiple scenarios of response measures.
+epidemics model functions can accept lists of intervention sets and vaccination regimes, allowing multiple intervention, vaccination, and combined intervention-and-vaccination sets to be modelled on the same population.
+Some benefits of passing lists of composable elements:
+Combinations of intervention and vaccination scenarios can be conveniently created, as model functions will automatically create all possible combinations of the arguments to intervention
and vaccination
;
Input-checking and cross-checking is reduced by checking each element of the intervention
and vaccination
list independently before the combination is created (and against the population
, for cross-checking); hence for \(N\) intervention sets and \(M\) vaccination regimes there are only \(N+M\) cross-checks, rather than \(N \time M\) cross-checks;
Model output is organised to provide a scenario identifier, making subsetting easier (more on this below).
It may help to read the “Modelling a non-pharmaceutical intervention targeting social contacts” and the “Modelling the effect of a vaccination campaign” vignettes first!
+epidemics currently offers the ability to pass intervention sets and vaccination regimes as lists.
+It is not currently possible to pass lists of populations, seasonality, or population changes to any models.
+In future, it may be possible to pass multiple populations as a list, to rapidly and conveniently model an epidemic on different populations, or to examine the effect of assumptions about the population’s social contacts or demographic structure.
+We shall create a list of ‘intervention sets’, each set representing a scenario of epidemic response measures.
+Note that each intervention set is simply a list of <intervention>
objects; typically, a single <contacts_intervention>
and any <rate_interventions>
on infection parameters.
+# prepare durations as starting at 25% of the way through an epidemic
+# and ending halfway through
+time_begin <- max_time / 4
+time_end <- max_time / 2
+
+# create three distinct contact interventions
+# prepare an intervention that models school closures for 180 days
+close_schools <- intervention(
+ name = "School closure",
+ type = "contacts",
+ time_begin = time_begin,
+ time_end = time_end,
+ reduction = matrix(c(0.3, 0.01, 0.01))
+)
+
+# prepare an intervention which mostly affects adults 20 -- 65
+close_workplaces <- intervention(
+ name = "Workplace closure",
+ type = "contacts",
+ time_begin = time_begin,
+ time_end = time_end,
+ reduction = matrix(c(0.01, 0.3, 0.01))
+)
+
+# prepare a combined intervention
+combined_intervention <- c(close_schools, close_workplaces)
+# create a mask-mandate rate intervention
+# prepare an intervention that models mask mandates for 300 days
+mask_mandate <- intervention(
+ name = "mask mandate",
+ type = "rate",
+ time_begin = time_begin,
+ time_end = time_end,
+ reduction = 0.1
+)
Having prepared the interventions, we create intervention sets, and pass them to a model function.
+
+# create intervention sets, which are combinations of contacts and rate
+# interventions
+intervention_scenarios <- list(
+ baseline = NULL,
+ scenario_01 = list(
+ contacts = close_schools
+ ),
+ scenario_02 = list(
+ contacts = close_workplaces
+ ),
+ scenario_03 = list(
+ contacts = combined_intervention
+ ),
+ scenario_04 = list(
+ transmission_rate = mask_mandate
+ ),
+ scenario_05 = list(
+ contacts = close_schools,
+ transmission_rate = mask_mandate
+ ),
+ scenario_06 = list(
+ contacts = close_workplaces,
+ transmission_rate = mask_mandate
+ ),
+ scenario_07 = list(
+ contacts = combined_intervention,
+ transmission_rate = mask_mandate
+ )
+)
Note that there is no parameter uncertainty included here. We can visualise the effect of each intervention set in the form of the epidemic’s final size, aggregating over age groups.
+The output is a nested <data.table>
as before — we can quickly get a table of the final epidemic sizes for each scenarios — this is a key initial indicator of the effectiveness of interventions.
Note that in this example, we use the model’s default \(R\) estimate of 1.3. This is because at higher values of \(R\), we get counter-intuitive results for the effects of interventions that reduce transmission. This is explored in more detail later in this vignette.
+
+# pass the list of intervention sets to the model function
+output <- model_default(
+ uk_population,
+ intervention = intervention_scenarios,
+ time_end = 600
+)
+
+# examine the output
+head(output)
+#> transmission_rate infectiousness_rate recovery_rate time_end param_set
+#> <num> <num> <num> <num> <int>
+#> 1: 0.1857143 0.5 0.1428571 600 1
+#> 2: 0.1857143 0.5 0.1428571 600 1
+#> 3: 0.1857143 0.5 0.1428571 600 1
+#> 4: 0.1857143 0.5 0.1428571 600 1
+#> 5: 0.1857143 0.5 0.1428571 600 1
+#> 6: 0.1857143 0.5 0.1428571 600 1
+#> population intervention vaccination time_dependence increment scenario
+#> <list> <list> <list> <list> <num> <int>
+#> 1: <population[4]> <list[1]> 1 1
+#> 2: <population[4]> <list[1]> <list[1]> 1 2
+#> 3: <population[4]> <list[1]> <list[1]> 1 3
+#> 4: <population[4]> <list[1]> <list[1]> 1 4
+#> 5: <population[4]> <list[1]> <list[1]> 1 5
+#> 6: <population[4]> <list[2]> <list[1]> 1 6
+#> data
+#> <list>
+#> 1: <data.table[9015x4]>
+#> 2: <data.table[9015x4]>
+#> 3: <data.table[9015x4]>
+#> 4: <data.table[9015x4]>
+#> 5: <data.table[9015x4]>
+#> 6: <data.table[9015x4]>
The output of model_*()
when either intervention
or vaccination
is passed a list, is a nested <data.table>
.
+This is similar to the output type when parameters are passed as vectors, but with the "scenario"
column indicating each intervention set as a distinct scenario, which helps with grouping the model outputs in "data"
.
The function epidemic_size()
can be applied to the nested data column data
to get the final size for each scenario.
+# set intervention labels
+labels <- c(
+ "No response", "Close schools", "Close workplaces", "Close both",
+ "Mask mandate", "Masks + close schools", "Masks + close workplaces",
+ "Masks + close both"
+)
+
+epidemic_size_estimates <- mutate(
+ output,
+ scenario = labels,
+ size = scales::comma(
+ map_dbl(data, epidemic_size, by_group = FALSE, include_deaths = FALSE)
+ )
+) %>%
+ select(scenario, size)
+
+# view the final epidemic sizes
+epidemic_size_estimates
+#> scenario size
+#> <char> <char>
+#> 1: No response 23,084,960
+#> 2: Close schools 21,495,236
+#> 3: Close workplaces 22,423,830
+#> 4: Close both 6,622,568
+#> 5: Mask mandate 22,712,782
+#> 6: Masks + close schools 17,215,645
+#> 7: Masks + close workplaces 20,564,673
+#> 8: Masks + close both 2,044,188
This example shows how implementing interventions that reduce transmission can reduce the final size of an epidemic.
+When either or both intervention
and vaccination
are passed as lists (of intervention sets, or of vaccination regimes, respectively), the model is run for each combination of elements.
+Thus for \(M\) intervention sets and \(N\) vaccination regimes, the model runs \(M \times N\) times, and each combination is treated as a unique scenario.
Note that ‘no intervention’ and ‘no vaccination’ scenarios are not automatically created, and must be passed explicitly as NULL
in the respective lists.
While the number of intervention and vaccination combinations are not expected to be very many, the addition of parameter uncertainty for each scenario (next section) may rapidly multiply the number of times the model is run. +Users are advised to be mindful of the number of scenario combinations they create.
+The unnesting of the output follows the same procedure as before, and is not shown here.
+epidemics allows parameter uncertainty to be combined with scenario models of multiple intervention sets or vaccination campaigns.
+Here, we shall draw 100 samples of the transmission rate with a mean of 0 and a low standard deviation of 0.005.
+
+# this example includes 100 samples of transmission rates for each intervention
+# including the baseline
+output <- model_default(
+ uk_population,
+ transmission_rate = beta,
+ intervention = intervention_scenarios,
+ time_end = 600
+)
The output is a nested <data.table>
as before, and can be handled in the same way.
The output of model_*()
when either intervention
or vaccination
is passed a list, and when any of the infection parameters is passed as a vector is a nested <data.table>
.
+The "scenario"
column indicates each intervention set as a distinct scenario, while the "param_set"
column indicates the parameter set used for the model run.
The same parameters are used for each scenario, allowing for comparability between scenarios at the aggregate level.
+The output has \(M \times N \times S\) rows (and nested model outputs), for \(M\) intervention sets, \(N\) vaccination regimes, and \(S\) infection parameter sets.
+Here, we show one way of visualising the epidemic size across scenarios, accounting for parameter uncertainty.
+
+# select the data, the parameter set, and the scenario
+epidemic_size_estimates <- select(output, param_set, scenario, data) %>%
+ mutate(
+ size = map_dbl(
+ data, epidemic_size,
+ by_group = FALSE, include_deaths = FALSE
+ )
+ ) %>%
+ select(-data)
+
+# Extract baseline and alternative scenarios
+df_scenario1 <- epidemic_size_estimates %>% filter(scenario == 1)
+df_scenarios_rest <- epidemic_size_estimates %>% filter(scenario != 1)
+
+# Calculate differences as infections averted
+df_differences <- df_scenarios_rest %>%
+ left_join(df_scenario1, by = "param_set", suffix = c("_other", "_1")) %>%
+ mutate(difference = size_1 - size_other) %>%
+ select(param_set, scenario = scenario_other, difference)
+# Plot distribution of differences
+ggplot(
+ df_differences,
+ aes(x = difference, y = factor(scenario), fill = factor(scenario))
+) +
+ stat_histinterval(
+ normalize = "xy", breaks = 11,
+ show.legend = FALSE
+ ) +
+ labs(
+ title = "Scenario comparison",
+ x = "Infections averted"
+ ) +
+ scale_x_continuous(
+ labels = scales::comma
+ ) +
+ scale_y_discrete(
+ name = NULL,
+ labels = labels[-1]
+ ) +
+ scale_fill_discrete_qualitative(
+ palette = "Dynamic"
+ ) +
+ theme_bw()
The intervention scenarios modelled above suggest that:
+interventions on disease transmission rates reduce infections and hence epidemic final sizes;
multiple interventions reduce epidemic final sizes more than single interventions.
In this simple example we show how this need not always be the case, and that counter-intuitively, implementing more interventions can lead to a larger epidemic final size than implementing fewer interventions.
+This phenomenon depends on the baseline transmission rate of the infection, so we select a relatively low \(R\) of 1.30 (corresponding to pandemic influenza), and a higher \(R\) of 1.58 from the \(R\) estimation step at the start of the vignette.
+
+# run each scenario for two values of R
+# no parameter uncertainty
+r_values <- c(1.3, round(r_estimate_mean, 2))
+
+output <- model_default(
+ uk_population,
+ transmission_rate = r_values / 7,
+ intervention = intervention_scenarios,
+ time_end = 600
+)
+
+# obtain epidemic sizes
+epidemic_size_estimates <- mutate(
+ output,
+ size = map_dbl(
+ data, epidemic_size,
+ by_group = FALSE, include_deaths = FALSE
+ ),
+ r_value = rep(r_values, each = length(intervention_scenarios)),
+ scenario = rep(factor(labels, labels), 2)
+) %>%
+ select(r_value, scenario, size)
+# plot the epidemic final size for each scenario for each R
+ggplot(epidemic_size_estimates) +
+ geom_col(
+ aes(scenario, size, fill = scenario),
+ show.legend = FALSE
+ ) +
+ scale_x_discrete(
+ name = NULL,
+ guide = guide_axis(n.dodge = 2)
+ ) +
+ scale_y_continuous(
+ labels = scales::comma,
+ name = "Epidemic size"
+ ) +
+ scale_fill_discrete_qualitative(
+ palette = "Dynamic"
+ ) +
+ facet_grid(
+ cols = vars(r_value),
+ labeller = labeller(
+ r_value = function(x) sprintf("R = %s", x)
+ )
+ ) +
+ theme_bw()
Plotting the epidemic trajectories for each scenario for each value of \(R\) gives a fuller picture of why we see this unexpected effect.
+
+# get new infections per day
+daily_incidence <- mutate(
+ output,
+ r_value = rep(r_values, each = length(intervention_scenarios)),
+ scenario = rep(factor(labels, labels), 2),
+ incidence_data = map(data, new_infections, by_group = FALSE)
+) %>%
+ select(scenario, r_value, incidence_data) %>%
+ unnest(incidence_data)
+ggplot(daily_incidence) +
+ annotate(
+ geom = "rect",
+ xmin = time_begin, xmax = time_end,
+ ymin = -Inf, ymax = Inf,
+ fill = "grey90", colour = "grey"
+ ) +
+ geom_line(
+ aes(time, new_infections, col = as.factor(r_value)),
+ show.legend = TRUE, linewidth = 1
+ ) +
+ scale_y_continuous(
+ labels = scales::label_comma(scale = 1e-3, suffix = "K")
+ ) +
+ scale_colour_discrete_qualitative(
+ palette = "Dynamic",
+ name = "R"
+ ) +
+ facet_wrap(facets = vars(scenario), nrow = 2) +
+ theme_bw() +
+ theme(legend.position = "top") +
+ labs(
+ x = "Time since epidemic start",
+ y = "New infections"
+ )
Implementing interventions on the disease transmission rate leads to a ‘flattening of the curve’ (reduced daily incidence in comparison with no response) while the interventions are active.
+However, these cases are deferred rather than prevented entirely, and in both scenarios this is seen as a shifting of the incidence curve to the right (towards the end of the simulation period).
+When \(R\) is low, the incidence curve begins to rise later than when \(R\) is high (interventions applied to these epidemics actually miss the projected peak of the epidemic, but still shift cases to the right). +Thus low \(R\) epidemics do not run their course by the time the simulation ends, making is appear as though interventions have succeeded in averting infections.
+At higher \(R\), the peak of the projected incidence curve without interventions falls within the intervention period, and thus all interventions reduce new infections. +In scenarios with multiple active interventions, a large number of susceptibles remain uninfected, compared to scenarios with fewer interventions. +The former thus see higher incidence peaks, leading to counter-intuitively larger final epidemic sizes.
+vignettes/modelling_time_dependence.Rmd
+ modelling_time_dependence.Rmd
New to epidemics? It may help to read the “Get started” vignette first, and to browse through some of the other vignettes such as “Modelling a non-pharmaceutical intervention”, or “Modelling interventions on epidemic parameters”.
+This vignette shows how to compose a model in which one or more epidemic parameters vary gradually over time.
+This functionality can be used to compose models that include a seasonal component, i.e., where the epidemic parameters vary due to variation in ambient conditions over the modelled time-period.
+This is intended to be different from modelling interventions on epidemic parameters by allowing small, continuous changes to the epidemic parameters, while rate interventions are intended to be large step-changes as would be expected from policies such as mask mandates or the roll-out of therapeutics.
+The time-dependence functionality shown in this vignette is also expected to be a starting point for data-driven modelling in which epidemic parameters vary as a function of externally provided data, such as climate data.
+
+# set up initial conditions
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 40),
+ symmetric = TRUE
+)
+contact_matrix <- t(contact_data$matrix)
+demography_vector <- contact_data$demography$population
+
+# Prepare some initial objects
+uk_population <- population(
+ name = "UK population",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = matrix(
+ c(1 - 1e-6, 0, 1e-6, 0, 0),
+ nrow = nrow(contact_matrix), ncol = 5L,
+ byrow = TRUE
+ )
+)
Next we prepare a function that affects the transmission rate (often denoted \(\beta\)) in such a way that there are two peaks and two troughs in the transmission rate over the course of a year.
+Note that this example uses an arbitrary function, and that we might want to choose a more realistic function in a model. +Nonetheless, as the figures later will show, this can still generate realistic looking epidemic curves and is a good starting point for understanding how this feature works.
+
+# prepare function, refer to transmissiblity as beta in function name
+mod_beta <- function(time, x, tmax = 365 / 4) {
+ x + (x * sinpi(time / tmax))
+}
We can plot the output of this function over time to examine its effect on \(\beta\).
+
+# get the function output
+# calculate beta - this is done automatically inside epidemic_*()
+output <- mod_beta(seq(0, 365), 1.3 / 7.0)
+
+# plot the effect of time dependence
+ggplot() +
+ geom_line(
+ aes(x = seq(0, 365), y = output)
+ ) +
+ labs(x = "Days", y = "Transmission rate (beta)") +
+ theme_bw()
Next we run an epidemic model while including the time dependence of the transmission rate.
+
+# note transmission_rate is the function/model parameter
+# the function that affects it may have any name
+data <- model_default(
+ population = uk_population,
+ time_dependence = list(
+ transmission_rate = mod_beta
+ ),
+ time_end = 365, increment = 1
+)
+
+# get data on new infections
+data_infections <- new_infections(data)
We plot the number of newly infectious individuals to check the model dynamics. +Note that plotting code is folded and can be expanded.
+
+# plot data on new infections
+ggplot(data_infections) +
+ geom_line(
+ aes(x = time, y = new_infections, col = demography_group)
+ ) +
+ scale_y_continuous(
+ labels = scales::comma,
+ name = "New infections"
+ ) +
+ scale_color_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ theme_bw() +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ labs(
+ x = "Time (days)",
+ title = "Effect of time-dependence of transmission rate"
+ ) +
+ theme(
+ legend.position = "top",
+ plot.title = element_text(face = "bold")
+ )
Here, we can see that the epidemic has a large first wave, followed by a smaller second wave later in the model run (which represents one year).
+We can also model the effect of imposing non-pharmaceutical interventions that reduce social contacts, on the trajectory of an epidemic with time-dependent changes in the transmission rate.
+In this example, we impose an intervention with three phases: (1) closing schools, which primarily affects the age group 0 – 19, then (2) closing workplaces, which affects the age groups > 20 years old, and then (3) partially reopening schools so that the intervention has a lower effect on the social contacts of the age group 0 – 19.
+First we construct the intervention, an object of the <contacts_intervention>
class.
+# school closures affecting younger age groups
+close_schools <- intervention(
+ type = "contacts", time_begin = 50, time_end = 120,
+ reduction = c(0.1, 0.01, 0.01)
+)
+
+# workplace closures affecting mostly adults
+close_workplaces <- intervention(
+ type = "contacts", time_begin = 65, time_end = 90,
+ reduction = c(0.01, 0.2, 0.2)
+)
+
+# partially reopen schools
+partial_schools <- intervention(
+ type = "contacts", time_begin = 120, time_end = 180,
+ reduction = c(0.05, 0.01, 0.01)
+)
+
+# combine interventions
+npis <- c(close_schools, close_workplaces, partial_schools)
+
+# print to examine
+npis
+#>
+#> Intervention name:
+#> Begins at:
+#> npi_1 npi_2 npi_3
+#> [1,] 50 65 120
+#>
+#> Ends at:
+#> npi_1 npi_2 npi_3
+#> [1,] 120 90 180
+#>
+#> Reduction:
+#> Interv. 1 Interv. 2 Interv. 3
+#> Demo. grp. 1 0.10 0.01 0.05
+#> Demo. grp. 2 0.01 0.20 0.01
+#> Demo. grp. 3 0.01 0.20 0.01
Then we run the model while specifying the time-dependence of the transmission rate, as well as the intervention on social contacts.
+
+# run the model with interventions and time-dependence
+data_npi <- model_default(
+ population = uk_population,
+ intervention = list(contacts = npis),
+ time_dependence = list(
+ transmission_rate = mod_beta
+ ),
+ time_end = 365, increment = 1
+)
+
+# get the infections
+data_infections_npi <- new_infections(data_npi)
We assign scenario names to the data, combine them, and plot a comparison.
+
+data_infections$scenario <- "baseline"
+data_infections_npi$scenario <- "intervention"
+
+# combine the data
+data_npi_compare <- bind_rows(data_infections, data_infections_npi)
+ggplot(data_npi_compare) +
+ geom_line(
+ aes(
+ x = time, y = new_infections,
+ col = demography_group, linetype = scenario
+ )
+ ) +
+ scale_y_continuous(
+ labels = scales::comma,
+ name = "New infections"
+ ) +
+ scale_color_brewer(
+ palette = "Dark2",
+ name = "Age group",
+ labels = c("Baseline", "Intervention")
+ ) +
+ scale_linetype_manual(
+ name = "Scenario",
+ values = c(
+ baseline = "dashed",
+ intervention = "solid"
+ )
+ ) +
+ theme_bw() +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ labs(
+ x = "Time (days)",
+ title = "Effect of multiple interventions on contacts"
+ ) +
+ theme(
+ legend.position = "top",
+ plot.title = element_text(face = "bold")
+ )
We can observe that implementing interventions on social contacts can substantially reduce the number of infections in all age groups in both epidemic waves.
+We can model the effect of timing vaccination doses to begin with the end of the first wave, at about 120 days. +This example does not include non-pharmaceutical interventions.
+First we define a vaccination regime that targets adults aged over 40 years as a priority group. +All other age groups are not vaccinated in this campaign. +We also assume that a single dose of the vaccine confers immunity (i.e., non-leaky vaccination).
+
+# define vaccination object
+vax_regime <- vaccination(
+ nu = matrix(0.001, nrow = 3, ncol = 1),
+ time_begin = matrix(c(0, 0, 120)),
+ time_end = matrix(c(0, 0, 220))
+)
+
+# view the vaccination object
+vax_regime
+#>
+#> Vaccination name:
+#> Begins at:
+#> dose_1
+#> [1,] 0
+#> [2,] 0
+#> [3,] 120
+#>
+#> Ends at:
+#> dose_1
+#> [1,] 0
+#> [2,] 0
+#> [3,] 220
+#>
+#> Vaccination rate:
+#> dose_1
+#> [1,] 0.001
+#> [2,] 0.001
+#> [3,] 0.001
We model the effect of administering vaccine doses between the expected peaks of the epidemic waves, and plot the outcome.
+
+# pass time dependence and vaccination. Note no interventions
+data_vax <- model_default(
+ population = uk_population,
+ vaccination = vax_regime,
+ time_dependence = list(
+ transmission_rate = mod_beta
+ ),
+ time_end = 365, increment = 1
+)
+
+# collect data and add scenario
+data_vax_infections <- new_infections(
+ data_vax,
+ compartments_from_susceptible = c("exposed", "vaccinated")
+)
+data_vax_infections$scenario <- "vaccination"
+
+# combine data
+data_vax_compare <- bind_rows(data_infections, data_vax_infections)
+ggplot(data_vax_compare) +
+ geom_rect(
+ aes(
+ xmin = 120, xmax = 220,
+ ymin = 0, ymax = 20e3
+ ),
+ fill = "grey", alpha = 0.1
+ ) +
+ geom_line(
+ data = filter(
+ data_vax, compartment == "vaccinated", demography_group == "40+"
+ ),
+ aes(time, value / 1e2),
+ colour = "darkblue"
+ ) +
+ annotate(
+ geom = "text",
+ x = 190,
+ y = 10e3,
+ label = "Vaccines administered (100 days)",
+ angle = 90,
+ colour = "darkblue"
+ ) +
+ geom_line(
+ aes(
+ x = time, y = new_infections,
+ col = demography_group, linetype = scenario
+ )
+ ) +
+ scale_y_continuous(
+ labels = scales::comma,
+ name = "New infections",
+ sec.axis = dup_axis(
+ trans = function(x) x * 1e2,
+ name = "Individuals vaccinated",
+ labels = function(x) {
+ scales::comma(x, scale = 1e-6, suffix = "M")
+ }
+ )
+ ) +
+ scale_color_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ scale_linetype_manual(
+ name = "Scenario",
+ values = c(
+ baseline = "dashed",
+ vaccination = "solid"
+ )
+ ) +
+ theme_bw() +
+ coord_cartesian(
+ ylim = c(0, 20e3),
+ expand = FALSE
+ ) +
+ labs(
+ x = "Time (days)",
+ title = "Effect of a vaccination regime"
+ ) +
+ theme(
+ legend.position = "top",
+ plot.title = element_text(face = "bold")
+ )
Here, we can see that over 2 million individuals are vaccinated (and immunised; blue line, right-hand Y axis) over the 100 days between the end of the first wave of infections, and the start of the second wave of infections.
+Vaccination reduces the number of daily infections among individuals of all age groups in the second wave. +At its peak, the vaccination scenario sees approximately 5,000 fewer daily infections than the baseline scenario, which may represent a substantial benefit for public health.
+vignettes/modelling_vaccination.Rmd
+ modelling_vaccination.Rmd
Prepare population and contact data.
+
+# load contact and population data from socialmixr::polymod
+polymod <- socialmixr::polymod
+contact_data <- socialmixr::contact_matrix(
+ polymod,
+ countries = "United Kingdom",
+ age.limits = c(0, 20, 65),
+ symmetric = TRUE
+)
+#> Removing participants that have contacts without age information. To change this behaviour, set the 'missing.contact.age' option
+
+# prepare contact matrix
+contact_matrix <- t(contact_data$matrix)
+
+# prepare the demography vector
+demography_vector <- contact_data$demography$population
+names(demography_vector) <- rownames(contact_matrix)
Prepare initial conditions for each age group.
+
+# initial conditions
+initial_i <- 1e-6
+initial_conditions <- c(
+ S = 1 - initial_i, E = 0, I = initial_i, R = 0, V = 0
+)
+
+# build for all age groups
+initial_conditions <- rbind(
+ initial_conditions,
+ initial_conditions,
+ initial_conditions
+)
+
+# assign rownames for clarity
+rownames(initial_conditions) <- rownames(contact_matrix)
Prepare a population as a population
class object.
+uk_population <- population(
+ name = "UK",
+ contact_matrix = contact_matrix,
+ demography_vector = demography_vector,
+ initial_conditions = initial_conditions
+)
Prepare a vaccination campaign targeting individuals aged over 65.
+
+# prepare a vaccination object
+vaccinate_elders <- vaccination(
+ name = "vaccinate elders",
+ time_begin = matrix(100, nrow(contact_matrix)),
+ time_end = matrix(250, nrow(contact_matrix)),
+ nu = matrix(c(0.0001, 0, 0))
+)
+
+# view vaccination object
+vaccinate_elders
+#> <vaccination> object
+#>
+#> Vaccination name:
+#> "vaccinate elders"
+#>
+#> Begins at:
+#> dose_1
+#> [1,] 100
+#> [2,] 100
+#> [3,] 100
+#>
+#> Ends at:
+#> dose_1
+#> [1,] 250
+#> [2,] 250
+#> [3,] 250
+#>
+#> Vaccination rate:
+#> dose_1
+#> [1,] 1e-04
+#> [2,] 0e+00
+#> [3,] 0e+00
+# run an epidemic model using `epidemic`
+output <- model_default(
+ population = uk_population,
+ vaccination = vaccinate_elders,
+ time_end = 600, increment = 1.0
+)
Plot epidemic over time, showing only the number of individuals in the exposed, infected, and vaccinated compartments.
+
+# plot figure of epidemic curve
+filter(output, compartment %in% c("exposed", "infectious")) %>%
+ ggplot(
+ aes(
+ x = time,
+ y = value,
+ col = demography_group,
+ linetype = compartment
+ )
+ ) +
+ geom_line() +
+ scale_y_continuous(
+ labels = scales::comma
+ ) +
+ scale_colour_brewer(
+ palette = "Dark2",
+ name = "Age group"
+ ) +
+ expand_limits(
+ y = c(0, 500e3)
+ ) +
+ coord_cartesian(
+ expand = FALSE
+ ) +
+ theme_bw() +
+ theme(
+ legend.position = "top"
+ ) +
+ labs(
+ x = "Simulation time (days)",
+ linetype = "Compartment",
+ y = "Individuals"
+ )