|
a |
|
b/R/spine.R |
|
|
1 |
#' @include prevalence.R |
|
|
2 |
|
|
|
3 |
|
|
|
4 |
|
|
|
5 |
#' @title Command line |
|
|
6 |
#' |
|
|
7 |
#' @param target_arm_size Number of patients required per |
|
|
8 |
#' treatment arm |
|
|
9 |
#' @param target_interim Number of patients required per |
|
|
10 |
#' arm for interim analysis; defaults to `target_arm_size \ 2` |
|
|
11 |
#' @param target_control Number of patients required for the |
|
|
12 |
#' control arm(s) |
|
|
13 |
#' @param shared_control TRUE if all treatment arms share the |
|
|
14 |
#' same control arm; FALSE if each treatment arm has its own |
|
|
15 |
#' control. Defaults to TRUE. |
|
|
16 |
#' @param accrual_period Recruitment period (months). |
|
|
17 |
#' @param interim_period Recruitment period to interim (months). |
|
|
18 |
#' @param precision For the Dirichlet model of biomarker prevalences, |
|
|
19 |
#' variability decreases as precision increases. Defaults to 10. |
|
|
20 |
#' @param ctrl_ratio Ratio of patient allocation to treatment arm |
|
|
21 |
#' versus control for all active arms; defaults to c(1, 1). |
|
|
22 |
#' @param centres_file Name of CSV file with information about |
|
|
23 |
#' each recruitment centre; this should have columns "site", |
|
|
24 |
#' "start_month", "mean_rate", "region" and optionally "site_cap" |
|
|
25 |
#' if recruitment is capped per site. Defaults to `centres.csv`. |
|
|
26 |
#' @param prop_file Name of CSV file with expected biomarker prevalences |
|
|
27 |
#' for each region; this should have one column "category", naming |
|
|
28 |
#' the biomarkers or biomarker combinations, and one column per |
|
|
29 |
#' region. Defaults to `proportions.csv`. |
|
|
30 |
#' @param arms_file Name of JSON file describing which recruitment |
|
|
31 |
#' arms (defined by biomarkers) recruit to which treatment arms. |
|
|
32 |
#' Defaults to `arms_json`. |
|
|
33 |
#' @param data_path Folder where `centres_file`, `prop_file` and |
|
|
34 |
#' `arms_file` are located. Defaults to the location of the package |
|
|
35 |
#' example data in the package installation; this should be changed. |
|
|
36 |
#' @param output_path Folder where data generated during execution |
|
|
37 |
#' will be stored; defaults to `../biomkrAccrual_output_data/`. |
|
|
38 |
#' @param figs_path Folder where figures generated during execution |
|
|
39 |
#' will be stored; defaults to the `figures` subdirectory in |
|
|
40 |
#' `output_path`. |
|
|
41 |
#' @param fixed_centre_starts TRUE if centres are assumed to start |
|
|
42 |
#' exactly when planned; FALSE if some randomisation should be added. |
|
|
43 |
#' @param fixed_site_rates TRUE if centre recruitment rates should |
|
|
44 |
#' be treated as exact; FALSE if they should be drawn from a gamma |
|
|
45 |
#' distribution with a mean of the specified rate. |
|
|
46 |
#' @param fixed_region_prevalences TRUE if biomarker prevalences |
|
|
47 |
#' should be considered to be identical for all sites within a |
|
|
48 |
#' region; FALSE if they should be drawn from a Dirichlet distribution |
|
|
49 |
#' with a mean of the specified prevalence. |
|
|
50 |
#' @param quietly Defaults to FALSE, which displays the output from |
|
|
51 |
#' each run. Set to TRUE to generate data and figures without displaying |
|
|
52 |
#' them. |
|
|
53 |
#' @param keep_files Save data files and plots generated during the run. |
|
|
54 |
#' Defaults to TRUE. |
|
|
55 |
#' |
|
|
56 |
#' @examples |
|
|
57 |
#' biomkrAccrual() |
|
|
58 |
#' |
|
|
59 |
#' @import checkmate |
|
|
60 |
#' @importFrom jsonlite read_json |
|
|
61 |
#' @importFrom rlang abort warn |
|
|
62 |
#' @importFrom utils read.csv |
|
|
63 |
#' @importFrom ggplot2 ggsave |
|
|
64 |
#' |
|
|
65 |
#' @export |
|
|
66 |
biomkrAccrual <- function( |
|
|
67 |
target_arm_size = 60, |
|
|
68 |
target_interim = target_arm_size / 2, |
|
|
69 |
target_control = 180, |
|
|
70 |
shared_control = TRUE, |
|
|
71 |
accrual_period = 50 / 4, |
|
|
72 |
interim_period = accrual_period / 2, |
|
|
73 |
precision = 10, |
|
|
74 |
# active : control ratio (all active the same) |
|
|
75 |
ctrl_ratio = c(1, 1), |
|
|
76 |
centres_file = "centres.csv", |
|
|
77 |
prop_file = "proportions.csv", |
|
|
78 |
arms_file = "arms.json", |
|
|
79 |
data_path = "extdata/", |
|
|
80 |
output_path = "../biomkrAccrual_output_data/", |
|
|
81 |
figs_path = paste0(output_path, "figures/"), |
|
|
82 |
fixed_centre_starts = TRUE, |
|
|
83 |
fixed_site_rates = FALSE, |
|
|
84 |
fixed_region_prevalences = FALSE, |
|
|
85 |
quietly = FALSE, |
|
|
86 |
keep_files = TRUE |
|
|
87 |
) { |
|
|
88 |
|
|
|
89 |
checkmate::assert_logical( |
|
|
90 |
fixed_region_prevalences, |
|
|
91 |
any.missing = FALSE, |
|
|
92 |
null.ok = FALSE |
|
|
93 |
) |
|
|
94 |
|
|
|
95 |
if (fixed_region_prevalences && |
|
|
96 |
checkmate::test_numeric( |
|
|
97 |
precision, |
|
|
98 |
any.missing = FALSE, |
|
|
99 |
null.ok = FALSE |
|
|
100 |
) |
|
|
101 |
) { |
|
|
102 |
rlang::warn(paste("Value given for precision when", |
|
|
103 |
"fixed_region_prevalences is TRUE: fixed_region_prevalences", |
|
|
104 |
"will take precendence." |
|
|
105 |
)) |
|
|
106 |
precision <- NULL |
|
|
107 |
} |
|
|
108 |
|
|
|
109 |
checkmate::assert_numeric( |
|
|
110 |
precision, |
|
|
111 |
any.missing = FALSE, |
|
|
112 |
lower = 10^-6, |
|
|
113 |
finite = TRUE, |
|
|
114 |
len = 1, |
|
|
115 |
null.ok = TRUE |
|
|
116 |
) |
|
|
117 |
|
|
|
118 |
if (!fixed_region_prevalences && is.null(precision)) { |
|
|
119 |
rlang::abort(paste("Either fixed_region_prevalences", |
|
|
120 |
"must be TRUE, or a value must be given for the", |
|
|
121 |
"precision of the Dirichlet distribution." |
|
|
122 |
)) |
|
|
123 |
} |
|
|
124 |
|
|
|
125 |
|
|
|
126 |
# Verify inputs |
|
|
127 |
## append "/" if no slash on end |
|
|
128 |
data_path <- gsub("(\\w+)$", "\\1/", data_path) |
|
|
129 |
output_path <- gsub("(\\w+)$", "\\1/", output_path) |
|
|
130 |
figs_path <- gsub("(\\w+)$", "\\1/", figs_path) |
|
|
131 |
|
|
|
132 |
# Make into full path so only one set of syntax needed |
|
|
133 |
if (!grepl("^/", output_path)) { |
|
|
134 |
output_path <- paste0(getwd(), "/", output_path) |
|
|
135 |
} |
|
|
136 |
|
|
|
137 |
## Check for switches e.g. av_site_rate_month first |
|
|
138 |
checkmate::assert_directory_exists(system.file( |
|
|
139 |
data_path, package = "biomkrAccrual" |
|
|
140 |
), access = "rx") |
|
|
141 |
checkmate::assert_file_exists(system.file( |
|
|
142 |
data_path, prop_file, package = "biomkrAccrual" |
|
|
143 |
), access = "r") |
|
|
144 |
checkmate::assert_file_exists(system.file( |
|
|
145 |
data_path, arms_file, package = "biomkrAccrual" |
|
|
146 |
), access = "r") |
|
|
147 |
checkmate::assert_file_exists(system.file( |
|
|
148 |
data_path, centres_file, package = "biomkrAccrual" |
|
|
149 |
), access = "r") |
|
|
150 |
|
|
|
151 |
# Set up output directory if does not already exist |
|
|
152 |
makeifnot_dir(output_path, min_access = "rwx") |
|
|
153 |
|
|
|
154 |
# Set up output figures directory if does not already exist |
|
|
155 |
makeifnot_dir(figs_path, min_access = "rwx") |
|
|
156 |
|
|
|
157 |
# Read parameters |
|
|
158 |
prop_params_df <- utils::read.csv(system.file( |
|
|
159 |
data_path, prop_file, package = "biomkrAccrual" |
|
|
160 |
)) |
|
|
161 |
|
|
|
162 |
arms_ls <- |
|
|
163 |
jsonlite::read_json(system.file( |
|
|
164 |
data_path, arms_file, package = "biomkrAccrual" |
|
|
165 |
), simplifyVector = TRUE) |
|
|
166 |
|
|
|
167 |
centres_df <- utils::read.csv(system.file( |
|
|
168 |
data_path, centres_file, package = "biomkrAccrual" |
|
|
169 |
)) |
|
|
170 |
|
|
|
171 |
# Add fail if read fails |
|
|
172 |
|
|
|
173 |
# Fail if centres_file is in wrong format |
|
|
174 |
if (isFALSE(all.equal( |
|
|
175 |
names(centres_df), |
|
|
176 |
c("site", "start_month", "mean_rate", "region", "site_cap") |
|
|
177 |
# site_cap is optional, no cap if not present |
|
|
178 |
)) || isFALSE(all.equal( |
|
|
179 |
names(centres_df), c("site", "start_month", "mean_rate", "region") |
|
|
180 |
))) { |
|
|
181 |
rlang::abort(paste( |
|
|
182 |
"Format error: centres.csv should have columns site,", |
|
|
183 |
"start_month, mean_rate, region, and optionally site_cap" |
|
|
184 |
)) |
|
|
185 |
} |
|
|
186 |
|
|
|
187 |
# Set run_time to timestamp output files |
|
|
188 |
run_time <- format(Sys.time(), "%F-%H-%M-%S") |
|
|
189 |
|
|
|
190 |
# Get start weeks & order centres_df by start week and site number |
|
|
191 |
centres_df <- do_clean_centres(centres_df) |
|
|
192 |
centres_df$start_week <- get_weeks(centres_df$start_month - 1) + 1 |
|
|
193 |
|
|
|
194 |
# Make control ratio sum to 1 |
|
|
195 |
if (is.null(ctrl_ratio)) { |
|
|
196 |
if (!is.null(target_control)) { |
|
|
197 |
ctrl_ratio <- c(1, target_control / target_arm_size) |
|
|
198 |
} else { |
|
|
199 |
rlang::abort(paste( |
|
|
200 |
"For shared control, either ctrl_ratio or", |
|
|
201 |
"target_control must be specified." |
|
|
202 |
)) |
|
|
203 |
} |
|
|
204 |
} |
|
|
205 |
ctrl_ratio <- ctrl_ratio / sum(ctrl_ratio) |
|
|
206 |
|
|
|
207 |
# Generate target_control if needed |
|
|
208 |
if (is.null(target_control) && shared_control) { |
|
|
209 |
target_control <- target_arm_size * ctrl_ratio[2] |
|
|
210 |
} |
|
|
211 |
|
|
|
212 |
# Total target recruitment |
|
|
213 |
target_recruit <- ifelse( |
|
|
214 |
shared_control, |
|
|
215 |
target_arm_size * length(arms_ls) + target_control, |
|
|
216 |
target_arm_size * length(arms_ls) * (2 * ctrl_ratio[2]) |
|
|
217 |
) |
|
|
218 |
|
|
|
219 |
# Complete site cap if incomplete, using recruitment target |
|
|
220 |
if (!("site_cap" %in% names(centres_df))) { |
|
|
221 |
centres_df$site_cap <- target_recruit |
|
|
222 |
} else { |
|
|
223 |
centres_df$site_cap[is.na(centres_df$site_cap)] <- target_recruit |
|
|
224 |
} |
|
|
225 |
|
|
|
226 |
# Create structure object |
|
|
227 |
trial_structure_instance <- |
|
|
228 |
trial_structure( |
|
|
229 |
props_df = prop_params_df, |
|
|
230 |
arms_ls = arms_ls, |
|
|
231 |
centres_df = centres_df, |
|
|
232 |
precision = precision, |
|
|
233 |
shared_control = shared_control, |
|
|
234 |
ctrl_ratio = ctrl_ratio, |
|
|
235 |
fixed_region_prevalences = fixed_region_prevalences |
|
|
236 |
) |
|
|
237 |
|
|
|
238 |
# Create accrual object |
|
|
239 |
accrual_instance <- accrual( |
|
|
240 |
treatment_arm_ids = trial_structure_instance@treatment_arm_ids, |
|
|
241 |
shared_control = shared_control, |
|
|
242 |
target_arm_size = target_arm_size, |
|
|
243 |
target_control = target_control, |
|
|
244 |
target_interim = target_interim, |
|
|
245 |
accrual_period = get_weeks(accrual_period), |
|
|
246 |
interim_period = get_weeks(interim_period), |
|
|
247 |
control_ratio = ctrl_ratio, |
|
|
248 |
centres_df = centres_df |
|
|
249 |
) |
|
|
250 |
|
|
|
251 |
while ( |
|
|
252 |
# Any arms are recruiting |
|
|
253 |
any(trial_structure_instance@treatment_arm_struct) && |
|
|
254 |
# Any sites are recruiting |
|
|
255 |
length(accrual_instance@active_sites) > 0 && |
|
|
256 |
# Not out of time |
|
|
257 |
accrual_instance@week <= accrual_instance@accrual_period |
|
|
258 |
) { |
|
|
259 |
|
|
|
260 |
# Add a week's accrual |
|
|
261 |
obj_list <- accrue_week( |
|
|
262 |
accrual_instance, |
|
|
263 |
trial_structure_instance, |
|
|
264 |
fixed_site_rates |
|
|
265 |
) |
|
|
266 |
|
|
|
267 |
accrual_instance <- obj_list[[1]] |
|
|
268 |
trial_structure_instance <- obj_list[[2]] |
|
|
269 |
|
|
|
270 |
# Increment pointer for the next week to accrue |
|
|
271 |
accrual_instance@week <- accrual_instance@week + as.integer(1) |
|
|
272 |
} |
|
|
273 |
|
|
|
274 |
# Trim accrual to actual recruitment length |
|
|
275 |
accrual_instance@accrual <- |
|
|
276 |
accrual_instance@accrual[seq(accrual_instance@week - 1), , ] |
|
|
277 |
|
|
|
278 |
if (keep_files) { |
|
|
279 |
write.csv( |
|
|
280 |
accrual_instance@accrual, |
|
|
281 |
paste0(output_path, "accrual-", run_time, ".csv"), |
|
|
282 |
row.names = FALSE |
|
|
283 |
) |
|
|
284 |
} |
|
|
285 |
|
|
|
286 |
# Plot outcome |
|
|
287 |
if (!quietly || keep_files) { |
|
|
288 |
p <- plot(accrual_instance) |
|
|
289 |
|
|
|
290 |
if (!quietly) { |
|
|
291 |
print(p) |
|
|
292 |
} |
|
|
293 |
if (keep_files) { |
|
|
294 |
ggplot2::ggsave( |
|
|
295 |
paste0(figs_path, "accrual-", run_time, ".png"), |
|
|
296 |
plot = p, |
|
|
297 |
width = 12, |
|
|
298 |
height = 8, |
|
|
299 |
dpi = 400 |
|
|
300 |
) |
|
|
301 |
} |
|
|
302 |
} |
|
|
303 |
|
|
|
304 |
if (!quietly) { |
|
|
305 |
# Print accrual object |
|
|
306 |
print(accrual_instance) |
|
|
307 |
|
|
|
308 |
# Print summary of accrual object |
|
|
309 |
summary(accrual_instance) |
|
|
310 |
|
|
|
311 |
# Print trial structure object |
|
|
312 |
print(trial_structure_instance) |
|
|
313 |
|
|
|
314 |
cat("\n\nTreatment arm ids\n") |
|
|
315 |
print(trial_structure_instance@treatment_arm_ids_start) |
|
|
316 |
|
|
|
317 |
# Print summary of trial structure object |
|
|
318 |
cat("\n\nSite prevalences\n") |
|
|
319 |
summary(trial_structure_instance)$site_prev |
|
|
320 |
} |
|
|
321 |
|
|
|
322 |
# Plot trial structure object |
|
|
323 |
if (!quietly || keep_files) { |
|
|
324 |
p <- plot(trial_structure_instance) |
|
|
325 |
|
|
|
326 |
if (!quietly) { |
|
|
327 |
print(p) |
|
|
328 |
} |
|
|
329 |
|
|
|
330 |
if (keep_files) { |
|
|
331 |
ggplot2::ggsave( |
|
|
332 |
paste0(figs_path, "structure-", run_time, ".png"), |
|
|
333 |
plot = p, |
|
|
334 |
width = 12, |
|
|
335 |
height = 8, |
|
|
336 |
dpi = 400 |
|
|
337 |
) |
|
|
338 |
} |
|
|
339 |
} |
|
|
340 |
|
|
|
341 |
# Return accrual object |
|
|
342 |
return(accrual_instance) |
|
|
343 |
} |