Skip to content

Commit f2441df

Browse files
authored
Enhance extinction functionality (#161)
* remove cap_cases argument from detect_extinct and extinct_prob and assign cap_cases as attribute in scenario_sim * rename outbreak_df_week argument to scenario * replace as.data.table with setDT in detect_extinct * rename week_range argument to extinction_week * remove assigning returned variable in detect_extinct from rebasing branch * add extinct_prob check for extinction_week in scenario data * update default of extinction_week of extinct_prob and detect_extinct to last 2 weeks of outbreak * merge documentation of extinct_prob and detect_extinct, relates #118 * remove outdated cap in extinct_prob test * enable single integer or bound vector extinction_week and add function documentation, fixes #132 * DRY-out extinct_prob and detect_extinct * append extinct attribute to output in outbreak_model and scenario_sim, fixes #132 * use extinct attribute if present in detect_extinct, fixes #132 * temporarily remove attribute from scenario_sim output in extinction unit tests * update extinction_week default to NULL and update function documentation * add messages to detect_extinct for method of extinction calculation * check cap_cases attribute in detect_extinct * add extinct_prob examples using different extinction_week input * update extinct_prob test to fix extinction_week error * update RoxygenNote in DESCRIPTION * fix extinct variable scope issue
1 parent 8addd2e commit f2441df

File tree

10 files changed

+297
-198
lines changed

10 files changed

+297
-198
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Encoding: UTF-8
4242
LazyData: true
4343
Roxygen: list(markdown = TRUE, roclets = c("collate", "namespace", "rd",
4444
"roxyglobals::global_roclet"))
45-
RoxygenNote: 7.3.2
45+
RoxygenNote: 7.3.3
4646
Depends:
4747
R (>= 4.4.0)
4848
Imports:

NAMESPACE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ importFrom(data.table,.I)
2020
importFrom(data.table,.N)
2121
importFrom(data.table,.NGRP)
2222
importFrom(data.table,.SD)
23-
importFrom(data.table,as.data.table)
2423
importFrom(data.table,data.table)
2524
importFrom(data.table,fcase)
2625
importFrom(data.table,fifelse)
2726
importFrom(data.table,rbindlist)
27+
importFrom(data.table,setDT)
28+
importFrom(data.table,setattr)
2829
importFrom(sn,rsn)
2930
importFrom(stats,rnbinom)
3031
importFrom(stats,runif)

R/aux_functions.R

Lines changed: 129 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -111,67 +111,63 @@ presymptomatic_transmission_to_alpha <- function(presymptomatic_transmission) {
111111
res$minimum
112112
}
113113

114-
#' Calculate proportion of runs that have controlled outbreak
114+
#' Outbreak extinction functions
115115
#'
116-
#' @inherit detect_extinct details
116+
#' @description
117+
#' `extinct_prob()`: Calculate proportion of runs that have controlled outbreak
117118
#'
118-
#' @inheritParams detect_extinct
119+
#' `detect_extinct()`: Calculate whether outbreaks went extinct or not
119120
#'
120-
#' @return a single `numeric` with the probability of extinction
121-
#' @export
121+
#' @details
122+
#' The data passed to `scenario` has to be produced by [scenario_sim()].
123+
#' It cannot be produced by [outbreak_model()] as it requires the `sim` column,
124+
#' which is only appended in [scenario_sim()].
122125
#'
123-
#' @examples
124-
#' res <- scenario_sim(
125-
#' n = 10,
126-
#' initial_cases = 1,
127-
#' offspring = offspring_opts(
128-
#' community = \(n) rnbinom(n = n, mu = 2.5, size = 0.16),
129-
#' isolated = \(n) rnbinom(n = n, mu = 0.5, size = 1)
130-
#' ),
131-
#' delays = delay_opts(
132-
#' incubation_period = \(n) rweibull(n = n, shape = 2.32, scale = 6.49),
133-
#' onset_to_isolation = \(n) rweibull(n = n, shape = 1.65, scale = 4.28)
134-
#' ),
135-
#' event_probs = event_prob_opts(
136-
#' asymptomatic = 0,
137-
#' presymptomatic_transmission = 0.5,
138-
#' symptomatic_ascertained = 0.2
139-
#' ),
140-
#' interventions = intervention_opts(quarantine = FALSE),
141-
#' sim = sim_opts(cap_max_days = 350, cap_cases = 4500)
142-
#' )
143-
#' extinct_prob(res, cap_cases = 4500)
144-
extinct_prob <- function(outbreak_df_week, cap_cases, week_range = 12:16) {
145-
146-
checkmate::assert_data_frame(outbreak_df_week)
147-
checkmate::assert_number(cap_cases, lower = 0)
148-
checkmate::assert_numeric(week_range)
149-
150-
n <- max(outbreak_df_week$sim)
151-
152-
extinct_runs <- detect_extinct(outbreak_df_week, cap_cases, week_range)
153-
sum(extinct_runs$extinct) / n
154-
}
155-
156-
157-
#' Calculate whether outbreaks went extinct or not
126+
#' ***Warning***: the output from [scenario_sim()] contains an `cap_cases`
127+
#' attribute which is used by [extinct_prob()] and [detect_extinct()],
128+
#' therefore if you modify the output of [scenario_sim()] before passing
129+
#' to [extinct_prob()] be careful not to drop the attribute (e.g.
130+
#' from subsetting the `data.table`).
158131
#'
159-
#' @details
160-
#' The `cap_cases` argument should be equal to the value supplied to
161-
#' [outbreak_model()] (possibly passed from [scenario_sim()]).
132+
#' @param scenario a `data.table`: weekly cases output by [scenario_sim()]
133+
#' @param extinction_week By default `NULL` but also accepts a positive
134+
#' `integer` scalar or `integer` vector to test if the outbreak has gone
135+
#' extinct (i.e. no new cases) by this week (zero indexed):
136+
#' * By default (`NULL`) the extinction status is stored in the output of
137+
#' `scenario_sim()` which is supplied to the `scenario` argument. If
138+
#' `extinction_week` is not specified or specified as `NULL` then the
139+
#' pre-computed extinction status from the outbreak simulation will be used.
140+
#' This is defined as all infectious cases have had the opportunity to
141+
#' transmit but no new cases are generated.
142+
#' * A single `integer` to test if extinction has occurred by this week.
143+
#' For example, `extinction_week = 5` tests whether the outbreak is
144+
#' extinct by week 5 (inclusive) until the end of the outbreak.
145+
#' * An `integer` vector of length two can be supplied to provide the lower
146+
#' and upper bounds (inclusive) of the week range to test for whether
147+
#' extinction occurred by this window. For example
148+
#' `extinction_week = c(5, 10)` will test whether the outbreak went extinct
149+
#' by week 5 and there we no new cases between weeks 5 and 10 (inclusive).
150+
#' * An `integer` vector of length _n_ can be supplied to provide the weeks
151+
#' to test for whether extinction occurred by this window. For example
152+
#' `extinction_week = 12:16` will test that there are no new infections
153+
#' between 12 and 16 weeks after the initial cases (inclusive). These
154+
#' integer sequences will most likely be contiguous but the function
155+
#' does allow non-contiguous integer sequences.
156+
#'
157+
#' If extinction occurs before the `extinction_week` window then the outbreak
158+
#' extinction is considered extinct, however, if the extinction occurs within
159+
#' the `extinction_week` window it is not considered extinct. Therefore,
160+
#' using a single `integer` for `extinction_week` and thinking of this as
161+
#' "_has the outbreak gone extinct by week X_".
162162
#'
163-
#' @param outbreak_df_week a `data.table`: weekly cases produced by the
164-
#' outbreak model
165-
#' @inheritParams sim_opts
166-
#' @param week_range a positive `integer` vector: giving the (zero indexed)
167-
#' week range to test for whether an extinction occurred. Default is `12:16`.
168-
#' @importFrom data.table as.data.table fifelse
163+
#' @importFrom data.table setDT fifelse data.table
169164
#'
170-
#' @return A `data.table`, with two columns `sim` and `extinct`, for a binary
165+
#' @return
166+
#' `extinct_prob()`: a single `numeric` with the probability of extinction
167+
#'
168+
#' `detect_extinct()`: a `data.table`, with two columns `sim` and `extinct`, for a binary
171169
#' classification of whether the outbreak went extinct in each simulation
172170
#' replicate. `1` is an outbreak that went extinct, `0` if not.
173-
#' @autoglobal
174-
#' @export
175171
#'
176172
#' @examples
177173
#' res <- scenario_sim(
@@ -193,16 +189,89 @@ extinct_prob <- function(outbreak_df_week, cap_cases, week_range = 12:16) {
193189
#' interventions = intervention_opts(quarantine = FALSE),
194190
#' sim = sim_opts(cap_max_days = 350, cap_cases = 4500)
195191
#' )
196-
#' detect_extinct(outbreak_df_week = res, cap_cases = 4500)
197-
detect_extinct <- function(outbreak_df_week, cap_cases, week_range = 12:16) {
192+
#'
193+
#' # calculate probability of extinction
194+
#' extinct_prob(res)
195+
#'
196+
#' # determine if each outbreak simulation replicate has gone extinct
197+
#' detect_extinct(res)
198+
#'
199+
#' # calculate extinction in the last 2 weeks of the simulated outbreak
200+
#' # (i.e. the penultimate and last week of the outbreak)
201+
#' extinct_prob(res, extinction_week = max(res$week) - 1)
202+
#'
203+
#' # calculate extinction as no new cases between weeks 12 and 16 of the outbreak
204+
#' extinct_prob(res, extinction_week = 12:16)
205+
#' @name extinction
206+
NULL
198207

199-
checkmate::assert_data_frame(outbreak_df_week)
200-
checkmate::assert_number(cap_cases, lower = 0)
201-
checkmate::assert_integerish(week_range)
208+
#' @rdname extinction
209+
#' @export
210+
extinct_prob <- function(scenario,
211+
extinction_week = NULL) {
212+
213+
extinct_runs <- detect_extinct(
214+
scenario = scenario,
215+
extinction_week = extinction_week
216+
)
217+
sum(extinct_runs$extinct) / max(scenario$sim)
218+
}
219+
220+
#' @rdname extinction
221+
#' @autoglobal
222+
#' @export
223+
detect_extinct <- function(scenario,
224+
extinction_week = NULL) {
225+
226+
extinct <- attr(scenario, which = "extinct", exact = TRUE)
227+
if (is.null(extinction_week)) {
228+
if (!is.null(extinct)) {
229+
message(
230+
"Calculating extinction using the extinction status from ",
231+
"the simulation."
232+
)
233+
return(
234+
data.table(
235+
sim = 1:max(scenario$sim),
236+
extinct = as.integer(extinct)
237+
)
238+
)
239+
} else {
240+
stop(
241+
"`extinction_week` not specified and `scenario` is missing the ",
242+
"`extinct` attribute.\n Use `scenario_sim()` to simulate `scenario`, ",
243+
"or specify `extinction_week`.",
244+
call. = FALSE
245+
)
246+
}
247+
}
248+
249+
checkmate::assert_data_frame(scenario)
250+
checkmate::assert_integerish(extinction_week, min.len = 1)
251+
252+
if (length(extinction_week) == 1) {
253+
extinction_week <- extinction_week:max(scenario$week)
254+
} else if (length(extinction_week) == 2) {
255+
extinction_week <- min(extinction_week):max(extinction_week)
256+
}
257+
stopifnot(
258+
"`extinction_week` not in simulated outbreak data" =
259+
all(extinction_week %in% scenario$week)
260+
)
261+
message(
262+
"Calculating extinction as no new cases within weeks: ",
263+
min(extinction_week), " to ", max(extinction_week), " (inclusive)."
264+
)
265+
266+
cap_cases <- attr(scenario, which = "cap_cases", exact = TRUE)
267+
stopifnot(
268+
"`scenario` is missing the `cap_cases` attribute.
269+
Use `scenario_sim()` to simulate `scenario`" = !is.null(cap_cases)
270+
)
202271

203-
outbreak_df_week <- as.data.table(outbreak_df_week)
204-
outbreak_df_week <- outbreak_df_week[week %in% week_range]
205-
outbreak_df_week[, list(
272+
scenario <- setDT(scenario)
273+
scenario <- scenario[week %in% extinction_week]
274+
scenario[, list(
206275
extinct = fifelse(all(weekly_cases == 0 & cumulative < cap_cases), 1, 0)
207276
), by = sim][]
208277
}

R/outbreak_model.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#' @autoglobal
1616
#' @export
1717
#'
18-
#' @importFrom data.table rbindlist
18+
#' @importFrom data.table rbindlist setattr
1919
#'
2020
#' @examples
2121
#' set.seed(1)
@@ -143,6 +143,9 @@ outbreak_model <- function(initial_cases,
143143
weekly_cases <- weekly_cases[, `:=`(effective_r0 = mean(effective_r0_vect,
144144
na.rm = TRUE),
145145
cases_per_gen = list(cases_in_gen_vect))]
146+
147+
setattr(weekly_cases, name = "extinct", value = all(case_data$isolated))
148+
146149
# return
147150
weekly_cases[]
148151
}

R/scenario_sim.R

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#' @inheritParams outbreak_step
66
#' @inheritParams outbreak_model
77
#'
8-
#' @importFrom data.table rbindlist
8+
#' @importFrom data.table rbindlist setattr
99
#' @return A `data.table` object returning the results for multiple simulations
1010
#' using the same set of parameters. The table has columns
1111
#' * week: The week in the simulation.
@@ -77,6 +77,15 @@ scenario_sim <- function(n,
7777
simplify = FALSE
7878
)
7979

80-
# bind output together and add simulation index and return
81-
data.table::rbindlist(res, idcol = "sim")[]
80+
extinct <- vapply(
81+
res, attr, FUN.VALUE = logical(1), which = "extinct", exact = TRUE
82+
)
83+
84+
# bind output together and add simulation index
85+
res <- data.table::rbindlist(res, idcol = "sim")
86+
87+
setattr(res, name = "cap_cases", value = sim$cap_cases)
88+
setattr(res, name = "extinct", value = extinct)
89+
90+
res[]
8291
}

README.Rmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ ggplot(
9999
### Estimate extinction probability
100100

101101
```{r extinct_prob}
102-
extinct_prob(res, cap_cases = 4500)
102+
extinct_prob(res)
103103
```
104104

105105
## Contributors

man/detect_extinct.Rd

Lines changed: 0 additions & 52 deletions
This file was deleted.

man/extinct_prob.Rd

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)