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

High dose hook #182

Merged
merged 4 commits into from
Oct 17, 2024
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
67 changes: 64 additions & 3 deletions R/classes-model.R
Original file line number Diff line number Diff line change
Expand Up @@ -310,23 +310,84 @@ predict.Model <- function(object, mfi, ...) {
#' Name of the analyte for which we want to create the model
#' @param data_type (`character(1)`)
#' Data type of the value we want to use to fit the model - the same datatype as in the plate file. By default, it equals to `Median`
#'
#' @param source_mfi_range_from_all_analytes (`logical(1)`)
#' If `TRUE`, the MFI range is calculated from all analytes; if `FALSE`, the MFI range is calculated only for the current analyte
#' Defaults to `FALSE`
#' @param detect_high_dose_hook (`logical(1)`) If `TRUE`, the high dose hook effect is detected and handled.
#' For more information, please see the \link[PvSTATEM]{handle_high_dose_hook} function documentation.
#' @param ... Additional arguments passed to the model
#'
#' Standard curve samples should not contain `na` values in mfi values nor in dilutions.
#'
#' @return (`Model()`) Standard Curve model
#'
#' @export
create_standard_curve_model_analyte <- function(plate, analyte_name, data_type = "Median", source_mfi_range_from_all_analytes = FALSE, ...) {
create_standard_curve_model_analyte <- function(plate, analyte_name,
data_type = "Median",
source_mfi_range_from_all_analytes = FALSE,
detect_high_dose_hook = TRUE,
...) {
mfi <- plate$get_data(analyte_name, "STANDARD CURVE", data_type = data_type)
dilutions_numeric <- plate$get_dilution_values("STANDARD CURVE")

mfi_source <- ifelse(source_mfi_range_from_all_analytes, "ALL", analyte_name)
if (detect_high_dose_hook) {
sample_selector <- handle_high_dose_hook(mfi, dilutions_numeric)
mfi <- mfi[sample_selector]
dilutions_numeric <- dilutions_numeric[sample_selector]
}

mfi_source <- ifelse(source_mfi_range_from_all_analytes, "ALL", analyte_name)
mfi_min <- min(plate$get_data(mfi_source, "STANDARD CURVE", data_type = data_type), na.rm = TRUE)
mfi_max <- max(plate$get_data(mfi_source, "STANDARD CURVE", data_type = data_type), na.rm = TRUE)

Model$new(analyte_name, dilutions_numeric, mfi, mfi_min = mfi_min, mfi_max = mfi_max, ...)
}


#' @title Detect and handle the high dose hook effect
#'
#' @description
#' Typically, the MFI values associated with standard curve
#' samples should decrease as we dilute the samples. However,
#' sometimes in high dilutions, the MFI presents a non monotonic behavior.
#' In that case, MFI values associated with dilutions above (or equal to)
#' `high_dose_threshold` should be removed from the analysis.
#'
#' For the `nplr` model the recommended number of standard curve samples
#' is at least 4. If the high dose hook effect is detected but the number
#' of samples below the `high_dose_threshold` is lower than 4,
#' additional warning is printed and the samples are not removed.
#'
#' The function returns a logical vector that can be used to subset the MFI values.
#'
#' @param mfi (`numeric()`)
#' @param dilutions (`numeric()`)
#' @param high_dose_threshold (`numeric(1)`) MFI values associated
#' with dilutions above this threshold should be checked for the high dose hook effect
#'
#' @return sample selector (`logical()`)
handle_high_dose_hook <- function(mfi, dilutions, high_dose_threshold = 1 / 200) {
nizwant marked this conversation as resolved.
Show resolved Hide resolved
total_samples <- length(mfi)
correct_order <- order(dilutions, decreasing = TRUE)
high_dose_hook_samples <- dilutions[correct_order] >= high_dose_threshold
mfi <- mfi[correct_order]
if (!is.decreasing(mfi[high_dose_hook_samples])) {
# High dose hook detected
if ((total_samples - length(mfi[high_dose_hook_samples])) < 4) {
warning(
"High dose hook detected but the number of samples
below the high dose threshold is lower than 4.
The samples will not be removed from the analysis."
)
return(rep(TRUE, total_samples))
} else {
warning(
"High dose hook detected.
Removing samples with dilutions above the high dose threshold."
)
return((!high_dose_hook_samples)[order(correct_order)]) # Return the initial order
}
} else {
return(rep(TRUE, total_samples))
}
}
19 changes: 19 additions & 0 deletions R/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,22 @@ format_dilutions <- function(dilutions, dilution_values, sample_types) {
dilution_to_rau <- function(predicted_dilution) {
return(predicted_dilution * 1e6)
}

#' @title Check if the vector is monotically decreasing
#'
#' @param x (`numeric()`) Vector of numeric values
#'
#' @return (`logical(1)`) `TRUE` if the vector is monotonically decreasing, `FALSE` otherwise
#'
is.decreasing <- function(x) {
stopifnot(is.numeric(x) || is.null(x))
if (any(is.na(x))) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this function is gonna be used only in handle_high_dose_hook then as a user I would prefer to see different error message. I know that this check fits here but I would move it to handle_high_dose_hook and there gave it different, more specific error message such as "NA values detected in the dilutions while handling high dose hook" or something similar

Copy link
Collaborator

@Fersoil Fersoil Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess such a situation should not happen. In general, I think, it would mean that a standard curve sample has a missing dilution value

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I'd love to see a test in which we process a plate with high-hook

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes to the function is.decreasing the na handling is there because this function is located in the helpers.R and provided as a utility function to be used across the package.

I will add a doc with an explanation about it to the handle_high_dose_hook and create a test specific for phenomenon

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to approve these changes. In general, if there were a problem with NA dilutions, it would also affect different parts of model creation. There are no specific exceptions thrown that could help track if there is standard curve sample without dilution.

I think, if @nizwant agrees we can approve these changes and optionally add this problem as a new issue, but I don't think that this is necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to manipulate the plate object so that the standard curve sample would not have the dilution value, and it turns out that then it is read as a test sample instead. I am not sure how to enforce the behaviour that @nizwant wants to avoid other than through manual modification of plate object data.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then it's fine

stop(
"NA values detected in the input vector for `is.decreasing` function."
)
}
if (is.null(x) || (length(x) < 2)) {
return(TRUE)
}
all(diff(x) < 0)
}
4 changes: 4 additions & 0 deletions man/create_standard_curve_model_analyte.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions man/handle_high_dose_hook.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions man/is.decreasing.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions tests/testthat/test-helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,15 @@ test_that("Test format dilution function with dilutions equal null", {

expect_equal(format_dilutions(dilutions, dilution_values, sample_types), NULL)
})


test_that("Test is.decreasing function", {
expect_true(is.decreasing(NULL))
expect_true(is.decreasing(c()))
expect_true(is.decreasing(c(2)))
expect_true(is.decreasing(c(3, 2, 1)))
expect_false(is.decreasing(c(1, 2, 3)))
expect_false(is.decreasing(c(1, 2, 2)))
expect_error(is.decreasing(c(1, 2, NA)))
expect_error(is.decreasing("wrong"))
})
41 changes: 38 additions & 3 deletions tests/testthat/test-model.R
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
library(testthat)




test_that("Artificial model with insufficient number of analytes", {
expect_no_error(
model <- Model$new(
Expand All @@ -17,3 +14,41 @@ test_that("Artificial model with insufficient number of analytes", {
expect_equal(model$top_asymptote, 100)
expect_equal(model$bottom_asymptote, 50)
})

test_that("Test high dose hook detection and handling", {
dilutions <- c(
1 / 50, 1 / 100, 1 / 200, 1 / 400, 1 / 800, 1 / 1600, 1 / 4000, 1 / 16000
)
# Normal case
mfi <- c(2000, 1000, 500, 300, 200, 100, 50, 25)
expect_true(all(handle_high_dose_hook(mfi, dilutions)))

# High dose hook
mfi <- c(2000, 500, 1000, 300, 200, 100, 50, 200)
expect_warning(p <- handle_high_dose_hook(mfi, dilutions))
expect_equal(p, c(FALSE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE))

# High dose hook insufficient samples
# That would remove all but 3 samples which is less than the minimum required
expect_warning(p <- handle_high_dose_hook(mfi, dilutions, high_dose_threshold = 1 / 800))
expect_true(all(p))

# Another high dose hook
mfi <- c(2000, 1000, 500, 300, 200, 100, 50, 200)
dilutions <- c(
1 / 100, 1 / 50, 1 / 200, 1 / 400, 1 / 800, 1 / 1600, 1 / 4000, 1 / 16000
)
expect_warning(p <- handle_high_dose_hook(mfi, dilutions))
expect_equal(p, c(FALSE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE))
})

test_that("Test high dose hook on a plate object", {
path <- system.file("extdata", "CovidOISExPONTENT.csv", package = "PvSTATEM", mustWork = TRUE)
layout_path <- system.file("extdata", "CovidOISExPONTENT_layout.xlsx", package = "PvSTATEM", mustWork = TRUE)
expect_no_error(plate <- read_luminex_data(path, format = "xPONENT", layout_filepath = layout_path, verbose = FALSE))

plate$dilutions[c(2, 3)] <- plate$dilutions[c(3, 2)]
plate$dilution_values[c(2, 3)] <- plate$dilution_values[c(3, 2)]

expect_warning(model <- create_standard_curve_model_analyte(plate, "S2"))
})
Loading