diff --git a/DESCRIPTION b/DESCRIPTION index 8610a2b08012c934edfcb5b64b782a98315e9f1d..422ecaa2ca550bd37402085b183fab19ea0aa1be 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: stressaddition Type: Package Title: Modeling Tri-Phasic Concentration-Response Relationships -Version: 2.2.1 -Date: 2020-03-03 +Version: 2.3.0 +Date: 2020-03-11 Authors@R: c(person("Sebastian", "Henz", role = c("aut", "cre"), diff --git a/NEWS.md b/NEWS.md index b954e4d6be1cda2075ff18af1db1fa5baebe4fde..2b8b99f441931ee20633ddb96ff63ca1c671240e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# stressaddition 2.3.0 + +* `predict_mixture()` accepts multiple values for the concentration of the second toxicant. Both concentration vectors must be the same length. +* `predict_mixture()` returns a data frame with the concentrations and effects. Previously it was only a vector of effects. +* `predict_mixture()` received a new argument "effect_max" which scales the returned effect values. +* Renamed the arguments of `predict_mixture()` to use underscore letters a and b instad of 1 and 2. For example model_1 is now model_a. + # stressaddition 2.2.1 * Improve documentation of `predict_mixture()` and include example of symmetry. diff --git a/R/predict_mixture.R b/R/predict_mixture.R index f70f6a60e381d8cdd0de15759dd3ac24a83fe7e3..c79582c6ef3dea42682f09cc94e9dc105fe8fd0f 100644 --- a/R/predict_mixture.R +++ b/R/predict_mixture.R @@ -22,127 +22,126 @@ #' Given the ecxsys models of two toxicants this method predicts the effects of #' different mixtures of both. #' -#' The prediction happens at one or multiple concentrations of toxicant 1 and -#' one concentration of toxicant 2. This allows for easy plotting over the -#' concentrations of toxicant 1. -#' #' The predictions are symmetric, i.e. it does not matter which of the toxicant -#' models is 1 or 2 as long as the concentration arguments are supplied -#' in the right order. See the example below. +#' models is 1 or 2 as long as the concentration arguments are supplied in the +#' right order. See the example below. #' #' This method is only suitable for experiments without environmental stress. -#' Any environmental stress in \code{model_1} or \code{model_2} is ignored. +#' Any environmental stress in \code{model_a} or \code{model_b} is ignored. #' -#' @param model_1 The ecxsys model of the toxicant that varies in concentration. -#' @param model_2 The ecxsys model of the toxicant with a fixed concentration. -#' @param concentration_1 The concentration of toxicant 1 in the mixture. -#' @param concentration_2 The concentration of toxicant 2 in the mixture. -#' @param proportion_ca How much of the combined toxicant stress is determined -#' by concentration addition. Must be between 0 and 1. +#' @param model_a,model_b The ecxsys models of the toxicants. +#' @param concentration_a,concentration_b The concentrations of the toxicants in +#' the mixture. Both vectors must be the same length. +#' @param proportion_ca The proportion of concentration addition in the +#' calculation of the toxicant stress of the mixture. Must be between 0 and 1. +#' @param effect_max Controls the scaling of the result. This represents the +#' maximum value the effect could possibly reach. For survival data in percent +#' this should be 100 (the default). #' -#' @return A vector of the effects of the mixture, scaled to the effect_max -#' value of model_1. +#' @return A data frame with columns of the supplied concentrations and the +#' corresponding mixture effects. #' -#' @examples toxicant_1 <- ecxsys( +#' @examples toxicant_a <- ecxsys( #' concentration = c(0, 0.05, 0.5, 5, 30), #' hormesis_concentration = 0.5, #' effect_tox_observed = c(90, 81, 92, 28, 0), #' ) -#' toxicant_2 <- ecxsys( +#' toxicant_b <- ecxsys( #' concentration = c(0, 0.1, 1, 10, 100, 1000), #' hormesis_concentration = 10, #' effect_tox_observed = c(26, 25, 24, 27, 5, 0), #' effect_max = 30 #' ) #' predict_mixture( -#' toxicant_1 , -#' toxicant_2 , +#' toxicant_a , +#' toxicant_b , #' c(0, 0.02, 0.2, 2, 20), -#' 3 +#' c(0, 0.03, 0.4, 5, 10) #' ) #' #' # Example of symmetric prediction: -#' conc_1 <- c(0, 0.03, 0.3, 3) -#' conc_2 <- 5.5 +#' conc_a <- c(0, 0.03, 0.3, 3) +#' conc_b <- rep(5.5, 4) #' prop_ca <- 0.75 -#' effect_a <- predict_mixture(toxicant_1 , toxicant_2 , conc_1, conc_2, prop_ca) -#' effect_b <- sapply( -#' conc_1, -#' function(x) predict_mixture(toxicant_2 , toxicant_1 , conc_2, x, prop_ca) -#' ) -#' # The effect_max values of the models are different. effect_b is scaled to -#' # the one from toxicant 2 but to compare the results effect_b must be scaled -#' # to the effect_max of toxicant 1: -#' effect_b <- effect_b / toxicant_2 $args$effect_max * toxicant_1$args$effect_max -#' identical(effect_a, effect_b) +#' effect_a <- predict_mixture(toxicant_a , toxicant_b , conc_a, conc_b, prop_ca) +#' effect_b <- predict_mixture(toxicant_b , toxicant_a , conc_b, conc_a, prop_ca) +#' identical(effect_a$mixture_effect, effect_b$mixture_effect) #' #' @export -predict_mixture <- function(model_1, - model_2, - concentration_1, - concentration_2, - proportion_ca = 0.5) { +predict_mixture <- function(model_a, + model_b, + concentration_a, + concentration_b, + proportion_ca = 0.5, + effect_max = 100) { stopifnot( - inherits(model_1, "ecxsys"), - inherits(model_2, "ecxsys"), - is.numeric(concentration_1), - is.numeric(concentration_2), - length(concentration_2) == 1, - !is.na(concentration_2), + inherits(model_a, "ecxsys"), + inherits(model_b, "ecxsys"), + is.numeric(concentration_a), + is.numeric(concentration_b), + length(concentration_a) > 0, + length(concentration_b) > 0, + length(concentration_a) == length(concentration_b), + all(!is.na(concentration_a)), + all(!is.na(concentration_b)), proportion_ca >= 0, proportion_ca <= 1, - model_1$args$p == model_2$args$p, - model_1$args$q == model_2$args$q + model_a$args$p == model_b$args$p, + model_a$args$q == model_b$args$q ) - predicted_model_1 <- predict_ecxsys(model_1, concentration_1) - predicted_model_2 <- predict_ecxsys(model_2, concentration_2) + predicted_model_a <- predict_ecxsys(model_a, concentration_a) + predicted_model_b <- predict_ecxsys(model_b, concentration_b) # tox stress ---------------------------------------------------------- - stress_tox_sam <- predicted_model_1$stress_tox + predicted_model_2$stress_tox + stress_tox_sam <- predicted_model_a$stress_tox + predicted_model_b$stress_tox - # Convert the model_2 concentration into an equivalent model_1 concentration - # anc vice versa. Clamp the response levels because drc::ED can't deal with - # 0 and 100. - response_level_2 <- 100 - predicted_model_2$effect_tox / model_2$args$effect_max * 100 - response_level_2 <- clamp(response_level_2, 1e-10, 100 - 1e-10) - concentration_2_equivalent <- drc::ED( - model_1$effect_tox_mod, - response_level_2, - display = FALSE - )[, "Estimate"] - effect_tox_ca_1 <- predict( - model_1$effect_tox_mod, - data.frame(concentration = concentration_1 + concentration_2_equivalent) + # Convert the model_b concentration into an equivalent model_a concentration + # and vice versa. + concentration_b_equivalent <- W1.2_inverse( + model_a$effect_tox_mod, + predicted_model_b$effect_tox / model_b$args$effect_max ) - stress_tox_ca_1 <- effect_to_stress(effect_tox_ca_1) + effect_tox_ca_a <- predict( + model_a$effect_tox_mod, + data.frame(concentration = concentration_a + concentration_b_equivalent) + ) + stress_tox_ca_a <- effect_to_stress(effect_tox_ca_a) - response_level_1 <- 100 - predicted_model_1$effect_tox / model_1$args$effect_max * 100 - response_level_1 <- clamp(response_level_1, 1e-10, 100 - 1e-10) - concentration_1_equivalent <- drc::ED( - model_2$effect_tox_mod, - response_level_1, - display = FALSE - )[, "Estimate"] - effect_tox_ca_2 <- predict( - model_2$effect_tox_mod, - data.frame(concentration = concentration_2 + concentration_1_equivalent) + concentration_a_equivalent <- W1.2_inverse( + model_b$effect_tox_mod, + predicted_model_a$effect_tox / model_a$args$effect_max + ) + effect_tox_ca_b <- predict( + model_b$effect_tox_mod, + data.frame(concentration = concentration_b + concentration_a_equivalent) ) - stress_tox_ca_2 <- effect_to_stress(effect_tox_ca_2) + stress_tox_ca_b <- effect_to_stress(effect_tox_ca_b) - stress_tox_ca <- (stress_tox_ca_1 + stress_tox_ca_2) / 2 + stress_tox_ca <- (stress_tox_ca_a + stress_tox_ca_b) / 2 # sys ----------------------------------------------------------------- - sys_1 <- predict(model_1$sys_tox_mod, data.frame(stress_tox = stress_tox_ca)) - sys_2 <- predict(model_2$sys_tox_mod, data.frame(stress_tox = stress_tox_ca)) - sys_total <- sys_1 * 0.5 + sys_2 * 0.5 + sys_a <- predict(model_a$sys_tox_mod, data.frame(stress_tox = stress_tox_ca)) + sys_b <- predict(model_b$sys_tox_mod, data.frame(stress_tox = stress_tox_ca)) + sys_total <- (sys_a + sys_b) / 2 # combined stress and result ------------------------------------------ proportion_sam <- 1 - proportion_ca stress_tox_total <- stress_tox_ca * proportion_ca + stress_tox_sam * proportion_sam stress_total <- stress_tox_total + sys_total - effect_total <- stress_to_effect(stress_total) * model_1$args$effect_max + mixture_effect <- stress_to_effect(stress_total) * effect_max + + # unname() to remove the name when concentration_a is a single number. + mixture_effect <- unname(mixture_effect) + data.frame(concentration_a, concentration_b, mixture_effect) +} + - # unname() to remove the name when concentration_1 is a single number. - unname(effect_total) +W1.2_inverse <- function(mod, x) { + # Using drc::ED() with respLev = 0 does not work, which is why I use + # this function instead. + # x = 1 - respLev / 100 + b <- mod$fit$par[1] + e <- mod$fit$par[2] + exp(log(-log(x)) / b + log(e)) } diff --git a/man/predict_mixture.Rd b/man/predict_mixture.Rd index 8595ac80da97ffe27d7fcc38bd4e9361212800cc..297c291ba1662e2ff5c3320bb3396fed24e650e6 100644 --- a/man/predict_mixture.Rd +++ b/man/predict_mixture.Rd @@ -5,77 +5,68 @@ \title{Predict the effect of a mixture of two toxicants} \usage{ predict_mixture( - model_1, - model_2, - concentration_1, - concentration_2, - proportion_ca = 0.5 + model_a, + model_b, + concentration_a, + concentration_b, + proportion_ca = 0.5, + effect_max = 100 ) } \arguments{ -\item{model_1}{The ecxsys model of the toxicant that varies in concentration.} +\item{model_a, model_b}{The ecxsys models of the toxicants.} -\item{model_2}{The ecxsys model of the toxicant with a fixed concentration.} +\item{concentration_a, concentration_b}{The concentrations of the toxicants in +the mixture. Both vectors must be the same length.} -\item{concentration_1}{The concentration of toxicant 1 in the mixture.} +\item{proportion_ca}{The proportion of concentration addition in the +calculation of the toxicant stress of the mixture. Must be between 0 and 1.} -\item{concentration_2}{The concentration of toxicant 2 in the mixture.} - -\item{proportion_ca}{How much of the combined toxicant stress is determined -by concentration addition. Must be between 0 and 1.} +\item{effect_max}{Controls the scaling of the result. This represents the +maximum value the effect could possibly reach. For survival data in percent +this should be 100 (the default).} } \value{ -A vector of the effects of the mixture, scaled to the effect_max - value of model_1. +A data frame with columns of the supplied concentrations and the + corresponding mixture effects. } \description{ Given the ecxsys models of two toxicants this method predicts the effects of different mixtures of both. } \details{ -The prediction happens at one or multiple concentrations of toxicant 1 and -one concentration of toxicant 2. This allows for easy plotting over the -concentrations of toxicant 1. - The predictions are symmetric, i.e. it does not matter which of the toxicant -models is 1 or 2 as long as the concentration arguments are supplied -in the right order. See the example below. +models is 1 or 2 as long as the concentration arguments are supplied in the +right order. See the example below. This method is only suitable for experiments without environmental stress. -Any environmental stress in \code{model_1} or \code{model_2} is ignored. +Any environmental stress in \code{model_a} or \code{model_b} is ignored. } \examples{ -toxicant_1 <- ecxsys( +toxicant_a <- ecxsys( concentration = c(0, 0.05, 0.5, 5, 30), hormesis_concentration = 0.5, effect_tox_observed = c(90, 81, 92, 28, 0), ) -toxicant_2 <- ecxsys( +toxicant_b <- ecxsys( concentration = c(0, 0.1, 1, 10, 100, 1000), hormesis_concentration = 10, effect_tox_observed = c(26, 25, 24, 27, 5, 0), effect_max = 30 ) predict_mixture( - toxicant_1 , - toxicant_2 , + toxicant_a , + toxicant_b , c(0, 0.02, 0.2, 2, 20), - 3 + c(0, 0.03, 0.4, 5, 10) ) # Example of symmetric prediction: -conc_1 <- c(0, 0.03, 0.3, 3) -conc_2 <- 5.5 +conc_a <- c(0, 0.03, 0.3, 3) +conc_b <- rep(5.5, 4) prop_ca <- 0.75 -effect_a <- predict_mixture(toxicant_1 , toxicant_2 , conc_1, conc_2, prop_ca) -effect_b <- sapply( - conc_1, - function(x) predict_mixture(toxicant_2 , toxicant_1 , conc_2, x, prop_ca) -) -# The effect_max values of the models are different. effect_b is scaled to -# the one from toxicant 2 but to compare the results effect_b must be scaled -# to the effect_max of toxicant 1: -effect_b <- effect_b / toxicant_2 $args$effect_max * toxicant_1$args$effect_max -identical(effect_a, effect_b) +effect_a <- predict_mixture(toxicant_a , toxicant_b , conc_a, conc_b, prop_ca) +effect_b <- predict_mixture(toxicant_b , toxicant_a , conc_b, conc_a, prop_ca) +identical(effect_a$mixture_effect, effect_b$mixture_effect) } diff --git a/tests/testthat/test-predict_mixture.R b/tests/testthat/test-predict_mixture.R index 715697f791ee352f83cbf77a79c15fbd8ff880d1..2ad99b6f8d49429b6b8943e14ba3e1fbdb410c5e 100644 --- a/tests/testthat/test-predict_mixture.R +++ b/tests/testthat/test-predict_mixture.R @@ -17,14 +17,14 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. -model_1 <- ecxsys( +model_a <- ecxsys( concentration = c(0, 0.05, 0.5, 5, 30), hormesis_concentration = 0.5, effect_tox_observed = c(90, 81, 92, 28, 0), effect_tox_env_observed = c(29, 27, 33, 5, 0), effect_max = 101 ) -model_2 <- ecxsys( +model_b <- ecxsys( concentration = c(0, 0.1, 1, 10, 100, 1000), hormesis_concentration = 10, effect_tox_observed = c(26, 25, 24, 27, 5, 0), @@ -33,21 +33,58 @@ model_2 <- ecxsys( test_that("results have not changed", { - new <- predict_mixture(model_1, model_2, c(0, 0.01, 0.1, 1, 7, 15), 5, 0.3) - reference <- c(89.460324, 85.205168, 81.440100, 57.297855, 2.911545, 0) + # one concentration_b + new <- predict_mixture( + model_a, + model_b, + c(0, 0.01, 0.1, 1, 7, 15), + rep(5, 6), + 0.3 + )$mixture_effect + reference <- c(88.574578, 84.361552, 80.633762, 56.730550, 2.882718, 0) + expect_equal(new, reference, tolerance = 1e-5) + + # diverse concentration_b + new <- predict_mixture( + model_a, + model_b, + c(0, 0.01, 0.1, 1, 7, 15), + c(0, 0.02, 0.2, 2, 14, 30), + 0.3 + )$mixture_effect + reference <- c(88.2698383, 79.9617127, 78.1574808, 65.7999834, 0.3861678, 0) + expect_equal(new, reference, tolerance = 1e-5) + + # diverse concentration_b and custom effect_max + new <- predict_mixture( + model_a, + model_b, + c(0, 0.01, 0.1, 1, 7, 15), + c(0, 0.02, 0.2, 2, 14, 30), + 0.3, + 42 + )$mixture_effect + reference <- c(88.2698383, 79.9617127, 78.1574808, 65.7999834, 0.3861678, 0) * 0.42 expect_equal(new, reference, tolerance = 1e-5) }) test_that("predictions are symmetric", { - conc_1 <- c(0, 10^seq(log10(0.001), log10(40), length.out = 50)) - conc_2 <- 3.5 + conc_a <- c(0, 10^seq(log10(0.001), log10(40), length.out = 50)) + conc_b <- rep(3.5, length(conc_a)) prop_ca <- 0.8 - effect_12 <- predict_mixture(model_1, model_2, conc_1, conc_2, prop_ca) - effect_21 <- sapply( - conc_1, - function(x) predict_mixture(model_2, model_1, conc_2, x, prop_ca) - ) - effect_21 <- effect_21 / model_2$args$effect_max * model_1$args$effect_max + effect_12 <- predict_mixture(model_a, model_b, conc_a, conc_b, prop_ca)$mixture_effect + effect_21 <- predict_mixture(model_b, model_a, conc_b, conc_a, prop_ca)$mixture_effect expect_equal(effect_12, effect_21) }) + + +test_that("effect_tox_mod is a W1.2 model", { + # This is a safeguard for if I decide to use something else than drc::W1.2 + # for effect_tox_mod. It is extremely unlikely that this ever happens but + # better safe than sorry. Currently, the inverse only works for W1.2 models. + # If this test throws errors then I probably forget to adjust the inverse + # function accordingly. + expect_s3_class(model_a$effect_tox_mod$fct, "Weibull-1") + expect_equal(model_a$effect_tox_mod$fct$noParm, 2) +})