### structure.R ---
##----------------------------------------------------------------------
## Author: Brice Ozenne
## Created: May 31 2021 (15:28)
## Version:
## Last-Updated: maj 16 2024 (09:41)
## By: Brice Ozenne
## Update #: 1170
##----------------------------------------------------------------------
##
### Commentary:
##
### Change Log:
##----------------------------------------------------------------------
##
### Code:
## * ID (identity)
##' @title identity Structure
##' @description Variance-covariance structure where the residuals are independent and identically distributed.
##' Can be stratified on a categorical variable.
##'
##' @param formula formula indicating on which variable to stratify the residual variance (left hand side).
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param add.time not used.
##'
##' @details A typical formula would be \code{~1}.
##'
##' @return An object of class \code{IND} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##' ID(NULL, var.cluster = "id", var.time = "time")
##' ID(~1, var.cluster = "id", var.time = "time")
##' ID(~gender, var.cluster = "id", var.time = "time")
##' ID(gender~1, var.cluster = "id", var.time = "time")
##' @export
ID <- function(formula, var.cluster, var.time, add.time){
## ** normalize input
outCov <- .formulaStructure(formula, add.X = NULL, strata.X = TRUE, correlation = FALSE)
if(!missing(var.cluster) && inherits(var.cluster,"formula")){ ## possible user mistake ID(~1,~1) instead of ID(list(~1,~1))
stop("Argument \'var.cluster\' should not be a formula. \n",
"Consider using a list to collect the formula for the variance and correlation structure. \n")
}
## ** create structure
out <- list(call = match.call(),
name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
strata = if(length(outCov$strata)>0){outCov$strata}else{NA},
time = if(!missing(var.time)){var.time}else{NA},
var = NA,
cor = NA,
stringsAsFactors = FALSE),
formula = list(var = outCov$formula.var,
cor = NULL),
class = "ID")
## ** export
class(out) <- append("structure",class(out))
class(out) <- append("ID",class(out))
return(out)
}
## * IND (independence)
##' @title Independence Structure
##' @description Variance-covariance structure where the residuals are independent but may have different variance.
##' Can be stratified on a categorical variable.
##'
##' @param formula formula indicating variables influencing the residual variance,
##' using either as a multiplicative factor (right hand side) or stratification (left hand side) to model their effect.
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param add.time Should the default formula (i.e. when \code{NULL}) contain a time effect.
##'
##' @details A typical formula would be either \code{~1} indicating constant variance
##' or \code{~time} indicating a time dependent variance.
##'
##' @return An object of class \code{IND} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##' IND(NULL, var.cluster = "id", var.time = "time", add.time = TRUE)
##' IND(~1, var.cluster = "id", var.time = "time")
##' IND(gender~1, var.cluster = "id", var.time = "time")
##' IND(gender~time, var.cluster = "id", var.time = "time")
##' IND(~gender+time, var.cluster = "id", var.time = "time")
##'
##' @export
IND <- function(formula, var.cluster, var.time, add.time){
## ** normalize input
if(!missing(add.time)){
if(is.character(add.time)){
add.X <- list(variance = add.time,
correlation = NULL)
}else if(add.time){
add.X <- list(variance = var.time,
correlation = NULL)
}else if(!add.time){
add.X <- NULL
}else{
stop("Incorrect argument \'add.time\': should be logical or character. \n")
}
}else{
add.X <- NULL
}
outCov <- .formulaStructure(formula, add.X = add.X, strata.X = FALSE, correlation = FALSE)
if(!missing(var.cluster) && inherits(var.cluster,"formula")){ ## possible user mistake IND(~1,~1) instead of IND(list(~1,~1))
stop("Argument \'var.cluster\' should not be a formula. \n",
"Consider using a list to collect the formula for the variance and correlation structure. \n")
}
## ** create structure
out <- list(call = match.call(),
name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
strata = if(length(outCov$strata)>0){outCov$strata}else{NA},
time = if(!missing(var.time)){var.time}else{NA},
var = if(length(outCov$X.var)>0){I(list(outCov$X.var))}else{NA},
cor = NA,
stringsAsFactors = FALSE),
formula = list(var = outCov$formula.var,
cor = NULL),
class = "IND")
## ** export
class(out) <- append("structure",class(out))
class(out) <- append("IND",class(out))
return(out)
}
## * CS (compound symmetry)
##' @title Compound Symmetry Structure
##' @description Variance-covariance structure where the variance and correlation of the residuals is constant within covariate levels.
##' Can be stratified on a categorical variable.
##' The default has no covariate and therefore the variance and correlation are constant within cluster.
##'
##' @param formula formula indicating on which variable to stratify the residual variance and correlation (left hand side)
##' and variables influencing the residual variance and correlation (right hand side).
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param type [character] \itemize{
##' \item \code{"ho"}, \code{"homo"}, or \code{"homogeneous"}: constant variance and covariate-specific correlation.
##' Analogous to crossed or nested random effects.
##' \item \code{"he"}, \code{"hetero"}, or \code{"heterogeneous"}: variance and correlation specific to the level of covariates.
##' Can be seen as more flexible crossed or nested random effects model.
##' }
##' @param group.type [integer vector] grouping of the regressor for the correlation structure.
##' A constant value corresponds to nested random effects (default) and a regressor-specific value to crossed random effects
##' @param add.time not used.
##'
##' @details A typical formula would be \code{~1}, indicating a variance constant over time and the same correlation between all pairs of times.
##'
##' @return An object of class \code{CS} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##' ## no covariates
##' CS(~1, var.cluster = "id", var.time = "time")
##' CS(gender~1, var.cluster = "id", var.time = "time")
##'
##' ## covariates
##' CS(~time, var.cluster = "id", var.time = "time")
##' CS(gender~time, var.cluster = "id", var.time = "time")
##' CS(list(~time,~1), var.cluster = "id", var.time = "time")
##' CS(list(gender~time,gender~1), var.cluster = "id", var.time = "time")
##'
##' @export
CS <- function(formula, var.cluster, var.time, type = NULL, group.type = NULL, add.time){
## ** normalize input
type.ho <- c("ho","homo","homogeneous")
type.he <- c("he","hetero","heterogeneous")
if(!is.null(type)){
type <- match.arg(type, c(type.ho,type.he))
if(type %in% type.ho){
type <- type.ho[3]
}else if(type %in% type.he){
type <- type.he[3]
}
}else if(inherits(formula,"formula") || (is.list(formula) && length(formula)==1)){
type <- type.ho[3]
}else if((is.list(formula) && length(formula)==2)){
type <- type.he[3]
}
if(!missing(formula) && inherits(formula,"formula")){
if(type == "homogeneous"){
if(attr(stats::terms(formula),"response")==0){
formula <- list(variance = ~1,
correlation = formula)
}else{
formula <- list(variance = stats::update(formula,".~0"),
correlation = formula)
}
}else if(type == "heterogeneous" && !missing(var.time)){
var.f <- all.vars(formula)
if(length(var.f)>0 && any(var.f %in% c(var.time,attr(var.time,"original")))){
formula <- list(variance = formula,
correlation = formula)
}else{
add.var <- c(attr(var.time,"original"),var.time)[1]
if(attr(stats::terms(formula),"response")==0){
formula <- list(variance = stats::update(formula,paste0("~.+",add.var)),
correlation = formula)
}else{
formula <- list(variance = stats::update(formula,paste0(".~.+",add.var)),
correlation = formula)
}
}
}
}
if(!missing(formula)){
outCov <- .formulaStructure(formula, add.X = NULL, strata.X = FALSE, correlation = TRUE)
if(length(outCov$X.cor)==0){
type <- "homogeneous"
}
}else{
outCov <- NULL
}
if(is.null(group.type) || length(group.type) == 0){
if(length(outCov$X.cor)==0){
group.type <- NULL
}else{
group.type <- stats::setNames(rep(1,length(outCov$X.cor)), outCov$X.cor)
}
}else{
if(length(group.type) != length(outCov$X.cor)){
stop("Argument \'group.type\' should have length ",length(outCov$X.cor),", i.e. one value for each variable. \n")
}
if(any(duplicated(names(group.type)))){
stop("Argument \'group.type\' should no have duplicated names. \n")
}
if(!is.null(names(group.type))){
if(any(names(group.type) %in% outCov$X.cor == FALSE)){
stop("Argument \'group.type\' should contain regressors for the correlation structure. \n",
"Valid values: \"",paste(outCov$X.cor, collapse = "\", \""),"\". \n")
}
}else{
names(group.type) <- outCov$X.cor
}
}
if(!missing(var.cluster) && inherits(var.cluster,"formula")){ ## possible user mistake CS(~1,~1) instead of CS(list(~1,~1))
stop("Argument \'var.cluster\' should not be a formula. \n",
"Consider using a list to collect the formula for the variance and correlation structure. \n")
}
## ** create structure
out <- list(call = match.call(),
name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
strata = if(length(outCov$strata)>0){outCov$strata}else{NA},
time = if(!missing(var.time)){var.time}else{NA},
var = if(length(outCov$X.var)>0){I(list(outCov$X.var))}else{NA},
cor = if(length(outCov$X.cor)>0){I(list(outCov$X.cor))}else{NA},
stringsAsFactors = FALSE),
formula = list(var = outCov$formula.var,
cor = outCov$formula.cor),
type = type,
group.type = group.type,
class = "CS")
## export
class(out) <- append("structure",class(out))
class(out) <- append("CS",class(out))
return(out)
}
## * RE (random effect)
##' @title Random Effect Structure
##' @description Variance-covariance structure parametrized via random effects.
##' Can be stratified on a categorical variable.
##'
##' @param formula formula indicating on which variable to stratify the residual variance and correlation (left hand side)
##' and variables influencing the residual variance and correlation (right hand side).##'
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param ranef [list] characteristics of the random effects
##' @param add.time not used.
##'
##' @details A typical formula would be \code{~1}, indicating a variance constant over time and the same correlation between all pairs of times.
##'
##' @return An object of class \code{CS} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##' RE(~1, var.cluster = "id", var.time = "time")
##' RE(~gender, var.cluster = "id", var.time = "time")
##' RE(gender~(1|id), var.time = "time")
##'
##' @export
RE <- function(formula, var.cluster, var.time, ranef = NULL, add.time){
## ** normalize input
## ranef
test.ranef <- is.null(ranef)
## var.cluster
if(!missing(var.cluster) && inherits(var.cluster,"formula")){ ## possible user mistake RE(~1,~1) instead of RE(list(~1,~1))
stop("Argument \'var.cluster\' should not be a formula. \n",
"Consider using a list to collect the formula for the variance and correlation structure. \n")
}
## formula
if(!inherits(formula,"formula")){
stop("Argument \'formula\' must inherits from formula. \n")
}
detail.formula <- formula2var(formula)
## convert ~1|id to ~(1|id)
if(detail.formula$special=="repetition"){
if(length(detail.formula$vars$repetition)>2 || length(detail.formula$terms$all)!=1){
stop("Something when wrong when identifying the random effects. \n",
"Consider wrapping the random effect term in the formula by parentheses, e.g. ~ (1|id). \n")
}
newformula <- stats::as.formula(paste0(paste(detail.formula$vars$response, collapse = "+"),"~(",detail.formula$terms$repetition,")"))
detail.formula <- formula2var(newformula)
}
if(detail.formula$special=="none"){
if(length(detail.formula$vars$regressor)>0){
if(length(detail.formula$vars$response)>0){
stop("The strata variable in argument \'formula\' should be specified on the left or right hand side (not both). \n")
}
var.strata <- detail.formula$vars$regressor
}else{
var.strata <- detail.formula$vars$response
}
}else if(detail.formula$special=="ranef"){
var.strata <- detail.formula$vars$response
}else{
stop("Incorrect argument \'formula\' for structure RE. \n",
"Should be something like ~strata or strata ~ (1|id).")
}
## ** prepare variance structure
if(length(var.strata)>0){
ff.var <- stats::as.formula(paste0(var.strata,"~1"))
}else{
ff.var <- ~1
}
## ** prepare correlation / random effect structure
if(test.ranef && detail.formula$special=="ranef"){
ranef <- detail.formula$ranef
if(missing(var.cluster) && ranef$cross == FALSE){
var.cluster <- ranef$cluster
attr(var.cluster,"original") <- ranef$cluster
}
}
if(is.null(ranef) || (ranef$cross == FALSE && ranef$nested == FALSE)){ ## no random effect or random intercept
ff.cor <- ff.var
group <- NULL
}else if(ranef$cross == FALSE && ranef$nested == TRUE){
Ovar.cluster <- attr(var.cluster,"original")
if(!is.null(Ovar.cluster) && Ovar.cluster %in% ranef$vars){
## remove cluster level from formula
ff.cor <- stats::as.formula(paste0(var.strata,"~",paste0(setdiff(ranef$vars,Ovar.cluster), collapse = "+")))
}else{
stop("Something went wrong when identifying the nested random effects. \n",
"Cluster variable ",attr(var.cluster,"original")," not found among the variables defining the random effects \"",paste(ranef$vars, collapse = "\" \""),"\".\n")
}
}else if(ranef$cross == TRUE && ranef$nested == FALSE){
## crossed random effect: typically each random effect has been specified (e.g. (1|id) and (1|scan))
## but not the overall level (e.g. hospital where specific id and scans are performed)
ff.cor <- stats::as.formula(paste0(var.strata,"~",paste0(ranef$vars, collapse = "+")))
}else if(ranef$cross == TRUE && ranef$nested == TRUE){
stop("Random effect structre with both nested and cross random effect not (yet!) possible. \n")
}
## groups of random effects
n.group <- length(ranef$hierarchy)
group <- unlist(lapply(1:n.group, function(iG){
stats::setNames(rep(iG, length(ranef$hierarchy[[iG]])), ranef$hierarchy[[iG]])
}))
if(!missing(var.cluster) && !is.null(attr(var.cluster,"original")) && attr(var.cluster,"original") %in% names(group)){
groupCS <- group[names(group) != attr(var.cluster,"original")]
}else{
groupCS <- group
}
## ** create structure
out <- CS(list(variance = ff.var, correlation = ff.cor),
var.cluster = var.cluster, var.time = var.time,
type = "homogeneous",
group.type = groupCS) ## modify group.type to remove cluster variable (otherwise error as it does not match the design matrix)
out$group.type <- group ## use the proper group variable later
out$ranef <- ranef
## ** export
out$call <- match.call()
out$class <- "RE"
class(out) <- append("RE",class(out))
return(out)
}
## * TOEPLITZ (Toeplitz)
##' @title Toeplitz Structure
##' @description Variance-covariance structure where the correlation depends on time elapsed between two repetitions.
##' Can be stratified on a categorical variable.
##'
##' @param formula formula indicating on which variable to stratify the residual variance and correlation (left hand side)
##' and variables influencing the residual variance and correlation (right hand side).
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param type [character] degree of flexibility of the correlation structure within covariate (\code{"UN","LAG","CS"}).
##' Will also affect the variance structure when not explicit.
##' @param add.time Should the default formula (i.e. when \code{NULL}) contain a time effect.
##'
##' @details \bold{formula}: there can only be at most one covariate for the correlation structure.
##' A typical formula would be \code{~1}, indicating a variance constant over time and a correlation specific to each gap time.
##'
##' \bold{type}: for a binary covariate the correlation matrix can be decomposed into four blocs: A, B, B, C.
##' A correspond the correlation within level 0 of the covariate, C within level 1, and B between level 0 and 1.
##' Different correlation structures can be specified:\itemize{
##' \item \code{"UN"}: unstructured matrix except for the diagonal elements of C which are constrained to be equal.
##' \item \code{"LAG"}: Toeplitz structure within A, B, and C, i.e. correlation specific to each time lag and covariate level.
##' \item \code{"CS"}: block-specific value except for C which has a different value for its diagonal elements.
##'}
##' @return An object of class \code{TOEPLITZ} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##' ## no covariate
##' TOEPLITZ(~time, var.cluster = "id", var.time = "time")
##' TOEPLITZ(gender~time, var.cluster = "id", var.time = "time")
##' TOEPLITZ(list(~time,~time), var.cluster = "id", var.time = "time")
##' TOEPLITZ(list(gender~time,gender~time), var.cluster = "id", var.time = "time")
##'
##' ## with covariates
##' TOEPLITZ(~side, var.cluster = "id", type = "UN",
##' var.time = "time", add.time = TRUE)
##' TOEPLITZ(~side, var.cluster = "id", type = "LAG",
##' var.time = "time", add.time = TRUE)
##' TOEPLITZ(~side, var.cluster = "id", type = "CS",
##' var.time = "time", add.time = TRUE)
##' TOEPLITZ(gender~side, var.cluster = "id", type = "CS",
##' var.time = "time", add.time = TRUE)
##' @export
TOEPLITZ <- function(formula, var.cluster, var.time, type = "LAG", add.time){
## ** normalize input
if(is.null(type)){
type <- "LAG"
toeplitz.block <- FALSE
}else{
type <- match.arg(type, c("UN","LAG","CS"))
toeplitz.block <- NULL
}
if(!missing(add.time)){
if(is.character(add.time)){
if(type == "UN" || type == "LAG"){
add.X <- list(variance = add.time,
correlation = add.time)
}else if(type == "CS"){
if(length(add.time)>1){
add.X <- list(variance = utils::head(add.time,1),
correlation = add.time)
}else{
add.X <- list(variance = NULL,
correlation = add.time)
}
}
}else if(add.time){
if(type == "UN" || type == "LAG"){
add.X <- list(variance = var.time,
correlation = var.time)
}else if(type == "CS"){
if(length(var.time)>1){
add.X <- list(variance = utils::head(var.time,1),
correlation = var.time)
}else{
add.X <- list(variance = NULL,
correlation = var.time)
}
}
}else if(!add.time){
add.X <- NULL
}else{
stop("Incorrect argument \'add.time\': should be logical or character. \n")
}
}else{
add.X <- NULL
}
if(!missing(formula)){
outCov <- .formulaStructure(formula, add.X = add.X, strata.X = FALSE, correlation = TRUE)
## NOTE: if length(outCov$X.cor)==0 i.e. not time has (yet) be defined, no error is output since time will be automatically added later on
if(length(outCov$X.cor)>2){
stop("TOEPLITZ covariance structure does not support more than 2 covariates for the correlation structure. \n")
}else if(is.null(toeplitz.block)){
toeplitz.block <- length(outCov$X.cor) > 1
}
}else{
outCov <- NULL
}
if(!missing(var.cluster) && inherits(var.cluster,"formula")){ ## possible user mistake TOEPLITZ(~1,~1) instead of TOEPLITZ(list(~1,~1))
stop("Argument \'var.cluster\' should not be a formula. \n",
"Consider using a list to collect the formula for the variance and correlation structure. \n")
}
## ** create structure
out <- list(call = match.call(),
name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
strata = if(!is.null(outCov$strata)){outCov$strata}else{NA},
time = if(!missing(var.time)){var.time}else{NA},
var = if(length(outCov$X.var)>0){I(list(outCov$X.var))}else{NA},
cor = if(length(outCov$X.cor)>0){I(list(outCov$X.cor))}else{NA},
stringsAsFactors = FALSE),
formula = list(var = outCov$formula.var,
cor = outCov$formula.cor),
type = type,
block = toeplitz.block,
class = "TOEPLITZ")
## ** export
class(out) <- append("structure",class(out))
class(out) <- append("TOEPLITZ",class(out))
return(out)
}
## * UN (unstructured)
##' @title Unstructured Structure
##' @description Variance-covariance structure where the residuals have time-specific variance and correlation.
##' Can be stratified on a categorical variable.
##'
##' @param formula formula indicating on which variable to stratify the covariance structure.
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param add.time Should the default formula (i.e. when \code{NULL}) contain a time effect.
##'
##' @details A typical formula would be \code{~1}, indicating a time-specific variance parameter and a correlation parameter specific to each pair of times.
##'
##' @return An object of class \code{UN} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##' UN(NULL, var.cluster = "id", var.time = "time", add.time = TRUE)
##' UN(~gender, var.cluster = "id", var.time = "time", add.time = TRUE)
##' UN(gender ~ 1, var.cluster = "id", var.time = "time", add.time = TRUE)
##' UN(list(~gender,~1), var.cluster = "id", var.time = "time", add.time = TRUE)
##' UN(list(gender~age,gender~1), var.cluster = "id", var.time = "time", add.time = TRUE)
##'
##' @export
UN <- function(formula, var.cluster, var.time, add.time){
## ** normalize input
if(!missing(add.time)){
if(is.character(add.time)){
add.X <- list(variance = add.time,
correlation = add.time)
}else if(add.time){
add.X <- list(variance = var.time,
correlation = var.time)
}else if(!add.time){
add.X <- NULL
}else{
stop("Incorrect argument \'add.time\': should be logical or character. \n")
}
}else{
add.X <- NULL
}
outCov <- .formulaStructure(formula, add.X = add.X, strata.X = inherits(formula,"formula"), correlation = 2) ## 2 for fully stratified structure
if(!missing(var.cluster) && inherits(var.cluster,"formula")){ ## possible user mistake UN(~1,~1) instead of UN(list(~1,~1))
stop("Argument \'var.cluster\' should not be a formula. \n",
"Consider using a list to collect the formula for the variance and correlation structure. \n")
}
## ** create structure
out <- list(call = match.call(),
name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
strata = if(length(outCov$strata)>0){outCov$strata}else{NA},
time = if(!missing(var.time)){var.time}else{NA},
var = I(list(outCov$X.var)),
cor = I(list(outCov$X.cor)),
stringsAsFactors = FALSE),
formula = list(var = outCov$formula.var,
cor = outCov$formula.cor),
class = "UN")
## ** export
class(out) <- append("structure",class(out))
class(out) <- append("UN",class(out))
return(out)
}
## * LV (latent variable)
## ## * EXP (exponential)
## ##' @title Exponential Structure
## ##' @description Variance-covariance structure where the residuals have a correlation decreasing exponentially,
## ##' Can be stratified on a categorical variable.
## ##'
## ##' @param formula formula indicating on which variable to stratify the residual variance and correlation (left hand side)
## ##' and variables influencing the residual variance and correlation (right hand side).
## ##' @param var.cluster [character] cluster variable.
## ##' @param var.time [character] time variable.
## ##' @param nugget [logical] whether a nugget effect is present.
## ##' @param add.time not used.
## ##'
## ##' @details A typical formula would be \code{~1}, indicating a variance constant over time and correlation with exponential decrease over time.
## ##'
## ##' Inspired from \code{nlme::corExp} where if \eqn{K} denotes the nugget effect and \eqn{\rho} the time effect,
## ##' the correlation between two observations with a time gap \eqn{dt} is \eqn{exp(-\rho dt)} when no nugget effect is present and \eqn{(1-K) exp(-\rho dt)} when a nugget effect is assumed.
## ##'
## ##' @return An object of class \code{EXP} that can be passed to the argument \code{structure} of the \code{lmm} function.
## ##'
## ##' @keywords multivariate
## ##'
## ##' @examples
## ##' EXP(var.cluster = "id", var.time = "time", add.time = TRUE)
## ##' EXP(~space, var.cluster = "id", var.time = "time", add.time = TRUE)
## ##' EXP(list(~space,~space), var.cluster = "id", var.time = "time", add.time = TRUE)
## ##'
## ##' @export
## EXP <- function(formula, var.cluster, var.time, nugget = FALSE, add.time){
## if(missing(formula) || is.null(formula)){
## outCov <- .formulaStructure(list(~1,stats::as.formula(paste0("~",var.time))), heterogeneous = nugget)
## }else if(is.list(formula)){
## outCov <- .formulaStructure(formula, heterogeneous = nugget)
## }else if(!missing(add.time) && (is.character(add.time) || identical(add.time,TRUE)) && length(all.vars(stats::update(formula,0~.)))==0){
## if(is.character(add.time)){
## var.time <- add.time
## }
## if(attr(stats::terms(formula),"response")==1){ # with strata
## ff <- stats::as.formula(paste0(all.vars(formula),"~",var.time))
## }else{
## ff <- stats::as.formula(paste0("~",var.time))
## }
## outCov <- .formulaStructure(list(formula,ff), heterogeneous = nugget)
## }else{
## if(attr(stats::terms(formula),"response")==1){ # with strata
## outCov <- .formulaStructure(list(stats::as.formula(paste0(all.vars(formula),"~1")),formula), heterogeneous = nugget)
## }else{
## outCov <- .formulaStructure(list(~1,formula), heterogeneous = nugget)
## }
## }
## out <- list(call = match.call(),
## name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
## strata = if(!is.null(outCov$strata)){outCov$strata}else{NA},
## time = if(!missing(var.time)){var.time}else{NA},
## var = if(length(outCov$X.var)>0){I(list(outCov$X.var))}else{NA},
## cor = if(length(outCov$X.cor)>0){I(list(outCov$X.cor))}else{NA},
## stringsAsFactors = FALSE),
## formula = list(var = outCov$formula.var,
## cor = outCov$formula.cor),
## heterogeneous = nugget,
## class = "EXP")
## ## export
## class(out) <- append("structure",class(out))
## class(out) <- append("EXP",class(out))
## return(out)
## }
## * CUSTOM (user-specified)
##' @title Custom Structure
##' @description Variance-covariance structure specified by the user.
##'
##' @param formula formula indicating variables influencing the residual variance and correlation (right hand side).
##' @param var.cluster [character] cluster variable.
##' @param var.time [character] time variable.
##' @param FCT.sigma [function] take as argument the model parameters, time, and design matrix.
##' Output the vector of residuals standard deviations.
##' @param dFCT.sigma [list of vectors] list whose elements are the first derivative of argument \code{FCT.sigma}.
##' @param d2FCT.sigma [list of vectors] list whose elements are the second derivative of argument \code{FCT.sigma} (no cross-terms).
##' @param init.sigma [numeric vector] initial value for the variance parameters.
##' @param FCT.rho [function] take as argument the model parameters, time, and design matrix.
##' Output the matrix of residuals correlation.
##' @param dFCT.rho [list of matrices] list whose elements are the first derivative of argument \code{FCT.rho}.
##' @param d2FCT.rho [list of matrices] list whose elements are the second derivative of argument \code{FCT.rho} (no cross-terms).
##' @param init.rho [numeric vector] initial value for the correlation parameters.
##' @param add.time not used.
##'
##' @return An object of class \code{CUSTOM} that can be passed to the argument \code{structure} of the \code{lmm} function.
##'
##' @keywords multivariate
##'
##' @examples
##'
##' ## Compound symmetry structure
##' CUSTOM(~1,
##' FCT.sigma = function(p,n.time,X){rep(p,n.time)},
##' init.sigma = c("sigma"=1),
##' dFCT.sigma = function(p,n.time,X){list(sigma = rep(1,n.time))},
##' d2FCT.sigma = function(p,n.time,X){list(sigma = rep(0,n.time))},
##' FCT.rho = function(p,n.time,X){
##' matrix(p,n.time,n.time)+diag(1-p,n.time,n.time)
##' },
##' init.rho = c("rho"=0.5),
##' dFCT.rho = function(p,n.time,X){
##' list(rho = matrix(1,n.time,n.time)-diag(1,n.time,n.time))
##' },
##' d2FCT.rho = function(p,n.time,X){list(rho = matrix(0,n.time,n.time))}
##' )
##'
##' ## 2 block structure
##' rho.2block <- function(p,n.time,X){
##' rho <- matrix(0, nrow = n.time, ncol = n.time)
##' rho[1,2] <- rho[2,1] <- rho[4,5] <- rho[5,4] <- p["rho1"]
##' rho[1,3] <- rho[3,1] <- rho[4,6] <- rho[6,4] <- p["rho2"]
##' rho[2,3] <- rho[3,2] <- rho[5,6] <- rho[6,5] <- p["rho3"]
##' rho[4:6,1:3] <- rho[1:3,4:6] <- p["rho4"]
##' return(rho)
##' }
##' drho.2block <- function(p,n.time,X){
##' drho <- list(rho1 = matrix(0, nrow = n.time, ncol = n.time),
##' rho2 = matrix(0, nrow = n.time, ncol = n.time),
##' rho3 = matrix(0, nrow = n.time, ncol = n.time),
##' rho4 = matrix(0, nrow = n.time, ncol = n.time))
##' drho$rho1[1,2] <- drho$rho1[2,1] <- drho$rho1[4,5] <- drho$rho1[5,4] <- 1
##' drho$rho2[1,3] <- drho$rho2[3,1] <- drho$rho2[4,6] <- drho$rho2[6,4] <- 1
##' drho$rho3[2,3] <- drho$rho3[3,2] <- drho$rho3[5,6] <- drho$rho3[6,5] <- 1
##' drho$rho4[4:6,1:3] <- drho$rho4[1:3,4:6] <- 1
##' return(drho)
##' }
##' d2rho.2block <- function(p,n.time,X){
##' d2rho <- list(rho1 = matrix(0, nrow = n.time, ncol = n.time),
##' rho2 = matrix(0, nrow = n.time, ncol = n.time),
##' rho3 = matrix(0, nrow = n.time, ncol = n.time),
##' rho4 = matrix(0, nrow = n.time, ncol = n.time))
##' return(d2rho)
##' }
##'
##' CUSTOM(~variable,
##' FCT.sigma = function(p,n.time,X){rep(p,n.time)},
##' dFCT.sigma = function(p,n.time,X){list(sigma=rep(1,n.time))},
##' d2FCT.sigma = function(p,n.time,X){list(sigma=rep(0,n.time))},
##' init.sigma = c("sigma"=1),
##' FCT.rho = rho.2block,
##' dFCT.rho = drho.2block,
##' d2FCT.rho = d2rho.2block,
##' init.rho = c("rho1"=0.25,"rho2"=0.25,"rho3"=0.25,"rho4"=0.25))
##'
##' @export
CUSTOM <- function(formula, var.cluster, var.time,
FCT.sigma, dFCT.sigma = NULL, d2FCT.sigma = NULL, init.sigma,
FCT.rho, dFCT.rho = NULL, d2FCT.rho = NULL, init.rho, add.time){
if(is.null(formula)){
outCov <- .formulaStructure(~1, add.X = NULL, strata.X = FALSE, correlation = TRUE)
}else{
outCov <- .formulaStructure(formula, add.X = NULL, strata.X = FALSE, correlation = TRUE)
}
out <- list(call = match.call(),
name = data.frame(cluster = if(!missing(var.cluster)){var.cluster}else{NA},
strata = if(!is.null(outCov$strata)){outCov$strata}else{NA},
time = if(!missing(var.time)){var.time}else{NA},
var = if(length(outCov$X.var)>0){I(list(outCov$X.var))}else{NA},
cor = if(length(outCov$X.cor)>0){I(list(outCov$X.cor))}else{NA},
stringsAsFactors = FALSE),
formula = list(var = outCov$formula.var,
cor = outCov$formula.cor),
class = "CUSTOM")
## param
## if(!is.na(out$name$strata)){
## stop("CUSTOM structure cannot (yet?) handle strata. \n")
## }
if(!missing(FCT.sigma)){
if(any(names(formals(FCT.sigma)) %in% c("p","n.time","X","...") == FALSE)){
stop("Incorrect argument in \'FCT.sigma\': can be \"p\", \"n.time\", or \"X\". \n")
}
if(!is.null(dFCT.sigma) && any(names(formals(dFCT.sigma)) %in% c("p","n.time","X","...") == FALSE)){
stop("Incorrect argument in \'dFCT.sigma\': can be \"p\", \"n.time\", or \"X\". \n")
}
if(!is.null(d2FCT.sigma) && any(names(formals(d2FCT.sigma)) %in% c("p","n.time","X","...") == FALSE)){
stop("Incorrect argument in \'d2FCT.sigma\': can be \"p\", \"n.time\", or \"X\". \n")
}
out$FCT.sigma <- FCT.sigma
out$dFCT.sigma <- dFCT.sigma
out$d2FCT.sigma <- d2FCT.sigma
out$init.sigma <- init.sigma
if(length(out$init.sigma)>0 && is.null(names(out$init.sigma))){
names(out$init.sigma) <- paste0("sigma",1:length(out$init.sigma))
}
}else{
out$FCT.sigma <- function(p,time,X){rep(p,length(time))}
out$dFCT.sigma <- function(p,time,X){list(rep(1,length(time)))}
out$d2FCT.sigma <- function(p,time,X){list(rep(0,length(time)))}
out$init.sigma <- c(sigma = NA)
}
if(!missing(FCT.rho)){
if(any(names(formals(FCT.rho)) %in% c("p","n.time","X","...") == FALSE)){
stop("Incorrect argument in \'FCT.rho\': can be \"p\", \"n.time\", or \"X\". \n")
}
if(!is.null(dFCT.rho) && any(names(formals(dFCT.rho)) %in% c("p","n.time","X","...") == FALSE)){
stop("Incorrect argument in \'dFCT.rho\': can be \"p\", \"n.time\", or \"X\". \n")
}
if(!is.null(d2FCT.rho) && any(names(formals(d2FCT.rho)) %in% c("p","n.time","X","...") == FALSE)){
stop("Incorrect argument in \'d2FCT.rho\': can be \"p\", \"n.time\", or \"X\". \n")
}
out$FCT.rho <- FCT.rho
out$dFCT.rho <- dFCT.rho
out$d2FCT.rho <- d2FCT.rho
out$init.rho <- init.rho
if(length(init.rho)>0 && is.null(names(out$init.rho))){
names(out$init.rho) <- paste0("rho",1:length(out$init.rho))
}
}else{
out$FCT.rho <- NULL
out$dFCT.rho <- NULL
out$d2FCT.rho <- NULL
out$init.rho <- NULL
out$formula$cor <- NULL
}
## export
class(out) <- append("structure",class(out))
class(out) <- append("CUSTOM",class(out))
return(out)
}
## * helper
## ** .formulaStructure
##' @title Extract Variable From Formula For VCOV Structure
##' @description Extract the variables from the variance and correlation formula to be used to initialize the variance-covariance structure.
##' @noRd
##'
##' @param formula A formula or a list of two formulas.
##' @param add.X [character vector] additional covariates to be added to the variance and correlation structure.
##' @param strata.X [logical] all covariates should be used to stratify the variance and/or correlation structure.
##' @param correlation [logical] should a correlation structure be output? Otherwise no correlation parameter is considered.
##' If greater than 1 then all interaction terms are considered when having multiple variables, e.g. time:gender instead of time + gender.
##'
##' @keywords internal
.formulaStructure <- function(formula, add.X, strata.X, correlation){
## ** normalize to formula format
if(is.null(formula)){
formula <- ~1
}
class.formula <- inherits(formula,"formula")
if(class.formula){
detail.formula0 <- formula2var(formula)
formula <- list(variance = formula,
correlation = formula)
detail.formula <- list(variance = detail.formula0,
correlation = detail.formula0)
}else if(is.list(formula) && length(formula)==2 && all(sapply(formula,inherits,"formula"))){
if(is.null(names(formula))){
names(formula) <- c("variance","correlation")
}else if(all(names(formula) %in% c("var","cor"))){
names(formula)[names(formula)=="var"] <- "variance"
names(formula)[names(formula)=="cor"] <- "correlation"
}else if(all(names(formula) %in% c("variance","correlation"))){
formula <- formula[c("variance","correlation")]
}else{
stop("Incorrect names associated to the argument \'formula\' for the residual variance-covariance structure. \n",
"Should be \"variance\" and \"correlation\" (or \"var\" and \"cor\"). \n")
}
detail.formula <- lapply(formula, formula2var)
}else{
stop("Incorrect argument \'formula\': should be a formula or a list of 2 formula (var, cor).\n")
}
if(is.character(add.X)){
add.X <- list(variance = add.X,
correlation = add.X)
}else if(is.list(add.X) && length(add.X)==2){
if(is.null(names(add.X))){
names(add.X) <- c("variance","correlation")
}else if(all(names(add.X) %in% c("var","cor"))){
names(add.X)[names(add.X)=="var"] <- "variance"
names(add.X)[names(add.X)=="cor"] <- "correlation"
}else if(all(names(add.X) %in% c("variance","correlation"))){
add.X <- add.X[c("variance","correlation")]
}else{
stop("Incorrect names associated to the argument \'add.X\' for the residual variance-covariance structure. \n",
"Should be \"variance\" and \"correlation\" (or \"var\" and \"cor\"). \n")
}
}else if(!is.null(add.X)){
stop("Incorrect argument \'add.X\': should be a character or a list with 2 elements (var, cor).\n")
}
## ** type
if(any(sapply(detail.formula, function(iDetail){iDetail$special})!="none")){
stop("Incorrect argument \'formula\': there should be no special character, e.g. no |. \n")
}
## ** left hand side
ls.var.strata <- lapply(detail.formula, function(iDetail){iDetail$vars$response})
if(!identical(ls.var.strata[[1]],ls.var.strata[[2]])){
stop("Incorrect argument \'formula\': strata variable differ between the correlation and variance structure. \n")
}
var.strata <- ls.var.strata[[1]]
if(length(var.strata)==0){
var.strata <- NULL
}else if(length(var.strata)>1){
stop("There should be at most one strata variable. \n")
}
## ** right hand side
ls.var.X <- list(variance = unique(c(detail.formula$variance$vars$regressor, add.X$variance)),
correlation = unique(c(detail.formula$correlation$vars$regressor, add.X$correlation)))
test.interaction <- sapply(formula, function(iF){
any(attr(stats::delete.response(stats::terms(iF)),"order")>1)
})
if(any(test.interaction)){
stop("Does not handle interactions in the formula. \n")
}
## right hand side variables define strata
if(length(var.strata)==0 && strata.X && length(unlist(ls.var.X))>0){
if(length(ls.var.X$variance)>0 && length(ls.var.X$correlation)>0){
if(!identical(detail.formula$variance$vars$regressor,detail.formula$correlation$vars$regressor)){
stop("The strata variable should not differ between the variance and the correlation structure. \n")
}else{
var.strata <- detail.formula$variance$vars$regressor
ls.var.X$variance <- setdiff(ls.var.X$variance,detail.formula$variance$vars$regressor)
ls.var.X$correlation <- setdiff(ls.var.X$correlation,detail.formula$variance$vars$regressor)
}
}else if(length(ls.var.X$variance)>0){
var.strata <- detail.formula$variance$vars$regressor
ls.var.X$variance <- setdiff(ls.var.X$variance,detail.formula$variance$vars$regressor)
}else if(length(ls.var.X$correlation)>0){
var.strata <- detail.formula$correlation$vars$regressor
ls.var.X$correlation <- setdiff(ls.var.X$correlation,detail.formula$correlation$vars$regressor)
}
}
## ** combine left and right hand side
## *** variance
if(length(ls.var.X$variance)==0){
X.var <- NULL
if(length(var.strata)==0){
if(attr(stats::terms(formula$variance),"intercept")){
formula.var <- ~1
}else{
formula.var <- ~0
}
}else{
formula.var <- stats::as.formula(paste("~0+",var.strata))
}
}else{
X.var <- unname(ls.var.X$variance)
if(length(var.strata)==0){
formula.var <- stats::as.formula(paste("~",paste(ls.var.X$variance,collapse=":")))
}else{
formula.var <- stats::as.formula(paste("~0+",var.strata,"+ ",paste(c(ls.var.X$variance, var.strata),collapse=":")))
}
}
## *** correlation
if(length(ls.var.X$correlation)==0){
X.cor <- NULL
if(correlation==FALSE){
formula.cor <- NULL
}else if(length(var.strata)==0){
formula.cor <- ~1
}else{
formula.cor <- stats::as.formula(paste("~0+",var.strata))
}
}else{
X.cor <- unname(ls.var.X$correlation)
if(correlation==FALSE){
formula.cor <- NULL
if(!class.formula){
warning("Variable(s) \"",paste(ls.var.X$correlation, collapse = "\", \""),"\" in the correlation structure are ignored. \n")
}
}else if(length(var.strata)==0){
if(correlation>1){
formula.cor <- stats::as.formula(paste("~0+",paste(ls.var.X$correlation,collapse=":")))
}else{
formula.cor <- stats::as.formula(paste("~0+",paste(ls.var.X$correlation,collapse="+")))
}
}else{
if(correlation>1){
formula.cor <- stats::as.formula(paste("~0 + ",paste(c(ls.var.X$correlation, var.strata), collapse=":")))
}else{
formula.cor <- stats::as.formula(paste("~0 + (",paste(ls.var.X$correlation, collapse="+"),"):",var.strata))
## NOTE: wrapping the non-strata variable in () ensures proper ordering of the column names (i.e. strata variable at the end)
##' df <- data.frame(day = c(1, 1, 2, 2, 1, 1, 2, 2),
##' gender = c("1", "1", "1", "1", "0", "0", "0", "0"),
##' session = c(1, 2, 3, 4, 1, 2, 3, 4))
##' X <- stats::model.matrix(~0 + session:gender + day:gender, df)
##' colnames(X) ## "session:gender0" "session:gender1" "gender0:day" "gender1:day"
}
}
}
## ** export
out <- list(strata = var.strata,
X.var = X.var,
X.cor = X.cor,
formula.var = formula.var,
formula.cor = formula.cor
)
return(out)
}
##----------------------------------------------------------------------
### structure.R ends here
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.