diff --git a/NAMESPACE b/NAMESPACE index 5f1c3bffb..3d82f9ed9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ export(PKNCA_build_units_table) export(PKNCA_calculate_nca) export(PKNCA_create_data_object) export(PKNCA_hl_rules_exclusion) +export(PKNCA_impute_method_end_conc_drop) export(PKNCA_impute_method_start_c1) export(PKNCA_impute_method_start_logslope) export(PKNCA_update_data_object) diff --git a/NEWS.md b/NEWS.md index 2be10a3cb..a598df5f9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,7 @@ * Partial interval parameters section supports calculations beyond `AUCINT`: `RCAMINT`, `AUCINTD`, `CAVGINT`, and others. Table starts empty by default with a Remove Row button (#524, #1249) * "Min. Points for Half-life" setting added (range 2–10, default 3) (#1155) * BLQ imputation rules via `NCA Setup > Data Imputation` (#139) +* "Drop End Concentration" switch in `NCA Setup > Data Imputation` drops the concentration at the end of each non-last main interval before regular parameter calculations, so a next-dose boundary point (e.g. an imputed C0) does not affect the previous dose profile. Partial/interval parameters and the last dose profile per group are unaffected (#1411) * General Exclusions section for in-app NCA exclusions, with "Excl. TLG" checkbox per entry (#851, #1018) * Parameter Exclusions tab: exclude individual PK parameter rows from descriptive statistics and ADPP export via PPSUMFL/PPSUMRSN flags (#1040) * NCA flag rules (NCAwXRS) from ADNCA standards — flagged records are excluded from NCA (#752) diff --git a/R/PKNCA.R b/R/PKNCA.R index f1410f0fe..d31cfae18 100644 --- a/R/PKNCA.R +++ b/R/PKNCA.R @@ -283,6 +283,9 @@ PKNCA_create_data_object <- function( # nolint: object_name_linter #' (forwarded to [update_main_intervals()]). #' @param blq_imputation_rule Optional list defining the BLQ imputation rule #' (forwarded to [update_main_intervals()]). +#' @param drop_end_conc Logical indicating whether to drop the concentration at +#' the end of each main interval for regular parameters (forwarded to +#' [update_main_intervals()]). Default `FALSE`. #' @param custom_units_table Optional data frame with PPSTRESU overrides. #' When provided, applied via [dplyr::rows_update()] on the PKNCAdata units table. #' @@ -308,6 +311,7 @@ PKNCA_update_data_object <- function( # nolint: object_name_linter parameter_selections = NULL, int_parameters = NULL, blq_imputation_rule = NULL, + drop_end_conc = FALSE, custom_units_table = NULL) { data <- adnca_data @@ -371,7 +375,8 @@ PKNCA_update_data_object <- function( # nolint: object_name_linter parameter_selections = parameter_selections, int_parameters = int_parameters, impute = start_impute, - blq_imputation_rule = blq_imputation_rule + blq_imputation_rule = blq_imputation_rule, + drop_end_conc = drop_end_conc ) # Apply custom units table @@ -583,6 +588,45 @@ PKNCA_impute_method_start_c1 <- function(conc, time, start, end, ..., options = d_conc_time } +#' Drop the concentration measured exactly at the end of the interval +#' +#' Removes a concentration sitting exactly at `time == end` for an interval, if +#' one is present (no-op otherwise). This is typically used with multiple-dose +#' data where the boundary point belongs to the next dose (e.g. an imputed C0), +#' so it should not contribute to parameters on the current interval. +#' +#' @param conc Numeric vector of concentrations. +#' @param time Numeric vector of times corresponding to the concentrations. +#' @param end Numeric value (or vector) indicating the end time of the interval. +#' @param ... Additional arguments (currently not used). +#' @param options List of options (currently not used). +#' +#' @returns A data frame of `conc`/`time` with any end-boundary point removed. +#' @details +#' This function adheres to the structure required by the `PKNCA` package to work +#' with its imputation functionality. For more information, see the +#' [PKNCA Data Imputation Vignette](https://CRAN.R-project.org/package=PKNCA). +#' +#' TODO(end_conc_drop): remove this local copy once humanpred/pknca#572 is merged +#' and aNCA depends on a PKNCA release that exports +#' `PKNCA_impute_method_end_conc_drop()`. At that point drop this definition and +#' the corresponding `@export`/NAMESPACE entry and rely on the PKNCA-native method. +#' @export +#' +#' @examples +#' conc <- c(10, 5, 1) +#' time <- c(0, 12, 24) +#' end <- 24 +#' PKNCA_impute_method_end_conc_drop(conc, time, end) +PKNCA_impute_method_end_conc_drop <- function(conc, time, end, ..., options = list()) { # nolint + d_conc_time <- data.frame(conc = conc, time = time) + mask_end <- time %in% end + if (any(mask_end)) { + d_conc_time <- d_conc_time[!mask_end, , drop = FALSE] + } + d_conc_time +} + #' Build Units Table for PKNCA #' #' This function generates a PKNCA units table including the potential unit segregating columns diff --git a/R/intervals.R b/R/intervals.R index 0db736fa0..69dee91a4 100644 --- a/R/intervals.R +++ b/R/intervals.R @@ -206,9 +206,15 @@ format_pkncadata_intervals <- function(pknca_conc, #' Each element can be a numeric value (substituting the BLQ value), or a string such as #' `"drop"` (ignores the value) or `"keep"` (keeps the value as 0). Default is NULL, #' which does not specify any BLQ imputation in any interval. +#' @param drop_end_conc Logical. If `TRUE`, the concentration measured exactly at +#' the end of each main interval is dropped before regular (non-partial) parameter +#' calculations via the `end_conc_drop` imputation. This does not affect +#' interval/partial parameter rows (`type_interval == "manual"`) nor the last dose +#' profile per concentration group, whose end-boundary point is a genuine terminal +#' sample rather than a next-dose C0. Default `FALSE`. #' #' @importFrom dplyr left_join mutate across where select all_of if_else bind_rows filter -#' @importFrom dplyr group_by ungroup slice_max distinct +#' @importFrom dplyr group_by ungroup slice_max distinct group_vars #' @importFrom purrr pmap #' @returns An updated PKNCAdata object with parameter intervals based on user selections. #' @export @@ -217,7 +223,8 @@ update_main_intervals <- function( parameter_selections = NULL, int_parameters = NULL, impute = TRUE, - blq_imputation_rule = NULL + blq_imputation_rule = NULL, + drop_end_conc = FALSE ) { if (is.null(parameter_selections)) parameter_selections <- list() @@ -309,6 +316,35 @@ update_main_intervals <- function( # Remove any imputation from the observational parameters data <- rm_impute_obs_params(data, metadata_nca_parameters) + ############################################ + # Drop the end-boundary concentration for main intervals. Applied after + # rm_impute_obs_params() so it also covers observational parameters (e.g. Cmax, + # Tmax), where a spurious concentration at time == end would otherwise distort + # the result. Partial/interval parameters (type_interval == "manual") are + # untouched. The last dose profile per concentration group is also left + # untouched: its end-boundary point is a genuine terminal sample (the interval + # end is finite only because there is no next dose), not a next-dose C0. + if (isTRUE(drop_end_conc)) { + conc_groups <- intersect(group_vars(data$conc), names(data$intervals)) + data$intervals <- data$intervals %>% + group_by(across(all_of(conc_groups))) %>% + mutate( + .is_last_main = type_interval == "main" & + start == suppressWarnings(max(start[type_interval == "main"])), + impute = ifelse( + type_interval == "main" & !.is_last_main, + ifelse( + is.na(impute) | impute == "", + "end_conc_drop", + paste0(impute, ", end_conc_drop") + ), + impute + ) + ) %>% + ungroup() %>% + select(-.is_last_main) + } + data } diff --git a/R/zzz.R b/R/zzz.R index 1ecb1cf17..46e6b5f8f 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -4,6 +4,7 @@ ".dose_time", ".facet_label_values", ".facet_n", + ".is_last_main", ".pp_excl", ".pp_excl_reason", ".ROWID", diff --git a/inst/WORDLIST b/inst/WORDLIST index 4d990a028..b685d1de8 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -7,9 +7,7 @@ AEFRLT AFRLT ARRLT ATPTREF -AUCIFOD AUCINT -AUCLSTD AUCPEO AUCPEP AUCall @@ -28,7 +26,6 @@ Bioavailability Buckeridge CDISC CMAX -CMAXD CRITy CRITyFL Clast @@ -148,7 +145,6 @@ adnca analysing analyte analytes -aucint aucpext aucs bioavailability @@ -176,6 +172,7 @@ frac ggplot ggplots hardcoded +humanpred ifelse img intravascular @@ -187,15 +184,12 @@ logslope mL macroparameters md -multidose -nav nca ng normalised oligo pak pc -pcspec pharmacodynamics pharmacokinetic pharmacokinetics @@ -204,12 +198,10 @@ pkgdown pknca plotly pptest -pptestcd pre pred px qmd -rds renderable reproducibility resizable @@ -217,7 +209,6 @@ reupload roadmap rpptx runnable -scrollable selectable signif src diff --git a/inst/shiny/modules/tab_nca/nca_setup.R b/inst/shiny/modules/tab_nca/nca_setup.R index 7ec06ed93..8cad31105 100644 --- a/inst/shiny/modules/tab_nca/nca_setup.R +++ b/inst/shiny/modules/tab_nca/nca_setup.R @@ -134,7 +134,8 @@ nca_setup_server <- function(id, data, adnca_data, extra_group_vars, settings_ov min_hl_points = settings()$min_hl_points, parameter_selections = parameters_output$selections(), int_parameters = settings()$int_parameters, - blq_imputation_rule = settings()$data_imputation$blq_imputation_rule + blq_imputation_rule = settings()$data_imputation$blq_imputation_rule, + drop_end_conc = settings()$data_imputation$drop_end_conc ) # Wait for study types to settle during reactive transitions diff --git a/inst/shiny/modules/tab_nca/setup/data_imputation.R b/inst/shiny/modules/tab_nca/setup/data_imputation.R index eb5c5f6f9..45e03b592 100644 --- a/inst/shiny/modules/tab_nca/setup/data_imputation.R +++ b/inst/shiny/modules/tab_nca/setup/data_imputation.R @@ -128,6 +128,44 @@ data_imputation_ui <- function(id) { width = "500px" ) ) + ), + hr(), + # Drop end-of-interval concentration widget and help button + fluidRow( + column( + width = 10, + input_switch( + id = ns("drop_end_conc"), + label = "Drop End Concentration", + value = FALSE + ) + ), + column( + width = 2, + dropdown( + div( + tags$h2("Drop End Concentration Help"), + p( + "Drops the concentration measured exactly at the end of each", + "interval before regular (non-partial) parameter calculations.", + "Only applies to main intervals; partial/interval parameters are", + "left untouched." + ), + p( + "This is useful for multiple-dose data where the concentration at", + "the interval end really belongs to the next dose (e.g. an imputed", + "C0). Including it can distort parameters such as Cmax, Tmax and AUC;", + "genuine troughs are unaffected when the point does not sit exactly", + "at the interval end." + ) + ), + style = "unite", + right = TRUE, + icon = icon("question"), + status = "primary", + width = "500px" + ) + ) ) ) } @@ -149,6 +187,11 @@ data_imputation_server <- function(id, settings_override) { update_switch("should_impute_c0", value = imputation$impute_c0) } + # Restore drop_end_conc switch + if (!is.null(imputation$drop_end_conc)) { + update_switch("drop_end_conc", value = imputation$drop_end_conc) + } + # Restore BLQ strategy dropdown valid_strategies <- c( "Tmax based imputation", @@ -249,6 +292,7 @@ data_imputation_server <- function(id, settings_override) { list( should_impute_c0 = reactive(input$should_impute_c0), + drop_end_conc = reactive(input$drop_end_conc), blq_strategy = reactive(input$select_blq_strategy), blq_imputation_rule = blq_imputation_rule ) diff --git a/inst/shiny/modules/tab_nca/setup/settings.R b/inst/shiny/modules/tab_nca/setup/settings.R index 331423222..db3bc0ce2 100644 --- a/inst/shiny/modules/tab_nca/setup/settings.R +++ b/inst/shiny/modules/tab_nca/setup/settings.R @@ -460,6 +460,7 @@ settings_server <- function(id, data, adnca_data, settings_override) { min_hl_points = input$min_hl_points, data_imputation = list( impute_c0 = data_imputation$should_impute_c0(), + drop_end_conc = data_imputation$drop_end_conc(), blq_strategy = data_imputation$blq_strategy(), blq_imputation_rule = data_imputation$blq_imputation_rule() ), diff --git a/inst/www/templates/clinical_template.yaml b/inst/www/templates/clinical_template.yaml index ce26251fa..f68755207 100644 --- a/inst/www/templates/clinical_template.yaml +++ b/inst/www/templates/clinical_template.yaml @@ -2,6 +2,7 @@ settings: method: lin up/log down data_imputation: impute_c0: yes + drop_end_conc: no blq_strategy: No BLQ handling blq_imputation_rule: first: keep diff --git a/inst/www/templates/preclinical_LM_template.yaml b/inst/www/templates/preclinical_LM_template.yaml index 688c8ca5d..879da02e5 100644 --- a/inst/www/templates/preclinical_LM_template.yaml +++ b/inst/www/templates/preclinical_LM_template.yaml @@ -3,6 +3,7 @@ settings: bioavailability: f_aucinf.obs data_imputation: impute_c0: yes + drop_end_conc: no blq_strategy: Set value for all BLQ blq_imputation_rule: first: drop diff --git a/inst/www/templates/preclinical_SM_template.yaml b/inst/www/templates/preclinical_SM_template.yaml index c581f34c2..e05ff98df 100644 --- a/inst/www/templates/preclinical_SM_template.yaml +++ b/inst/www/templates/preclinical_SM_template.yaml @@ -3,6 +3,7 @@ settings: bioavailability: f_aucinf.obs data_imputation: impute_c0: yes + drop_end_conc: no blq_strategy: Set value for all BLQ blq_imputation_rule: first: drop diff --git a/inst/www/templates/script_template.R b/inst/www/templates/script_template.R index bbf5862fc..0e8c07b99 100644 --- a/inst/www/templates/script_template.R +++ b/inst/www/templates/script_template.R @@ -38,6 +38,7 @@ pknca_obj <- adnca_data %>% selected_profile = settings_list$settings$profile, selected_pcspec = settings_list$settings$pcspec, start_impute = settings_list$settings$data_imputation$impute_c0, + drop_end_conc = settings_list$settings$data_imputation$drop_end_conc %||% FALSE, exclusion_list = settings_list$settings$general_exclusions, hl_adj_rules = slope_rules, keep_interval_cols = setdiff(extra_vars_to_keep, c("DOSEA", "ATPTREF", "ROUTE")), diff --git a/inst/www/templates/settings_template.yaml b/inst/www/templates/settings_template.yaml index c667cdfa6..291b3fc42 100644 --- a/inst/www/templates/settings_template.yaml +++ b/inst/www/templates/settings_template.yaml @@ -3,6 +3,7 @@ settings: bioavailability: f_aucinf.obs data_imputation: impute_c0: yes + drop_end_conc: no blq_strategy: No BLQ handling blq_imputation_rule: first: keep diff --git a/man/PKNCA_impute_method_end_conc_drop.Rd b/man/PKNCA_impute_method_end_conc_drop.Rd new file mode 100644 index 000000000..b630accdc --- /dev/null +++ b/man/PKNCA_impute_method_end_conc_drop.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PKNCA.R +\name{PKNCA_impute_method_end_conc_drop} +\alias{PKNCA_impute_method_end_conc_drop} +\title{Drop the concentration measured exactly at the end of the interval} +\usage{ +PKNCA_impute_method_end_conc_drop(conc, time, end, ..., options = list()) +} +\arguments{ +\item{conc}{Numeric vector of concentrations.} + +\item{time}{Numeric vector of times corresponding to the concentrations.} + +\item{end}{Numeric value (or vector) indicating the end time of the interval.} + +\item{...}{Additional arguments (currently not used).} + +\item{options}{List of options (currently not used).} +} +\value{ +A data frame of \code{conc}/\code{time} with any end-boundary point removed. +} +\description{ +Removes a concentration sitting exactly at \code{time == end} for an interval, if +one is present (no-op otherwise). This is typically used with multiple-dose +data where the boundary point belongs to the next dose (e.g. an imputed C0), +so it should not contribute to parameters on the current interval. +} +\details{ +This function adheres to the structure required by the \code{PKNCA} package to work +with its imputation functionality. For more information, see the +\href{https://CRAN.R-project.org/package=PKNCA}{PKNCA Data Imputation Vignette}. + +TODO(end_conc_drop): remove this local copy once humanpred/pknca#572 is merged +and aNCA depends on a PKNCA release that exports +\code{PKNCA_impute_method_end_conc_drop()}. At that point drop this definition and +the corresponding \verb{@export}/NAMESPACE entry and rely on the PKNCA-native method. +} +\examples{ +conc <- c(10, 5, 1) +time <- c(0, 12, 24) +end <- 24 +PKNCA_impute_method_end_conc_drop(conc, time, end) +} diff --git a/man/PKNCA_update_data_object.Rd b/man/PKNCA_update_data_object.Rd index 5d25cc246..6a0be640e 100644 --- a/man/PKNCA_update_data_object.Rd +++ b/man/PKNCA_update_data_object.Rd @@ -18,6 +18,7 @@ PKNCA_update_data_object( parameter_selections = NULL, int_parameters = NULL, blq_imputation_rule = NULL, + drop_end_conc = FALSE, custom_units_table = NULL ) } @@ -61,6 +62,10 @@ by study type (forwarded to \code{\link[=update_main_intervals]{update_main_inte \item{blq_imputation_rule}{Optional list defining the BLQ imputation rule (forwarded to \code{\link[=update_main_intervals]{update_main_intervals()}}).} +\item{drop_end_conc}{Logical indicating whether to drop the concentration at +the end of each main interval for regular parameters (forwarded to +\code{\link[=update_main_intervals]{update_main_intervals()}}). Default \code{FALSE}.} + \item{custom_units_table}{Optional data frame with PPSTRESU overrides. When provided, applied via \code{\link[dplyr:rows]{dplyr::rows_update()}} on the PKNCAdata units table.} } diff --git a/man/update_main_intervals.Rd b/man/update_main_intervals.Rd index 00d7aecf8..23d191b6e 100644 --- a/man/update_main_intervals.Rd +++ b/man/update_main_intervals.Rd @@ -9,7 +9,8 @@ update_main_intervals( parameter_selections = NULL, int_parameters = NULL, impute = TRUE, - blq_imputation_rule = NULL + blq_imputation_rule = NULL, + drop_end_conc = FALSE ) } \arguments{ @@ -27,6 +28,13 @@ imputation rule using PKNCA format. The list should either contain three element Each element can be a numeric value (substituting the BLQ value), or a string such as \code{"drop"} (ignores the value) or \code{"keep"} (keeps the value as 0). Default is NULL, which does not specify any BLQ imputation in any interval.} + +\item{drop_end_conc}{Logical. If \code{TRUE}, the concentration measured exactly at +the end of each main interval is dropped before regular (non-partial) parameter +calculations via the \code{end_conc_drop} imputation. This does not affect +interval/partial parameter rows (\code{type_interval == "manual"}) nor the last dose +profile per concentration group, whose end-boundary point is a genuine terminal +sample rather than a next-dose C0. Default \code{FALSE}.} } \value{ An updated PKNCAdata object with parameter intervals based on user selections. diff --git a/tests/testthat/data/test-end-conc-leak-ADNCA.csv b/tests/testthat/data/test-end-conc-leak-ADNCA.csv new file mode 100644 index 000000000..6c0938aa7 --- /dev/null +++ b/tests/testthat/data/test-end-conc-leak-ADNCA.csv @@ -0,0 +1,15 @@ +STUDYID,USUBJID,PARAM,PCSPEC,AVAL,AVALU,DOSNOP,ROUTE,ADOSEDUR,DOSEDURU,NFRLT,AFRLT,ARRLT,AEFRLT,NRRLT,RRLTU,DOSEA,VOLUME,VOLUMEU,DOSEU,DOSETRT,GROUP,SUBJID,SPECIES,GENDER,STRAIN,ANIMAL_STATUS,ASSAY_LLQ,ASSAY_LLQ_UNIT,tr_route_of_admin,tr_form_of_route,MODALITY,PROJECT,THEME,FORMULATION,Concentration,ADDITIVE,stt_stud_study_type,METABFL,WTBL,WTBLU +Study01,1,Analyte01,Plasma,1000,ng/mL,1,INTRAVASCULAR,0,h,0,0,0,0,0,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,1000,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,800,ng/mL,1,INTRAVASCULAR,0,h,0.5,0.5,0.5,0.5,0.5,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,800,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,650,ng/mL,1,INTRAVASCULAR,0,h,1,1,1,1,1,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,650,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,420,ng/mL,1,INTRAVASCULAR,0,h,2,2,2,2,2,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,420,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,180,ng/mL,1,INTRAVASCULAR,0,h,4,4,4,4,4,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,180,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,35,ng/mL,1,INTRAVASCULAR,0,h,8,8,8,8,8,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,35,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,8,ng/mL,1,INTRAVASCULAR,0,h,12,12,12,12,12,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,8,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,1050,ng/mL,2,INTRAVASCULAR,0,h,24,24,0,24,0,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,1050,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,820,ng/mL,2,INTRAVASCULAR,0,h,24.5,24.5,0.5,24.5,0.5,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,820,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,670,ng/mL,2,INTRAVASCULAR,0,h,25,25,1,25,1,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,670,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,430,ng/mL,2,INTRAVASCULAR,0,h,26,26,2,26,2,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,430,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,185,ng/mL,2,INTRAVASCULAR,0,h,28,28,4,28,4,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,185,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,36,ng/mL,2,INTRAVASCULAR,0,h,32,32,8,32,8,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,36,NO ADDITIVE,PK,NA,0.5,kg +Study01,1,Analyte01,Plasma,8,ng/mL,2,INTRAVASCULAR,0,h,36,36,12,36,12,h,1,NA,,mg/kg,Analyte01,Group 1,1,RAT,MALE,Wistar,Fed,1,ng/mL,Intravenous,BOLUS,Small molecule,Project01,XXXXX,22740,8,NO ADDITIVE,PK,NA,0.5,kg diff --git a/tests/testthat/test-PKNCA.R b/tests/testthat/test-PKNCA.R index 0ee05afc2..4057907db 100644 --- a/tests/testthat/test-PKNCA.R +++ b/tests/testthat/test-PKNCA.R @@ -670,6 +670,62 @@ describe("PKNCA_impute_method_start_c1", { }) }) +describe("PKNCA_impute_method_end_conc_drop", { + it("drops a concentration sitting exactly at the end", { + expect_equal( + PKNCA_impute_method_end_conc_drop(conc = c(10, 5, 1), time = c(0, 12, 24), end = 24), + data.frame(conc = c(10, 5), time = c(0, 12)), + ignore_attr = TRUE + ) + }) + + it("is a no-op when nothing sits exactly at the end", { + expect_equal( + PKNCA_impute_method_end_conc_drop(conc = c(10, 5, 1), time = c(0, 12, 23), end = 24), + data.frame(conc = c(10, 5, 1), time = c(0, 12, 23)) + ) + }) + + it("drops only the end point, leaving earlier points untouched", { + expect_equal( + PKNCA_impute_method_end_conc_drop(conc = c(10, 8, 6, 100), time = c(0, 1, 2, 24), end = 24), + data.frame(conc = c(10, 8, 6), time = c(0, 1, 2)), + ignore_attr = TRUE + ) + }) + + it("removes the boundary spike through the impute column of pk.nca", { + clean <- data.frame( + ID = 1, + time = c(0, 1, 2, 4, 8, 12, 24), + conc = c(10, 8, 6.5, 4, 2, 1, 0.5) + ) + spiked <- clean + spiked$conc[spiked$time == 24] <- 100 + dose <- data.frame(ID = 1, time = 0, dose = 100) + + make_tmax <- function(conc_df, impute) { + o_conc <- PKNCA::PKNCAconc(conc_df, formula = conc ~ time | ID) + o_dose <- PKNCA::PKNCAdose(dose, formula = dose ~ time | ID, route = "intravascular") + intervals <- data.frame(start = 0, end = 24, tmax = TRUE) + if (!is.null(impute)) intervals$impute <- impute + o_data <- PKNCA::PKNCAdata(o_conc, o_dose, intervals = intervals) + res <- as.data.frame(PKNCA::pk.nca(o_data)) + res$PPORRES[res$PPTESTCD == "tmax"] + } + + # Without imputation the boundary spike wins tmax (== end) + expect_equal(make_tmax(spiked, NULL), 24) + # With the drop imputation the spike is removed and tmax returns to 0 + expect_equal(make_tmax(spiked, "end_conc_drop"), 0) + # Applying the imputation to clean data (no boundary point) is a no-op + expect_equal( + make_tmax(clean, "end_conc_drop"), + make_tmax(clean, NULL) + ) + }) +}) + # Tests for PKNA_build_units_table describe("PKNCA_build_units_table", { # Subset the data to only include USUBJID 8 (2 analytes, A & B) diff --git a/tests/testthat/test-intervals.R b/tests/testthat/test-intervals.R index 82a73b7e0..dab6e3842 100644 --- a/tests/testthat/test-intervals.R +++ b/tests/testthat/test-intervals.R @@ -313,6 +313,66 @@ describe("update_main_intervals", { expect_false(all(is.na(result$intervals$impute))) }) + it("does not add end_conc_drop when drop_end_conc is FALSE", { + result <- update_main_intervals( + data, parameters, int_parameters, + impute = FALSE, drop_end_conc = FALSE + ) + expect_false(any(grepl("end_conc_drop", result$intervals$impute), na.rm = TRUE)) + }) + + it("adds end_conc_drop only to non-last main intervals per group", { + result <- update_main_intervals( + data, parameters, int_parameters, + impute = FALSE, drop_end_conc = TRUE + ) + main_rows <- result$intervals %>% filter(type_interval == "main") + + # Multi-dose subjects (2, 3, 8) have two main intervals per conc group. + # The earlier interval (start == 0) is followed by another dose -> tagged. + non_last <- main_rows %>% filter(USUBJID %in% c(2, 3, 8), start == 0) + expect_true(nrow(non_last) > 0) + expect_true(all(grepl("end_conc_drop", non_last$impute))) + + # The last interval per group (start == 5) has no following dose -> untouched. + last_multi <- main_rows %>% filter(USUBJID %in% c(2, 3, 8), start == 5) + expect_true(nrow(last_multi) > 0) + expect_false(any(grepl("end_conc_drop", last_multi$impute), na.rm = TRUE)) + + # Single-dose subjects have only one main interval per group -> untouched. + single <- main_rows %>% filter(USUBJID %in% c(1, 4, 5, 6, 7)) + expect_true(nrow(single) > 0) + expect_false(any(grepl("end_conc_drop", single$impute), na.rm = TRUE)) + }) + + it("does not add end_conc_drop to manual (partial) intervals", { + int_parameters_partial <- data.frame( + parameter = "AUCINT", + start_auc = c(0, 1), + end_auc = c(1, 2) + ) + result <- update_main_intervals( + data, parameters, int_parameters_partial, + impute = FALSE, drop_end_conc = TRUE + ) + manual_rows <- result$intervals %>% filter(type_interval == "manual") + expect_false(any(grepl("end_conc_drop", manual_rows$impute), na.rm = TRUE)) + }) + + it("appends end_conc_drop after existing impute methods", { + result <- update_main_intervals( + data, parameters, int_parameters, + impute = TRUE, drop_end_conc = TRUE + ) + main_with_impute <- result$intervals %>% + filter(type_interval == "main", !is.na(impute), grepl(",", impute)) + # Where an existing method was present, end_conc_drop is appended at the end + if (nrow(main_with_impute) > 0) { + expect_true(all(grepl("end_conc_drop$", main_with_impute$impute))) + } + succeed() + }) + it("handles empty parameter selections and empty AUC data", { # Test with empty parameter list result_no_params <- update_main_intervals(data, list(), diff --git a/vignettes/manual.Rmd b/vignettes/manual.Rmd index 8189bf135..6d317bb79 100644 --- a/vignettes/manual.Rmd +++ b/vignettes/manual.Rmd @@ -207,7 +207,7 @@ NCA settings control how PKNCA processes your data during calculations. These ar The imputation actions are specified exclusively for parameters that are not exclusively calculated based on observed concentrations (e.g., Cmax, Tmax, AUClast, etc.) but also for parameters that require extrapolation to infinity (e.g., AUCinf) or parameters that are calculated based on the terminal slope (e.g., half-life, clearance, volume). The imputation rules are applied during the interval creation step in `update_main_intervals()`, where the appropriate imputation method is selected based on the characteristics of each record (e.g., dose route, dose duration, metabolite status, etc.) and the availability of data (e.g., presence of pre-dose concentrations, presence of multiple post-dose concentrations for log-linear regression). They are specified in the `PKNCAdata` object via the `impute` column. The order of imputation is as follows: -**Order of Application**: `start_concentration` -> `BLQ imputation` +**Order of Application**: `start_concentration` -> `BLQ imputation` -> `end concentration drop` ### 3.1.1 Start Concentration Imputation: `create_start_impute()` @@ -266,6 +266,32 @@ blq_rule <- list( ) ``` +### 3.1.3 End Concentration Drop: `PKNCA_impute_method_end_conc_drop()` + +**Implementation**: Via the `drop_end_conc` argument in `update_main_intervals()`, +exposed in the app as the **Drop End Concentration** switch in the NCA Setup +data-imputation panel. + +**Purpose**: For regular (non-interval/partial) parameter calculations, optionally +drop the concentration measured exactly at the end of the interval (`time == end`) +before the parameters are computed. + +This is useful for multiple-dose profiles where the concentration at the interval +end really belongs to the next dose (e.g. an imputed Cā‚€). Including that boundary +point can distort parameters such as Cmax, Tmax, and AUC. Genuine troughs are left +untouched because the method is a no-op when no point sits exactly at the interval +end. + +**Scope**: Applied only to main intervals (`type_interval == "main"`). Partial / +interval parameters (`type_interval == "manual"`, e.g. `AUCINT`) are not affected. +When enabled, `end_conc_drop` is appended to the `impute` column, after any start +concentration or BLQ methods. + +> **Note**: This mirrors the `end_conc_drop` imputation method proposed upstream in +> [humanpred/pknca#572](https://github.com/humanpred/pknca/pull/572). While that PR +> is unreleased, aNCA ships a self-contained `PKNCA_impute_method_end_conc_drop()`. +> Once a PKNCA release exports the method, the local copy will be removed. + ## 3.2 BLQ (Below Limit of Quantification) Handling BLQ values are handled using position-based or Tmax-based strategies.