diff --git a/NAMESPACE b/NAMESPACE index e1ee121a..07fa0df1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -52,6 +52,10 @@ S3method(group_by,PKNCAresults) S3method(inner_join,PKNCAconc) S3method(inner_join,PKNCAdose) S3method(inner_join,PKNCAresults) +S3method(interval_add_impute,PKNCAdata) +S3method(interval_add_impute,data.frame) +S3method(interval_remove_impute,PKNCAdata) +S3method(interval_remove_impute,data.frame) S3method(is_sparse_pk,PKNCAconc) S3method(is_sparse_pk,PKNCAdata) S3method(is_sparse_pk,PKNCAresults) @@ -148,6 +152,8 @@ export(inner_join) export(interp.extrap.conc) export(interp.extrap.conc.dose) export(interpolate.conc) +export(interval_add_impute) +export(interval_remove_impute) export(is_sparse_pk) export(left_join) export(mutate) diff --git a/NEWS.md b/NEWS.md index 874bcda2..318aa773 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,11 +32,13 @@ the dosing including dose amount and route. * `group_vars()` methods were added for `PKNCAdata` and `PKNCAresults` objects. * If intervals have attributes on the columns, there will no longer be an error during parameter calculation, and the attributes are preserved (#381) +* New functions are available to simplify the modification of intervals: + `intervals_add_impute()`, `intervals_remove_impute()` (#384) * When adding units, if some but not all units are provided, then an error will be raised. This error can be converted to a warning using the option `allow_partial_missing_units = TRUE`. (#398) -# Minor changes (unlikely to affect PKNCA use) +## Minor changes (unlikely to affect PKNCA use) * PKNCA will now verify the `intervals` data.frame when creating PKNCAdata. The checking includes confirming intended column naming and ensuring the correct diff --git a/R/intervals_support.R b/R/intervals_support.R new file mode 100644 index 00000000..e8a56016 --- /dev/null +++ b/R/intervals_support.R @@ -0,0 +1,388 @@ +#' Add specified imputation methods to the intervals in a PKNCAdata or data.frame object. +#' +#' @param data A PKNCAdata object containing the intervals data frame, or a data frame of intervals. +#' @param target_impute A character string specifying the imputation method to be added. +#' @param after Numeric value specifying the index in which the imputation will be added (optional). +#' First is 0, last Inf. If missing, the imputation method is added at the end (Inf). +#' @param target_params A character vector specifying the parameters to be targeted (optional). +#' If missing, all TRUE in the intervals are taken. +#' @param target_groups A data frame specifying the intervals to be targeted (optional). +#' If missing, all relevant groups are considered. +#' @param ... arguments passed to `interval_add_impute`. +#' @details If already present the target_impute method will be added substituting the existing one. +#' All new intervals created will be added right after their original ones. +#' @returns A modified PKNCAdata object with specified imputation methods on the target intervals. +#' @examples +#' d_conc <- data.frame( +#' conc = c(1, 0.6, 0.2, 0.1, 0.9, 0.4, 1.2, 0.8, 0.3, 0.2, 1.1, 0.5), +#' time = rep(0:5, 2), +#' ID = rep(1:2, each = 6), +#' analyte = rep(c("Analyte1", "Analyte2"), each = 6) +#' ) +#' +#' d_dose <- data.frame( +#' dose = c(100, 200), +#' time = c(0, 0), +#' ID = c(1, 2) +#' ) +#' +#' o_conc <- PKNCAconc(d_conc, conc ~ time | ID / analyte) +#' o_dose <- PKNCAdose(d_dose, dose ~ time | ID) +#' +#' intervals <- data.frame( +#' start = c(0, 0, 0), +#' end = c(3, 5, Inf), +#' half.life = c(TRUE, TRUE, TRUE), +#' cmax = c(TRUE, TRUE, TRUE), +#' impute = c("start_conc0,start_predose", "start_predose", "start_conc0"), +#' analyte = c("Analyte1", "Analyte2", "Analyte1") +#' ) +#' +#' o_data <- PKNCAdata(o_conc, o_dose, intervals = intervals) +#' +#' # Apply interval_add_impute function +#' o_data <- interval_add_impute(o_data, +#' target_impute = "start_conc0", +#' target_params = "half.life", +#' target_groups = data.frame(analyte = "Analyte1")) +#' @export +interval_add_impute <- function(data, target_impute, after, target_params, target_groups, ...) { + UseMethod("interval_add_impute", data) +} + +#' Remove specified imputation from the intervals in a PKNCAdata or data.frame (intervals) object. +#' +#' @inheritParams interval_add_impute +#' @param target_impute A character string specifying the imputation method to remove. +#' @param ... arguments passed to `interval_remove_impute`. +#' @returns A modified object with the specified imputations removed from the targeted intervals. +#' @examples +#' d_conc <- data.frame( +#' conc = c(1, 0.6, 0.2, 0.1, 0.9, 0.4, 1.2, 0.8, 0.3, 0.2, 1.1, 0.5), +#' time = rep(0:5, 2), +#' ID = rep(1:2, each = 6), +#' analyte = rep(c("Analyte1", "Analyte2"), each = 6) +#' ) +#' +#' d_dose <- data.frame( +#' dose = c(100, 200), +#' time = c(0, 0), +#' ID = c(1, 2) +#' ) +#' +#' o_conc <- PKNCAconc(d_conc, conc ~ time | ID / analyte) +#' o_dose <- PKNCAdose(d_dose, dose ~ time | ID) +#' +#' intervals <- data.frame( +#' start = c(0, 0, 0), +#' end = c(3, 5, Inf), +#' half.life = c(TRUE, FALSE, TRUE), +#' cmax = c(TRUE, TRUE, TRUE), +#' impute = c("start_conc0,start_predose", "start_predose", "start_conc0"), +#' analyte = c("Analyte1", "Analyte2", "Analyte1") +#' ) +#' +#' o_data <- PKNCAdata(o_conc, o_dose, intervals = intervals) +#' +#' # Apply interval_remove_impute function +#' o_data <- interval_remove_impute(data = o_data, +#' target_impute = "start_conc0", +#' target_params = "half.life", +#' target_groups = data.frame(analyte = "Analyte1")) +#' @export +interval_remove_impute <- function(data, target_impute, ...) { + UseMethod("interval_remove_impute", data) +} + +#' @export +interval_add_impute.PKNCAdata <- function(data, target_impute, after = Inf, + target_params = NULL, target_groups = NULL, ...) { + # If the impute column is not present, add it to the intervals + if (!"impute" %in% names(data$intervals) && !is.null(data$impute)) { + data$intervals$impute <- data$impute + data$impute <- NA_character_ + } + data$intervals <- interval_add_impute.data.frame(data$intervals, target_impute, + after, target_params, target_groups) + data +} + +#' @export +interval_remove_impute.PKNCAdata <- function(data, target_impute, target_params = NULL, + target_groups = NULL, ...) { + # If the impute column is not present in the intervals... + if (!"impute" %in% colnames(data$intervals)) { + if (is.null(data$impute) || is.na(data$impute)) { + # a. If there is neither a global impute, return the input as it is (nothing to remove) + warning("No default impute column or global method identified. No impute methods to remove") + return(data) + } + + # b & c. If there is a global impute.. + if (is.null(target_params) && is.null(target_groups)) { + # b. and user changes apply to all intervals, just remove global impute + data$impute <- remove_impute_method(data$impute, target_impute) + return(data) + } + + # c. but user changes are specific (target_params or target_groups), creates an impute column + data$intervals$impute <- data$impute + data$impute <- NA_character_ + } + + data$intervals <- interval_remove_impute.data.frame(data$intervals, target_impute, + target_params, target_groups) + data +} + +#' @export +interval_add_impute.data.frame <- function(data, target_impute, after = Inf, + target_params = NULL, target_groups = NULL, ...) { + # Validate inputs + if (missing(data) || missing(target_impute)) { + stop("Both 'data' and 'target_impute' must be provided.") + } + if (!is.character(target_impute)) { + stop("'target_impute' must be a character string.") + } + if (is.na(target_impute) || target_impute == "") { + warning("No impute method specified. No changes made.") + return(data) + } + + # Ensure the impute column exists and is a character column + if (!"impute" %in% colnames(data)) { + data$impute <- NA_character_ + } else if (!is.character(data$impute)) { + stop("The 'impute' column in the intervals data.frame must be a character column.") + } + + # Add an index column to preserve the original order + index_colname <- make.unique(c("index", names(data)))[1] + data[[index_colname]] <- seq_len(nrow(data)) + + # Get all parameter column names in the data frame + all_param_options <- setdiff(names(get.interval.cols()), c("start", "end")) + param_cols <- intersect(names(data), all_param_options) + + # If missing, define target parameters as all parameter columns with at least one TRUE. + if (is.null(target_params)) { + target_params <- param_cols + } + + assert_subset(target_params, all_param_options) + + # Identify the target interval rows based on: + target_rows <- identify_target_rows(data, target_impute, target_params, target_groups, after) + new_intervals <- data[target_rows, ] + + # If no target intervals are found, nothing to change + if (nrow(new_intervals) == 0) { + warning("No intervals found with the specified target parameters,", + " groups, and/or after-change needed. No changes made.") + return(data[, !names(data) %in% index_colname]) + + # If target intervals are found... + } else { + # The new imputation should not be used non-target parameters + new_intervals[, setdiff(param_cols, target_params)] <- NA + + # Index the new intervals to be after the original ones + new_intervals[["impute"]] <- add_impute_method(new_intervals[["impute"]], target_impute, after) + new_intervals[[index_colname]] <- new_intervals[[index_colname]] + 0.5 + } + + # Remove the target parameters calculation from the original target intervals + data[target_rows, target_params] <- NA + + # Combine the new and original intervals + data <- rbind(data, new_intervals) + + # Filter rows where all row values for param_cols are NA or FALSE + param_data <- data[, param_cols, drop = FALSE] + rows_no_params <- rowSums(replace(param_data, is.na(param_data), FALSE)) == 0 + data <- data[!rows_no_params, , drop = FALSE] + + # Order the intervals by the index column and then remove it + data <- data[order(data[[index_colname]]), ] + rownames(data) <- seq_len(nrow(data)) + data[, !names(data) %in% index_colname] +} + +#' @export +interval_remove_impute.data.frame <- function(data, + target_impute, + target_params = NULL, + target_groups = NULL, + ...) { + # Validate inputs + if (missing(data) || missing(target_impute)) { + stop("Both 'data' and 'target_impute' must be provided.") + } + if (!is.character(target_impute)) { + stop("'target_impute' must be a character string.") + } + if (is.na(target_impute) || target_impute == "") { + warning("No impute method specified. No changes made.") + return(data) + } + + # Ensure the impute column exists and is a character column + if (!"impute" %in% colnames(data)) { + warning("No default impute column identified. No impute methods to remove") + return(data) + } else if (!is.character(data$impute)) { + stop("The 'impute' column in the intervals data.frame must be a character column.") + } + + # Add an index column to preserve the original order + index_colname <- make.unique(c("index", names(data)))[1] + data[[index_colname]] <- seq_len(nrow(data)) + + # Get all parameter column names in the data frame + all_param_options <- setdiff(names(get.interval.cols()), c("start", "end")) + param_cols <- intersect(names(data), all_param_options) + + # Handle target_params + if (is.null(target_params)) { + target_params <- param_cols + } + + assert_subset(target_params, all_param_options) + + # Identify the interval rows that need to be changed + target_rows <- identify_target_rows(data, target_impute, target_params, target_groups) + new_intervals <- data[target_rows, ] + + # If no target intervals are found, nothing to change + if (nrow(new_intervals) == 0) { + warning(paste0("No intervals found with the specified target parameters,", + " groups and/or impute method. No changes made.")) + return(data[, !names(data) %in% index_colname]) + + # If target intervals are found... + } else { + # The new imputation should not involve non-target parameters + new_intervals[, setdiff(param_cols, target_params)] <- NA + + # Index the new intervals to be after the original ones + new_intervals[["impute"]] <- remove_impute_method(new_intervals[["impute"]], target_impute) + new_intervals[[index_colname]] <- new_intervals[[index_colname]] + 0.5 + } + + # Remove the target parameters calculation from the original target intervals + data[target_rows, target_params] <- NA + + # Combine the new and original intervals + data <- rbind(data, new_intervals) + + # Filter rows where all row values for param_cols are NA/FALSE + param_data <- data[, param_cols, drop = FALSE] + rows_no_params <- rowSums(replace(param_data, is.na(param_data), FALSE)) == 0 + data <- data[!rows_no_params, , drop = FALSE] + + # Order the intervals by the index column and then remove it + data <- data[order(data[[index_colname]]), ] + rownames(data) <- seq_len(nrow(data)) + data[, !names(data) %in% index_colname] +} + +#' Add impute method to the impute column +#' +#' This is an internal helper function used to add an impute method to the impute column. +#' +#' @param impute_vals Character vector of impute methods. +#' @param target_impute The imputation method to be added. +#' @param after Numeric value specifying the index position in which to add the impute. +#' @returns A character string or vector with the added impute method. +#' @keywords internal +add_impute_method <- function(impute_vals, target_impute, after) { + # Make sure the character vector has length + if (length(impute_vals) == 0) return(impute_vals) + + # Remove the impute from the other methods in each value + impute_vals <- ifelse(is.na(impute_vals), "", impute_vals) + strsplit(impute_vals, split = "[ ,]+") |> + lapply(FUN = setdiff, target_impute) |> + lapply(FUN = append, values = target_impute, after = after) |> + vapply(FUN = paste, collapse = ",", FUN.VALUE = "") +} + +#' Remove impute method from the impute column +#' +#' This is an internal helper function used to remove an impute method from the impute column. +#' +#' @param impute_vals Character vector of impute methods. +#' @param target_impute The imputation method to be removed. +#' @returns A character string or vector without the specified impute method. +#' @details Resulting empty string values are replaced with NA_character_. +#' @keywords internal +remove_impute_method <- function(impute_vals, target_impute) { + # Make sure the character vector has length + if (length(impute_vals) == 0) return(impute_vals) + + # Remove the impute from the other methods in each value + impute_vals <- ifelse(is.na(impute_vals), "", impute_vals) + impute_vals <- strsplit(impute_vals, split = "[ ,]+") |> + lapply(FUN = setdiff, target_impute) |> + vapply(FUN = paste, collapse = ",", FUN.VALUE = "") + + # Replace empty strings with NA_character_ + ifelse(impute_vals == "", NA_character_, impute_vals) +} + +#' Identify target rows based on groups, parameters, and impute method +#' +#' This is an internal helper function used to identify the target rows in the data frame +#' based on the specified groups, parameters, and impute method. +#' +#' @param data A data frame containing the intervals. +#' @param target_impute The imputation method to be added or removed. +#' @param target_params A character vector specifying the parameters to be targeted. +#' @param target_groups A data frame specifying the intervals to be targeted. +#' @param after Numeric value specifying the index position in which to add the impute (optional). +#' @returns A logical vector indicating the target rows. +#' @keywords internal +identify_target_rows <- function(data, target_impute, target_params, target_groups, after = NULL) { + # Identify the target interval rows based on: + ## 1. The target groups (perfect match) + is_target_group <- { + if (!is.null(target_groups)) { + sapply(data[, names(target_groups), drop = FALSE], paste0) %in% sapply(target_groups, paste0) + } else { + rep(TRUE, nrow(data)) + } + } + + ## 2. The target parameters (at least one calculated: not-FALSE/not-NA) + target_params_data <- data[, target_params, drop = FALSE] + is_target_param <- rowSums(replace(target_params_data, is.na(target_params_data), FALSE)) > 0 + + ## 3. The target impute method is not present and correctly positioned (if after is provided) + if (!is.null(after)) { + after_vals <- sapply(strsplit(data$impute, "[ ,]+"), \(x) { + after_x <- which(x == target_impute) + if (length(after_x) == 0) return(TRUE) + if (after_x == length(x)) Inf else after_x + }) + is_after <- after_vals != after | is.na(after_vals) + } else { + is_after <- grepl(target_impute, data$impute, fixed = TRUE) + } + + is_target_group & is_target_param & is_after +} + +#' Checks if a vector is a subset of another. If there are any values in `a` that are not present +#' in `b`, throws an error. +#' @param a Vector to check. +#' @param b Vector with possible values. +#' @noRd +assert_subset <- function(a, b) { + if (!all(a %in% b)) { + stop( + "The following parameters are invalid interval columns: ", + paste0(setdiff(a, b), collapse = ", ") + ) + } +} diff --git a/man/add_impute_method.Rd b/man/add_impute_method.Rd new file mode 100644 index 00000000..39d35321 --- /dev/null +++ b/man/add_impute_method.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/intervals_support.R +\name{add_impute_method} +\alias{add_impute_method} +\title{Add impute method to the impute column} +\usage{ +add_impute_method(impute_vals, target_impute, after) +} +\arguments{ +\item{impute_vals}{Character vector of impute methods.} + +\item{target_impute}{The imputation method to be added.} + +\item{after}{Numeric value specifying the index position in which to add the impute.} +} +\value{ +A character string or vector with the added impute method. +} +\description{ +This is an internal helper function used to add an impute method to the impute column. +} +\keyword{internal} diff --git a/man/identify_target_rows.Rd b/man/identify_target_rows.Rd new file mode 100644 index 00000000..0f0dc239 --- /dev/null +++ b/man/identify_target_rows.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/intervals_support.R +\name{identify_target_rows} +\alias{identify_target_rows} +\title{Identify target rows based on groups, parameters, and impute method} +\usage{ +identify_target_rows( + data, + target_impute, + target_params, + target_groups, + after = NULL +) +} +\arguments{ +\item{data}{A data frame containing the intervals.} + +\item{target_impute}{The imputation method to be added or removed.} + +\item{target_params}{A character vector specifying the parameters to be targeted.} + +\item{target_groups}{A data frame specifying the intervals to be targeted.} + +\item{after}{Numeric value specifying the index position in which to add the impute (optional).} +} +\value{ +A logical vector indicating the target rows. +} +\description{ +This is an internal helper function used to identify the target rows in the data frame +based on the specified groups, parameters, and impute method. +} +\keyword{internal} diff --git a/man/interval_add_impute.Rd b/man/interval_add_impute.Rd new file mode 100644 index 00000000..bb4ecad1 --- /dev/null +++ b/man/interval_add_impute.Rd @@ -0,0 +1,75 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/intervals_support.R +\name{interval_add_impute} +\alias{interval_add_impute} +\title{Add specified imputation methods to the intervals in a PKNCAdata or data.frame object.} +\usage{ +interval_add_impute( + data, + target_impute, + after, + target_params, + target_groups, + ... +) +} +\arguments{ +\item{data}{A PKNCAdata object containing the intervals data frame, or a data frame of intervals.} + +\item{target_impute}{A character string specifying the imputation method to be added.} + +\item{after}{Numeric value specifying the index in which the imputation will be added (optional). +First is 0, last Inf. If missing, the imputation method is added at the end (Inf).} + +\item{target_params}{A character vector specifying the parameters to be targeted (optional). +If missing, all TRUE in the intervals are taken.} + +\item{target_groups}{A data frame specifying the intervals to be targeted (optional). +If missing, all relevant groups are considered.} + +\item{...}{arguments passed to \code{interval_add_impute}.} +} +\value{ +A modified PKNCAdata object with specified imputation methods on the target intervals. +} +\description{ +Add specified imputation methods to the intervals in a PKNCAdata or data.frame object. +} +\details{ +If already present the target_impute method will be added substituting the existing one. +All new intervals created will be added right after their original ones. +} +\examples{ +d_conc <- data.frame( + conc = c(1, 0.6, 0.2, 0.1, 0.9, 0.4, 1.2, 0.8, 0.3, 0.2, 1.1, 0.5), + time = rep(0:5, 2), + ID = rep(1:2, each = 6), + analyte = rep(c("Analyte1", "Analyte2"), each = 6) +) + +d_dose <- data.frame( + dose = c(100, 200), + time = c(0, 0), + ID = c(1, 2) +) + +o_conc <- PKNCAconc(d_conc, conc ~ time | ID / analyte) +o_dose <- PKNCAdose(d_dose, dose ~ time | ID) + +intervals <- data.frame( + start = c(0, 0, 0), + end = c(3, 5, Inf), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_conc0,start_predose", "start_predose", "start_conc0"), + analyte = c("Analyte1", "Analyte2", "Analyte1") +) + +o_data <- PKNCAdata(o_conc, o_dose, intervals = intervals) + +# Apply interval_add_impute function +o_data <- interval_add_impute(o_data, + target_impute = "start_conc0", + target_params = "half.life", + target_groups = data.frame(analyte = "Analyte1")) +} diff --git a/man/interval_remove_impute.Rd b/man/interval_remove_impute.Rd new file mode 100644 index 00000000..180b5123 --- /dev/null +++ b/man/interval_remove_impute.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/intervals_support.R +\name{interval_remove_impute} +\alias{interval_remove_impute} +\title{Remove specified imputation from the intervals in a PKNCAdata or data.frame (intervals) object.} +\usage{ +interval_remove_impute(data, target_impute, ...) +} +\arguments{ +\item{data}{A PKNCAdata object containing the intervals data frame, or a data frame of intervals.} + +\item{target_impute}{A character string specifying the imputation method to remove.} + +\item{...}{arguments passed to \code{interval_remove_impute}.} +} +\value{ +A modified object with the specified imputations removed from the targeted intervals. +} +\description{ +Remove specified imputation from the intervals in a PKNCAdata or data.frame (intervals) object. +} +\examples{ +d_conc <- data.frame( + conc = c(1, 0.6, 0.2, 0.1, 0.9, 0.4, 1.2, 0.8, 0.3, 0.2, 1.1, 0.5), + time = rep(0:5, 2), + ID = rep(1:2, each = 6), + analyte = rep(c("Analyte1", "Analyte2"), each = 6) +) + +d_dose <- data.frame( + dose = c(100, 200), + time = c(0, 0), + ID = c(1, 2) +) + +o_conc <- PKNCAconc(d_conc, conc ~ time | ID / analyte) +o_dose <- PKNCAdose(d_dose, dose ~ time | ID) + +intervals <- data.frame( + start = c(0, 0, 0), + end = c(3, 5, Inf), + half.life = c(TRUE, FALSE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_conc0,start_predose", "start_predose", "start_conc0"), + analyte = c("Analyte1", "Analyte2", "Analyte1") +) + +o_data <- PKNCAdata(o_conc, o_dose, intervals = intervals) + +# Apply interval_remove_impute function +o_data <- interval_remove_impute(data = o_data, + target_impute = "start_conc0", + target_params = "half.life", + target_groups = data.frame(analyte = "Analyte1")) +} diff --git a/man/remove_impute_method.Rd b/man/remove_impute_method.Rd new file mode 100644 index 00000000..6273767f --- /dev/null +++ b/man/remove_impute_method.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/intervals_support.R +\name{remove_impute_method} +\alias{remove_impute_method} +\title{Remove impute method from the impute column} +\usage{ +remove_impute_method(impute_vals, target_impute) +} +\arguments{ +\item{impute_vals}{Character vector of impute methods.} + +\item{target_impute}{The imputation method to be removed.} +} +\value{ +A character string or vector without the specified impute method. +} +\description{ +This is an internal helper function used to remove an impute method from the impute column. +} +\details{ +Resulting empty string values are replaced with NA_character_. +} +\keyword{internal} diff --git a/tests/testthat/test-intervals_support.R b/tests/testthat/test-intervals_support.R new file mode 100644 index 00000000..1ecc67fa --- /dev/null +++ b/tests/testthat/test-intervals_support.R @@ -0,0 +1,530 @@ +# Create sample data for testing +d_conc <- data.frame( + conc = c(1, 0.6, 0.2, 0.1, 0.9, 0.4, 1.2, 0.8, 0.3, 0.2, 1.1, 0.5), + time = rep(0:5, 2), + analyte = rep(c("Analyte1", "Analyte2"), each = 6), + include_hl = c(FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, FALSE, NA, TRUE, TRUE, TRUE, TRUE), + ID = rep(1:2, each = 6) +) + +d_dose <- data.frame( + dose = c(100, 200), + time = c(0, 0), + ID = c(1, 2) +) + +intervals <- data.frame( + start = c(0, 0, 0), + end = c(24, 48, Inf), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_conc0,start_predose", "start_predose", "start_conc0"), + analyte = c("Analyte1", "Analyte2", "Analyte1"), + ID = c(1, 2, 1) +) + +o_conc <- PKNCA::PKNCAconc(d_conc, conc ~ time | ID / analyte, include_half.life = "include_hl") +o_dose <- PKNCA::PKNCAdose(d_dose, dose ~ time | ID) +o_data <- PKNCA::PKNCAdata(o_conc, o_dose, intervals = intervals) + +describe("interval_add_impute", { + it("adds the impute method in the impute column of a dummy intervals dataframe", { + simple_df <- data.frame( + cmax = TRUE, + impute = c("", "m0", "m0,m1") + ) + expected_res <- data.frame( + cmax = TRUE, + impute = c("mlast", "m0,mlast", "m0,m1,mlast") + ) + res <- interval_add_impute(simple_df, target_impute = "mlast") + expect_equal(res, expected_res) + }) + + it("throws an error if either data or target_impute is missing", { + expect_error(interval_add_impute(o_data), "Both 'data' and 'target_impute' must be provided.") + }) + + it("throws an error for non-character target_impute", { + expect_error(interval_add_impute(o_data, target_impute = 123), + "'target_impute' must be a character string.") + }) + + it("throws an error when input data is not a proper format object", { + expect_error(interval_add_impute(data = o_conc, target_impute = "start_conc0")) + expect_no_error(interval_add_impute(data = o_data, target_impute = "start_conc0")) + }) + + it("throws an error for unknown target_params", { + expect_error(interval_add_impute(o_data, + target_impute = "start_conc0", + target_params = "unknown_param")) + }) + + it("handles impute column with FALSE values correctly", { + o_data_with_na_impute <- o_data + o_data_with_na_impute$intervals$impute <- NA_character_ + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = rep("new_impute", 3)) + result <- interval_add_impute(o_data_with_na_impute, target_impute = "new_impute") + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("reports an error when the impute column is not a character", { + o_data_not_character_impute <- o_data + o_data_not_character_impute$intervals$impute <- 1 + expect_error(interval_add_impute(o_data_not_character_impute, target_impute = "new_impute"), + "The 'impute' column in the intervals data.frame must be a character column.") + }) + + it("warns and makes no changes when target_impute is NA or empty", { + expect_warning({ + result <- interval_add_impute(o_data, target_impute = NA_character_) + expect_equal(result, o_data) + }, + "No impute method specified. No changes made." + ) + + expect_warning({ + result <- interval_add_impute(o_data, target_impute = "") + expect_equal(result, o_data) + }, + "No impute method specified. No changes made." + ) + }) + + it("creates missing impute col as NA_char & adds impute", { + d_no_imp <- o_data + d_no_imp$intervals$impute <- NULL + res <- interval_add_impute(d_no_imp, target_impute = "new_impute") + expect_equal(res$intervals, transform(d_no_imp$intervals, impute = "new_impute")) + res <- interval_add_impute(d_no_imp$intervals, target_impute = "new_impute") + expect_equal(res, transform(d_no_imp$intervals, impute = "new_impute")) + }) + + it("with no optional parameters uses all, with new intervals below", { + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_conc0,start_predose,new_impute", + "start_predose,new_impute", + "start_conc0,new_impute")) + result <- interval_add_impute(o_data, target_impute = "new_impute") + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("handles specified target_params correctly", { + expected_result_half_life <- data.frame( + analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + impute = c("start_conc0,start_predose,new_impute", + "start_predose,new_impute", + "start_conc0,new_impute") + ) + expected_result_cmax <- o_data$intervals[o_data$intervals$cmax, + c("analyte", "cmax", "impute")] |> + `rownames<-`(NULL) + result <- interval_add_impute(o_data, target_impute = "new_impute", target_params = "half.life") + expect_equal(result$intervals[result$intervals$half.life & !is.na(result$intervals$half.life), + c("analyte", "half.life", "impute")] |> + `rownames<-`(NULL), expected_result_half_life) + expect_equal(result$intervals[result$intervals$cmax & !is.na(result$intervals$cmax), + c("analyte", "cmax", "impute")] |> + `rownames<-`(NULL), expected_result_cmax) + }) + + it("handles target_groups correctly", { + expected_result_analyte1 <- data.frame(analyte = c("Analyte1", "Analyte1"), + half.life = c(TRUE, TRUE), + cmax = c(TRUE, TRUE), + impute = c("start_conc0,start_predose,new_impute", + "start_conc0,new_impute")) + expected_result_analyte2 <- o_data$intervals[o_data$intervals$analyte == "Analyte2", + c("analyte", "half.life", "cmax", "impute")] |> + `rownames<-`(NULL) + result <- interval_add_impute(o_data, target_impute = "new_impute", + target_groups = data.frame(analyte = "Analyte1")) + expect_equal(result$intervals[result$intervals$analyte == "Analyte1", + c("analyte", "half.life", "cmax", "impute")] |> + `rownames<-`(NULL), expected_result_analyte1) + expect_equal(result$intervals[result$intervals$analyte == "Analyte2", + c("analyte", "half.life", "cmax", "impute")] |> + `rownames<-`(NULL), expected_result_analyte2) + }) + + it("handles multiple target_params correctly", { + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_conc0,start_predose,new_impute", + "start_predose,new_impute", + "start_conc0,new_impute")) + result <- interval_add_impute(o_data, + target_impute = "new_impute", + target_params = c("half.life", "cmax")) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("makes no changes and warns when no matching intervals are found", { + expect_warning({ + res <- interval_remove_impute(o_data, + target_impute = "start_conc0", + target_groups = data.frame(analyte = "Analyte3")) + expect_equal(res, o_data) + }, + paste0("No intervals found with the specified target parameters,", + " groups and/or impute method. No changes made.") + ) + }) + + it("handles mixed TRUE/FALSE for cmax and half.life correctly", { + intervals_mixed <- data.frame( + start = c(0, 0, 0, 0), + end = c(24, 48, Inf, 72), + half.life = c(TRUE, FALSE, TRUE, FALSE), + cmax = c(FALSE, TRUE, FALSE, TRUE), + impute = c("start_conc0,start_predose", "start_predose", "start_conc0", "start_predose"), + analyte = c("Analyte1", "Analyte2", "Analyte1", "Analyte2"), + ID = c(1, 2, 1, 2) + ) + + o_data_mixed <- PKNCA::PKNCAdata(o_conc, o_dose, intervals = intervals_mixed) + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1", "Analyte2"), + half.life = c(TRUE, FALSE, TRUE, FALSE), + cmax = c(FALSE, TRUE, FALSE, TRUE), + impute = c("start_conc0,start_predose,new_impute", + "start_predose,new_impute", + "start_conc0,new_impute", + "start_predose,new_impute")) + result <- interval_add_impute(o_data_mixed, target_impute = "new_impute") + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("does not create duplicates but removes the originals & adds impute method based on after", { + result <- interval_add_impute(o_data, target_impute = "start_conc0", after = Inf) + expected_result <- data.frame( + analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_predose,start_conc0", + "start_predose,start_conc0", + "start_conc0") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], + expected_result) + }) + + it("adds new rows with added imputations after the original ones", { + result <- interval_add_impute(o_data, target_impute = "new_impute", target_param = "cmax") + expected_result <- data.frame( + analyte = c("Analyte1", "Analyte1", "Analyte2", + "Analyte2", "Analyte1", "Analyte1"), + half.life = c(TRUE, NA, TRUE, NA, TRUE, NA), + cmax = c(NA, TRUE, NA, TRUE, NA, TRUE), + impute = c("start_conc0,start_predose", + "start_conc0,start_predose,new_impute", + "start_predose", + "start_predose,new_impute", + "start_conc0", + "start_conc0,new_impute") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], + expected_result) + }) + + it("does not add new interval if non-target & target params share target impute", { + intervals_mixed <- data.frame( + start = c(0, 0), + end = c(24, 48), + half.life = c(TRUE, TRUE), + cmax = c(TRUE, TRUE), + impute = c("start_conc0,start_predose", "start_predose"), + analyte = c("Analyte1", "Analyte2"), + ID = 1 + ) + + o_data_mixed <- PKNCA::PKNCAdata(o_conc, o_dose, intervals = intervals_mixed) + result <- suppressWarnings(interval_add_impute(o_data_mixed, + target_impute = "start_predose", + target_param = "cmax", + after = Inf)) + expected_result <- data.frame( + analyte = c("Analyte1", "Analyte2"), + half.life = c(TRUE, TRUE), + cmax = c(TRUE, TRUE), + impute = c("start_conc0,start_predose", "start_predose") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], + expected_result) + }) +}) + +describe("interval_remove_impute", { + it("removes the impute method in the impute column of a dummy intervals dataframe", { + simple_df <- data.frame( + cmax = TRUE, + impute = c("", "m0", "m0,m1") + ) + expected_res <- data.frame( + cmax = TRUE, + impute = c("", NA_character_, "m1") + ) + res <- interval_remove_impute(simple_df, target_impute = "m0") + expect_equal(res, expected_res) + }) + + it("throws an error if either data or target_impute is missing", { + expect_error(interval_remove_impute(o_data), + "Both 'data' and 'target_impute' must be provided.") + }) + + it("throws an error for non-character target_impute", { + expect_error(interval_remove_impute(o_data, target_impute = 123), + "'target_impute' must be a character string.") + }) + + it("throws an error when input data is not in correct format", { + expect_error(interval_remove_impute(data = o_conc, target_impute = "start_conc0")) + expect_no_error(interval_remove_impute(data = o_data, target_impute = "start_conc0")) + }) + + it("throws an error for unknown target_params", { + expect_error(interval_remove_impute(o_data, + target_impute = "start_conc0", + target_params = "unknown_param")) + }) + + it("handles impute column with FALSE values correctly", { + o_data_with_na_impute <- o_data + o_data_with_na_impute$intervals$impute <- NA_character_ + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c(NA_character_, NA_character_, NA_character_)) + result <- suppressWarnings( + interval_remove_impute(o_data_with_na_impute, target_impute = "start_conc0") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("reports an error when impute column is not a character", { + o_data_not_character_impute <- o_data + o_data_not_character_impute$intervals$impute <- 1 + expect_error(interval_remove_impute(o_data_not_character_impute, target_impute = "start_conc0"), + "The 'impute' column in the intervals data.frame must be a character column.") + }) + + it("warns and makes no changes when target_impute is NA or empty", { + expect_warning({ + result <- interval_remove_impute(o_data, target_impute = NA_character_) + expect_equal(result, o_data) + }, + "No impute method specified. No changes made." + ) + + expect_warning({ + result <- interval_remove_impute(o_data, target_impute = "") + expect_equal(result, o_data) + }, + "No impute method specified. No changes made." + ) + }) + + it("does not modify data if global impute & column are missing", { + d_no_imp <- o_data + d_no_imp$intervals <- d_no_imp$intervals[, !names(d_no_imp$intervals) %in% "impute"] + d_no_imp$impute <- NA_character_ + expect_warning({ + res <- interval_remove_impute(d_no_imp, target_impute = "start_conc0") + expect_equal(res, d_no_imp) + }, + paste0("No default impute column or global method identified.", + " No impute methods to remove") + ) + + expect_warning({ + res <- interval_remove_impute(d_no_imp$intervals, target_impute = "start_conc0") + expect_equal(res, d_no_imp$intervals) + }, "No default impute column identified. No impute methods to remove") + }) + + it("if impute col is missing uses global impute", { + o_d_no_imp <- o_data + o_d_no_imp$intervals <- o_d_no_imp$intervals[, !names(o_d_no_imp$intervals) %in% "impute"] + o_d_no_imp$impute <- "start_conc0, start_predose" + + # When targets are all intervals, global method is changed + res_no_target <- interval_remove_impute(o_d_no_imp, target_impute = "start_conc0") + expect_equal(res_no_target$impute, "start_predose") + + # When targets are specific intervals, then a new column is created and the action handled + res_target <- interval_remove_impute(o_d_no_imp, target_impute = "start_conc0", + target_groups = data.frame(analyte = "Analyte1")) + expect_equal(unique(res_target$intervals[res_target$intervals$analyte == "Analyte1", "impute"]), + "start_predose") + expect_equal(res_target$intervals[res_target$intervals$analyte == "Analyte2", "impute"], + "start_conc0, start_predose") + }) + + it("with no optional parameters uses all relevant cases", { + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_predose", "start_predose", NA_character_)) + result <- interval_remove_impute(o_data, target_impute = "start_conc0") + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("handles specified target_params correctly", { + expected_result_half_life <- data.frame( + analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + impute = c("start_predose", "start_predose", NA_character_) + ) + expected_result_cmax <- o_data$intervals[o_data$intervals$cmax, + c("analyte", "cmax", "impute")] |> + `rownames<-`(NULL) + result <- interval_remove_impute(o_data, + target_impute = "start_conc0", + target_params = "half.life") + expect_equal(result$intervals[result$intervals$half.life & !is.na(result$intervals$half.life), + c("analyte", "half.life", "impute")] |> + `rownames<-`(NULL), expected_result_half_life) + expect_equal(result$intervals[result$intervals$cmax & !is.na(result$intervals$cmax), + c("analyte", "cmax", "impute")] |> + `rownames<-`(NULL), expected_result_cmax) + }) + + it("handles target_groups correctly", { + expected_result_analyte1 <- data.frame(analyte = c("Analyte1", "Analyte1"), + half.life = c(TRUE, TRUE), + cmax = c(TRUE, TRUE), + impute = c("start_predose", NA_character_)) + expected_result_analyte2 <- o_data$intervals[o_data$intervals$analyte == "Analyte2", + c("analyte", "half.life", "cmax", "impute")] + result <- interval_remove_impute(o_data, target_impute = "start_conc0", + target_groups = data.frame(analyte = "Analyte1")) + expect_equal(result$intervals[result$intervals$analyte == "Analyte1", + c("analyte", "half.life", "cmax", "impute")] |> + `rownames<-`(NULL), expected_result_analyte1) + expect_equal(result$intervals[result$intervals$analyte == "Analyte2", + c("analyte", "half.life", "cmax", "impute")], + expected_result_analyte2) + }) + + it("handles multiple target_params correctly", { + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_predose", "start_predose", NA_character_)) + result <- interval_remove_impute( + o_data, + target_impute = "start_conc0", + target_params = c("half.life", "cmax") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("makes no changes and warns when no matching intervals found", { + expect_warning({ + res <- interval_remove_impute(o_data, + target_impute = "start_conc0", + target_groups = data.frame(analyte = "Analyte3")) + expect_equal(res, o_data) + paste0("No intervals found with the specified target parameters,", + " groups and/or impute method. No changes made.") + }) + }) + + it("handles properly impute character method with multiple imputes", { + o_data_multiple_imputes <- o_data + o_data_multiple_imputes$intervals$impute <- "start_conc0,start_predose" + result <- interval_remove_impute(o_data_multiple_imputes, target_impute = "start_conc0") + expected_result <- data.frame( + analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_predose", "start_predose", "start_predose") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], + expected_result) + }) + + it("handles mixed TRUE/FALSE for cmax and half.life correctly", { + intervals_mixed <- data.frame( + start = c(0, 0, 0, 0), + end = c(24, 48, Inf, 72), + half.life = c(TRUE, FALSE, TRUE, FALSE), + cmax = c(FALSE, TRUE, FALSE, TRUE), + impute = c("start_conc0,start_predose", "start_predose", "start_conc0", "start_predose"), + analyte = c("Analyte1", "Analyte2", "Analyte1", "Analyte2"), + ID = c(1, 2, 1, 2) + ) + + o_data_mixed <- PKNCA::PKNCAdata(o_conc, o_dose, intervals = intervals_mixed) + + expected_result <- data.frame(analyte = c("Analyte1", "Analyte2", "Analyte1", "Analyte2"), + half.life = c(TRUE, FALSE, TRUE, FALSE), + cmax = c(FALSE, TRUE, FALSE, TRUE), + impute = c("start_predose", "start_predose", + NA_character_, "start_predose")) + result <- interval_remove_impute( + o_data_mixed, + target_impute = "start_conc0", + target_params = c("half.life", "cmax") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], expected_result) + }) + + it("removes all target_impute even if is several times", { + o_data_multiple_imputes <- o_data + o_data_multiple_imputes$intervals$impute <- "start_conc0,start_predose,start_conc0" + result <- interval_remove_impute(o_data_multiple_imputes, target_impute = "start_conc0") + expected_result <- data.frame( + analyte = c("Analyte1", "Analyte2", "Analyte1"), + half.life = c(TRUE, TRUE, TRUE), + cmax = c(TRUE, TRUE, TRUE), + impute = c("start_predose", "start_predose", "start_predose") + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], + expected_result) + }) + + it("includes new rows right after the original ones", { + result <- interval_remove_impute(o_data, target_impute = "start_conc0", target_param = "cmax") + expected_result <- data.frame( + analyte = c("Analyte1", "Analyte1", "Analyte2", "Analyte1", "Analyte1"), + half.life = c(TRUE, NA, TRUE, TRUE, NA), + cmax = c(NA, TRUE, TRUE, NA, TRUE), + impute = c("start_conc0,start_predose", + "start_predose", + "start_predose", + "start_conc0", + NA_character_) + ) + expect_equal(result$intervals[, c("analyte", "half.life", "cmax", "impute")], + expected_result) + }) +}) + +describe("interval_add_impute and interval_remove_impute", { + it("are inverses of each other", { + result_add <- interval_add_impute(o_data, target_impute = "new_impute") + result_remove <- interval_remove_impute(result_add, target_impute = "new_impute") + expect_equal(result_remove, o_data) + }) +}) + +describe("add_impute_method", { + it("does not crush when impute_vals is empty, returns the empty vector", { + expect_equal(add_impute_method(character(), "new_impute"), character()) + }) +}) + +describe("remove_impute_method", { + it("does not crush when impute_vals is empty, returns the empty vector", { + expect_equal(remove_impute_method(character(), "new_impute"), character()) + }) +})