Diff of /R/spine.R [000000] .. [d9ee58]

Switch to unified view

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
}