diff options
Diffstat (limited to 'R/saem.R')
-rw-r--r-- | R/saem.R | 402 |
1 files changed, 318 insertions, 84 deletions
@@ -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) +} |