Skip to content

Commit 35600e2

Browse files
committed
repeat doses + safer with NAs to nonmem
1 parent cb628f1 commit 35600e2

8 files changed

Lines changed: 155 additions & 13 deletions

R/reformat_data_modeling_to_modeling.R

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
#' @param data dataset formatted as modeling-ready dataset
55
#' @param dictionary a data dictionary that maps expected variable names to
66
#' variables in the data.
7+
#' @param na what to set NA values to. E.g. ".", (default) or NA (keep NA),
8+
#' or NULL (do nothing).
79
#'
810
#' @returns data.frame with population PK input data in NONMEM-style
911
#' format.
1012
#'
1113
#' @export
1214
reformat_data_modeling_to_modeling <- function(
1315
data,
14-
dictionary = NULL
16+
dictionary = NULL,
17+
na = "."
1518
) {
1619

1720
data <- data |>
@@ -34,6 +37,12 @@ reformat_data_modeling_to_modeling <- function(
3437
data$GROUP <- 1 # dummy grouper
3538
}
3639
}
37-
40+
41+
## Convert NA's to dots (or something else)
42+
if(!is.null(na)) {
43+
data <- data |>
44+
dplyr::mutate(dplyr::across(dplyr::everything(), ~ifelse(is.na(.) | . == "NA", na, .)))
45+
}
46+
3847
data
3948
}

