diff options
Diffstat (limited to 'R')
-rw-r--r-- | R/anova.saem.mmkin.R | 106 | ||||
-rw-r--r-- | R/aw.R | 36 | ||||
-rw-r--r-- | R/illparms.R | 137 | ||||
-rw-r--r-- | R/intervals.R | 5 | ||||
-rw-r--r-- | R/llhist.R | 43 | ||||
-rw-r--r-- | R/mhmkin.R | 192 | ||||
-rw-r--r-- | R/mixed.mmkin.R | 3 | ||||
-rw-r--r-- | R/mkinfit.R | 9 | ||||
-rw-r--r-- | R/mmkin.R | 51 | ||||
-rw-r--r-- | R/multistart.R | 224 | ||||
-rw-r--r-- | R/nlme.R | 4 | ||||
-rw-r--r-- | R/parms.R | 82 | ||||
-rw-r--r-- | R/parms.mkinfit.R | 62 | ||||
-rw-r--r-- | R/parplot.R | 105 | ||||
-rw-r--r-- | R/plot.mixed.mmkin.R | 5 | ||||
-rw-r--r-- | R/saem.R | 402 | ||||
-rw-r--r-- | R/set_nd_nq.R | 164 | ||||
-rw-r--r-- | R/status.R | 113 | ||||
-rw-r--r-- | R/summary.mkinfit.R | 4 | ||||
-rw-r--r-- | R/summary.mmkin.R | 56 | ||||
-rw-r--r-- | R/summary.saem.mmkin.R | 27 |
21 files changed, 1617 insertions, 213 deletions
diff --git a/R/anova.saem.mmkin.R b/R/anova.saem.mmkin.R new file mode 100644 index 00000000..9937a919 --- /dev/null +++ b/R/anova.saem.mmkin.R @@ -0,0 +1,106 @@ +#' Anova method for saem.mmkin objects +#' +#' Generate an anova object. The method to calculate the BIC is that from +#' the saemix package. As in other prominent anova methods, models are sorted +## by likelihood, and the test is relative to the model on the previous line. +#' +#' @param object An [saem.mmkin] object +#' @param ... further such objects +#' @param method Method for likelihood calculation: "is" (importance sampling), +#' "lin" (linear approximation), or "gq" (Gaussian quadrature). Passed +#' to [saemix::logLik.SaemixObject] +#' @param test Should a likelihood ratio test be performed? If TRUE, +#' the alternative models are tested against the first model. Should +#' only be done for nested models. +#' @param model.names Optional character vector of model names +#' @importFrom stats anova logLik update pchisq terms +#' @importFrom methods is +#' @importFrom utils capture.output +#' @export +#' @return an "anova" data frame; the traditional (S3) result of anova() +anova.saem.mmkin <- function(object, ..., + method = c("is", "lin", "gq"), test = FALSE, model.names = NULL) +{ + # The following code is heavily inspired by anova.lmer in the lme4 package + mCall <- match.call(expand.dots = TRUE) + dots <- list(...) + method <- match.arg(method) + + is_model <- sapply(dots, is, "saem.mmkin") + if (any(is_model)) { + mods <- c(list(object), dots[is_model]) + + # Ensure same data, ignoring covariates + same_data <- sapply(dots[is_model], function(x) { + identical(object$data[c("ds", "name", "time", "value")], + x$data[c("ds", "name", "time", "value")]) + }) + if (!all(same_data)) { + stop(sum(!same_data), " objects have not been fitted to the same data") + } + + if (is.null(model.names)) + model.names <- vapply(as.list(mCall)[c(FALSE, TRUE, is_model)], deparse1, "") + + # Sanitize model names + if (length(model.names) != length(mods)) + stop("Model names vector and model list have different lengths") + + if (any(duplicated(model.names))) + stop("Duplicate model names are not allowed") + + if (max(nchar(model.names)) > 200) { + warning("Model names longer than 200 characters, assigning generic names") + model.names <- paste0("MODEL",seq_along(model.names)) + } + names(mods) <- model.names + + llks <- lapply(model.names, function(x) { + llk <- try(logLik(mods[[x]], method = method)) + if (inherits(llk, "try-error")) + stop("Could not obtain log likelihood with '", method, "' method for ", x) + return(llk) + }) + + # Order models by increasing degrees of freedom: + npar <- vapply(llks, attr, FUN.VALUE=numeric(1), "df") + ii <- order(npar) + mods <- mods[ii] + llks <- llks[ii] + npar <- npar[ii] + + # Describe data for the header, as in summary.saem.mmkin + header <- paste("Data:", nrow(object$data), "observations of", + length(unique(object$data$name)), "variable(s) grouped in", + length(unique(object$data$ds)), "datasets\n") + + # Calculate statistics + llk <- unlist(llks) + chisq <- 2 * pmax(0, c(NA, diff(llk))) + dfChisq <- c(NA, diff(npar)) + + bic <- function(x, method) { + BIC(x$so, method = method) + } + val <- data.frame( + npar = npar, + AIC = sapply(llks, AIC), + BIC = sapply(mods, bic, method = method), # We use the saemix method here + Lik = llk, + row.names = names(mods), check.names = FALSE) + + if (test) { + testval <- data.frame( + Chisq = chisq, + Df = dfChisq, + "Pr(>Chisq)" = ifelse(dfChisq == 0, NA, + pchisq(chisq, dfChisq, lower.tail = FALSE)), + row.names = names(mods), check.names = FALSE) + val <- cbind(val, testval) + } + class(val) <- c("anova", class(val)) + structure(val, heading = c(header)) + } else { + stop("Currently, no anova method is implemented for the case of a single saem.mmkin object") + } +} @@ -30,6 +30,14 @@ #' @export aw <- function(object, ...) UseMethod("aw") +.aw <- function(all_objects) { + AIC_all <- sapply(all_objects, AIC) + delta_i <- AIC_all - min(AIC_all) + denom <- sum(exp(-delta_i/2)) + w_i <- exp(-delta_i/2) / denom + return(w_i) +} + #' @export #' @rdname aw aw.mkinfit <- function(object, ...) { @@ -43,11 +51,7 @@ aw.mkinfit <- function(object, ...) { } } all_objects <- list(object, ...) - AIC_all <- sapply(all_objects, AIC) - delta_i <- AIC_all - min(AIC_all) - denom <- sum(exp(-delta_i/2)) - w_i <- exp(-delta_i/2) / denom - return(w_i) + .aw(all_objects) } #' @export @@ -56,3 +60,25 @@ aw.mmkin <- function(object, ...) { if (ncol(object) > 1) stop("Please supply an mmkin column object") do.call(aw, object) } + +#' @export +#' @rdname aw +aw.mixed.mmkin <- function(object, ...) { + oo <- list(...) + data_object <- object$data[c("ds", "name", "time", "value")] + for (i in seq_along(oo)) { + if (!inherits(oo[[i]], "mixed.mmkin")) stop("Please supply objects inheriting from mixed.mmkin") + data_other_object <- oo[[i]]$data[c("ds", "name", "time", "value")] + if (!identical(data_object, data_other_object)) { + stop("It seems that the mixed.mmkin objects have not all been fitted to the same data") + } + } + all_objects <- list(object, ...) + .aw(all_objects) +} + +#' @export +#' @rdname aw +aw.multistart <- function(object, ...) { + do.call(aw, object) +} diff --git a/R/illparms.R b/R/illparms.R new file mode 100644 index 00000000..931d8f05 --- /dev/null +++ b/R/illparms.R @@ -0,0 +1,137 @@ +#' Method to get the names of ill-defined parameters +#' +#' The method for generalised nonlinear regression fits as obtained +#' with [mkinfit] and [mmkin] checks if the degradation parameters +#' pass the Wald test (in degradation kinetics often simply called t-test) for +#' significant difference from zero. For this test, the parameterisation +#' without parameter transformations is used. +#' +#' The method for hierarchical model fits, also known as nonlinear +#' mixed-effects model fits as obtained with [saem] and [mhmkin] +#' checks if any of the confidence intervals for the random +#' effects expressed as standard deviations include zero, and if +#' the confidence intervals for the error model parameters include +#' zero. +#' +#' @param object The object to investigate +#' @param x The object to be printed +#' @param conf.level The confidence level for checking p values +#' @param \dots For potential future extensions +#' @param random For hierarchical fits, should random effects be tested? +#' @param errmod For hierarchical fits, should error model parameters be +#' tested? +#' @return For [mkinfit] or [saem] objects, a character vector of parameter +#' names. For [mmkin] or [mhmkin] objects, a matrix like object of class +#' 'illparms.mmkin' or 'illparms.mhmkin'. The latter objects have a suitable +#' printing method. +#' @export +illparms <- function(object, ...) +{ + UseMethod("illparms", object) +} + +#' @rdname illparms +#' @export +#' @examples +#' fit <- mkinfit("FOMC", FOCUS_2006_A, quiet = TRUE) +#' illparms(fit) +illparms.mkinfit <- function(object, conf.level = 0.95, ...) { + p_values <- suppressWarnings(summary(object)$bpar[, "Pr(>t)"]) + na <- is.na(p_values) + failed <- p_values > 1 - conf.level + names(parms(object))[na | failed] +} + +#' @rdname illparms +#' @export +#' @examples +#' \dontrun{ +#' fits <- mmkin( +#' c("SFO", "FOMC"), +#' list("FOCUS A" = FOCUS_2006_A, +#' "FOCUS C" = FOCUS_2006_C), +#' quiet = TRUE) +#' illparms(fits) +#' } +illparms.mmkin <- function(object, conf.level = 0.95, ...) { + result <- lapply(object, + function(fit) { + if (inherits(fit, "try-error")) return("E") + ill <- illparms(fit, conf.level = conf.level) + if (length(ill) > 0) { + return(paste(ill, collapse = ", ")) + } else { + return("") + } + }) + result <- unlist(result) + dim(result) <- dim(object) + dimnames(result) <- dimnames(object) + class(result) <- "illparms.mmkin" + return(result) +} + +#' @rdname illparms +#' @export +print.illparms.mmkin <- function(x, ...) { + class(x) <- NULL + print(x, quote = FALSE) +} + +#' @rdname illparms +#' @export +illparms.saem.mmkin <- function(object, conf.level = 0.95, random = TRUE, errmod = TRUE, ...) { + if (inherits(object, "try-error")) { + return(NA) + } else { + ints <- intervals(object, conf.level = conf.level) + failed <- NULL + if (random) { + failed_random <- ints$random[, "lower"] < 0 + failed <- c(failed, names(which(failed_random))) + } + if (errmod) { + failed_errmod <- ints$errmod[, "lower"] < 0 & ints$errmod[, "est."] > 0 + failed <- c(failed, names(which(failed_errmod))) + } + } + return(failed) +} + +#' @rdname illparms +#' @export +illparms.mhmkin <- function(object, conf.level = 0.95, random = TRUE, errmod = TRUE, ...) { + if (inherits(object[[1]], "saem.mmkin")) { + check_failed <- function(x) if (inherits(x$so, "try-error")) TRUE else FALSE + } + result <- lapply(object, + function(fit) { + if (check_failed(fit)) { + return("E") + } else { + if (!is.null(fit$FIM_failed) && + "random effects and error model parameters" %in% fit$FIM_failed) { + return("NA") + } else { + ill <- illparms(fit, conf.level = conf.level, random = random, errmod = errmod) + if (length(ill) > 0) { + return(paste(ill, collapse = ", ")) + } else { + return("") + } + } + } + }) + result <- unlist(result) + dim(result) <- dim(object) + dimnames(result) <- dimnames(object) + class(result) <- "illparms.mhmkin" + return(result) +} + +#' @rdname illparms +#' @export +print.illparms.mhmkin <- function(x, ...) { + class(x) <- NULL + print(x, quote = FALSE) +} diff --git a/R/intervals.R b/R/intervals.R index 258eb4ad..705ef6eb 100644 --- a/R/intervals.R +++ b/R/intervals.R @@ -77,8 +77,9 @@ intervals.saem.mmkin <- function(object, level = 0.95, backtransform = TRUE, ... attr(confint_ret, "label") <- "Fixed effects:" # Random effects - ranef_ret <- as.matrix(conf.int[paste0("SD.", pnames), c("lower", "est.", "upper")]) - rownames(ranef_ret) <- paste0(gsub("SD\\.", "sd(", rownames(ranef_ret)), ")") + sdnames <- intersect(rownames(conf.int), paste("SD", pnames, sep = ".")) + ranef_ret <- as.matrix(conf.int[sdnames, c("lower", "est.", "upper")]) + rownames(ranef_ret) <- paste0(gsub("SD\\.", "sd(", sdnames), ")") attr(ranef_ret, "label") <- "Random effects:" diff --git a/R/llhist.R b/R/llhist.R new file mode 100644 index 00000000..e158495d --- /dev/null +++ b/R/llhist.R @@ -0,0 +1,43 @@ +#' Plot the distribution of log likelihoods from multistart objects +#' +#' Produces a histogram of log-likelihoods. In addition, the likelihood of the +#' original fit is shown as a red vertical line. +#' +#' @param object The [multistart] object +#' @param breaks Passed to [hist] +#' @param lpos Positioning of the legend. +#' @param main Title of the plot +#' @param \dots Passed to [hist] +#' @seealso [multistart] +#' @export +llhist <- function(object, breaks = "Sturges", lpos = "topleft", main = "", + ...) +{ + oldpar <- par(no.readonly = TRUE) + on.exit(par(oldpar, no.readonly = TRUE)) + + if (inherits(object, "multistart.saem.mmkin")) { + llfunc <- function(object) { + if (inherits(object$so, "try-error")) return(NA) + else return(logLik(object$so)) + } + } else { + stop("llhist is only implemented for multistart.saem.mmkin objects") + } + + ll_orig <- logLik(attr(object, "orig")) + ll <- stats::na.omit(sapply(object, llfunc)) + + par(las = 1) + h <- hist(ll, freq = TRUE, + xlab = "", main = main, + ylab = "Frequency of log likelihoods", breaks = breaks, ...) + + freq_factor <- h$counts[1] / h$density[1] + + abline(v = ll_orig, col = 2) + + legend(lpos, inset = c(0.05, 0.05), bty = "n", + lty = 1, col = c(2), + legend = "original fit") +} diff --git a/R/mhmkin.R b/R/mhmkin.R new file mode 100644 index 00000000..7f3ff9fa --- /dev/null +++ b/R/mhmkin.R @@ -0,0 +1,192 @@ +#' Fit nonlinear mixed-effects models built from one or more kinetic +#' degradation models and one or more error models +#' +#' The name of the methods expresses that (**m**ultiple) **h**ierarchichal +#' (also known as multilevel) **m**ulticompartment **kin**etic models are +#' fitted. Our kinetic models are nonlinear, so we can use various nonlinear +#' mixed-effects model fitting functions. +#' +#' @param objects A list of [mmkin] objects containing fits of the same +#' degradation models to the same data, but using different error models. +#' @param backend The backend to be used for fitting. Currently, only saemix is +#' supported +#' @param algorithm The algorithm to be used for fitting (currently not used) +#' @param \dots Further arguments that will be passed to the nonlinear mixed-effects +#' model fitting function. +#' @param cores The number of cores to be used for multicore processing. This +#' is only used when the \code{cluster} argument is \code{NULL}. On Windows +#' machines, cores > 1 is not supported, you need to use the \code{cluster} +#' argument to use multiple logical processors. Per default, all cores detected +#' by [parallel::detectCores()] are used, except on Windows where the default +#' is 1. +#' @param cluster A cluster as returned by [makeCluster] to be used for +#' parallel execution. +#' @importFrom parallel mclapply parLapply detectCores +#' @return A two-dimensional [array] of fit objects and/or try-errors that can +#' be indexed using the degradation model names for the first index (row index) +#' and the error model names for the second index (column index), with class +#' attribute 'mhmkin'. +#' @author Johannes Ranke +#' @seealso \code{\link{[.mhmkin}} for subsetting [mhmkin] objects +#' @export +mhmkin <- function(objects, backend = "saemix", algorithm = "saem", ...) { + UseMethod("mhmkin") +} + +#' @export +#' @rdname mhmkin +mhmkin.list <- function(objects, backend = "saemix", + ..., + cores = if (Sys.info()["sysname"] == "Windows") 1 else parallel::detectCores(), cluster = NULL) +{ + call <- match.call() + dot_args <- list(...) + backend_function <- switch(backend, + saemix = "saem" + ) + + deg_models <- lapply(objects[[1]][, 1], function(x) x$mkinmod) + names(deg_models) <- dimnames(objects[[1]])$model + n.deg <- length(deg_models) + + ds <- lapply(objects[[1]][1, ], function(x) x$data) + + for (other in objects[-1]) { + # Check if the degradation models in all objects are the same + for (deg_model_name in names(deg_models)) { + if (!all.equal(other[[deg_model_name, 1]]$mkinmod$spec, + deg_models[[deg_model_name]]$spec)) + { + stop("The mmkin objects have to be based on the same degradation models") + } + } + # Check if they have been fitted to the same dataset + other_object_ds <- lapply(other[1, ], function(x) x$data) + for (i in 1:length(ds)) { + if (!all.equal(ds[[i]][c("time", "variable", "observed")], + other_object_ds[[i]][c("time", "variable", "observed")])) + { + stop("The mmkin objects have to be fitted to the same datasets") + } + } + } + + n.o <- length(objects) + + error_models = sapply(objects, function(x) x[[1]]$err_mod) + n.e <- length(error_models) + + n.fits <- n.deg * n.e + fit_indices <- matrix(1:n.fits, ncol = n.e) + dimnames(fit_indices) <- list(degradation = names(deg_models), + error = error_models) + + fit_function <- function(fit_index) { + w <- which(fit_indices == fit_index, arr.ind = TRUE) + deg_index <- w[1] + error_index <- w[2] + mmkin_row <- objects[[error_index]][deg_index, ] + res <- try(do.call(backend_function, args = c(list(mmkin_row), dot_args))) + return(res) + } + + fit_time <- system.time({ + if (is.null(cluster)) { + results <- parallel::mclapply(as.list(1:n.fits), fit_function, + mc.cores = cores, mc.preschedule = FALSE) + } else { + results <- parallel::parLapply(cluster, as.list(1:n.fits), fit_function) + } + }) + + attributes(results) <- attributes(fit_indices) + attr(results, "call") <- call + attr(results, "time") <- fit_time + class(results) <- "mhmkin" + return(results) +} + +#' Subsetting method for mhmkin objects +#' +#' @param x An [mhmkin] object. +#' @param i Row index selecting the fits for specific models +#' @param j Column index selecting the fits to specific datasets +#' @param drop If FALSE, the method always returns an mhmkin object, otherwise +#' either a list of fit objects or a single fit object. +#' @return An object of class \code{\link{mhmkin}}. +#' @rdname mhmkin +#' @export +`[.mhmkin` <- function(x, i, j, ..., drop = FALSE) { + class(x) <- NULL + x_sub <- x[i, j, drop = drop] + + if (!drop) { + class(x_sub) <- "mhmkin" + } + return(x_sub) +} + +#' Print method for mhmkin objects +#' +#' @rdname mhmkin +#' @export +print.mhmkin <- function(x, ...) { + cat("<mhmkin> object\n") + cat("Status of individual fits:\n\n") + print(status(x)) +} + +#' @export +AIC.mhmkin <- function(object, ..., k = 2) { + if (inherits(object[[1]], "saem.mmkin")) { + check_failed <- function(x) if (inherits(x$so, "try-error")) TRUE else FALSE + } + res <- sapply(object, function(x) { + if (check_failed(x)) return(NA) + else return(AIC(x$so, k = k)) + }) + dim(res) <- dim(object) + dimnames(res) <- dimnames(object) + return(res) +} + +#' @export +BIC.mhmkin <- function(object, ...) { + if (inherits(object[[1]], "saem.mmkin")) { + check_failed <- function(x) if (inherits(x$so, "try-error")) TRUE else FALSE + } + res <- sapply(object, function(x) { + if (check_failed(x)) return(NA) + else return(BIC(x$so)) + }) + dim(res) <- dim(object) + dimnames(res) <- dimnames(object) + return(res) +} + +#' @export +update.mhmkin <- function(object, ..., evaluate = TRUE) { + call <- attr(object, "call") + # For some reason we get mhkin.list in call[[1]] when using mhmkin from the + # loaded package so we need to fix this so we do not have to export + # mhmkin.list in addition to the S3 method mhmkin + call[[1]] <- mhmkin + + update_arguments <- match.call(expand.dots = FALSE)$... + + if (length(update_arguments) > 0) { + update_arguments_in_call <- !is.na(match(names(update_arguments), names(call))) + } + + for (a in names(update_arguments)[update_arguments_in_call]) { + call[[a]] <- update_arguments[[a]] + } + + update_arguments_not_in_call <- !update_arguments_in_call + if(any(update_arguments_not_in_call)) { + call <- c(as.list(call), update_arguments[update_arguments_not_in_call]) + call <- as.call(call) + } + if(evaluate) eval(call, parent.frame()) + else call +} diff --git a/R/mixed.mmkin.R b/R/mixed.mmkin.R index 682a9a34..fd6d975f 100644 --- a/R/mixed.mmkin.R +++ b/R/mixed.mmkin.R @@ -1,5 +1,6 @@ #' Create a mixed effects model from an mmkin row object #' +#' @importFrom rlang !!! #' @param object An [mmkin] row object #' @param method The method to be used #' @param \dots Currently not used @@ -65,7 +66,7 @@ mixed.mmkin <- function(object, method = c("none"), ...) { function(x) x$data[c("variable", "time", "observed", "predicted", "residual")]) names(ds_list) <- ds_names - res$data <- purrr::map_dfr(ds_list, function(x) x, .id = "ds") + res$data <- vctrs::vec_rbind(!!!ds_list, .names_to = "ds") names(res$data)[1:4] <- c("ds", "name", "time", "value") res$data$name <- as.character(res$data$name) res$data$ds <- ordered(res$data$ds, levels = unique(res$data$ds)) diff --git a/R/mkinfit.R b/R/mkinfit.R index 1c2f8b5e..693778fd 100644 --- a/R/mkinfit.R +++ b/R/mkinfit.R @@ -844,8 +844,13 @@ mkinfit <- function(mkinmod, observed, } } + dim_hessian <- length(c(degparms, errparms)) + fit$hessian <- try(numDeriv::hessian(cost_function, c(degparms, errparms), OLS = FALSE, update_data = FALSE), silent = TRUE) + if (inherits(fit$hessian, "try-error")) { + fit$hessian <- matrix(NA, nrow = dim_hessian, ncol = dim_hessian) + } dimnames(fit$hessian) <- list(names(c(degparms, errparms)), names(c(degparms, errparms))) @@ -858,7 +863,9 @@ mkinfit <- function(mkinmod, observed, fit$hessian_notrans <- try(numDeriv::hessian(cost_function, c(bparms.optim, errparms), OLS = FALSE, trans = FALSE, update_data = FALSE), silent = TRUE) - + if (inherits(fit$hessian_notrans, "try-error")) { + fit$hessian_notrans <- matrix(NA, nrow = dim_hessian, ncol = dim_hessian) + } dimnames(fit$hessian_notrans) <- list(names(c(bparms.optim, errparms)), names(c(bparms.optim, errparms))) }) @@ -114,15 +114,18 @@ mmkin <- function(models = c("SFO", "FOMC", "DFOP"), datasets, return(res) } - if (is.null(cluster)) { - results <- parallel::mclapply(as.list(1:n.fits), fit_function, - mc.cores = cores, mc.preschedule = FALSE) - } else { - results <- parallel::parLapply(cluster, as.list(1:n.fits), fit_function) - } + fit_time <- system.time({ + if (is.null(cluster)) { + results <- parallel::mclapply(as.list(1:n.fits), fit_function, + mc.cores = cores, mc.preschedule = FALSE) + } else { + results <- parallel::parLapply(cluster, as.list(1:n.fits), fit_function) + } + }) attributes(results) <- attributes(fit_indices) attr(results, "call") <- call + attr(results, "time") <- fit_time class(results) <- "mmkin" return(results) } @@ -168,41 +171,7 @@ mmkin <- function(models = c("SFO", "FOMC", "DFOP"), datasets, print.mmkin <- function(x, ...) { cat("<mmkin> object\n") cat("Status of individual fits:\n\n") - all_summary_warnings <- character() - sww <- 0 # Counter for Shapiro-Wilks warnings - - display <- lapply(x, - function(fit) { - if (inherits(fit, "try-error")) return("E") - sw <- fit$summary_warnings - swn <- names(sw) - if (length(sw) > 0) { - if (any(grepl("S", swn))) { - sww <<- sww + 1 - swn <- gsub("S", paste0("S", sww), swn) - } - warnstring <- paste(swn, collapse = ", ") - names(sw) <- swn - all_summary_warnings <<- c(all_summary_warnings, sw) - return(warnstring) - } else { - return("OK") - } - }) - display <- unlist(display) - dim(display) <- dim(x) - dimnames(display) <- dimnames(x) - print(display, quote = FALSE) - - cat("\n") - if (any(display == "OK")) cat("OK: No warnings\n") - if (any(display == "E")) cat("E: Error\n") - u_swn <- unique(names(all_summary_warnings)) - u_w <- all_summary_warnings[u_swn] - for (i in seq_along(u_w)) { - cat(names(u_w)[i], ": ", u_w[i], "\n", sep = "") - } - + print(status(x)) } #' @export diff --git a/R/multistart.R b/R/multistart.R new file mode 100644 index 00000000..61ef43dc --- /dev/null +++ b/R/multistart.R @@ -0,0 +1,224 @@ +#' Perform a hierarchical model fit with multiple starting values +#' +#' The purpose of this method is to check if a certain algorithm for fitting +#' nonlinear hierarchical models (also known as nonlinear mixed-effects models) +#' will reliably yield results that are sufficiently similar to each other, if +#' started with a certain range of reasonable starting parameters. It is +#' inspired by the article on practical identifiabiliy in the frame of nonlinear +#' mixed-effects models by Duchesne et al (2021). +#' +#' @param object The fit object to work with +#' @param n How many different combinations of starting parameters should be +#' used? +#' @param cores How many fits should be run in parallel (only on posix platforms)? +#' @param cluster A cluster as returned by [parallel::makeCluster] to be used +#' for parallel execution. +#' @param \dots Passed to the update function. +#' @param x The multistart object to print +#' @return A list of [saem.mmkin] objects, with class attributes +#' 'multistart.saem.mmkin' and 'multistart'. +#' @seealso [parplot], [llhist] +#' +#' @references Duchesne R, Guillemin A, Gandrillon O, Crauste F. Practical +#' identifiability in the frame of nonlinear mixed effects models: the example +#' of the in vitro erythropoiesis. BMC Bioinformatics. 2021 Oct 4;22(1):478. +#' doi: 10.1186/s12859-021-04373-4. +#' @export +#' @examples +#' \dontrun{ +#' library(mkin) +#' dmta_ds <- lapply(1:7, function(i) { +#' ds_i <- dimethenamid_2018$ds[[i]]$data +#' ds_i[ds_i$name == "DMTAP", "name"] <- "DMTA" +#' ds_i$time <- ds_i$time * dimethenamid_2018$f_time_norm[i] +#' ds_i +#' }) +#' names(dmta_ds) <- sapply(dimethenamid_2018$ds, function(ds) ds$title) +#' dmta_ds[["Elliot"]] <- rbind(dmta_ds[["Elliot 1"]], dmta_ds[["Elliot 2"]]) +#' dmta_ds[["Elliot 1"]] <- dmta_ds[["Elliot 2"]] <- NULL +#' +#' f_mmkin <- mmkin("DFOP", dmta_ds, error_model = "tc", cores = 7, quiet = TRUE) +#' f_saem_full <- saem(f_mmkin) +#' f_saem_full_multi <- multistart(f_saem_full, n = 16, cores = 16) +#' parplot(f_saem_full_multi, lpos = "topleft") +#' illparms(f_saem_full) +#' +#' f_saem_reduced <- update(f_saem_full, no_random_effect = "log_k2") +#' illparms(f_saem_reduced) +#' # On Windows, we need to create a cluster first. When working with +#' # such a cluster, we need to export the mmkin object to the cluster +#' # nodes, as it is referred to when updating the saem object on the nodes. +#' library(parallel) +#' cl <- makePSOCKcluster(12) +#' f_saem_reduced_multi <- multistart(f_saem_reduced, n = 16, cluster = cl) +#' parplot(f_saem_reduced_multi, lpos = "topright") +#' stopCluster(cl) +#' } +multistart <- function(object, n = 50, + cores = if (Sys.info()["sysname"] == "Windows") 1 else parallel::detectCores(), + cluster = NULL, ...) +{ + UseMethod("multistart", object) +} + +#' @rdname multistart +#' @export +multistart.saem.mmkin <- function(object, n = 50, cores = 1, + cluster = NULL, ...) { + call <- match.call() + if (n <= 1) stop("Please specify an n of at least 2") + + mmkin_object <- object$mmkin + + mmkin_parms <- parms(mmkin_object, errparms = FALSE, + transformed = object$transformations == "mkin") + start_parms <- apply( + mmkin_parms, 1, + function(x) stats::runif(n, min(x), max(x))) + + saem_call <- object$call + saem_call[[1]] <- saem + saem_call[[2]] <- mmkin_object + i_startparms <- which(names(saem_call) == "degparms_start") + + fit_function <- function(x) { + + new_startparms <- str2lang( + paste0(capture.output(dput(start_parms[x, ])), + collapse = "")) + + if (length(i_startparms) == 0) { + saem_call <- c(as.list(saem_call), degparms_start = new_startparms) + saem_call <- as.call(saem_call) + } else { + saem_call[i_startparms] <- new_startparms + } + + ret <- eval(saem_call) + + return(ret) + } + + if (is.null(cluster)) { + res <- parallel::mclapply(1:n, fit_function, mc.cores = cores) + } else { + res <- parallel::parLapply(cluster, 1:n, fit_function) + } + attr(res, "orig") <- object + attr(res, "start_parms") <- start_parms + attr(res, "call") <- call + class(res) <- c("multistart.saem.mmkin", "multistart") + return(res) +} + +#' @export +status.multistart <- function(object, ...) { + all_summary_warnings <- character() + + result <- lapply(object, + function(fit) { + if (inherits(fit, "try-error")) return("E") + else { + return("OK") + } + }) + result <- unlist(result) + + class(result) <- "status.multistart" + return(result) +} + +#' @export +status.multistart.saem.mmkin <- function(object, ...) { + all_summary_warnings <- character() + + result <- lapply(object, + function(fit) { + if (inherits(fit$so, "try-error")) return("E") + else { + return("OK") + } + }) + result <- unlist(result) + + class(result) <- "status.multistart" + return(result) +} + +#' @export +print.status.multistart <- function(x, ...) { + class(x) <- NULL + print(table(x, dnn = NULL)) + if (any(x == "OK")) cat("OK: Fit terminated successfully\n") + if (any(x == "E")) cat("E: Error\n") +} + +#' @rdname multistart +#' @export +print.multistart <- function(x, ...) { + cat("<multistart> object with", length(x), "fits:\n") + print(status(x)) +} + +#' @rdname multistart +#' @export +best <- function(object, ...) +{ + UseMethod("best", object) +} + +#' @export +#' @return The object with the highest likelihood +#' @rdname multistart +best.default <- function(object, ...) +{ + return(object[[which.best(object)]]) +} + +#' @return The index of the object with the highest likelihood +#' @rdname multistart +#' @export +which.best <- function(object, ...) +{ + UseMethod("which.best", object) +} + +#' @rdname multistart +#' @export +which.best.default <- function(object, ...) +{ + llfunc <- function(object) { + ret <- try(logLik(object)) + if (inherits(ret, "try-error")) return(NA) + else return(ret) + } + ll <- sapply(object, llfunc) + return(which.max(ll)) +} + +#' @export +update.multistart <- function(object, ..., evaluate = TRUE) { + call <- attr(object, "call") + # For some reason we get multistart.saem.mmkin in call[[1]] when using multistart + # from the loaded package so we need to fix this so we do not have to export + # multistart.saem.mmkin + call[[1]] <- multistart + + update_arguments <- match.call(expand.dots = FALSE)$... + + if (length(update_arguments) > 0) { + update_arguments_in_call <- !is.na(match(names(update_arguments), names(call))) + } + + for (a in names(update_arguments)[update_arguments_in_call]) { + call[[a]] <- update_arguments[[a]] + } + + update_arguments_not_in_call <- !update_arguments_in_call + if(any(update_arguments_not_in_call)) { + call <- c(as.list(call), update_arguments[update_arguments_not_in_call]) + call <- as.call(call) + } + if(evaluate) eval(call, parent.frame()) + else call +} @@ -125,7 +125,7 @@ nlme_function <- function(object) { } #' @rdname nlme -#' @importFrom purrr map_dfr +#' @importFrom rlang !!! #' @return A \code{\link{groupedData}} object #' @export nlme_data <- function(object) { @@ -134,7 +134,7 @@ nlme_data <- function(object) { ds_list <- lapply(object, function(x) x$data[c("time", "variable", "observed")]) names(ds_list) <- ds_names - ds_nlme <- purrr::map_dfr(ds_list, function(x) x, .id = "ds") + ds_nlme <- vctrs::vec_rbind(!!!ds_list, .names_to = "ds") ds_nlme$variable <- as.character(ds_nlme$variable) ds_nlme$ds <- ordered(ds_nlme$ds, levels = unique(ds_nlme$ds)) ds_nlme_renamed <- data.frame(ds = ds_nlme$ds, name = ds_nlme$variable, diff --git a/R/parms.R b/R/parms.R new file mode 100644 index 00000000..bd4e479b --- /dev/null +++ b/R/parms.R @@ -0,0 +1,82 @@ +#' Extract model parameters +#' +#' This function returns degradation model parameters as well as error +#' model parameters per default, in order to avoid working with a fitted model +#' without considering the error structure that was assumed for the fit. +#' +#' @param object A fitted model object. +#' @param \dots Not used +#' @return Depending on the object, a numeric vector of fitted model parameters, +#' a matrix (e.g. for mmkin row objects), or a list of matrices (e.g. for +#' mmkin objects with more than one row). +#' @seealso [saem], [multistart] +#' @examples +#' # mkinfit objects +#' fit <- mkinfit("SFO", FOCUS_2006_C, quiet = TRUE) +#' parms(fit) +#' parms(fit, transformed = TRUE) +#' +#' # mmkin objects +#' ds <- lapply(experimental_data_for_UBA_2019[6:10], +#' function(x) subset(x$data[c("name", "time", "value")])) +#' names(ds) <- paste("Dataset", 6:10) +#' \dontrun{ +#' fits <- mmkin(c("SFO", "FOMC", "DFOP"), ds, quiet = TRUE, cores = 1) +#' parms(fits["SFO", ]) +#' parms(fits[, 2]) +#' parms(fits) +#' parms(fits, transformed = TRUE) +#' } +#' @export +parms <- function(object, ...) +{ + UseMethod("parms", object) +} + +#' @param transformed Should the parameters be returned as used internally +#' during the optimisation? +#' @param errparms Should the error model parameters be returned +#' in addition to the degradation parameters? +#' @rdname parms +#' @export +parms.mkinfit <- function(object, transformed = FALSE, errparms = TRUE, ...) +{ + res <- if (transformed) object$par + else c(object$bparms.optim, object$errparms) + if (!errparms) { + res[setdiff(names(res), names(object$errparms))] + } + else return(res) +} + +#' @rdname parms +#' @export +parms.mmkin <- function(object, transformed = FALSE, errparms = TRUE, ...) +{ + if (nrow(object) == 1) { + res <- sapply(object, parms, transformed = transformed, + errparms = errparms, ...) + colnames(res) <- colnames(object) + } else { + res <- list() + for (i in 1:nrow(object)) { + res[[i]] <- parms(object[i, ], transformed = transformed, + errparms = errparms, ...) + } + names(res) <- rownames(object) + } + return(res) +} + +#' @param exclude_failed For [multistart] objects, should rows for failed fits +#' be removed from the returned parameter matrix? +#' @rdname parms +#' @export +parms.multistart <- function(object, exclude_failed = TRUE, ...) { + res <- t(sapply(object, parms)) + successful <- which(!is.na(res[, 1])) + first_success <- successful[1] + colnames(res) <- names(parms(object[[first_success]])) + if (exclude_failed) res <- res[successful, ] + return(res) +} diff --git a/R/parms.mkinfit.R b/R/parms.mkinfit.R deleted file mode 100644 index a1f2d209..00000000 --- a/R/parms.mkinfit.R +++ /dev/null @@ -1,62 +0,0 @@ -#' Extract model parameters from mkinfit models -#' -#' This function always returns degradation model parameters as well as error -#' model parameters, in order to avoid working with a fitted model without -#' considering the error structure that was assumed for the fit. -#' -#' @param object A fitted model object. Methods are implemented for -#' [mkinfit()] objects and for [mmkin()] objects. -#' @param \dots Not used -#' @return For mkinfit objects, a numeric vector of fitted model parameters. -#' For mmkin row objects, a matrix with the parameters with a -#' row for each dataset. If the mmkin object has more than one row, a list of -#' such matrices is returned. -#' @examples -#' # mkinfit objects -#' fit <- mkinfit("SFO", FOCUS_2006_C, quiet = TRUE) -#' parms(fit) -#' parms(fit, transformed = TRUE) -#' -#' # mmkin objects -#' ds <- lapply(experimental_data_for_UBA_2019[6:10], -#' function(x) subset(x$data[c("name", "time", "value")])) -#' names(ds) <- paste("Dataset", 6:10) -#' \dontrun{ -#' fits <- mmkin(c("SFO", "FOMC", "DFOP"), ds, quiet = TRUE, cores = 1) -#' parms(fits["SFO", ]) -#' parms(fits[, 2]) -#' parms(fits) -#' parms(fits, transformed = TRUE) -#' } -#' @export -parms <- function(object, ...) -{ - UseMethod("parms", object) -} - -#' @param transformed Should the parameters be returned -#' as used internally during the optimisation? -#' @rdname parms -#' @export -parms.mkinfit <- function(object, transformed = FALSE, ...) -{ - if (transformed) object$par - else c(object$bparms.optim, object$errparms) -} - -#' @rdname parms -#' @export -parms.mmkin <- function(object, transformed = FALSE, ...) -{ - if (nrow(object) == 1) { - res <- sapply(object, parms, transformed = transformed, ...) - colnames(res) <- colnames(object) - } else { - res <- list() - for (i in 1:nrow(object)) { - res[[i]] <- parms(object[i, ], transformed = transformed, ...) - } - names(res) <- rownames(object) - } - return(res) -} diff --git a/R/parplot.R b/R/parplot.R new file mode 100644 index 00000000..627a4eb8 --- /dev/null +++ b/R/parplot.R @@ -0,0 +1,105 @@ +#' Plot parameter variability of multistart objects +#' +#' Produces a boxplot with all parameters from the multiple runs, scaled +#' either by the parameters of the run with the highest likelihood, +#' or by their medians as proposed in the paper by Duchesne et al. (2021). +#' +#' @param object The [multistart] object +#' @param llmin The minimum likelihood of objects to be shown +#' @param scale By default, scale parameters using the best available fit. +#' If 'median', parameters are scaled using the median parameters from all fits. +#' @param main Title of the plot +#' @param lpos Positioning of the legend. +#' @param \dots Passed to [boxplot] +#' @references Duchesne R, Guillemin A, Gandrillon O, Crauste F. Practical +#' identifiability in the frame of nonlinear mixed effects models: the example +#' of the in vitro erythropoiesis. BMC Bioinformatics. 2021 Oct 4;22(1):478. +#' doi: 10.1186/s12859-021-04373-4. +#' @seealso [multistart] +#' @importFrom stats median +#' @export +parplot <- function(object, ...) { + UseMethod("parplot") +} + +#' @rdname parplot +#' @export +parplot.multistart.saem.mmkin <- function(object, llmin = -Inf, scale = c("best", "median"), + lpos = "bottomleft", main = "", ...) +{ + oldpar <- par(no.readonly = TRUE) + on.exit(par(oldpar, no.readonly = TRUE)) + + orig <- attr(object, "orig") + orig_parms <- parms(orig) + start_parms <- orig$mean_dp_start + all_parms <- parms(object) + + if (inherits(object, "multistart.saem.mmkin")) { + llfunc <- function(object) { + if (inherits(object$so, "try-error")) return(NA) + else return(logLik(object$so)) + } + } else { + stop("parplot is only implemented for multistart.saem.mmkin objects") + } + ll <- sapply(object, llfunc) + selected <- which(ll > llmin) + selected_parms <- all_parms[selected, ] + + par(las = 1) + if (orig$transformations == "mkin") { + degparm_names_transformed <- names(start_parms) + degparm_index <- which(names(orig_parms) %in% degparm_names_transformed) + orig_parms[degparm_names_transformed] <- backtransform_odeparms( + orig_parms[degparm_names_transformed], + orig$mmkin[[1]]$mkinmod, + transform_rates = orig$mmkin[[1]]$transform_rates, + transform_fractions = orig$mmkin[[1]]$transform_fractions) + start_parms <- backtransform_odeparms(start_parms, + orig$mmkin[[1]]$mkinmod, + transform_rates = orig$mmkin[[1]]$transform_rates, + transform_fractions = orig$mmkin[[1]]$transform_fractions) + degparm_names <- names(start_parms) + + names(orig_parms) <- c(degparm_names, names(orig_parms[-degparm_index])) + + selected_parms[, degparm_names_transformed] <- + t(apply(selected_parms[, degparm_names_transformed], 1, backtransform_odeparms, + orig$mmkin[[1]]$mkinmod, + transform_rates = orig$mmkin[[1]]$transform_rates, + transform_fractions = orig$mmkin[[1]]$transform_fractions)) + colnames(selected_parms)[1:length(degparm_names)] <- degparm_names + } + + scale <- match.arg(scale) + parm_scale <- switch(scale, + best = selected_parms[which.best(object[selected]), ], + median = apply(selected_parms, 2, median) + ) + + # Boxplots of all scaled parameters + selected_scaled_parms <- t(apply(selected_parms, 1, function(x) x / parm_scale)) + boxplot(selected_scaled_parms, log = "y", main = main, , + ylab = "Normalised parameters", ...) + + # Show starting parameters + start_scaled_parms <- rep(NA_real_, length(orig_parms)) + names(start_scaled_parms) <- names(orig_parms) + start_scaled_parms[names(start_parms)] <- + start_parms / parm_scale[names(start_parms)] + points(start_scaled_parms, col = 3, cex = 3) + + # Show parameters of original run + orig_scaled_parms <- orig_parms / parm_scale + points(orig_scaled_parms, col = 2, cex = 2) + + abline(h = 1, lty = 2) + + legend(lpos, inset = c(0.05, 0.05), bty = "n", + pch = 1, col = 3:1, lty = c(NA, NA, 1), + legend = c( + "Starting parameters", + "Original run", + "Multistart runs")) +} diff --git a/R/plot.mixed.mmkin.R b/R/plot.mixed.mmkin.R index 3a444253..6815bfd2 100644 --- a/R/plot.mixed.mmkin.R +++ b/R/plot.mixed.mmkin.R @@ -153,7 +153,7 @@ plot.mixed.mmkin <- function(x, outtimes <- sort(unique(c(x$data$time, seq(xlim[1], xlim[2], length.out = 50)))) - pred_ds <- purrr::map_dfr(i, function(ds_i) { + pred_list <- lapply(i, function(ds_i) { odeparms_trans <- degparms_all[ds_i, odeparms_names] names(odeparms_trans) <- odeparms_names # needed if only one odeparm if (backtransform) { @@ -171,8 +171,9 @@ plot.mixed.mmkin <- function(x, out <- mkinpredict(x$mkinmod, odeparms, odeini, outtimes, solution_type = solution_type, atol = fit_1$atol, rtol = fit_1$rtol) - return(cbind(as.data.frame(out), ds = ds_names[ds_i])) }) + names(pred_list) <- ds_names[i] + pred_ds <- vctrs::vec_rbind(!!!pred_list, .names_to = "ds") odeparms_pop_trans <- degparms_all_pop[odeparms_names] @@ -15,35 +15,46 @@ utils::globalVariables(c("predicted", "std")) #' #' @importFrom utils packageVersion #' @param object An [mmkin] row object containing several fits of the same -#' [mkinmod] model to different datasets +#' [mkinmod] model to different datasets #' @param verbose Should we print information about created objects of -#' type [saemix::SaemixModel] and [saemix::SaemixData]? +#' type [saemix::SaemixModel] and [saemix::SaemixData]? #' @param transformations Per default, all parameter transformations are done -#' in mkin. If this argument is set to 'saemix', parameter transformations -#' are done in 'saemix' for the supported cases. Currently this is only -#' supported in cases where the initial concentration of the parent is not fixed, -#' SFO or DFOP is used for the parent and there is either no metabolite or one. +#' in mkin. If this argument is set to 'saemix', parameter transformations +#' are done in 'saemix' for the supported cases, i.e. (as of version 1.1.2) +#' SFO, FOMC, DFOP and HS without fixing `parent_0`, and SFO or DFOP with +#' one SFO metabolite. #' @param degparms_start Parameter values given as a named numeric vector will -#' be used to override the starting values obtained from the 'mmkin' object. +#' be used to override the starting values obtained from the 'mmkin' object. #' @param test_log_parms If TRUE, an attempt is made to use more robust starting -#' values for population parameters fitted as log parameters in mkin (like -#' rate constants) by only considering rate constants that pass the t-test -#' when calculating mean degradation parameters using [mean_degparms]. +#' values for population parameters fitted as log parameters in mkin (like +#' rate constants) by only considering rate constants that pass the t-test +#' when calculating mean degradation parameters using [mean_degparms]. #' @param conf.level Possibility to adjust the required confidence level -#' for parameter that are tested if requested by 'test_log_parms'. +#' for parameter that are tested if requested by 'test_log_parms'. #' @param solution_type Possibility to specify the solution type in case the -#' automatic choice is not desired +#' automatic choice is not desired +#' @param no_random_effect Character vector of degradation parameters for +#' which there should be no variability over the groups. Only used +#' if the covariance model is not explicitly specified. +#' @param covariance.model Will be passed to [saemix::SaemixModel()]. Per +#' default, uncorrelated random effects are specified for all degradation +#' parameters. +#' @param covariates A data frame with covariate data for use in +#' 'covariate_models', with dataset names as row names. +#' @param covariate_models A list containing linear model formulas with one explanatory +#' variable, i.e. of the type 'parameter ~ covariate'. Covariates must be available +#' in the 'covariates' data frame. #' @param fail_with_errors Should a failure to compute standard errors -#' from the inverse of the Fisher Information Matrix be a failure? +#' from the inverse of the Fisher Information Matrix be a failure? #' @param quiet Should we suppress the messages saemix prints at the beginning -#' and the end of the optimisation process? +#' and the end of the optimisation process? #' @param nbiter.saemix Convenience option to increase the number of -#' iterations +#' iterations #' @param control Passed to [saemix::saemix]. #' @param \dots Further parameters passed to [saemix::saemixModel]. #' @return An S3 object of class 'saem.mmkin', containing the fitted -#' [saemix::SaemixObject] as a list component named 'so'. The -#' object also inherits from 'mixed.mmkin'. +#' [saemix::SaemixObject] as a list component named 'so'. The +#' object also inherits from 'mixed.mmkin'. #' @seealso [summary.saem.mmkin] [plot.mixed.mmkin] #' @examples #' \dontrun{ @@ -58,7 +69,13 @@ utils::globalVariables(c("predicted", "std")) #' f_saem_sfo <- saem(f_mmkin_parent["SFO", ]) #' f_saem_fomc <- saem(f_mmkin_parent["FOMC", ]) #' f_saem_dfop <- saem(f_mmkin_parent["DFOP", ]) +#' anova(f_saem_sfo, f_saem_fomc, f_saem_dfop) +#' anova(f_saem_sfo, f_saem_dfop, test = TRUE) +#' illparms(f_saem_dfop) +#' f_saem_dfop_red <- update(f_saem_dfop, no_random_effect = "g_qlogis") +#' anova(f_saem_dfop, f_saem_dfop_red, test = TRUE) #' +#' anova(f_saem_sfo, f_saem_fomc, f_saem_dfop) #' # The returned saem.mmkin object contains an SaemixObject, therefore we can use #' # functions from saemix #' library(saemix) @@ -70,7 +87,7 @@ utils::globalVariables(c("predicted", "std")) #' #' f_mmkin_parent_tc <- update(f_mmkin_parent, error_model = "tc") #' f_saem_fomc_tc <- saem(f_mmkin_parent_tc["FOMC", ]) -#' compare.saemix(f_saem_fomc$so, f_saem_fomc_tc$so) +#' anova(f_saem_fomc, f_saem_fomc_tc, test = TRUE) #' #' sfo_sfo <- mkinmod(parent = mkinsub("SFO", "A1"), #' A1 = mkinsub("SFO")) @@ -118,6 +135,10 @@ saem.mmkin <- function(object, test_log_parms = TRUE, conf.level = 0.6, solution_type = "auto", + covariance.model = "auto", + covariates = NULL, + covariate_models = NULL, + no_random_effect = NULL, nbiter.saemix = c(300, 100), control = list(displayProgress = FALSE, print = FALSE, nbiter.saemix = nbiter.saemix, @@ -125,56 +146,68 @@ saem.mmkin <- function(object, fail_with_errors = TRUE, verbose = FALSE, quiet = FALSE, ...) { + call <- match.call() transformations <- match.arg(transformations) m_saemix <- saemix_model(object, verbose = verbose, degparms_start = degparms_start, test_log_parms = test_log_parms, conf.level = conf.level, solution_type = solution_type, - transformations = transformations, ...) - d_saemix <- saemix_data(object, verbose = verbose) + transformations = transformations, + covariance.model = covariance.model, + covariates = covariates, + covariate_models = covariate_models, + no_random_effect = no_random_effect, + ...) + d_saemix <- saemix_data(object, covariates = covariates, verbose = verbose) + fit_failed <- FALSE + FIM_failed <- NULL fit_time <- system.time({ - utils::capture.output(f_saemix <- saemix::saemix(m_saemix, d_saemix, control), split = !quiet) - FIM_failed <- NULL + utils::capture.output(f_saemix <- try(saemix::saemix(m_saemix, d_saemix, control)), split = !quiet) + if (inherits(f_saemix, "try-error")) fit_failed <- TRUE + }) + + return_data <- nlme_data(object) + + if (!fit_failed) { if (any(is.na(f_saemix@results@se.fixed))) FIM_failed <- c(FIM_failed, "fixed effects") if (any(is.na(c(f_saemix@results@se.omega, f_saemix@results@se.respar)))) { - FIM_failed <- c(FIM_failed, "random effects and residual error parameters") + FIM_failed <- c(FIM_failed, "random effects and error model parameters") } if (!is.null(FIM_failed) & fail_with_errors) { stop("Could not invert FIM for ", paste(FIM_failed, collapse = " and ")) } - }) - transparms_optim <- f_saemix@results@fixed.effects - names(transparms_optim) <- f_saemix@results@name.fixed + transparms_optim <- f_saemix@results@fixed.effects + names(transparms_optim) <- f_saemix@results@name.fixed - if (transformations == "mkin") { - bparms_optim <- backtransform_odeparms(transparms_optim, - object[[1]]$mkinmod, - object[[1]]$transform_rates, - object[[1]]$transform_fractions) - } else { - bparms_optim <- transparms_optim - } + if (transformations == "mkin") { + bparms_optim <- backtransform_odeparms(transparms_optim, + object[[1]]$mkinmod, + object[[1]]$transform_rates, + object[[1]]$transform_fractions) + } else { + bparms_optim <- transparms_optim + } - return_data <- nlme_data(object) - saemix_data_ds <- f_saemix@data@data$ds - mkin_ds_order <- as.character(unique(return_data$ds)) - saemix_ds_order <- unique(saemix_data_ds) - - psi <- saemix::psi(f_saemix) - rownames(psi) <- saemix_ds_order - return_data$predicted <- f_saemix@model@model( - psi = psi[mkin_ds_order, ], - id = as.numeric(return_data$ds), - xidep = return_data[c("time", "name")]) - - return_data <- transform(return_data, - residual = value - predicted, - std = sigma_twocomp(predicted, - f_saemix@results@respar[1], f_saemix@results@respar[2])) - return_data <- transform(return_data, - standardized = residual / std) + saemix_data_ds <- f_saemix@data@data$ds + mkin_ds_order <- as.character(unique(return_data$ds)) + saemix_ds_order <- unique(saemix_data_ds) + + psi <- saemix::psi(f_saemix) + rownames(psi) <- saemix_ds_order + return_data$predicted <- f_saemix@model@model( + psi = psi[mkin_ds_order, ], + id = as.numeric(return_data$ds), + xidep = return_data[c("time", "name")]) + + return_data <- transform(return_data, + residual = value - predicted, + std = sigma_twocomp(predicted, + f_saemix@results@respar[1], f_saemix@results@respar[2])) + return_data <- transform(return_data, + standardized = residual / std) + } result <- list( mkinmod = object[[1]]$mkinmod, @@ -183,14 +216,14 @@ saem.mmkin <- function(object, transformations = transformations, transform_rates = object[[1]]$transform_rates, transform_fractions = object[[1]]$transform_fractions, + sm = m_saemix, so = f_saemix, + call = call, time = fit_time, + FIM_failed = FIM_failed, mean_dp_start = attr(m_saemix, "mean_dp_start"), - bparms.optim = bparms_optim, bparms.fixed = object[[1]]$bparms.fixed, data = return_data, - mkin_ds_order = mkin_ds_order, - saemix_ds_order = saemix_ds_order, err_mod = object[[1]]$err_mod, date.fit = date(), saemixversion = as.character(utils::packageVersion("saemix")), @@ -198,6 +231,12 @@ saem.mmkin <- function(object, Rversion = paste(R.version$major, R.version$minor, sep=".") ) + if (!fit_failed) { + result$mkin_ds_order <- mkin_ds_order + result$saemix_ds_order <- saemix_ds_order + result$bparms.optim <- bparms_optim + } + class(result) <- c("saem.mmkin", "mixed.mmkin") return(result) } @@ -217,18 +256,20 @@ print.saem.mmkin <- function(x, digits = max(3, getOption("digits") - 3), ...) { length(unique(x$data$name)), "variable(s) grouped in", length(unique(x$data$ds)), "datasets\n") - cat("\nLikelihood computed by importance sampling\n") - print(data.frame( - AIC = AIC(x$so, type = "is"), - BIC = BIC(x$so, type = "is"), - logLik = logLik(x$so, type = "is"), - row.names = " "), digits = digits) - - cat("\nFitted parameters:\n") - conf.int <- x$so@results@conf.int[c("estimate", "lower", "upper")] - rownames(conf.int) <- x$so@results@conf.int[["name"]] - conf.int.var <- grepl("^Var\\.", rownames(conf.int)) - print(conf.int[!conf.int.var, ], digits = digits) + if (inherits(x$so, "try-error")) { + cat("\nFit did not terminate successfully\n") + } else { + cat("\nLikelihood computed by importance sampling\n") + print(data.frame( + AIC = AIC(x$so, type = "is"), + BIC = BIC(x$so, type = "is"), + logLik = logLik(x$so, type = "is"), + row.names = " "), digits = digits) + + cat("\nFitted parameters:\n") + conf.int <- parms(x, ci = TRUE) + print(conf.int, digits = digits) + } invisible(x) } @@ -236,14 +277,23 @@ print.saem.mmkin <- function(x, digits = max(3, getOption("digits") - 3), ...) { #' @rdname saem #' @return An [saemix::SaemixModel] object. #' @export -saemix_model <- function(object, solution_type = "auto", transformations = c("mkin", "saemix"), - degparms_start = numeric(), test_log_parms = FALSE, conf.level = 0.6, verbose = FALSE, ...) +saemix_model <- function(object, solution_type = "auto", + transformations = c("mkin", "saemix"), degparms_start = numeric(), + covariance.model = "auto", no_random_effect = NULL, + covariates = NULL, covariate_models = NULL, + test_log_parms = FALSE, conf.level = 0.6, verbose = FALSE, ...) { if (nrow(object) > 1) stop("Only row objects allowed") mkin_model <- object[[1]]$mkinmod degparms_optim <- mean_degparms(object, test_log_parms = test_log_parms) + na_degparms <- names(which(is.na(degparms_optim))) + if (length(na_degparms) > 0) { + message("Did not find valid starting values for ", paste(na_degparms, collapse = ", "), "\n", + "Now trying with test_log_parms = FALSE") + degparms_optim <- mean_degparms(object, test_log_parms = FALSE) + } if (transformations == "saemix") { degparms_optim <- backtransform_odeparms(degparms_optim, object[[1]]$mkinmod, @@ -252,7 +302,8 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk } degparms_fixed <- object[[1]]$bparms.fixed - # Transformations are done in the degradation function + # Transformations are done in the degradation function by default + # (transformations = "mkin") transform.par = rep(0, length(degparms_optim)) odeini_optim_parm_names <- grep('_0$', names(degparms_optim), value = TRUE) @@ -275,7 +326,10 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk # Parent only if (length(mkin_model$spec) == 1) { parent_type <- mkin_model$spec[[1]]$type - if (length(odeini_fixed) == 1) { + if (length(odeini_fixed) == 1 && !grepl("_bound$", names(odeini_fixed))) { + if (transformations == "saemix") { + stop("saemix transformations are not supported for parent fits with fixed initial parent value") + } if (parent_type == "SFO") { stop("saemix needs at least two parameters to work on.") } @@ -303,6 +357,9 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk } } } else { + if (length(odeini_fixed) == 2) { + stop("SFORB with fixed initial parent value is not supported") + } if (parent_type == "SFO") { if (transformations == "mkin") { model_function <- function(psi, id, xidep) { @@ -316,8 +373,15 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk } } if (parent_type == "FOMC") { - model_function <- function(psi, id, xidep) { - psi[id, 1] / (xidep[, "time"]/exp(psi[id, 3]) + 1)^exp(psi[id, 2]) + if (transformations == "mkin") { + model_function <- function(psi, id, xidep) { + psi[id, 1] / (xidep[, "time"]/exp(psi[id, 3]) + 1)^exp(psi[id, 2]) + } + } else { + model_function <- function(psi, id, xidep) { + psi[id, 1] / (xidep[, "time"]/psi[id, 3] + 1)^psi[id, 2] + } + transform.par = c(0, 1, 1) } } if (parent_type == "DFOP") { @@ -338,14 +402,57 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk transform.par = c(0, 1, 1, 3) } } + if (parent_type == "SFORB") { + if (transformations == "mkin") { + model_function <- function(psi, id, xidep) { + k_12 <- exp(psi[id, 3]) + k_21 <- exp(psi[id, 4]) + k_1output <- exp(psi[id, 2]) + t <- xidep[, "time"] + + sqrt_exp = sqrt(1/4 * (k_12 + k_21 + k_1output)^2 + k_12 * k_21 - (k_12 + k_1output) * k_21) + b1 = 0.5 * (k_12 + k_21 + k_1output) + sqrt_exp + b2 = 0.5 * (k_12 + k_21 + k_1output) - sqrt_exp + + psi[id, 1] * (((k_12 + k_21 - b1)/(b2 - b1)) * exp(-b1 * t) + + ((k_12 + k_21 - b2)/(b1 - b2)) * exp(-b2 * t)) + } + } else { + model_function <- function(psi, id, xidep) { + k_12 <- psi[id, 3] + k_21 <- psi[id, 4] + k_1output <- psi[id, 2] + t <- xidep[, "time"] + + sqrt_exp = sqrt(1/4 * (k_12 + k_21 + k_1output)^2 + k_12 * k_21 - (k_12 + k_1output) * k_21) + b1 = 0.5 * (k_12 + k_21 + k_1output) + sqrt_exp + b2 = 0.5 * (k_12 + k_21 + k_1output) - sqrt_exp + + psi[id, 1] * (((k_12 + k_21 - b1)/(b2 - b1)) * exp(-b1 * t) + + ((k_12 + k_21 - b2)/(b1 - b2)) * exp(-b2 * t)) + } + transform.par = c(0, 1, 1, 1) + } + } if (parent_type == "HS") { - model_function <- function(psi, id, xidep) { - tb <- exp(psi[id, 4]) - t <- xidep[, "time"] - k1 = exp(psi[id, 2]) - psi[id, 1] * ifelse(t <= tb, - exp(- k1 * t), - exp(- k1 * tb) * exp(- exp(psi[id, 3]) * (t - tb))) + if (transformations == "mkin") { + model_function <- function(psi, id, xidep) { + tb <- exp(psi[id, 4]) + t <- xidep[, "time"] + k1 <- exp(psi[id, 2]) + psi[id, 1] * ifelse(t <= tb, + exp(- k1 * t), + exp(- k1 * tb) * exp(- exp(psi[id, 3]) * (t - tb))) + } + } else { + model_function <- function(psi, id, xidep) { + tb <- psi[id, 4] + t <- xidep[, "time"] + psi[id, 1] * ifelse(t <= tb, + exp(- psi[id, 2] * t), + exp(- psi[id, 2] * tb) * exp(- psi[id, 3] * (t - tb))) + } + transform.par = c(0, 1, 1, 1) } } } @@ -518,8 +625,52 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk degparms_psi0 <- degparms_optim degparms_psi0[names(degparms_start)] <- degparms_start - psi0_matrix <- matrix(degparms_psi0, nrow = 1) - colnames(psi0_matrix) <- names(degparms_psi0) + psi0_matrix <- matrix(degparms_psi0, nrow = 1, + dimnames = list("(Intercept)", names(degparms_psi0))) + + if (covariance.model[1] == "auto") { + covariance_diagonal <- rep(1, length(degparms_optim)) + if (!is.null(no_random_effect)) { + degparms_no_random <- which(names(degparms_psi0) %in% no_random_effect) + covariance_diagonal[degparms_no_random] <- 0 + } + covariance.model = diag(covariance_diagonal) + } + + if (is.null(covariate_models)) { + covariate.model <- matrix(nrow = 0, ncol = 0) # default in saemixModel() + } else { + degparms_dependent <- sapply(covariate_models, function(x) as.character(x[[2]])) + covariates_in_models = unique(unlist(lapply( + covariate_models, function(x) + colnames(attr(terms(x), "factors")) + ))) + covariates_not_available <- setdiff(covariates_in_models, names(covariates)) + if (length(covariates_not_available) > 0) { + stop("Covariate(s) ", paste(covariates_not_available, collapse = ", "), + " used in the covariate models not available in the covariate data") + } + psi0_matrix <- rbind(psi0_matrix, + matrix(0, nrow = length(covariates), ncol = ncol(psi0_matrix), + dimnames = list(names(covariates), colnames(psi0_matrix)))) + covariate.model <- matrix(0, nrow = length(covariates), + ncol = ncol(psi0_matrix), + dimnames = list( + covariates = names(covariates), + degparms = colnames(psi0_matrix))) + if (transformations == "saemix") { + stop("Covariate models with saemix transformations currently not supported") + } + parms_trans <- as.data.frame(t(sapply(object, parms, transformed = TRUE))) + for (covariate_model in covariate_models) { + covariate_name <- as.character(covariate_model[[2]]) + model_data <- cbind(parms_trans, covariates) + ini_model <- lm(covariate_model, data = model_data) + ini_coef <- coef(ini_model) + psi0_matrix[names(ini_coef), covariate_name] <- ini_coef + covariate.model[names(ini_coef)[-1], covariate_name] <- as.numeric(as.logical(ini_coef[-1])) + } + } res <- saemix::saemixModel(model_function, psi0 = psi0_matrix, @@ -527,6 +678,8 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk transform.par = transform.par, error.model = error.model, verbose = verbose, + covariance.model = covariance.model, + covariate.model = covariate.model, ... ) attr(res, "mean_dp_start") <- degparms_optim @@ -534,26 +687,107 @@ saemix_model <- function(object, solution_type = "auto", transformations = c("mk } #' @rdname saem +#' @importFrom rlang !!! #' @return An [saemix::SaemixData] object. #' @export -saemix_data <- function(object, verbose = FALSE, ...) { +saemix_data <- function(object, covariates = NULL, verbose = FALSE, ...) { if (nrow(object) > 1) stop("Only row objects allowed") ds_names <- colnames(object) ds_list <- lapply(object, function(x) x$data[c("time", "variable", "observed")]) names(ds_list) <- ds_names - ds_saemix_all <- purrr::map_dfr(ds_list, function(x) x, .id = "ds") + ds_saemix_all <- vctrs::vec_rbind(!!!ds_list, .names_to = "ds") ds_saemix <- data.frame(ds = ds_saemix_all$ds, name = as.character(ds_saemix_all$variable), time = ds_saemix_all$time, value = ds_saemix_all$observed, stringsAsFactors = FALSE) + if (!is.null(covariates)) { + name.covariates <- names(covariates) + covariates$ds <- rownames(covariates) + ds_saemix <- merge(ds_saemix, covariates, sort = FALSE) + } else { + name.covariates <- character(0) + } res <- saemix::saemixData(ds_saemix, name.group = "ds", name.predictors = c("time", "name"), name.response = "value", + name.covariates = name.covariates, verbose = verbose, ...) return(res) } + +#' logLik method for saem.mmkin objects +#' +#' @param object The fitted [saem.mmkin] object +#' @param \dots Passed to [saemix::logLik.SaemixObject] +#' @param method Passed to [saemix::logLik.SaemixObject] +#' @export +logLik.saem.mmkin <- function(object, ..., method = c("is", "lin", "gq")) { + method <- match.arg(method) + return(logLik(object$so, method = method)) +} + +#' @export +update.saem.mmkin <- function(object, ..., evaluate = TRUE) { + call <- object$call + # For some reason we get saem.mmkin in the call when using mhmkin + # so we need to fix this so we do not have to export saem.mmkin in + # addition to the S3 method + call[[1]] <- saem + + # We also need to provide the mmkin object in the call, so it + # will also be found when called by testthat or pkgdown + call[[2]] <- object$mmkin + + update_arguments <- match.call(expand.dots = FALSE)$... + + if (length(update_arguments) > 0) { + update_arguments_in_call <- !is.na(match(names(update_arguments), names(call))) + } + + for (a in names(update_arguments)[update_arguments_in_call]) { + call[[a]] <- update_arguments[[a]] + } + + update_arguments_not_in_call <- !update_arguments_in_call + if(any(update_arguments_not_in_call)) { + call <- c(as.list(call), update_arguments[update_arguments_not_in_call]) + call <- as.call(call) + } + if(evaluate) eval(call, parent.frame()) + else call +} + +#' @export +#' @rdname saem +#' @param ci Should a matrix with estimates and confidence interval boundaries +#' be returned? If FALSE (default), a vector of estimates is returned. +parms.saem.mmkin <- function(object, ci = FALSE, ...) { + cov.mod <- object$sm@covariance.model + n_cov_mod_parms <- sum(cov.mod[upper.tri(cov.mod, diag = TRUE)]) + n_parms <- length(object$sm@name.modpar) + + n_cov_mod_parms + + length(object$sm@name.sigma) + + if (inherits(object$so, "try-error")) { + conf.int <- matrix(rep(NA, 3 * n_parms), ncol = 3) + colnames(conf.int) <- c("estimate", "lower", "upper") + } else { + conf.int <- object$so@results@conf.int[c("estimate", "lower", "upper")] + rownames(conf.int) <- object$so@results@conf.int[["name"]] + conf.int.var <- grepl("^Var\\.", rownames(conf.int)) + conf.int <- conf.int[!conf.int.var, ] + conf.int.cov <- grepl("^Cov\\.", rownames(conf.int)) + conf.int <- conf.int[!conf.int.cov, ] + } + estimate <- conf.int[, "estimate"] + + names(estimate) <- rownames(conf.int) + + if (ci) return(conf.int) + else return(estimate) +} diff --git a/R/set_nd_nq.R b/R/set_nd_nq.R new file mode 100644 index 00000000..37b9a894 --- /dev/null +++ b/R/set_nd_nq.R @@ -0,0 +1,164 @@ +#' Set non-detects and unquantified values in residue series without replicates +#' +#' This function automates replacing unquantified values in residue time and +#' depth series. For time series, the function performs part of the residue +#' processing proposed in the FOCUS kinetics guidance for parent compounds +#' and metabolites. For two-dimensional residue series over time and depth, +#' it automates the proposal of Boesten et al (2015). +#' +#' @param res_raw Character vector of a residue time series, or matrix of +#' residue values with rows representing depth profiles for a specific sampling +#' time, and columns representing time series of residues at the same depth. +#' Values below the limit of detection (lod) have to be coded as "nd", values +#' between the limit of detection and the limit of quantification, if any, have +#' to be coded as "nq". Samples not analysed have to be coded as "na". All +#' values that are not "na", "nd" or "nq" have to be coercible to numeric +#' @param lod Limit of detection (numeric) +#' @param loq Limit of quantification(numeric). Must be specified if the FOCUS rule to +#' stop after the first non-detection is to be applied +#' @param time_zero_presence Do we assume that residues occur at time zero? +#' This only affects samples from the first sampling time that have been +#' reported as "nd" (not detected). +#' @references Boesten, J. J. T. I., van der Linden, A. M. A., Beltman, W. H. +#' J. and Pol, J. W. (2015). Leaching of plant protection products and their +#' transformation products; Proposals for improving the assessment of leaching +#' to groundwater in the Netherlands — Version 2. Alterra report 2630, Alterra +#' Wageningen UR (University & Research centre) +#' @references FOCUS (2014) Generic Guidance for Estimating Persistence and Degradation +#' Kinetics from Environmental Fate Studies on Pesticides in EU Registration, Version 1.1, +#' 18 December 2014, p. 251 +#' @return A numeric vector, if a vector was supplied, or a numeric matrix otherwise +#' @export +#' @examples +#' # FOCUS (2014) p. 75/76 and 131/132 +#' parent_1 <- c(.12, .09, .05, .03, "nd", "nd", "nd", "nd", "nd", "nd") +#' set_nd_nq(parent_1, 0.02) +#' parent_2 <- c(.12, .09, .05, .03, "nd", "nd", .03, "nd", "nd", "nd") +#' set_nd_nq(parent_2, 0.02) +#' set_nd_nq_focus(parent_2, 0.02, loq = 0.05) +#' parent_3 <- c(.12, .09, .05, .03, "nd", "nd", .06, "nd", "nd", "nd") +#' set_nd_nq(parent_3, 0.02) +#' set_nd_nq_focus(parent_3, 0.02, loq = 0.05) +#' metabolite <- c("nd", "nd", "nd", 0.03, 0.06, 0.10, 0.11, 0.10, 0.09, 0.05, 0.03, "nd", "nd") +#' set_nd_nq(metabolite, 0.02) +#' set_nd_nq_focus(metabolite, 0.02, 0.05) +#' # +#' # Boesten et al. (2015), p. 57/58 +#' table_8 <- matrix( +#' c(10, 10, rep("nd", 4), +#' 10, 10, rep("nq", 2), rep("nd", 2), +#' 10, 10, 10, "nq", "nd", "nd", +#' "nq", 10, "nq", rep("nd", 3), +#' "nd", "nq", "nq", rep("nd", 3), +#' rep("nd", 6), rep("nd", 6)), +#' ncol = 6, byrow = TRUE) +#' set_nd_nq(table_8, 0.5, 1.5, time_zero_presence = TRUE) +#' table_10 <- matrix( +#' c(10, 10, rep("nd", 4), +#' 10, 10, rep("nd", 4), +#' 10, 10, 10, rep("nd", 3), +#' "nd", 10, rep("nd", 4), +#' rep("nd", 18)), +#' ncol = 6, byrow = TRUE) +#' set_nd_nq(table_10, 0.5, time_zero_presence = TRUE) +set_nd_nq <- function(res_raw, lod, loq = NA, time_zero_presence = FALSE) { + if (!is.character(res_raw)) { + stop("Please supply a vector or a matrix of character values") + } + if (is.vector(res_raw)) { + was_vector <- TRUE + res_raw <- as.matrix(res_raw) + } else { + was_vector <- FALSE + if (!is.matrix(res_raw)) { + stop("Please supply a vector or a matrix of character values") + } + } + nq <- 0.5 * (loq + lod) + nda <- 0.5 * lod # not detected but adjacent to detection + res_raw[res_raw == "nq"] <- nq + + if (!time_zero_presence) { + for (j in 1:ncol(res_raw)) { + if (res_raw[1, j] == "nd") res_raw[1, j] <- "na" + } + } + res_raw[res_raw == "na"] <- NA + + not_nd_na <- function(value) !(grepl("nd", value) | is.na(value)) + + for (i in 1:nrow(res_raw)) { + for (j in 1:ncol(res_raw)) { + if (!is.na(res_raw[i, j]) && res_raw[i, j] == "nd") { + if (i > 1) { # check earlier sample in same layer + if (not_nd_na(res_raw[i - 1, j])) res_raw[i, j] <- "nda" + } + if (i < nrow(res_raw)) { # check later sample + if (not_nd_na(res_raw[i + 1, j])) res_raw[i, j] <- "nda" + } + if (j > 1) { # check above sample at the same time + if (not_nd_na(res_raw[i, j - 1])) res_raw[i, j] <- "nda" + } + if (j < ncol(res_raw)) { # check sample below at the same time + if (not_nd_na(res_raw[i, j + 1])) res_raw[i, j] <- "nda" + } + } + } + } + res_raw[res_raw == "nda"] <- nda + res_raw[res_raw == "nd"] <- NA + + result <- as.numeric(res_raw) + dim(result) <- dim(res_raw) + dimnames(result) <- dimnames(res_raw) + if (was_vector) result <- as.vector(result) + return(result) +} + +#' @describeIn set_nd_nq Set non-detects in residue time series according to FOCUS rules +#' @param set_first_sample_nd Should the first sample be set to "first_sample_nd_value" +#' in case it is a non-detection? +#' @param first_sample_nd_value Value to be used for the first sample if it is a non-detection +#' @param ignore_below_loq_after_first_nd Should we ignore values below the LOQ after the first +#' non-detection that occurs after the quantified values? +#' @export +set_nd_nq_focus <- function(res_raw, lod, loq = NA, + set_first_sample_nd = TRUE, first_sample_nd_value = 0, + ignore_below_loq_after_first_nd = TRUE) +{ + + if (!is.vector(res_raw)) stop("FOCUS rules are only specified for one-dimensional time series") + + if (ignore_below_loq_after_first_nd & is.na(loq)) { + stop("You need to specify an LOQ") + } + + n <- length(res_raw) + if (ignore_below_loq_after_first_nd) { + for (i in 3:n) { + if (!res_raw[i - 2] %in% c("na", "nd")) { + if (res_raw[i - 1] == "nd") { + res_remaining <- res_raw[i:n] + res_remaining_unquantified <- ifelse(res_remaining == "na", TRUE, + ifelse(res_remaining == "nd", TRUE, + ifelse(res_remaining == "nq", TRUE, + ifelse(suppressWarnings(as.numeric(res_remaining)) < loq, TRUE, FALSE)))) + res_remaining_numeric <- suppressWarnings(as.numeric(res_remaining)) + res_remaining_below_loq <- ifelse(res_remaining == "nq", TRUE, + ifelse(!is.na(res_remaining_numeric) & res_remaining_numeric < loq, TRUE, FALSE)) + if (all(res_remaining_unquantified)) { + res_raw[i:n] <- ifelse(res_remaining_below_loq, "nd", res_remaining) + } + } + } + } + } + + result <- set_nd_nq(res_raw, lod = lod, loq = loq) + + if (set_first_sample_nd) { + if (res_raw[1] == "nd") result[1] <- first_sample_nd_value + } + + return(result) +} diff --git a/R/status.R b/R/status.R new file mode 100644 index 00000000..8bcd3a16 --- /dev/null +++ b/R/status.R @@ -0,0 +1,113 @@ +#' Method to get status information for fit array objects +#' +#' @param object The object to investigate +#' @param x The object to be printed +#' @param \dots For potential future extensions +#' @return An object with the same dimensions as the fit array +#' suitable printing method. +#' @export +status <- function(object, ...) +{ + UseMethod("status", object) +} + +#' @rdname status +#' @export +#' @examples +#' \dontrun{ +#' fits <- mmkin( +#' c("SFO", "FOMC"), +#' list("FOCUS A" = FOCUS_2006_A, +#' "FOCUS B" = FOCUS_2006_C), +#' quiet = TRUE) +#' status(fits) +#' } +status.mmkin <- function(object, ...) { + all_summary_warnings <- character() + sww <- 0 # Counter for Shapiro-Wilks warnings + + result <- lapply(object, + function(fit) { + if (inherits(fit, "try-error")) return("E") + sw <- fit$summary_warnings + swn <- names(sw) + if (length(sw) > 0) { + if (any(grepl("S", swn))) { + sww <<- sww + 1 + swn <- gsub("S", paste0("S", sww), swn) + } + warnstring <- paste(swn, collapse = ", ") + names(sw) <- swn + all_summary_warnings <<- c(all_summary_warnings, sw) + return(warnstring) + } else { + return("OK") + } + }) + result <- unlist(result) + dim(result) <- dim(object) + dimnames(result) <- dimnames(object) + + u_swn <- unique(names(all_summary_warnings)) + attr(result, "unique_warnings") <- all_summary_warnings[u_swn] + class(result) <- "status.mmkin" + return(result) +} + +#' @rdname status +#' @export +print.status.mmkin <- function(x, ...) { + u_w <- attr(x, "unique_warnings") + attr(x, "unique_warnings") <- NULL + class(x) <- NULL + print(x, quote = FALSE) + cat("\n") + for (i in seq_along(u_w)) { + cat(names(u_w)[i], ": ", u_w[i], "\n", sep = "") + } + if (any(x == "OK")) cat("OK: No warnings\n") + if (any(x == "E")) cat("E: Error\n") +} + +#' @rdname status +#' @export +status.mhmkin <- function(object, ...) { + if (inherits(object[[1]], "saem.mmkin")) { + test_func <- function(fit) { + if (inherits(fit$so, "try-error")) { + return("E") + } else { + if (!is.null(fit$FIM_failed)) { + return_values <- c("fixed effects" = "Fth", + "random effects and error model parameters" = "FO") + return(paste(return_values[fit$FIM_failed], collapse = ", ")) + } else { + return("OK") + } + } + } + } else { + stop("Only mhmkin objects containing saem.mmkin objects currently supported") + } + result <- lapply(object, test_func) + result <- unlist(result) + dim(result) <- dim(object) + dimnames(result) <- dimnames(object) + + class(result) <- "status.mhmkin" + return(result) +} + +#' @rdname status +#' @export +print.status.mhmkin <- function(x, ...) { + class(x) <- NULL + print(x, quote = FALSE) + cat("\n") + if (any(x == "OK")) cat("OK: Fit terminated successfully\n") + if (any(x == "Fth")) cat("Fth: Could not invert FIM for fixed effects\n") + if (any(x == "FO")) cat("FO: Could not invert FIM for random effects and error model parameters\n") + if (any(x == "Fth, FO")) cat("Fth, FO: Could not invert FIM for fixed effects, nor for random effects and error model parameters\n") + if (any(x == "E")) cat("E: Error\n") +} + diff --git a/R/summary.mkinfit.R b/R/summary.mkinfit.R index f9858c32..4122873f 100644 --- a/R/summary.mkinfit.R +++ b/R/summary.mkinfit.R @@ -47,7 +47,7 @@ #' \url{http://esdac.jrc.ec.europa.eu/projects/degradation-kinetics} #' @examples #' -#' summary(mkinfit(mkinmod(parent = mkinsub("SFO")), FOCUS_2006_A, quiet = TRUE)) +#' summary(mkinfit("SFO", FOCUS_2006_A, quiet = TRUE)) #' #' @export summary.mkinfit <- function(object, data = TRUE, distimes = TRUE, alpha = 0.05, ...) { @@ -172,7 +172,7 @@ summary.mkinfit <- function(object, data = TRUE, distimes = TRUE, alpha = 0.05, if (distimes) ans$distimes <- ep$distimes if (length(ep$SFORB) != 0) ans$SFORB <- ep$SFORB if (!is.null(object$d_3_message)) ans$d_3_message <- object$d_3_message - class(ans) <- c("summary.mkinfit", "summary.modFit") + class(ans) <- "summary.mkinfit" return(ans) } diff --git a/R/summary.mmkin.R b/R/summary.mmkin.R new file mode 100644 index 00000000..06472e18 --- /dev/null +++ b/R/summary.mmkin.R @@ -0,0 +1,56 @@ +#' Summary method for class "mmkin" +#' +#' Shows status information on the [mkinfit] objects contained in the object +#' and gives an overview of ill-defined parameters calculated by [illparms]. +#' +#' @param object an object of class [mmkin] +#' @param x an object of class \code{summary.mmkin}. +#' @param conf.level confidence level for testing parameters +#' @param digits number of digits to use for printing +#' @param \dots optional arguments passed to methods like \code{print}. +#' @examples +#' +#' fits <- mmkin( +#' c("SFO", "FOMC"), +#' list("FOCUS A" = FOCUS_2006_A, +#' "FOCUS C" = FOCUS_2006_C), +#' quiet = TRUE, cores = 1) +#' summary(fits) +#' +#' @export +summary.mmkin <- function(object, conf.level = 0.95, ...) { + + ans <- list( + err_mod = object[[1, 1]]$err_mod, + time = attr(object, "time"), + illparms = illparms(object), + status = status(object) + ) + + class(ans) <- c("summary.mmkin") + return(ans) +} + +#' @rdname summary.mmkin +#' @export +print.summary.mmkin <- function(x, digits = max(3, getOption("digits") - 3), ...) { + if (!is.null(x$err_mod)) { + cat("Error model: ") + cat(switch(x$err_mod, + const = "Constant variance", + obs = "Variance unique to each observed variable", + tc = "Two-component variance function"), "\n") + } + cat("Fitted in", x$time[["elapsed"]], "s\n") + + cat("\nStatus:\n") + print(x$status) + + if (any(x$illparms != "")) { + cat("\nIll-defined parameters:\n") + print(x$illparms) + } + + invisible(x) +} + diff --git a/R/summary.saem.mmkin.R b/R/summary.saem.mmkin.R index fa52a579..651cc29e 100644 --- a/R/summary.saem.mmkin.R +++ b/R/summary.saem.mmkin.R @@ -73,7 +73,12 @@ #' f_mmkin_dfop_sfo <- mmkin(list(dfop_sfo), ds_syn_dfop_sfo, #' quiet = TRUE, error_model = "tc", cores = 5) #' f_saem_dfop_sfo <- saem(f_mmkin_dfop_sfo) -#' summary(f_saem_dfop_sfo, data = TRUE) +#' print(f_saem_dfop_sfo) +#' illparms(f_saem_dfop_sfo) +#' f_saem_dfop_sfo_2 <- update(f_saem_dfop_sfo, covariance.model = diag(c(0, 0, 1, 1, 1, 0))) +#' illparms(f_saem_dfop_sfo_2) +#' intervals(f_saem_dfop_sfo_2) +#' summary(f_saem_dfop_sfo_2, data = TRUE) #' } #' #' @export @@ -82,18 +87,19 @@ summary.saem.mmkin <- function(object, data = FALSE, verbose = FALSE, distimes = mod_vars <- names(object$mkinmod$diffs) pnames <- names(object$mean_dp_start) - np <- length(pnames) + names_fixed_effects <- object$so@results@name.fixed + n_fixed <- length(names_fixed_effects) conf.int <- object$so@results@conf.int rownames(conf.int) <- conf.int$name - confint_trans <- as.matrix(conf.int[pnames, c("estimate", "lower", "upper")]) + confint_trans <- as.matrix(parms(object, ci = TRUE)) colnames(confint_trans)[1] <- "est." # In case objects were produced by earlier versions of saem if (is.null(object$transformations)) object$transformations <- "mkin" if (object$transformations == "mkin") { - bp <- backtransform_odeparms(confint_trans[, "est."], object$mkinmod, + bp <- backtransform_odeparms(confint_trans[pnames, "est."], object$mkinmod, object$transform_rates, object$transform_fractions) bpnames <- names(bp) @@ -126,20 +132,20 @@ summary.saem.mmkin <- function(object, data = FALSE, verbose = FALSE, distimes = } } } else { - confint_back <- confint_trans + confint_back <- confint_trans[names_fixed_effects, ] } # Correlation of fixed effects (inspired by summary.nlme) - varFix <- vcov(object$so)[1:np, 1:np] + varFix <- vcov(object$so)[1:n_fixed, 1:n_fixed] stdFix <- sqrt(diag(varFix)) object$corFixed <- array( t(varFix/stdFix)/stdFix, dim(varFix), - list(pnames, pnames)) + list(names_fixed_effects, names_fixed_effects)) # Random effects - rnames <- paste0("SD.", pnames) - confint_ranef <- as.matrix(conf.int[rnames, c("estimate", "lower", "upper")]) + sdnames <- intersect(rownames(conf.int), paste0("SD.", pnames)) + confint_ranef <- as.matrix(conf.int[sdnames, c("estimate", "lower", "upper")]) colnames(confint_ranef)[1] <- "est." # Error model @@ -147,7 +153,6 @@ summary.saem.mmkin <- function(object, data = FALSE, verbose = FALSE, distimes = confint_errmod <- as.matrix(conf.int[enames, c("estimate", "lower", "upper")]) colnames(confint_errmod)[1] <- "est." - object$confint_trans <- confint_trans object$confint_ranef <- confint_ranef object$confint_errmod <- confint_errmod @@ -202,7 +207,7 @@ print.summary.saem.mmkin <- function(x, digits = max(3, getOption("digits") - 3) cat("\nModel predictions using solution type", x$solution_type, "\n") cat("\nFitted in", x$time[["elapsed"]], "s\n") - cat("Using", paste(x$so@options$nbiter.saemix, collapse = ", "), + cat("Using", paste(x$so@options$nbiter.saemix, collapse = ", "), "iterations and", x$so@options$nb.chains, "chains\n") cat("\nVariance model: ") |