#' 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
#' number of parameters, and the tests (if requested) are always 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])
successful <- sapply(mods, function(x) !inherits(x$so, "try-error"))
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
mods <- mods[successful]
# Ensure same data, ignoring covariates
same_data <- sapply(mods[-1], function(x) {
identical(mods[[1]]$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")
}
llks <- lapply(names(mods), function(x) {
llk <- try(logLik(mods[[x]], method = method), silent = TRUE)
if (inherits(llk, "try-error")) {
warning("Could not obtain log likelihood with '", method, "' method for ", x)
llk <- NA
}
return(llk)
})
available <- !sapply(llks, is.na)
llks <- llks[available]
mods <- mods[available]
# 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")
}
}