Understanding how an infectious disease might spread, and under what
conditions it can be controlled, is crucial for public health planning.
The {ringbp} R package provides tools for simulating
infectious disease transmission using a branching process model with
explicit representation of case detection, isolation, and contact
tracing. The package was developed in the early phase of the COVID-19
pandemic to support epidemiological analyses of outbreak dynamics and
control feasibility.
This vignette provides a brief introduction to the package and demonstrates how to:
{ringbp}It concludes with a simplified version of the analysis of COVID-19 control feasibility from Hellewell et al. (2020).
Firstly, we load the {ringbp} package, as well as the
{data.table} and {tinyplot} packages which are
used in this vignette.
At its core, {ringbp} simulates outbreaks using a
branching process model. Each infected individual generates secondary
infections according to an offspring distribution, with modifications
arising from:
Simulations are constructed by combining a set of options that describe these components and then running one or more stochastic outbreak realisations.
For a more detailed exposition of the {ringbp}
epidemiological model and how non-pharmaceutical interventions influence
disease transmission see the ringbp-model.Rmd vignette.
The offspring distribution is a discrete probability distribution that describes the random number of secondary infections (offspring/descendants) generated by a single infected individual during their entire infectious period.
The mean of the offspring distribution is the basic reproduction number (\(R_0\)), which is defined as the average number of secondary cases caused by one infected individual in a fully susceptible population. The model assumes that an infinite supply of susceptibles is always available to infect, making it suitable for modelling of early outbreaks of pathogens that have not previously spread in a population.
The number of secondary infections is defined using
offspring_opts(). Different distributions can be specified
for individuals in the community (community),
those who are isolated (isolated)
and infected individuals that are asymptomatic (or paucisymptomatic) (asymptomatic).
offspring <- offspring_opts(
community = \(n) rnbinom(n = n, mu = 2.5, size = 0.16),
isolated = \(n) rgeom(n = n, prob = 0.8),
asymptomatic = \(n) rpois(n = n, lambda = 1.5)
)The offspring distribution for asymptomatic individuals (asymptomatic)
does not need to be specified, in which case it is assumed to be the
same as the offspring distribution of symptomatic infectors in the
community (community).
Key epidemiological delays, such as the incubation period (incubation_period)
and time from symptom onset to isolation (onset_to_isolation),
are specified using delay_opts().
delays <- delay_opts(
incubation_period = \(n) rlnorm(n, meanlog = 0.9, sdlog = 0.5),
onset_to_isolation = \(n) rgamma(n, shape = 3, scale = 2)
)Isolation is the separation of infectious individuals from others, with the aim to prevent further transmission.
In the {ringbp} model, isolated individuals
have their own transmission dynamics (see offspring_opts()
above). {ringbp} allows infected individuals in isolation
to transmit (i.e. non-zero reproduction number), and can even have
higher transmissibility than infectors in the community.
The arguments to both offspring_opts() and
delay_opts() are functions of n: they take an
integer n and return a vector of n random
samples from a given distribution. This lets you sample offspring or
delay values for multiple individuals in a single step of the
simulation.
Probabilities governing infection and detection events are set using
event_prob_opts(). The probability that an infector is
asymptomatic (asymptomatic),
the proportion of disease transmission that occurs before symptom onset
(presymptomatic_transmission),
the proportion of symptomatic individuals that are ascertained by
contact tracing (symptomatic_traced),
and the proportion of symptomatic individuals that self-isolate (symptomatic_self_isolate,
which defaults to 0, i.e. no self-isolation).
event_probs <- event_prob_opts(
asymptomatic = 0.1,
presymptomatic_transmission = 0.5,
symptomatic_traced = 0.2
)Contact tracing is the process of identifying and isolating people who have been in contact with individuals that have tested positive for the infection.
Quarantine in the {ringbp} model is defined as the
isolation of individuals independent of their infection status. Contacts
can be isolated before they are symptomatic once the infecting
individual is confirmed to be infected and goes into isolation. It
differs from isolation in the model, which requires the infectee to be
symptomatic.
Interventions, thus far only quarantine (quarantine)
is implemented, can be turned on or off via
intervention_opts().
By default, isolation of symptomatic cases is active and quarantine is not active.
Finally, global simulation limits (for example, maximum duration (cap_max_days)
or outbreak size (cap_cases))
are specified using sim_opts().
With all components defined, simulations can be run using
scenario_sim(). The example below runs 100 independent
outbreak simulations starting from a single initial case.
Stochastic epidemic model: the {ringbp} model
uses random simulations to reflect the inherent uncertainty in
real‑world outbreaks.
We set the seed to ensure we have the same output each time the
vignette is rendered. When using {ringbp}, setting the seed
is not required unless you need to simulate the same outbreak multiple
times.
outbreak <- scenario_sim(
n = 100,
initial_cases = 1,
offspring = offspring,
delays = delays,
event_probs = event_probs,
interventions = interventions,
sim = sim
)The returned object is a data.table containing the
simulated outbreak trajectories. The columns are:
sim: simulation replicate IDweek: outbreak week, zero indexedweekly_cases: number of new cases that weekcumulative: cumulative weekly caseseffective_r0: the effective reproduction number, the
same for each simulation replicatecases_per_gen: the number of cases per generation of
the outbreakA simple way to explore results is to plot cumulative cases over time.
tinyplot(
cumulative ~ week | as.factor(sim),
data = outbreak,
type = "l",
lwd = 3,
ylab = "Cumulative number of cases",
xlab = "Week",
legend = FALSE,
theme = "clean"
)We can also plot the weekly incidence of cases. Note: the maximum number of cases in the simulation is set to 4,500, so the decline in cases to zero is due to reaching that upper bound.
tinyplot(
weekly_cases ~ week | as.factor(sim),
data = outbreak,
type = "l",
lwd = 3,
ylab = "Weekly number of cases",
xlab = "Week",
legend = FALSE,
theme = "clean"
)A common quantity of interest is the probability that an outbreak
goes extinct (i.e. dies out without sustained transmission). This can be
calculated using extinct_prob().
extinct_prob(outbreak)
#> Calculating extinction using the extinction status from the simulation.
#> [1] 0.8By default, extinction is defined as all infectious individuals have
had the opportunity to transmit but no new infections are generated. The
extinction_week argument in extinct_prob() can
also be used to specify whether extinction has happened by a certain
week, e.g. extinct_prob(..., extinction_week = 5) checks
whether extinction has occurred by week 5 of the outbreak. See
?extinct_prob for other methods of checking for
extinction.
Next we demonstrate how to use {ringbp} to reproduce a
simplified version of the outbreak control analysis from Hellewell et al. (2020). This study, published in the
first months of the COVID-19 pandemic, modelled the probability that an
introduced COVID-19 outbreak is contained by isolation and contact
tracing. Containment was defined as no disease transmission between
weeks 12-16 of the outbreak and the outbreak not reaching 5,000 total
cases.
Using the functions described above we define several parameter sets to analyse how varying the basic reproduction number in the community, the proportion of contacts successfully traced, and the number of initial infectors that seed independent outbreaks influence the likelihood of outbreak control.
This is a simplified version of the analysis from Hellewell et al. (2020), for the full analysis see the GitHub repository with the analysis scripts.
n <- 10
scenarios[, sims := lapply(data, \(x, n) {
scenario_sim(
n = n,
initial_cases = x$initial_cases,
offspring = offspring_opts(
community = \(n) rnbinom(n = n, mu = x$r0_community, size = 0.16),
isolated = \(n) rnbinom(n = n, mu = 0, size = 1)
),
delays = delay_opts(
incubation_period = \(n) rweibull(n, shape = 1.65, scale = 4.28),
onset_to_isolation = \(n) rweibull(n, shape = 1.65, scale = 2.31)
),
event_probs = event_prob_opts(
asymptomatic = 0.1,
presymptomatic_transmission = 0.1,
symptomatic_traced = x$symptomatic_traced
),
interventions = intervention_opts(quarantine = FALSE),
sim = sim_opts(cap_max_days = 365, cap_cases = 5000)
)
},
n = n
)]For a more detailed example of running {ringbp} across
multiple parameter sets see the parameter-sweep.Rmd
vignette, which also includes information on how to parallelise the
simulation.
scenarios[,
pext := extinct_prob(sims[[1]], extinction_week = 12:16),
by = scenario
]
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
#> Calculating extinction as no new cases within weeks: 12 to 16 (inclusive).
# probability of extinction for each scenario
scenarios$pext
#> [1] 1.0 0.7 0.7 0.0 1.0 1.0 0.8 0.7 1.0 1.0 1.0 1.0Hellewell et al. (2020) used {ringbp} to
show that:
{ringbp} Use CasesThis vignette has focused on the high-level workflow for running
simulations with {ringbp}. Further uses of the package
include, but are not limited to: