Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude manual point half-life values calculated to be negative #375

Merged
merged 4 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ the dosing including dose amount and route.
* `NA` results from calculating `c0` will now add an exclusion reason.
* AUC for intravenous dosing (all the `auciv*` parameters) now more robustly
calculate `c0` and does not raise an error when `is.na(c0)` (#353).
* Manual calculation of half.life no longer allows negative half-live values
(#373).

## New Features

Expand All @@ -23,6 +25,10 @@ the dosing including dose amount and route.
* A new parameter `count_conc_measured` was added to enable quality checks,
typically on AUC measurements. An associated exclusion function,
`exclude_nca_conc_count_measured()` was also added.
* The `PKNCAconc()` arguments of `include_half.life` and `exclude_half.life` now
allow `NA` values. If all values are `NA`, then no inclusion or exclusion is
applied (the interval is treated as-is, like the argument had not been given).
If some values are `NA` for the interval, those are treated as `FALSE`.

# Minor changes (unlikely to affect PKNCA use)

Expand Down
3 changes: 3 additions & 0 deletions R/half.life.R
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ pk.calc.half.life <- function(conc, time, tmax, tlast,
if (nrow(data) > 0) {
fit <- fit_half_life(data=data, tlast=ret$tlast, conc_units=conc_units)
ret[,ret_replacements] <- fit[,ret_replacements]
if (ret$half.life <= 0) {
attr(ret, "exclude") <- "Negative half-life estimated with manually-selected points"
}
} else {
warning("No data to manually fit for half-life (all concentrations may be 0 or excluded)")
ret <-
Expand Down
14 changes: 8 additions & 6 deletions R/pk.calc.all.R
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,15 @@ pk.nca.interval <- function(conc, time, volume, duration.conc,
}
# Apply manual inclusion and exclusion
if (n %in% "half.life") {
if (!is.null(include_half.life)) {
call_args$conc <- call_args$conc[include_half.life]
call_args$time <- call_args$time[include_half.life]
if (!is.null(include_half.life) && !all(is.na(include_half.life))) {
include_tf <- include_half.life %in% TRUE
call_args$conc <- call_args$conc[include_tf]
call_args$time <- call_args$time[include_tf]
call_args$manually.selected.points <- TRUE
} else if (!is.null(exclude_half.life)) {
call_args$conc <- call_args$conc[!exclude_half.life]
call_args$time <- call_args$time[!exclude_half.life]
} else if (!is.null(exclude_half.life) && !all(is.na(exclude_half.life))) {
exclude_tf <- exclude_half.life %in% TRUE
call_args$conc <- call_args$conc[!exclude_tf]
call_args$time <- call_args$time[!exclude_tf]
}
}
# Do the calculation
Expand Down
23 changes: 21 additions & 2 deletions tests/testthat/test-half.life.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test_that("pk.calc.half.life", {
allow.tmax.in.half.life=TRUE,
adj.r.squared.factor=0.0001,
check=FALSE)$half.life

expect_equal(v2, 1)

# Ensure that min.hl.points is respected
Expand Down Expand Up @@ -56,7 +56,7 @@ test_that("pk.calc.half.life", {
check=FALSE)$half.life,
1.000346,
tolerance=0.00001))


# Make sure that when tmax and tlast are given that they are
# automatically included and not recalculated
Expand Down Expand Up @@ -171,6 +171,25 @@ test_that("half-life manual point selection", {
)
expect_true(all(is.na(unlist(manual_blq))),
info="All BLQ with manual point selection gives all NA results")

excluded_result <-
data.frame(
lambda.z = -log(2),
r.squared = 1,
adj.r.squared = 1,
lambda.z.time.first = 1L,
lambda.z.n.points = 3L,
clast.pred = 8,
half.life = -1,
span.ratio = -2,
tmax = 3L,
tlast = 3L
)
attr(excluded_result, "exclude") <- "Negative half-life estimated with manually-selected points"
expect_equal(
pk.calc.half.life(conc = 2^(1:3), time = 1:3, manually.selected.points = TRUE),
excluded_result
)
})

test_that("two-point half-life succeeds (fix #114)", {
Expand Down
47 changes: 47 additions & 0 deletions tests/testthat/test-pk.calc.all.R
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,53 @@ test_that("half life inclusion and exclusion", {
expect_false(identical(myresult$result, myresult_incl$result))
})

test_that("include_half.life and exclude_half.life work with NAs treated as missing for all NA and as FALSE for partial NA (#372)", {
# Partial NA include_hl is used
d_conc_incl <- data.frame(conc = c(1, 0.6, 0.3, 0.25, 0.15, 0.1), time = 0:5, include_hl = c(FALSE, NA, TRUE, TRUE, TRUE, TRUE))
o_conc_incl <- PKNCAconc(d_conc_incl, conc~time, include_half.life = "include_hl")
o_data_incl <- PKNCAdata(o_conc_incl, intervals = data.frame(start = 0, end = Inf, half.life = TRUE))
suppressMessages(o_nca_incl <- pk.nca(o_data_incl))
expect_equal(as.data.frame(o_nca_incl, out_format = "wide")$half.life, 1.820879, tolerance = 0.00001)

# All FALSE include_hl is used
d_conc_false <- data.frame(conc = c(1, 0.6, 0.3, 0.25, 0.15, 0.1), time = 0:5, include_hl = FALSE)
o_conc_false <- PKNCAconc(d_conc_false, conc~time, include_half.life = "include_hl")
o_data_false <- PKNCAdata(o_conc_false, intervals = data.frame(start = 0, end = Inf, half.life = TRUE))
suppressWarnings(suppressMessages(o_nca_false <- pk.nca(o_data_false)))
d_nca_false <- as.data.frame(o_nca_false)
expect_equal(d_nca_false$PPORRES[d_nca_false$PPTESTCD %in% "half.life"], NA_real_)

# All NA include_hl is ignored
d_conc <- data.frame(conc = c(1, 0.6, 0.3, 0.25, 0.15, 0.1), time = 0:5, include_hl = NA)
o_conc <- PKNCAconc(d_conc, conc~time, include_half.life = "include_hl")
o_data <- PKNCAdata(o_conc, intervals = data.frame(start = 0, end = Inf, half.life = TRUE))
suppressMessages(o_nca <- pk.nca(o_data))
expect_equal(as.data.frame(o_nca, out_format = "wide")$half.life, 1.512942, tolerance = 0.00001)

# Partial NA include_hl is used
d_conc_excl <- data.frame(conc = c(1, 0.6, 0.3, 0.25, 0.15, 0.1), time = 0:5, exclude_hl = c(FALSE, NA, TRUE, TRUE, TRUE, TRUE))
o_conc_excl <- PKNCAconc(d_conc_excl, conc~time, exclude_half.life = "exclude_hl")
o_data_excl <- PKNCAdata(o_conc_excl, intervals = data.frame(start = 0, end = Inf, half.life = TRUE))
suppressWarnings(suppressMessages(o_nca_excl <- pk.nca(o_data_excl)))
d_nca_excl <- as.data.frame(o_nca_excl)
expect_equal(d_nca_excl$PPORRES[d_nca_excl$PPTESTCD %in% "half.life"], NA_real_)

# All NA exclude_hl is ignored
d_conc <- data.frame(conc = c(1, 0.6, 0.3, 0.25, 0.15, 0.1), time = 0:5, exclude_hl = NA)
o_conc <- PKNCAconc(d_conc, conc~time, exclude_half.life = "exclude_hl")
o_data <- PKNCAdata(o_conc, intervals = data.frame(start = 0, end = Inf, half.life = TRUE))
suppressMessages(o_nca <- pk.nca(o_data))
expect_equal(as.data.frame(o_nca, out_format = "wide")$half.life, 1.512942, tolerance = 0.00001)

# All FALSE exclude_hl is used
d_conc_false <- data.frame(conc = c(1, 0.6, 0.3, 0.25, 0.15, 0.1), time = 0:5, exclude_hl = FALSE)
o_conc_false <- PKNCAconc(d_conc_false, conc~time, exclude_half.life = "exclude_hl")
o_data_false <- PKNCAdata(o_conc_false, intervals = data.frame(start = 0, end = Inf, half.life = TRUE))
suppressWarnings(suppressMessages(o_nca_false <- pk.nca(o_data_false)))
d_nca_false <- as.data.frame(o_nca_false)
expect_equal(d_nca_false$PPORRES[d_nca_false$PPTESTCD %in% "half.life"], 1.512942, tolerance = 0.00001)
})

test_that("No interval requested (e.g. for placebo)", {
tmpconc <- generate.conc(2, 1, 0:24)
tmpdose <- generate.dose(tmpconc)
Expand Down
6 changes: 6 additions & 0 deletions vignettes/v06-half-life-calculation.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ as.data.frame(result_obj)

# Manual Point Selection

For both exclusion and inclusion methods below, the same `NA` handling rules
apply on a per-interval basis. If all values are `NA`, then no inclusion or
exclusion is applied (the interval is treated as-is, like the argument had not
been given). If some values are `NA` for the interval, those are treated as
`FALSE`.

## Exclusion of Specific Points with Curve Stripping

In some cases, specific points will be known outliers, or there may be another reason to exclude specific points. And, with those points excluded, the half-life should be calculated using the normal curve stripping methods described above.
Expand Down
Loading