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

nMFI normalisation type #174

Merged
merged 28 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3cef75a
Implement plate plate_datetime field
ZetrextJG Oct 4, 2024
debfafd
Reorder fields to match across plate and builder
ZetrextJG Oct 4, 2024
88db88d
Suppress and self-handle warnings
ZetrextJG Oct 7, 2024
3b34c82
Change predicted dilutions to RAU
ZetrextJG Oct 10, 2024
e819479
Mark handle_datetime as internal
ZetrextJG Oct 10, 2024
9c9fe2b
Move from deprecated Bootstrap 3 to 5
ZetrextJG Oct 10, 2024
71514d1
Change description in vignettes
ZetrextJG Oct 10, 2024
ecfba2e
Fix handle_datetime after rebase
ZetrextJG Oct 10, 2024
5172407
Add dilution_to_rau helper function
ZetrextJG Oct 10, 2024
914f5d6
add description to `convert_dilutions_to_numeric`
Oct 11, 2024
d3a272a
`get_nmfi` function to calculate nMFI
Oct 11, 2024
966d5b8
added test for `get_nmfi`
Oct 11, 2024
77add4e
add nMFI normalisation to process plate function
Oct 11, 2024
1222bc7
renamed param from `target_dilution` to `reference_dilution`
Oct 11, 2024
317d77b
added tests for `process_plate`
Oct 11, 2024
eeb5534
reduced line width in examples
Oct 11, 2024
0f4eed4
added docs
Oct 11, 2024
6e4abb7
Merge branch 'dev' into nMFI
Oct 13, 2024
0815d9a
style the `get-nmfi` and `test-get-nmfi` file
Oct 14, 2024
7984996
added `if else` block
Oct 14, 2024
8c766d4
added nMFI section at the end
Oct 14, 2024
cb99a2a
added first column with name
Oct 14, 2024
d785977
added rownames to `process_plate` and `get_nmfi` output
Oct 14, 2024
e451eb5
Update vignettes/example_script.Rmd
Fersoil Oct 15, 2024
e6b6474
normalize -> normalise
Oct 15, 2024
b36c0bf
get_data and performance fixes
Oct 15, 2024
29d8c27
styling changes
Oct 15, 2024
f24bd5a
Merge branch 'dev' into nMFI
Oct 16, 2024
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
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ S3method(predict,Model)
S3method(summary,Plate)
export(PlateBuilder)
export(create_standard_curve_model_analyte)
export(get_nmfi)
export(is_valid_data_type)
export(is_valid_sample_type)
export(plot_counts)
Expand Down
9 changes: 9 additions & 0 deletions R/classes-plate_builder.R
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,15 @@ is_dilution <- function(character_vector) {
is_valid_dilution
}



