diff --git a/NEWS.md b/NEWS.md index 43ca82d0..ec1d0d80 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,11 @@ the dosing including dose amount and route. # Development version +* Bug fix: `pk.nca()` no longer errors on unsorted concentration-time data. + Group-level concentration data are now sorted by time before calculation, so + parameters that use the full group (e.g. `aucint.all` and the other `aucint*` + parameters) work when the input rows are not in time order (#568). + * Business functions (used for calculations of means, etc.) now return NA_real_ for empty inputs rather than giving an error (#559). diff --git a/R/pk.calc.all.R b/R/pk.calc.all.R index 417d3e7a..eb66d9cf 100644 --- a/R/pk.calc.all.R +++ b/R/pk.calc.all.R @@ -196,6 +196,12 @@ pk.nca.intervals <- function(data_conc, data_dose, data_intervals, sparse, # No intervals; potentially placebo data return(rlang::warning_cnd(class="pknca_no_intervals", message="No intervals for data")) } + # Sort the group-level concentration data in time order. The interval-level + # data are sorted below (per interval), but the group-level data are passed + # as-is to parameters that use `conc.group`/`time.group` (e.g. aucint*), and + # several of those (via interp.extrap.conc()) require time-sorted input. See + # https://github.com/humanpred/pknca/issues/568. + data_conc <- data_conc[order(data_conc$time), , drop=FALSE] # Hoist the debug check: options is already the fully-merged options object # (merged at the top of pk.nca()), so there is no need to re-query # PKNCA.options() from the environment on every iteration. diff --git a/tests/testthat/test-pk.calc.all.R b/tests/testthat/test-pk.calc.all.R index dd72dd8f..c9ae35e0 100644 --- a/tests/testthat/test-pk.calc.all.R +++ b/tests/testthat/test-pk.calc.all.R @@ -1050,3 +1050,49 @@ test_that("pk.nca.interval covers route, volume.group, duration.conc.group, dose result <- pk.nca(o_data) expect_true("pknca_test_grp_args_cov_col_" %in% as.data.frame(result)$PPTESTCD) }) + +test_that("pk.nca sorts group data by time so unsorted input works (#568)", { + conc_data <- + data.frame( + MRRLT = c(16, -0.8, 3, 6, 8, 12, 20, 25), + AVAL = c(120, 260, 340, 300, 210, 150, 110, 90) + ) + conc_sorted <- conc_data[order(conc_data$MRRLT), ] + dose_data <- data.frame(EXDOSE = 1) + + run_nca <- function(cdat) { + o_conc <- PKNCAconc(cdat, AVAL ~ MRRLT) + o_dose <- PKNCAdose(dose_data, EXDOSE ~ .) + intervals <- + data.frame( + start = 0, end = 24, + aucint.all = TRUE, aucint.last = TRUE, aucint.inf.obs = TRUE, + cmax = TRUE, half.life = TRUE + ) + o_data <- + PKNCAdata( + o_conc, o_dose, intervals = intervals, + options = list(auc.method = "linear") + ) + as.data.frame(suppressWarnings(pk.nca(o_data))) + } + + # Previously errored with "Assertion on 'time' failed: Must be sorted." + res_unsorted <- expect_no_error(run_nca(conc_data)) + res_sorted <- run_nca(conc_sorted) + + # aucint* parameters (which use the group-level time/conc) are calculable and + # identical regardless of the input ordering. + expect_equal( + res_unsorted$PPORRES[res_unsorted$PPTESTCD == "aucint.all"], + 4523.263157894737 + ) + # Every parameter matches the pre-sorted calculation. + merged <- + merge( + res_unsorted[, c("PPTESTCD", "PPORRES")], + res_sorted[, c("PPTESTCD", "PPORRES")], + by = "PPTESTCD", suffixes = c(".uns", ".srt") + ) + expect_equal(merged$PPORRES.uns, merged$PPORRES.srt) +})