Skip to content

Commit 7284884

Browse files
Fixes for use of models with IOV + grouping of samples + example busulfan (#5)
* misc updates + skeleton for first versions main functions * updates * add initial version + 1st test * updates * add comp with proseval * doc updates + minor fixes * remove windows CI for now * activate threads option * allow specifying model info only with library name * fix issue * add ids argument; add test for prior weight * updates * fix iov * updates for grouping * update parsing of proseval when data is grouped * update busulfan dataset * update grouping functions * add test grouping * misc fixes * misc fixes * misc fixes * fixes from copilot review * make sure dataset is reproducible * Grouping suggestions (#6) * make sure `nm_vanco` is an ungrouped tibble * make sure `nm_busulfan` is an ungrouped tibble * use tidyselect syntax, not data-masking * tidy grouping functions + add tests * rebuild README * refactor `check_input_data()`, add tests * tidy documentation * add TODO comments * tidy tests * tidy code * clarify abbrev * bump version --------- Co-authored-by: Michael McCarthy <51542091+mccarthy-m-g@users.noreply.github.com>
1 parent cb70f74 commit 7284884

37 files changed

Lines changed: 6481 additions & 372 deletions

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: mipdeval
22
Title: Run evaluation of iterative predictive performance based
33
on historical datasets
4-
Version: 0.0.0.9000
4+
Version: 0.1.0
55
Authors@R: c(
66
person("Ron", "Keizer", , "ron@insight-rx.com", role = c("aut", "cre")),
77
person("Michael", "McCarthy", , "michael.mccarthy@insight-rx.com", role = c("aut")),

NAMESPACE

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
S3method(parse_model,PKPDsim)
44
S3method(parse_model,character)
55
S3method(parse_model,default)
6+
export(add_grouping_column)
67
export(compare_psn_execute_results)
78
export(compare_psn_proseval_results)
9+
export(group_by_dose)
10+
export(group_by_time)
811
export(install_default_literature_model)
912
export(new_ode_model)
1013
export(parse_psn_proseval_results)
1114
export(reldiff_psn_execute_results)
1215
export(reldiff_psn_proseval_results)
1316
export(run_eval)
14-
export(run_eval_core)
1517
importFrom(PKPDsim,install_default_literature_model)
1618
importFrom(PKPDsim,new_ode_model)
1719
importFrom(rlang,.data)

R/add_grouping_column.R

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#' Add grouping column using a function
2+
#'
3+
#' @inheritParams run_eval
4+
#'
5+
#' @param fun Function used to define groups. Either (1) a quoted or character
6+
#' name referencing a function (e.g., [group_by_time()], [group_by_dose()], or a
7+
#' custom function), or (2) an anonymous function; see examples.
8+
#' @param label column name of grouper column in dataset
9+
#' @param ... Additional arguments passed onto `fun`. See the respective
10+
#' grouping functions for more details. E.g. for `group_by_time`, need a
11+
#' `bins` argument.
12+
#'
13+
#' @details
14+
#'
15+
#' All functions supplied to `add_grouping_column()` must take at least the
16+
#' argument `data`, and return a numeric vector (or a vector that can be cast to
17+
#' numeric) of the same length as the number of rows in `data`, which will be
18+
#' used as the grouping column.
19+
#'
20+
#' @returns data.frame
21+
#' @seealso [group_by_time()], [group_by_dose()]
22+
#' @examples
23+
#' # group_by_dose:
24+
#' add_grouping_column(nm_busulfan, fun = group_by_dose, label = "group")
25+
#'
26+
#' # group_by_time:
27+
#' add_grouping_column(nm_busulfan, fun = "group_by_time", label = "group")
28+
#'
29+
#' # Anonymous function:
30+
#' add_grouping_column(
31+
#' nm_busulfan,
32+
#' fun = function(data) {
33+
#' as.numeric(cut(
34+
#' data$TIME, breaks = c(0, 24, 48, 72, 96, Inf), include.lowest = TRUE
35+
#' ))
36+
#' },
37+
#' label = "group"
38+
#' )
39+
#'
40+
#' @export
41+
add_grouping_column <- function(
42+
data,
43+
fun = group_by_time,
44+
label = "group",
45+
...
46+
) {
47+
if(label %in% names(data)) {
48+
cli::cli_alert_warning("Overwriting {.field {label}} column in {.arg data}.")
49+
}
50+
if(inherits(fun, "character")) {
51+
fun <- get(fun)
52+
}
53+
data[[label]] <- fun(data, ...)
54+
data
55+
}
56+
57+
#' Group data by time using bin separators
58+
#'
59+
#' @inheritParams add_grouping_column
60+
#' @inheritParams run_eval
61+
#' @param bins vector of bin separators. Suggestion to keep the last bin
62+
#' separator `Inf`, to ensure all points are included in a group.
63+
#'
64+
#' @returns a numeric vector of the same length as the number of rows in `data`.
65+
#'
66+
#' @examples
67+
#' group_by_time(nm_busulfan)
68+
#'
69+
#' @export
70+
group_by_time <- function(
71+
data,
72+
bins = c(0, 24, 48, 72, 96, Inf),
73+
dictionary = list("TIME" = "TIME"),
74+
...
75+
) {
76+
as.numeric(cut(data[[dictionary$TIME]], breaks = bins, include.lowest = TRUE))
77+
}
78+
79+
#' Will create a separate group for each dose intervals that contains at least
80+
#' one sample
81+
#'
82+
#' @inheritParams add_grouping_column
83+
#' @inheritParams run_eval
84+
#'
85+
#' @returns a numeric vector of the same length as the number of rows in `data`.
86+
#'
87+
#' @examples
88+
#' group_by_dose(nm_busulfan)
89+
#'
90+
#' @export
91+
group_by_dose <- function(
92+
data,
93+
dictionary = list("ID" = "ID", "EVID" = "EVID", "TIME" = "TIME"),
94+
...
95+
) {
96+
data |>
97+
dplyr::group_by(dplyr::pick(dictionary$ID)) |>
98+
dplyr::mutate(`_dose_idx` = cumsum(.data[[dictionary$EVID]])) |>
99+
dplyr::group_by(dplyr::pick(dictionary$ID, "_dose_idx")) |>
100+
dplyr::mutate(`_has_sample` = any(.data[[dictionary$EVID]] == 0)) |>
101+
dplyr::group_by(dplyr::pick(dictionary$ID)) |>
102+
dplyr::mutate(
103+
`_group` = cumsum(.data[[dictionary$EVID]] == 1 & .data$`_has_sample`)
104+
) |>
105+
dplyr::pull(.data$`_group`)
106+
}

R/check_input_data.R

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,57 @@
88
#' @returns a data.frame
99
check_input_data <- function(data, dictionary) {
1010
if(!is.null(dictionary) && length(dictionary) > 0) {
11-
if(!all(unlist(dictionary) %in% names(data))) {
12-
cli::cli_abort("Not all columns listed in dictionary were found in input data.")
13-
}
11+
check_dictionary_columns(data, dictionary)
12+
check_valid_dictionary(dictionary)
13+
# TODO: We should move this into a different function like tidy_input_data().
14+
# Checks and data manipulation should occur separately.
1415
cli::cli_alert_info("Renaming dataset columns based on dictionary.")
15-
new_data <- dplyr::rename(data, dplyr::all_of(dictionary))
16+
data <- data |>
17+
dplyr::rename(
18+
!!!rlang::set_names(
19+
dictionary,
20+
names(dictionary)
21+
)
22+
)
1623
} else {
1724
cli::cli_alert_info("No data `dictionary` provided, assuming common NONMEM column names.")
1825
}
26+
check_required_cols(data)
1927
data
2028
}
29+
30+
check_dictionary_columns <- function(data, dictionary) {
31+
has_cols <- unlist(dictionary) %in% names(data)
32+
if(all(has_cols)) return()
33+
missing_cols <- unlist(dictionary)[!has_cols]
34+
cli::cli_abort(c(
35+
"Column names in {.arg dictionary} must exist in {.arg data}.",
36+
"x" = "{.str {missing_cols}} {?is/are} not {?a/} column{?s} in {.arg data}."
37+
))
38+
}
39+
40+
check_valid_dictionary <- function(dictionary) {
41+
valid_names <- c("ID", "TIME", "EVID", "DV")
42+
has_valid_names <- names(dictionary) %in% valid_names
43+
if (all(has_valid_names)) return()
44+
invalid_names <- names(dictionary)[!has_valid_names]
45+
cli::cli_abort(c(
46+
"{.arg dictionary} names must be any of the following: {.field {valid_names}}.",
47+
"x" = "{.field {invalid_names}} {?is/are} not {?a/} valid name{?s}.",
48+
"i" = "Did you make a typo?"
49+
))
50+
}
51+
52+
check_required_cols <- function(data) {
53+
required_cols <- c("ID", "TIME", "EVID", "DV")
54+
has_required <- required_cols %in% names(data)
55+
if (all(has_required)) return()
56+
missing_cols <- required_cols[!has_required]
57+
cli::cli_abort(c(
58+
"{.arg data} must have the following columns: {.field {required_cols}}.",
59+
"x" = "{.field {missing_cols}} {?is/are} missing.",
60+
"i" = "Did you forget to rename a column?"
61+
))
62+
}
63+
64+

R/compare_psn_results.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ compare_psn_results <- function(mipdeval, psn, tol = 0.1, .apriori) {
4848
# rel diff, whether all observations were within tolerance
4949
dplyr::summarise(
5050
reldiff_psn_results(mipdeval, psn, .apriori),
51-
within_tol = all(abs(.data$rel_diff) < tol)
51+
mean_rel_diff = mean(.data$rel_diff),
52+
sd_rel_diff = stats::sd(.data$rel_diff),
53+
max_rel_diff = max(abs(.data$rel_diff)),
54+
within_tol = all(.data$max_rel_diff < tol)
5255
)
5356
}
5457

R/parse_input_data.R

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
parse_input_data <- function(
99
data,
1010
covariates,
11-
ids
11+
ids,
12+
group = NULL
1213
) {
1314

1415
## Filter IDs, if needed
@@ -26,19 +27,30 @@ parse_input_data <- function(
2627
out <- purrr::map(
2728
new_data,
2829
parse_nm_data,
29-
covariates = covariates
30+
covariates = covariates,
31+
group = group
3032
)
3133

3234
out
3335
}
3436

35-
parse_nm_data <- function(data, covariates) {
37+
parse_nm_data <- function(data, covariates, group = NULL) {
3638
out <- list()
3739
out$covariates <- make_covariates_object(data, covariates)
3840
out$regimen <- PKPDsim::nm_to_regimen(data)
39-
out$observations <- data |>
41+
obs <- data |>
42+
dplyr::filter(.data$EVID == 0)
43+
if(!is.null(group)) {
44+
obs[["_grouper"]] <- obs[[group]]
45+
} else { # if no grouping, add a grouper that iterates over all observations
46+
obs <- obs |>
47+
dplyr::group_by(.data$ID) |>
48+
dplyr::mutate("_grouper" = 1:length(.data$ID)) |>
49+
dplyr::ungroup()
50+
}
51+
out$observations <- obs |>
4052
dplyr::filter(.data$EVID == 0) |>
41-
dplyr::select("ID", "TIME", "DV", "CMT") |>
53+
dplyr::select("ID", "TIME", "DV", "CMT", "_grouper") |>
4254
dplyr::rename(id = "ID", t = "TIME", y = "DV", cmt = "CMT")
4355
out
4456
}

R/parse_model.R

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,43 @@ parse_model.character <- function(
3131
parameters = NULL,
3232
ruv = NULL,
3333
omega = NULL,
34-
iov_bins = NULL
34+
fixed = NULL,
35+
iov = NULL
3536
) {
3637
check_installed_model_library(model)
38+
3739
withr::with_package(model, quietly = TRUE, {
3840
# Use model defaults if user has not supplied overrides:
3941
if (is.null(parameters)) parameters <- parameters()
4042
if (is.null(ruv)) ruv <- ruv()
4143
if (is.null(omega)) omega <- omega_matrix()
42-
if (is.null(iov_bins)) iov_bins <- iov()
43-
list(
44+
if (is.null(fixed)) fixed <- fixed()
45+
if (is.null(iov)) iov <- iov()
46+
bins <- as.numeric(iov$bins)
47+
out <- list(
4448
model = model(),
4549
parameters = parameters,
46-
# covariates = PKPDsim::get_model_covariates(model()),
4750
ruv = ruv,
48-
omega_matrix = omega,
49-
iov_bins = iov_bins,
50-
fixed = fixed()
51+
omega = omega,
52+
fixed = fixed(),
53+
bins = bins,
54+
kappa = NULL
5155
)
56+
if(!is.null(iov)) {
57+
iov_obj <- PKPDmap::create_iov_object(
58+
cv = iov$cv,
59+
omega = omega,
60+
bins = bins,
61+
parameters = parameters,
62+
fixed = fixed,
63+
ruv = ruv,
64+
verbose = FALSE
65+
)
66+
iov_updates <- c("parameters", "omega", "fixed", "bins", "kappa")
67+
out[iov_updates] <- iov_obj[iov_updates]
68+
}
5269
})
70+
out
5371
}
5472

5573
#' @export
@@ -60,16 +78,32 @@ parse_model.PKPDsim <- function(
6078
parameters,
6179
ruv,
6280
omega,
63-
iov_bins = NULL
81+
fixed = NULL,
82+
iov = NULL
6483
) {
65-
list(
84+
out <- list(
6685
model = model,
6786
parameters = parameters,
6887
ruv = ruv,
69-
omega_matrix = omega,
70-
iov_bins = iov_bins,
71-
fixed = attr(model, "fixed")
88+
omega = omega,
89+
fixed = fixed,
90+
bins = numeric(0),
91+
kappa = NULL
7292
)
93+
if(!is.null(iov)) {
94+
iov_obj <- PKPDmap::create_iov_object(
95+
cv = iov$cv,
96+
omega = omega,
97+
bins = as.numeric(iov$bins),
98+
parameters = parameters,
99+
fixed = fixed,
100+
ruv = ruv,
101+
verbose = FALSE
102+
)
103+
iov_updates <- c("parameters", "omega", "fixed", "bins", "kappa")
104+
out[iov_updates] <- iov_obj[iov_updates]
105+
}
106+
out
73107
}
74108

75109
#' Check if PKPDsim model library is installed

R/parse_psn_proseval_results.R

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
11
#' Parse PsN::proseval results.csv to filter out only the rows
2-
#' that we need (for prediction of next level)
2+
#' that we need (for prediction of next sample or group of samples)
33
#'
44
#' @param data results data.frame or path to csv file
5+
#' @param group optional. Group samples using the column name specified in
6+
#' `group`
57
#'
68
#' @returns A data.frame
79
#' @export
8-
parse_psn_proseval_results <- function(data) {
10+
parse_psn_proseval_results <- function(data, group = NULL) {
911
if(inherits(data, "character")) {
1012
res <- read.csv(file = data)
1113
} else {
1214
res <- data
1315
}
14-
res |>
16+
res <- res |>
1517
dplyr::group_by(.data$ID) |>
16-
dplyr::filter(.data$EVID == 2 & .data$DV > 0) |>
17-
dplyr::filter(!duplicated(.data$OBS)) |>
18-
dplyr::mutate(n = 1:length(.data$OBS)) |>
18+
dplyr::filter(.data$EVID == 2 & .data$DV > 0)
19+
if(is.null(group)) { # assume we just want to look at next observation
20+
group <- "group"
21+
res <- res |>
22+
dplyr::group_by(.data$OBS, .add = TRUE) |>
23+
dplyr::mutate(group = seq(.data$OBS[1] + 1, .data$OBS[1] + dplyr::n(), 1)) |>
24+
dplyr::ungroup()
25+
}
26+
## create a data.frame on which OBS iterations to take from proseval results
27+
idx_tab <- res |>
28+
dplyr::filter(.data$OBS == 1) |>
29+
dplyr::group_by(.data$ID) |>
30+
dplyr::mutate(idx = 1:dplyr::n()) |>
31+
dplyr::filter(!duplicated(group)) |>
32+
dplyr::ungroup() |>
33+
dplyr::filter(group > 1) |>
34+
dplyr::select("ID", "idx") |>
35+
dplyr::group_by(.data$ID) |>
36+
dplyr::summarise(idx = list(.data$idx))
37+
dplyr::left_join(res, idx_tab) |>
38+
dplyr::filter( # filter rows that match the index table
39+
purrr::map2_lgl(.data$idx, .data$OBS, ~ .y %in% .x)
40+
) |>
41+
dplyr::group_by(.data$ID, .data$OBS) |>
42+
dplyr::filter(.data$group == .data$group[1]) |>
1943
dplyr::mutate(res = .data$DV - .data$IPRED) |>
20-
dplyr::select("TIME", obs = "DV", pred = "IPRED", pop = "PRED", "res", "n") |>
44+
dplyr::select("TIME", obs = "DV", pred = "IPRED", pop = "PRED", "res", !!group) |>
2145
dplyr::ungroup()
2246
}

0 commit comments

Comments
 (0)