#' Convert dilutions to numeric values
#' @description
#' Convert dilutions saved as strings in format `1/\d+` into numeric values
#' @param dilutions vector of dilutions used during the examination saved
#' as strings in format `1/\d+`
#' @return a vector of numeric values representing the dilutions
#' @keywords internal
convert_dilutions_to_numeric <- function(dilutions) {
stopifnot(is.character(dilutions))

Expand Down
97 changes: 97 additions & 0 deletions R/get-nmfi.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#' @title Calculate normalised MFI values for a plate
#'
#' @description
#' The function calculates the normalised MFI (nMFI) values for each of the analytes in the plate.
#'
#' The nMFI values are calculated as the ratio of the MFI values of test samples to the MFI values of the standard curve samples with the target dilution.
#'
#'
#'
#' **When nMFI could be used?**
#' In general it is preferred to use Relative Antibody Unit (RAU) values for any kind of analysis.
#' However, in some cases it is impossible to fit a model to the standard curve samples.
#' This may happen if the MFI values of test samples are much higher than the MFI of standard curve samples.
#' Then, the prediction would require large data extrapolation, that could lead to unreliable results.
#'
#' In such cases, the nMFI values could be used as a proxy for RAU values, if we want, for instance, to account for plate-to-plate variation.
#'
#' @param plate (`Plate()`) a plate object for which to calculate the nMFI values
#' @param reference_dilution (`numeric(1) or character(1)`) the dilution value of the standard curve sample
#' to use as a reference for normalisation. Default is `1/400`.
#' It should refer to a dilution of a standard curve sample in the given plate object.
#' This parameter could be either a numeric value or a string.
#' In case it is a character string, it should have format `1/d+`, where `d+` is any positive integer.
#' @param data_type (`character(1)`) type of data to use for the computation. Median is the default
#' @param verbose (`logical(1)`) print additional information. Default is `TRUE`
#'
#' @return nmfi (`data.frame`) a data frame with normalised MFI values for each of the analytes in the plate and all test samples.
#'
#' @examples
#'
#' # read the plate
#' plate_file <- system.file("extdata", "CovidOISExPONTENT.csv", package = "PvSTATEM")
#' layout_file <- system.file("extdata", "CovidOISExPONTENT_layout.csv", package = "PvSTATEM")
#'
#' plate <- read_luminex_data(plate_file, layout_file)
#'
#' # artificially bump up the MFI values of the test samples (the Median data type is default one)
#' plate$data[["Median"]][plate$sample_types == "TEST", ] <-
#' plate$data[["Median"]][plate$sample_types == "TEST", ] * 10
#'
#' # calculate the nMFI values
#' nmfi <- get_nmfi(plate, reference_dilution = 1/400)
#'
#' # we don't do any extrapolation and the values should be comparable accross plates
#' head(nmfi)
#' # different params
#' nmfi <- get_nmfi(plate, reference_dilution = "1/50")
#'
#'
#'
#'
#' @export
get_nmfi <-
function(plate,
reference_dilution = 1 / 400,
data_type = "Median",
verbose = TRUE) {
stopifnot(inherits(plate, "Plate"))

stopifnot(length(reference_dilution) == 1)

# check if data_type is valid
stopifnot(is_valid_data_type(data_type))

# check if reference_dilution is numeric or string
if (is.character(reference_dilution)) {
reference_dilution <- convert_dilutions_to_numeric(reference_dilution)
}

stopifnot(is.numeric(reference_dilution))
stopifnot(reference_dilution > 0)

if (!reference_dilution %in% plate$get_dilution_values("STANDARD CURVE")) {
stop("The target ",
reference_dilution,
" dilution is not present in the plate.")
}


# get index of standard curve sample with the target dilution
reference_standard_curve_name <-
subset(plate$sample_names,
plate$dilution_values == reference_dilution)
reference_standard_curve_id <-
which(plate$sample_names == reference_standard_curve_name)
Fersoil marked this conversation as resolved.
Show resolved Hide resolved

plate_data <- plate$data[[data_type]]
Fersoil marked this conversation as resolved.
Show resolved Hide resolved

reference_mfi <- plate_data[reference_standard_curve_id, ]

test_mfi <- plate_data[plate$sample_types == "TEST", ]
reference_mfi <- reference_mfi[rep(1, nrow(test_mfi)), ]

nmfi <- test_mfi / reference_mfi

return(nmfi)
}
137 changes: 105 additions & 32 deletions R/process-plate.R
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
#' Process a plate and save computed RAU values to a CSV
VALID_NORMALISATION_TYPES <- c("RAU", "nMFI")

is_valid_normalisation_type <- function(normalisation_type) {
normalisation_type %in% VALID_NORMALISATION_TYPES
}

#' @title
#' Process a plate and save output values to a CSV
#'
#' @description
#' The behavior can be summarized as follows:
#' Depending on the `normalisation_type` argument, the function will compute the RAU or nMFI values for each analyte in the plate.
#' **RAU** is the default normalisation type.
#'
#'
#' The behavior of the function, in case of RAU normalisation type, can be summarized as follows:
#' 1. Adjust blanks if not already done.
#' 2. Fit a model to each analyte using standard curve samples.
#' 3. Predict RAU value for each analyte using the corresponding model.
#' 3. Compute RAU values for each analyte using the corresponding model.
#' 4. Aggregate computed RAU values into a single data frame.
#' 5. Save that data frame to a CSV file.
#' 5. Save the computed RAU values to a CSV file.
#'
#' More info about the RAU normalisation can be found in
#' `create_standard_curve_model_analyte` function documentation \link[PvSTATEM]{create_standard_curve_model_analyte} or in the Model reference \link[PvSTATEM]{Model}.
#'
#'
#'
#'
#' In case the normalisation type is **nMFI**, the function will:
#' 1. Adjust blanks if not already done.
#' 2. Compute nMFI values for each analyte using the target dilution.
#' 3. Aggregate computed nMFI values into a single data frame.
#' 4. Save the computed nMFI values to a CSV file.
#'
#' More info about the nMFI normalisation can be found in `get_nmfi` function documentation \link[PvSTATEM]{get_nmfi}.
#'
#' @param plate (`Plate()`) a plate object
#' @param output_path (`character(1)`) path to save the computed RAU values
#' If not provided the file will be saved in the working directory with the name `RAU_{plate_name}.csv`.
#' @param output_path (`character(1)`) path to save the computed RAU values.
#' If not provided the file will be saved in the working directory with the name `{normalisation_type}_{plate_name}.csv`.
#' Where the `{plate_name}` is the name of the plate.
#' @param normalisation_type (`character(1)`) type of normalisation to use. Available options are:
#' \cr \code{c(`r toString(VALID_NORMALISATION_TYPES)`)}.
#' In case
#' @param data_type (`character(1)`) type of data to use for the computation. Median is the default
#' @param adjust_blanks (`logical(1)`) adjust blanks before computing RAU values. Default is `FALSE`
#' @param verbose (`logical(1)`) print additional information. Default is `TRUE`
#' @param reference_dilution (`numeric(1)`) target dilution to use as reference for the nMFI normalisation. Ignored in case of RAU normalisation.
#' Default is `1/400`.
#' It should refer to a dilution of a standard curve sample in the given plate object.
#' This parameter could be either a numeric value or a string.
#' In case it is a character string, it should have format `1/d+`, where `d+` is any positive integer.
#' @param ... Additional arguments to be passed to the fit model function (`create_standard_curve_model_analyte`)
#'
#' @examples
Expand All @@ -29,34 +62,74 @@
#' process_plate(plate, output_path = temporary_filepath)
#' # create and save dataframe with computed dilutions
#'
#' # nMFI normalisation
#' process_plate(plate, output_path = temporary_filepath,
#' normalisation_type = "nMFI", reference_dilution = 1/400)
#'
#' @return a data frame with normalised values
#' @export
process_plate <- function(plate, output_path = NULL, data_type = "Median", adjust_blanks = FALSE, verbose = TRUE, ...) {
stopifnot(inherits(plate, "Plate"))
if (is.null(output_path)) {
output_path <- paste0("RAU_", plate$plate_name, ".csv")
}
stopifnot(is.character(output_path))
stopifnot(is.character(data_type))
process_plate <-
function(plate,
output_path = NULL,
normalisation_type = "RAU",
data_type = "Median",
adjust_blanks = FALSE,
verbose = TRUE,
reference_dilution = 1 / 400,
...) {
stopifnot(inherits(plate, "Plate"))

if (!plate$blank_adjusted && adjust_blanks) {
plate <- plate$blank_adjustment(in_place = FALSE)
}
stopifnot(is_valid_normalisation_type(normalisation_type))

test_sample_names <- plate$sample_names[plate$sample_types == "TEST"]
output_list <- list(
"SampleName" = test_sample_names
)
verbose_cat("Fitting the models and predicting RAU for each analyte\n", verbose = verbose)

for (analyte in plate$analyte_names) {
model <- create_standard_curve_model_analyte(plate, analyte, data_type = data_type, ...)
test_samples_mfi <- plate$get_data(analyte, "TEST", data_type = data_type)
test_sample_estimates <- predict(model, test_samples_mfi)
output_list[[analyte]] <- test_sample_estimates[, "RAU"]
}

output_df <- data.frame(output_list)
if (is.null(output_path)) {
output_path <-
paste0(normalisation_type, "_", plate$plate_name, ".csv")

Check warning on line 87 in R/process-plate.R

View check run for this annotation

Codecov / codecov/patch

R/process-plate.R#L86-L87

Added lines #L86 - L87 were not covered by tests
}
stopifnot(is.character(output_path))
stopifnot(is.character(data_type))

verbose_cat("Saving the computed RAU values to a CSV file located in: '", output_path, "'\n", verbose = verbose)
write.csv(output_df, output_path, row.names = FALSE)
}
if (!plate$blank_adjusted && adjust_blanks) {
plate <- plate$blank_adjustment(in_place = FALSE)

Check warning on line 93 in R/process-plate.R

View check run for this annotation

Codecov / codecov/patch

R/process-plate.R#L93

Added line #L93 was not covered by tests
}
if (normalisation_type == "nMFI") {
verbose_cat("Computing nMFI values for each analyte\n", verbose = verbose)
nmfi <-
get_nmfi(plate, reference_dilution = reference_dilution, data_type = data_type)
verbose_cat(
"Saving the computed nMFI values to a CSV file located in: '",
output_path,
"'\n",
verbose = verbose
)
write.csv(nmfi, output_path, row.names = FALSE)
return(nmfi)
}


# RAU normalisation
Fersoil marked this conversation as resolved.
Show resolved Hide resolved

test_sample_names <-
plate$sample_names[plate$sample_types == "TEST"]
output_list <- list("SampleName" = test_sample_names)
verbose_cat("Fitting the models and predicting RAU for each analyte\n",
verbose = verbose)

for (analyte in plate$analyte_names) {
model <-
create_standard_curve_model_analyte(plate, analyte, data_type = data_type, ...)
test_samples_mfi <-
plate$get_data(analyte, "TEST", data_type = data_type)
test_sample_estimates <- predict(model, test_samples_mfi)
output_list[[analyte]] <- test_sample_estimates[, "RAU"]
}

output_df <- data.frame(output_list)

verbose_cat("Saving the computed RAU values to a CSV file located in: '",
output_path,
"'\n",
verbose = verbose)
write.csv(output_df, output_path, row.names = FALSE)
return(output_df)
}
19 changes: 19 additions & 0 deletions man/convert_dilutions_to_numeric.Rd

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

66 changes: 66 additions & 0 deletions man/get_nmfi.Rd

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

Loading
Loading