R/reformat_data_modeling_to_nca.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
#' @param data dataset formatted as modeling-ready dataset
55
#' @param dictionary a data dictionary that maps expected variable names to
66
#' variables in the data.
7+
#' @param na what to set NA values to. E.g. ".", or NA (keep NA, default),
8+
#' or NULL (do nothing).
79
#'
810
#' @returns data.frame with population PK input data in NONMEM-style
911
#' format.
1012
#'
1113
#' @export
1214
reformat_data_modeling_to_nca <- function(
1315
data,
14-
dictionary = NULL
16+
dictionary = NULL,
17+
na = NA
1518
) {
1619
## TODO:
1720
# strip out EVID=2.

R/reformat_data_nca_to_modeling.R

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88
#' @param obs_compartment the observation compartment number
99
#' @param covariates a vector of covariate names that are to be extracted
1010
#' and added to the modeling dataset.
11+
#' @param na what to set NA values to. E.g. ".", (default) or NA (keep NA),
12+
#' or NULL (do nothing).
13+
#' @param repeat_doses Optional list for repeated dosing (MAD studies). Must
14+
#' contain `interval` (dosing interval in TIME units). Optionally contains `n`
15+
#' (total number of doses). If `n` is omitted, it is inferred per subject/group
16+
#' as `ceiling(max(observation_time) / interval)`. Only applies to column-wise
17+
#' dose data. Default `NULL` preserves existing behavior (no ADDL/II columns).
18+
#' Examples: `list(interval = 12)` or `list(n = 5, interval = 12)`.
1119
#'
1220
#' @returns data.frame with population PK input data in NONMEM-style
1321
#' format.
14-
#'
22+
#'
1523
#' @export
1624
reformat_data_nca_to_modeling <- function(
17-
data,
25+
data,
1826
dictionary = list(
1927
subject_id = "ID",
2028
group = "GROUP",
@@ -24,7 +32,9 @@ reformat_data_nca_to_modeling <- function(
2432
),
2533
dose_compartment = 1,
2634
obs_compartment = 1,
27-
covariates = NULL
35+
covariates = NULL,
36+
repeat_doses = NULL,
37+
na = "."
2838
) {
2939

3040
groups <- c(dictionary$subject_id, dictionary$group)
@@ -56,12 +66,40 @@ reformat_data_nca_to_modeling <- function(
5666
dplyr::filter(!is.na(AMT)) |>
5767
dplyr::mutate(EVID = 1, MDV = 1, DV = 0, CMT = dose_compartment) |>
5868
dplyr::left_join(ids, by = dplyr::join_by("ORIGID"))
69+
5970
if(nrow(doses) == nrow(data)) { # Dose is given as a column, and not row-wise using EVID
6071
doses <- doses |>
6172
dplyr::group_by(.data$ORIGID, .data$GROUP) |>
6273
dplyr::slice(1) |>
6374
dplyr::mutate(TIME = 0) |>
6475
dplyr::ungroup()
76+
77+
if (!is.null(repeat_doses)) {
78+
if (is.null(repeat_doses$interval)) {
79+
stop("`repeat_doses` must contain an `interval` element.")
80+
}
81+
interval <- repeat_doses$interval
82+
if (!is.null(repeat_doses$n)) {
83+
doses <- doses |>
84+
dplyr::mutate(ADDL = as.numeric(repeat_doses$n) - 1, II = interval)
85+
} else {
86+
max_obs_times <- data |>
87+
dplyr::select(
88+
ORIGID = !!dictionary$subject_id,
89+
GROUP = !!dictionary$group,
90+
TIME = !!dictionary$time
91+
) |>
92+
dplyr::group_by(.data$ORIGID, .data$GROUP) |>
93+
dplyr::summarise(max_obs_time = max(.data$TIME, na.rm = TRUE), .groups = "drop")
94+
doses <- doses |>
95+
dplyr::left_join(max_obs_times, by = c("ORIGID", "GROUP")) |>
96+
dplyr::mutate(
97+
ADDL = pmax(0, ceiling(.data$max_obs_time / interval) - 1),
98+
II = interval
99+
) |>
100+
dplyr::select(-"max_obs_time")
101+
}
102+
}
65103
}
66104

67105
## Observations
@@ -78,15 +116,19 @@ reformat_data_nca_to_modeling <- function(
78116
stringr::str_detect(tolower(.data$DV), "[<a-z]"), -99, .data$DV
79117
))) |>
80118
dplyr::left_join(ids, by = dplyr::join_by("ORIGID"))
81-
119+
120+
if (!is.null(repeat_doses)) {
121+
samples <- samples |> dplyr::mutate(ADDL = 0, II = 0)
122+
}
123+
82124
## Combine
83125
comb <- dplyr::bind_rows(
84126
doses,
85127
samples
86128
) |>
87129
dplyr::mutate(ifelse(is.null(.data$GROUP), 1, .data$GROUP)) |>
88130
dplyr::arrange(!!dictionary$subject_id, !!dictionary$group, !!dictionary$time, .data$EVID) |>
89-
dplyr::select("ID", "TIME", "CMT", "EVID", "MDV", "DV", "AMT", "GROUP", "ORIGID", !!covariates) |>
131+
dplyr::select("ID", "TIME", "CMT", "EVID", "MDV", "DV", "AMT", dplyr::any_of(c("ADDL", "II")), "GROUP", "ORIGID", !!covariates) |>
90132
dplyr::arrange(.data$GROUP, .data$ID, .data$TIME, -.data$EVID)
91133

92134
## Convert all character columns to categorical (but numeric)
@@ -99,7 +141,14 @@ reformat_data_nca_to_modeling <- function(
99141
}
100142

101143
## Remove any observations with DV = -99
102-
comb <- dplyr::filter(comb, .data$DV != -99)
144+
comb <- comb |>
145+
dplyr::filter(.data$DV != -99)
146+
147+
## Convert NA's to dots or something else
148+
if(!is.null(na)) {
149+
comb <- comb |>
150+
dplyr::mutate(dplyr::across(dplyr::everything(), ~ifelse(is.na(.) | . == "NA", na, .)))
151+
}
103152

104153
## Return
105154
comb

man/reformat_data_modeling_to_modeling.Rd

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

man/reformat_data_modeling_to_nca.Rd

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

man/reformat_data_nca_to_modeling.Rd

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

man/reformat_data_sdtm_to_modeling.Rd

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

tests/testthat/test-reformat_data_nca_to_modeling.R

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,66 @@ test_that("dose records are reduced to one per subject when dose is column-wise"
7575
expect_equal(out$TIME[out$EVID == 1], 0)
7676
})
7777

78+
test_that("repeat_doses = NULL produces no ADDL/II columns", {
79+
dat <- data.frame(
80+
ID = c(1, 1, 1),
81+
TIME = c(0, 12, 24),
82+
AMT = c(100, 100, 100),
83+
DV = c(NA, 5, 3)
84+
)
85+
out <- reformat_data_nca_to_modeling(data = dat)
86+
expect_false("ADDL" %in% names(out))
87+
expect_false("II" %in% names(out))
88+
})
89+
90+
test_that("repeat_doses with explicit n adds correct ADDL/II", {
91+
dat <- data.frame(
92+
ID = c(1, 1, 1),
93+
TIME = c(0, 12, 24),
94+
AMT = c(100, 100, 100),
95+
DV = c(NA, 5, 3)
96+
)
97+
out <- reformat_data_nca_to_modeling(data = dat, repeat_doses = list(n = 5, interval = 12))
98+
dose_rows <- out[out$EVID == 1, ]
99+
obs_rows <- out[out$EVID == 0, ]
100+
expect_equal(dose_rows$ADDL, 4)
101+
expect_equal(dose_rows$II, 12)
102+
expect_true(all(obs_rows$ADDL == 0))
103+
expect_true(all(obs_rows$II == 0))
104+
})
105+
106+
test_that("repeat_doses without n infers ADDL per subject from max obs time", {
107+
dat <- data.frame(
108+
ID = c(1, 1, 1, 2, 2, 2),
109+
TIME = c(0, 12, 24, 0, 6, 12),
110+
AMT = c(100, 100, 100, 200, 200, 200),
111+
DV = c(NA, 5, 3, NA, 8, 6)
112+
)
113+
out <- reformat_data_nca_to_modeling(data = dat, repeat_doses = list(interval = 12))
114+
dose_rows <- out[out$EVID == 1, ]
115+
# Subject 1: max obs time = 24, ceiling(24/12) - 1 = 1
116+
expect_equal(dose_rows$ADDL[dose_rows$ID == 1], 1)
117+
# Subject 2: max obs time = 12, ceiling(12/12) - 1 = 0 (pmax guard)
118+
expect_equal(dose_rows$ADDL[dose_rows$ID == 2], 0)
119+
expect_true(all(dose_rows$II == 12))
120+
obs_rows <- out[out$EVID == 0, ]
121+
expect_true(all(obs_rows$ADDL == 0))
122+
expect_true(all(obs_rows$II == 0))
123+
})
124+
125+
test_that("repeat_doses without interval raises an error", {
126+
dat <- data.frame(
127+
ID = c(1, 1),
128+
TIME = c(0, 12),
129+
AMT = c(100, 100),
130+
DV = c(NA, 5)
131+
)
132+
expect_error(
133+
reformat_data_nca_to_modeling(data = dat, repeat_doses = list(n = 3)),
134+
"interval"
135+
)
136+
})
137+
78138
test_that("reformat_data_nca_to_modeling handles multiple doses per subject", {
79139
# Create data with multiple dose events per subject (row-wise dosing)
80140
dat <- data.frame(

0 commit comments

Comments
 (0)