Skip to content

Commit 08dd621

Browse files
authored
Offspring distributions sample once per generation (#169)
* add sampled col to case_data and check in offspring sampling, relates #148 * remove subsetting by exposures before infector isolation time in outbreak_step, relates #148 * add sample_offspring function * change isolated to sampled in case_data and make new_cases an integer * update outbreak_step to call sample_offspring and use sampled instead of isolated * update globals.R * add alpha and latent_period arguments to sample_offspring and update incubation_to_generation_time calls * update sample_offspring call in outbreak_step * fix outbreak_continue to use sampled after merging main into iso-offspring-fix * update globals.R * remove old commented code from outbreak_step * fix bug in outbreak_step early return and simplify output list * fix ordering of generation times when constructing prob_samples in outbreak_step * $isolated -> $sampled for outbreak_model extinct attribute * make a copy of case_data in outbreak_step due to sample_offspring modifying by reference * update outbreak_setup test expectations and snapshot * update outbreak_step tests given new offspring sampling * update scenario_sim snapshot
1 parent f2441df commit 08dd621

File tree

13 files changed

+656
-487
lines changed

13 files changed

+656
-487
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ importFrom(data.table,.I)
2020
importFrom(data.table,.N)
2121
importFrom(data.table,.NGRP)
2222
importFrom(data.table,.SD)
23+
importFrom(data.table,copy)
2324
importFrom(data.table,data.table)
2425
importFrom(data.table,fcase)
2526
importFrom(data.table,fifelse)

R/aux_functions.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ outbreak_continue <- function(case_data, sim) {
299299

300300
latest_onset <- max(case_data$onset)
301301
total_cases <- nrow(case_data)
302-
extinct <- all(case_data$isolated)
302+
extinct <- all(case_data$sampled)
303303

304304
return(
305305
latest_onset < sim$cap_max_days &&

R/globals.R

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ utils::globalVariables(c(
1616
"isolated_time", # <outbreak_setup>
1717
"onset", # <outbreak_setup>
1818
"new_cases", # <outbreak_step>
19-
"isolated", # <outbreak_step>
19+
"sampled", # <outbreak_step>
20+
"caseid", # <outbreak_step>
21+
"isolated_time", # <outbreak_step>
2022
"asymptomatic", # <outbreak_step>
2123
"onset", # <outbreak_step>
2224
"exposure", # <outbreak_step>
23-
"caseid", # <outbreak_step>
24-
"isolated_time", # <outbreak_step>
25-
"infector_isolation_time", # <outbreak_step>
2625
"infector_asymptomatic", # <outbreak_step>
2726
"missed", # <outbreak_step>
27+
"infector_isolation_time", # <outbreak_step>
28+
"sampled", # <sample_offspring>
2829
NULL
2930
))

R/outbreak_model.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ outbreak_model <- function(initial_cases,
144144
na.rm = TRUE),
145145
cases_per_gen = list(cases_in_gen_vect))]
146146

147-
setattr(weekly_cases, name = "extinct", value = all(case_data$isolated))
147+
setattr(weekly_cases, name = "extinct", value = all(case_data$sampled))
148148

149149
# return
150150
weekly_cases[]

R/outbreak_setup.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
#' * `$infector`: `numeric`
1212
#' * `$missed`: `logical`
1313
#' * `$onset`: `numeric`
14-
#' * `$new_cases`: `logical`
14+
#' * `$new_cases`: `integer`
1515
#' * `$isolated_time`: `numeric`
16-
#' * `$isolated`: `logical`
16+
#' * `$sampled`: `logical`
1717
#' @autoglobal
1818
#' @export
1919
#' @importFrom data.table data.table
@@ -49,11 +49,11 @@ outbreak_setup <- function(initial_cases, delays, event_probs) {
4949
asymptomatic = runif(initial_cases) < event_probs$asymptomatic,
5050
caseid = seq_len(initial_cases), # set case id
5151
infector = 0,
52-
isolated = FALSE,
5352
missed = TRUE,
5453
onset = delays$incubation_period(initial_cases),
55-
new_cases = NA,
56-
isolated_time = Inf
54+
new_cases = NA_integer_,
55+
isolated_time = Inf,
56+
sampled = FALSE
5757
)
5858

5959
# isolate each symptomatic case after an onset-to-isolation delay after

R/outbreak_step.R

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#' the intervention settings for the \pkg{ringbp} model, returned by
1919
#' [intervention_opts()]. Contains one element: `quarantine`
2020
#'
21-
#' @importFrom data.table data.table rbindlist fcase fifelse
21+
#' @importFrom data.table data.table rbindlist fcase fifelse copy
2222
#' @importFrom stats runif
2323
#' @importFrom stats rnbinom
2424
#'
@@ -76,44 +76,40 @@ outbreak_step <- function(case_data,
7676
checkmate::assert_class(event_probs, "ringbp_event_prob_opts")
7777
checkmate::assert_class(interventions, "ringbp_intervention_opts")
7878

79-
# For each case in case_data, draw new_cases from a negative binomial
80-
# distribution with an R0 and dispersion dependent on if isolated = TRUE
81-
case_data[, new_cases := fcase(
82-
isolated, offspring$isolated(.N),
83-
asymptomatic, offspring$asymptomatic(.N),
84-
default = offspring$community(.N)
85-
)]
79+
# work with copy of case_data in outbreak_step due to
80+
# sample_offspring modify by reference
81+
case_data <- copy(case_data)
82+
83+
# case_data is modified by reference and generation times are returned
84+
gt <- sample_offspring(
85+
case_data = case_data,
86+
offspring = offspring,
87+
alpha = event_probs$alpha,
88+
latent_period = delays$latent_period
89+
)
8690

8791
# Select cases that have generated any new cases
88-
new_case_data <- case_data[new_cases > 0]
89-
# The total new cases generated
90-
total_new_cases <- case_data[, sum(new_cases), ]
92+
new_case_data <- case_data[new_cases > 0 & !sampled]
9193

9294
# If no new cases drawn, outbreak is over so return case_data
93-
if (total_new_cases == 0) {
94-
# If everyone is isolated it means that either control has worked or
95+
if (nrow(new_case_data) == 0) {
96+
# If everyone is sampled it means that either control has worked or
9597
# everyone has had a chance to infect but didn't
96-
case_data$isolated <- TRUE
97-
98-
effective_r0 <- 0
99-
cases_in_gen <- 0
100-
out <- list(case_data, effective_r0, cases_in_gen)
101-
names(out) <- c("cases", "effective_r0", "cases_in_gen")
102-
98+
case_data[, sampled := TRUE]
99+
out <- list(
100+
cases = case_data,
101+
effective_r0 = 0,
102+
cases_in_gen = 0
103+
)
103104
return(out)
104105
}
105106

106107
# Compile a data.table for all new cases, new_cases is the amount of people
107108
# that each infector has infected
108109
prob_samples <- new_case_data[, list(
109110
# time when new cases were exposed, a draw from generation time based on
110-
# infector's onset
111-
exposure = incubation_to_generation_time(
112-
symptom_onset_time = rep(onset, new_cases),
113-
exposure_time = rep(exposure, new_cases),
114-
alpha = event_probs$alpha,
115-
latent_period = delays$latent_period
116-
),
111+
# infector's onset, ordered to match order of caseid
112+
exposure = gt[order(as.numeric(names(gt)))],
117113
# records the infector of each new person
118114
infector = rep(caseid, new_cases),
119115
# records when infector was isolated
@@ -123,14 +119,11 @@ outbreak_step <- function(case_data,
123119
# cases whose parents are asymptomatic are automatically missed;
124120
# will draw this for infector_asymptomatic == FALSE
125121
missed = TRUE,
126-
isolated = FALSE,
127-
new_cases = NA
122+
new_cases = NA_integer_,
123+
sampled = FALSE
128124
)][,
129125
# draws a sample to see if this person is asymptomatic
130126
asymptomatic := runif(.N) < event_probs$asymptomatic
131-
][
132-
# keep only news cases that are pre-isolation
133-
exposure < infector_isolation_time
134127
][,
135128
# onset of new case is exposure + incubation period sample
136129
onset := exposure + delays$incubation_period(.N)
@@ -167,11 +160,11 @@ outbreak_step <- function(case_data,
167160
cases_in_gen <- nrow(prob_samples)
168161

169162
## Estimate the effective r0
170-
effective_r0 <- cases_in_gen / case_data[isolated == FALSE, .N]
163+
effective_r0 <- cases_in_gen / case_data[sampled == FALSE, .N]
171164

172165
# Everyone in case_data so far has had their chance to infect and are
173166
# therefore considered isolated
174-
case_data$isolated <- TRUE
167+
case_data[, sampled := TRUE]
175168

176169
# bind original cases + new secondary cases
177170
case_data <- data.table::rbindlist(list(case_data, prob_samples),

R/sample_offspring.R

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#' Sample the offspring distributions for cases that can be in either the
2+
#' _community_, _isolated_ or _asymptomatic_ states and transition between
3+
#' states
4+
#'
5+
#' Samples from the offspring distributions (see [offspring_opts()]) and adds
6+
#' the next generation of transmission events by reference to `case_data`.
7+
#' The generation times for each infector-infectee pair is also sampled and
8+
#' is returned from the function.
9+
#'
10+
#' @details The offspring distribution for a case in the community cannot simply
11+
#' be sampled from the `community` offspring distribution as it might become
12+
#' isolated before infecting some or all of those cases.
13+
#' To account for cases that transition between states (for now only
14+
#' _community_ -> _isolated_) we draw from the offspring from both
15+
#' distributions, assign all new cases a generation time, and then discard
16+
#' the ones that have generation time <= isolation time (for those from the
17+
#' isolated offspring distribution) or generation time > isolation time
18+
#' (for those from the community offspring distribution), respectively.
19+
#'
20+
#' @inheritParams outbreak_step
21+
#' @inheritParams incubation_to_generation_time
22+
#' @inheritParams delay_opts
23+
#'
24+
#' @autoglobal
25+
#'
26+
#' @return A `numeric` vector with the generation times for the new cases
27+
#' exposure/infection times.
28+
#'
29+
#' ***Note*** The `case_data` supplied to the function is modified by
30+
#' references, see [data.table::set()] for more information.
31+
#' @keywords internal
32+
sample_offspring <- function(case_data, offspring, alpha, latent_period) {
33+
34+
# subset to cases in current generation
35+
new_cases <- case_data[sampled == FALSE]
36+
37+
# logical vectors with the row index of asymptomatic and symptomatic cases
38+
asymptomatic_idx <- new_cases$asymptomatic
39+
symptomatic_idx <- !asymptomatic_idx
40+
41+
# cases cannot be isolated at the start of their generation so both community
42+
# and isolated offspring distribution are sampled for community cases and
43+
# are subset by whether the generation time is before or after the isolation
44+
# time
45+
community <- offspring$community(sum(symptomatic_idx))
46+
isolated <- offspring$isolated(sum(symptomatic_idx))
47+
48+
# asymptomatic cases are known from the start of their generation so their
49+
# offspring can be sampled directly from the asymptomatic offspring
50+
# distribution
51+
asymptomatic <- offspring$asymptomatic(sum(asymptomatic_idx))
52+
53+
# get generation times for community and isolated cases
54+
community_exposure <- incubation_to_generation_time(
55+
symptom_onset_time = rep(new_cases$onset[symptomatic_idx], community),
56+
exposure_time = rep(new_cases$exposure[symptomatic_idx], community),
57+
alpha = alpha,
58+
latent_period = latent_period
59+
)
60+
names(community_exposure) <- rep(new_cases$caseid[symptomatic_idx], community)
61+
isolated_exposure <- incubation_to_generation_time(
62+
symptom_onset_time = rep(new_cases$onset[symptomatic_idx], isolated),
63+
exposure_time = rep(new_cases$exposure[symptomatic_idx], isolated),
64+
alpha = alpha,
65+
latent_period = latent_period
66+
)
67+
names(isolated_exposure) <- rep(new_cases$caseid[symptomatic_idx], isolated)
68+
69+
# if there is any transmission from asymptomatic cases get generation time
70+
if (length(asymptomatic) > 0) {
71+
asymptomatic_exposure <- incubation_to_generation_time(
72+
symptom_onset_time = rep(new_cases$onset[asymptomatic_idx], asymptomatic),
73+
exposure_time = rep(new_cases$exposure[asymptomatic_idx], asymptomatic),
74+
alpha = alpha,
75+
latent_period = latent_period
76+
)
77+
names(asymptomatic_exposure) <- rep(new_cases$caseid[asymptomatic_idx], asymptomatic)
78+
} else {
79+
# create asymptomatic_exposure for all exposure vector below (NULL dropped)
80+
asymptomatic_exposure <- NULL
81+
}
82+
83+
# subset transmission events in community and isolation based on infector
84+
# isolation time and infectee exposure time
85+
infect_before_isolate <- community_exposure < rep(new_cases$isolated_time[symptomatic_idx], community)
86+
community_exposure <- community_exposure[infect_before_isolate]
87+
infect_after_isolate <- isolated_exposure > rep(new_cases$isolated_time[symptomatic_idx], isolated)
88+
isolated_exposure <- isolated_exposure[infect_after_isolate]
89+
90+
# infectee exposure time for all transmission events
91+
exposure <- c(community_exposure, isolated_exposure, asymptomatic_exposure)
92+
93+
next_gen <- table(names(exposure))
94+
95+
# assign next generation of cases by reference
96+
case_data[sampled == FALSE, new_cases := 0L]
97+
case_data[as.numeric(names(next_gen)), new_cases := next_gen]
98+
99+
# return generation times
100+
exposure
101+
}

man/outbreak_setup.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/sample_offspring.Rd

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/outbreak_setup.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
symptomatic_ascertained = 0.8))
99
Output
1010
Index: <asymptomatic>
11-
exposure asymptomatic caseid infector isolated missed onset new_cases
12-
<num> <lgcl> <int> <num> <lgcl> <lgcl> <num> <lgcl>
13-
1: 0 FALSE 1 0 FALSE TRUE 3.696253 NA
14-
2: 0 FALSE 2 0 FALSE TRUE 6.837342 NA
15-
3: 0 FALSE 3 0 FALSE TRUE 4.369195 NA
16-
4: 0 FALSE 4 0 FALSE TRUE 6.009202 NA
17-
5: 0 FALSE 5 0 FALSE TRUE 4.157019 NA
18-
isolated_time
19-
<num>
20-
1: 5.984245
21-
2: 12.350205
22-
3: 9.761168
23-
4: 7.443792
24-
5: 7.509917
11+
exposure asymptomatic caseid infector missed onset new_cases
12+
<num> <lgcl> <int> <num> <lgcl> <num> <int>
13+
1: 0 FALSE 1 0 TRUE 3.696253 NA
14+
2: 0 FALSE 2 0 TRUE 6.837342 NA
15+
3: 0 FALSE 3 0 TRUE 4.369195 NA
16+
4: 0 FALSE 4 0 TRUE 6.009202 NA
17+
5: 0 FALSE 5 0 TRUE 4.157019 NA
18+
isolated_time sampled
19+
<num> <lgcl>
20+
1: 5.984245 FALSE
21+
2: 12.350205 FALSE
22+
3: 9.761168 FALSE
23+
4: 7.443792 FALSE
24+
5: 7.509917 FALSE
2525

0 commit comments

Comments
 (0)