blob: 59763fdae6a18455102ca215666ee30550035832 [file] [log] [blame]
# File src/library/stats/R/mlm.R
# Part of the R package, https://www.R-project.org
#
# Copyright (C) 1998-2018 The R Core Team
# Copyright (C) 1998 B. D. Ripley
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A copy of the GNU General Public License is available at
# https://www.R-project.org/Licenses/
## mlm := multivariate lm(); ny, names: for efficiency in vcov.mlm()
summary.mlm <- function(object, ny = ncol(coef), names = TRUE, ...)
{
coef <- coef(object)
effects <- object$effects
resid <- object$residuals
fitted <- object$fitted.values
value <-
if(names) {
ynames <- colnames(coef)
if(is.null(ynames)) ynames <- {
lhs <- object$terms[[2L]]
if(mode(lhs) == "call" && lhs[[1L]] == "cbind")
as.character(lhs)[-1L]
else paste0("Y", seq_len(ny))
}
## we need to ensure that _all_ responses are named
ind <- ynames == ""
if(any(ind)) ynames[ind] <- paste0("Y", seq_len(ny))[ind]
setNames(vector("list", ny), paste("Response", ynames))
} else vector("list", ny)
cl <- oldClass(object)
class(object) <- cl[match("mlm", cl):length(cl)][-1L]
# Need to put the evaluated formula in place
object$call$formula <- formula(object)
for(i in seq(ny)) {
object$coefficients <- setNames(coef[, i], rownames(coef))
## if there is one coef, above drops names
object$residuals <- resid[, i]
object$fitted.values <- fitted[, i]
object$effects <- effects[, i]
object$call$formula[[2L]] <- object$terms[[2L]] <-
as.name(if(names) ynames[i] else paste0("Y", i))
value[[i]] <- summary(object, ...)
}
class(value) <- "listof"
value
}
### SSD(object) returns object of class "SSD":
### $SSD matrix of sums of squares & products
### $df degrees of freedom.
### estVar(object)returns the estimated covariance matrix
SSD <- function(object, ...) UseMethod("SSD")
estVar <- function(object, ...) UseMethod("estVar")
SSD.mlm <- function(object, ...){
## It's not all that hard to incorporate weights, but will
## anyone use them?
if (!is.null(object$weights))
stop("'mlm' objects with weights are not supported")
## avoid residuals(objects) -- if na.exclude was used
## that will introduce NAs
structure(list(SSD = crossprod(object$residuals),
call= object$call,
df = object$df.residual),
class="SSD")
}
estVar.SSD <- function(object, ...)
object$SSD/object$df
estVar.mlm <- function(object, ...)
estVar(SSD(object))
### Convenience functions:
### Tr: is the trace operator
### proj: the projection operator possibly generalized to matrices.
### Rg: matrix rank
### Thin.row, Thin.col: thin matrix to full (row/column) rank
Tr <- function(matrix) sum(diag(matrix))
proj.matrix <- function(X, orth=FALSE){
X <- Thin.col(X)
P <- if (ncol(X) == 0)
matrix(0,nrow(X),nrow(X))
else
## Brute force. There must be a better way...
X %*% solve(crossprod(X),t(X))
if (orth) diag(nrow=nrow(X)) - P else P
}
## qr() will miss the cases where a row has all near-zeros,
## sensibly in some ways, annoying in others...
Rank <- function(X, tol = 1e-7)
qr(zapsmall(X, digits = -log10(tol)+5),
tol=tol, LAPACK=FALSE)$rank
Thin.row <- function(X, tol = 1e-7) {
X <- zapsmall(X, digits = -log10(tol)+5)
QR <- qr(t(X), tol = tol, LAPACK = FALSE)
X[QR$pivot[seq_len(QR$rank)], , drop = FALSE]
}
Thin.col <- function(X, tol = 1e-7) {
X <- zapsmall(X, digits = -log10(tol)+5)
QR <- qr(X, tol = tol, LAPACK = FALSE)
X[,QR$pivot[seq_len(QR$rank)], drop = FALSE]
}
mauchly.test <- function(object, ...)
UseMethod("mauchly.test", object)
mauchly.test.mlm <- function(object, ...)
mauchly.test(SSD(object), ...)
mauchly.test.SSD <- function(object, Sigma=diag(nrow=p),
T = Thin.row(proj(M)-proj(X)),
M = diag(nrow=p),
X = ~0,
idata=data.frame(index=seq_len(p)),...)
{
p <- ncol(object$SSD)
Xmis <- missing(X)
Mmis <- missing(M)
if (missing(T)){
orig.X <- X
orig.M <- M
if (inherits(M, "formula")) M <- model.matrix(M, idata)
if (inherits(X, "formula")) X <- model.matrix(X, idata)
if (Rank(cbind(M,X)) != Rank(M))
stop("X does not define a subspace of M")
}
Psi <- T %*% Sigma %*% t(T)
B <- T %*% object$SSD %*% t(T)
pp <- nrow(T)
U <- solve(Psi,B)
n <- object$df
logW <- log(det(U)) - pp * log(Tr(U/pp))
## Asymptotic mumbojumbo (from TWA)....
rho <- 1 - (2*pp^2 + pp + 2)/(6*pp*n)
w2 <- (pp+2)*(pp-1)*(pp-2)*(2*pp^3+6*pp^2+3*p +
2)/(288*(n*pp*rho)^2)
z <- -n * rho * logW
f <- pp * (pp + 1)/2 - 1
Pr1 <- pchisq(z, f, lower.tail=FALSE)
Pr2 <- pchisq(z, f+4, lower.tail=FALSE)
pval <- Pr1 + w2 * (Pr2 - Pr1)
transformnote <- if (!missing(T))
c("\nContrast matrix", apply(format(T), 1L, paste, collapse=" "))
else
c(
if (!Xmis)
c("\nContrasts orthogonal to",
if (is.matrix(orig.X)) apply(format(X), 2L, paste, collapse=" ")
else deparse(formula(orig.X)),"",
if (!Mmis)
c("\nContrasts spanned by",
if (is.matrix(orig.M)) apply(format(M), 2L, paste, collapse=" ")
else deparse(formula(orig.M)),""
)
)
)
retval <- list(statistic=c(W=exp(logW)),p.value=pval,
method=c("Mauchly's test of sphericity", transformnote),
data.name=paste("SSD matrix from",
deparse(object$call), collapse=" "))
class(retval) <- "htest"
retval
}
sphericity <- function(object, Sigma=diag(nrow=p),
T = Thin.row(proj(M)-proj(X)),
M = diag(nrow=p),
X = ~0,
idata=data.frame(index=seq_len(p)))
{
p <- ncol(object$SSD)
if (missing(T)){
if (inherits(M, "formula")) M <- model.matrix(M, idata)
if (inherits(X, "formula")) X <- model.matrix(X, idata)
if (Rank(cbind(M,X)) != Rank(M))
stop("X does not define a subspace of M")
}
Psi <- T %*% Sigma %*% t(T)
B <- T %*% object$SSD %*% t(T)
pp <- nrow(T)
U <- solve(Psi,B)
sigma <- Tr(U)/pp/object$df
lambda <- Re(eigen(U, only.values = TRUE)$values)
GG.eps <- sum(lambda)^2/sum(lambda^2)/pp
n <- object$df
HF.eps <- ((n + 1) * pp * GG.eps - 2) / (pp * (n - pp * GG.eps))
return(list(GG.eps=GG.eps,HF.eps=HF.eps,sigma=sigma))
}
anova.mlm <-
function(object, ...,
test = c("Pillai", "Wilks", "Hotelling-Lawley", "Roy", "Spherical"),
Sigma = diag(nrow = p),
T = Thin.row(proj(M) - proj(X)),
M = diag(nrow = p),
X = ~0,
idata = data.frame(index = seq_len(p)), tol = 1e-7)
{
if(length(list(object, ...)) > 1){
cl <- match.call()
cl[[1L]] <- anova.mlmlist
return(eval.parent(cl))
} else {
p <- ncol(SSD(object)$SSD)
Xmis <- missing(X)
Mmis <- missing(M)
if (missing(T)){
orig.M <- M # keep for printing
orig.X <- X
if (inherits(M, "formula")) M <- model.matrix(M, idata)
if (inherits(X, "formula")) X <- model.matrix(X, idata)
if (Rank(cbind(M,X)) != Rank(M))
stop("X does not define a subspace of M")
}
title <- "Analysis of Variance Table\n"
transformnote <- if (!missing(T))
c("\nContrast matrix", apply(format(T), 1L, paste, collapse=" "))
else
c(
if (!Xmis)
c("\nContrasts orthogonal to",
if (is.matrix(orig.X))
apply(format(X), 2L, paste, collapse=" ")
else deparse(formula(orig.X)),"",
if (!Mmis)
c("\nContrasts spanned by",
if (is.matrix(orig.M))
apply(format(M), 2L, paste, collapse=" ")
else deparse(formula(orig.M)),""
)
)
)
epsnote <- NULL
ssd <- SSD(object)
rk <- object$rank
pp <- nrow(T)
if(rk > 0) {
p1 <- 1L:rk
comp <- object$effects[p1, , drop=FALSE]
asgn <- object$assign[object$qr$pivot][p1]
nmeffects <- c("(Intercept)", attr(object$terms, "term.labels"))
tlabels <- nmeffects[1 + unique(asgn)]
ix <- split(seq_len(nrow(comp)), asgn)
ss <- lapply(ix, function(i) crossprod(comp[i,,drop=FALSE]))
# This was broken. Something similar might work if we implement
# split.matrix a la split.data.frame
# ss <- lapply(split(comp,asgn), function(x) crossprod(t(x)))
df <- lengths(split(asgn, asgn))
} else {
# ss <- ssr
# df <- dfr
# tlabels <- character(0L)
}
test <- match.arg(test)
nmodels <- length(ss)
if(test == "Spherical"){
df.res <- ssd$df
sph <- sphericity(ssd, T=T, Sigma=Sigma)
epsnote <- c(paste(format(c("Greenhouse-Geisser epsilon:",
"Huynh-Feldt epsilon:")),
format(c(sph$GG.eps, sph$HF.eps), digits = 4L)),
"")
Psi <- T %*% Sigma %*% t(T)
stats <- matrix(NA, nmodels+1, 6L)
colnames(stats) <- c("F", "num Df", "den Df",
"Pr(>F)", "G-G Pr", "H-F Pr")
for(i in seq_len(nmodels)) {
s2 <- Tr(solve(Psi,T %*% ss[[i]] %*% t(T)))/pp/df[i]
Fval <- s2/sph$sigma
stats[i,1L:3L] <- abs(c(Fval, df[i]*pp, df.res*pp))
}
stats[,4] <- pf(stats[,1L], stats[,2L], stats[,3L], lower.tail=FALSE)
stats[,5] <- pf(stats[,1L],
stats[,2L]*sph$GG.eps, stats[,3L]*sph$GG.eps,
lower.tail=FALSE)
stats[,6] <- pf(stats[,1L],
stats[,2L]*min(1,sph$HF.eps),
stats[,3L]*min(1,sph$HF.eps),
lower.tail=FALSE)
} else {
## Try to distinguish bad scaling and near-perfect fit
## Notice that we must transform by T before scaling
sc <- sqrt(diag(T %*% ssd$SSD %*% t(T)))
D <- sqrt(sc^2 + rowSums(as.matrix(sapply(ss, function(X)
diag(T %*% X %*% t(T))))))
sc <- ifelse(sc/D < 1e-6, 1, 1/sc)
scm <- tcrossprod(sc)
df.res <- ssd$df
rss.qr <- qr((T %*% ssd$SSD %*% t(T)) * scm, tol=tol)
if(rss.qr$rank < pp)
stop(gettextf("residuals have rank %s < %s", rss.qr$rank, pp),
domain = NA)
eigs <- array(NA, c(nmodels, pp))
stats <- matrix(NA, nmodels+1L, 5L,
dimnames = list(NULL, c(test,
"approx F", "num Df", "den Df", "Pr(>F)")))
for(i in seq_len(nmodels)) {
eigs[i, ] <- Re(eigen(qr.coef(rss.qr,
(T %*% ss[[i]] %*% t(T)) * scm),
symmetric = FALSE, only.values = TRUE)$values)
stats[i, 1L:4L] <-
switch(test,
"Pillai" = Pillai(eigs[i, ], df[i], df.res),
"Wilks" = Wilks (eigs[i, ], df[i], df.res),
"Hotelling-Lawley" = HL (eigs[i, ], df[i], df.res),
"Roy" = Roy (eigs[i, ], df[i], df.res))
ok <- stats[, 2L] >= 0 & stats[, 3L] > 0 & stats[, 4L] > 0
ok <- !is.na(ok) & ok
stats[ok, 5L] <- pf(stats[ok, 2L], stats[ok, 3L], stats[ok, 4L],
lower.tail = FALSE)
}
}
table <- data.frame(Df=c(df,ssd$df), stats, check.names=FALSE)
row.names(table) <- c(tlabels, "Residuals")
# if(attr(object$terms,"intercept")) table <- table[-1, ]
structure(table, heading = c(title, transformnote, epsnote),
class = c("anova", "data.frame"))
# f <- ms/(ssr/dfr)
# P <- pf(f, df, dfr, lower.tail = FALSE)
# table <- data.frame(df, ss, ms, f, P)
# table[length(P), 4:5] <- NA
# dimnames(table) <- list(c(tlabels, "Residuals"),
# c("Df","Sum Sq", "Mean Sq", "F value", "Pr(>F)"))
# if(attr(object$terms,"intercept")) table <- table[-1, ]
# structure(table, heading = c("Analysis of Variance Table\n",
# paste("Response:", deparse(formula(object)[[2L]]))),
# class= c("anova", "data.frame"))# was "tabular"
}
}
Pillai <- function(eig, q, df.res)
{
test <- sum(eig/(1 + eig))
p <- length(eig)
s <- min(p, q)
n <- 0.5 * (df.res - p - 1)
m <- 0.5 * (abs(p - q) - 1)
tmp1 <- 2 * m + s + 1
tmp2 <- 2 * n + s + 1
c(test, (tmp2/tmp1 * test)/(s - test), s*tmp1, s*tmp2)
}
Wilks <- function(eig, q, df.res)
{
test <- prod(1/(1 + eig))
p <- length(eig)
tmp1 <- df.res - 0.5 * (p - q + 1)
tmp2 <- (p * q - 2)/4
tmp3 <- p^2 + q^2 - 5
tmp3 <- if(tmp3 > 0) sqrt(((p*q)^2 - 4)/tmp3) else 1
c(test, ((test^(-1/tmp3) - 1) * (tmp1 * tmp3 - 2 * tmp2))/p/q,
p * q, tmp1 * tmp3 - 2 * tmp2)
}
HL <- function(eig, q, df.res)
{
test <- sum(eig)
p <- length(eig)
m <- 0.5 * (abs(p - q) - 1)
n <- 0.5 * (df.res - p - 1)
s <- min(p, q)
tmp1 <- 2 * m + s + 1
tmp2 <- 2 * (s * n + 1)
c(test, (tmp2 * test)/s/s/tmp1, s * tmp1, tmp2)
}
Roy <- function(eig, q, df.res)
{
p <- length(eig)
test <- max(eig)
tmp1 <- max(p, q)
tmp2 <- df.res - tmp1 + q
c(test, (tmp2 * test)/tmp1, tmp1, tmp2)
}
anova.mlmlist <- function (object, ...,
test=c("Pillai", "Wilks",
"Hotelling-Lawley", "Roy","Spherical"),
Sigma=diag(nrow=p),
T = Thin.row(proj(M)-proj(X)),
M = diag(nrow=p),
X = ~0,
idata=data.frame(index=seq_len(p)), tol = 1e-7)
{
objects <- list(object, ...)
p <- ncol(SSD(object)$SSD)
Xmis <- missing(X)
Mmis <- missing(M)
if (missing(T)){
orig.M <- M # keep for printing
orig.X <- X
if (inherits(M, "formula")) M <- model.matrix(M, idata)
if (inherits(X, "formula")) X <- model.matrix(X, idata)
if (Rank(cbind(M,X)) != Rank(M))
stop("X does not define a subspace of M")
}
pp <- nrow(T)
responses <- as.character(lapply(objects,
function(x) deparse(x$terms[[2L]])))
sameresp <- responses == responses[1L]
if (!all(sameresp)) {
objects <- objects[sameresp]
warning(gettextf("models with response %s removed because response differs from model 1",
sQuote(deparse(responses[!sameresp]))),
domain = NA)
}
ns <- sapply(objects, function(x) length(x$residuals))
if(any(ns != ns[1L]))
stop("models were not all fitted to the same size of dataset")
## calculate the number of models
nmodels <- length(objects)
if (nmodels == 1)
return(anova.mlm(object))
## extract statistics
resdf <- as.numeric(lapply(objects, df.residual))
df <- c(NA,diff(resdf))
resssd <- lapply(objects, SSD)
deltassd <- mapply(function(x,y) y$SSD - x$SSD,
resssd[-nmodels], resssd[-1L], SIMPLIFY=FALSE)
resdet <- sapply(resssd,
function(x) det(T %*% (x$SSD/x$df) %*% t(T))^(1/pp))
## construct table and title
table <- data.frame(resdf, df, resdet)
variables <- lapply(objects, function(x)
paste(deparse(formula(x)), collapse = "\n") )
dimnames(table) <- list(seq_len(nmodels),
c("Res.Df", "Df", "Gen.var."))
title <- "Analysis of Variance Table\n"
topnote <- paste0("Model ", format(seq_len(nmodels)),": ", variables,
collapse = "\n")
transformnote <- if (!missing(T))
c("\nContrast matrix", apply(format(T), 1L, paste, collapse = " "))
else
c(
if (!Xmis)
c("\nContrasts orthogonal to",
if (is.matrix(orig.X)) apply(format(X), 2L, paste, collapse = " ")
else deparse(formula(orig.X)),"",
if (!Mmis)
c("\nContrasts spanned by",
if (is.matrix(orig.M)) apply(format(M), 2L, paste, collapse = " ")
else deparse(formula(orig.M)),
"")
)
)
epsnote <- NULL
## calculate test statistic
test <- match.arg(test)
if(test == "Spherical"){
bigmodel <- order(resdf)[1L]
df.res <- resdf[bigmodel]
sph <- sphericity(resssd[[bigmodel]],T=T,Sigma=Sigma)
epsnote <- c(paste(format(c("Greenhouse-Geisser epsilon:",
"Huynh-Feldt epsilon:")),
format(c(sph$GG.eps, sph$HF.eps), digits = 4L)),
"")
Psi <- T %*% Sigma %*% t(T)
stats <- matrix(NA, nmodels, 6L)
dimnames(stats) <- list(seq_len(nmodels),
c("F", "num Df", "den Df",
"Pr(>F)", "G-G Pr", "H-F Pr"))
for(i in 2:nmodels) {
s2 <- Tr(solve(Psi,T %*% deltassd[[i-1]] %*% t(T)))/pp/df[i]
Fval <- s2/sph$sigma
stats[i,1L:3] <- abs(c(Fval, df[i]*pp, df.res*pp))
}
stats[,4] <- pf(stats[,1], stats[,2], stats[,3], lower.tail = FALSE)
stats[,5] <- pf(stats[,1],
stats[,2]*sph$GG.eps, stats[,3]*sph$GG.eps,
lower.tail = FALSE)
stats[,6] <- pf(stats[,1],
stats[,2]*min(1,sph$HF.eps),
stats[,3]*min(1,sph$HF.eps),
lower.tail = FALSE)
table <- cbind(table, stats)
}
else if(!is.null(test)) {
bigmodel <- order(resdf)[1L]
df.res <- resdf[bigmodel]
## Try to distinguish bad scaling and near-perfect fit
## Notice that we must transform by T before scaling
sc <- sqrt(diag(T %*% resssd[[bigmodel]]$SSD %*% t(T)))
D <- sqrt(sc^2+apply(abs(sapply(deltassd,
function(X) diag((T %*% X %*% t(T))))),
1,max))
sc <- ifelse(sc/D < 1e-6, 1, 1/sc)
scm <- tcrossprod(sc)
rss.qr <- qr((T %*% resssd[[bigmodel]]$SSD %*% t(T)) * scm, tol=tol)
if(rss.qr$rank < pp)
stop(gettextf("residuals have rank %s < %s", rss.qr$rank, pp),
domain = NA)
eigs <- array(NA, c(nmodels, pp))
stats <- matrix(NA, nmodels, 5L)
dimnames(stats) <-
list(seq_len(nmodels),
c(test, "approx F", "num Df", "den Df", "Pr(>F)"))
for(i in 2:nmodels) {
sg <- (df[i] > 0) - (df[i] < 0)
eigs[i, ] <- Re(eigen(qr.coef(rss.qr,
sg * (T %*% deltassd[[i-1]] %*%
t(T)) * scm),
symmetric = FALSE, only.values = TRUE)$values)
stats[i, 1L:4] <-
switch(test,
"Pillai" = Pillai(eigs[i, ],
sg * df[i], resdf[bigmodel]),
"Wilks" = Wilks(eigs[i, ],
sg * df[i], resdf[bigmodel]),
"Hotelling-Lawley" = HL(eigs[i, ],
sg * df[i], resdf[bigmodel]),
"Roy" = Roy(eigs[i, ],
sg * df[i], resdf[bigmodel]))
ok <- stats[, 2] >= 0 & stats[, 3] > 0 & stats[, 4] > 0
ok <- !is.na(ok) & ok
stats[ok, 5] <- pf(stats[ok, 2], stats[ok, 3], stats[ok, 4],
lower.tail = FALSE)
}
table <- cbind(table,stats)
}
structure(table, heading = c(title, topnote, transformnote, epsnote),
class = c("anova", "data.frame"))
}
deviance.mlm <- function(object, ...)
{
colSums(if(is.null(w <- object$weights)) object$residuals^2
else w * object$residuals^2)
}
plot.mlm <- function (x, ...) .NotYetImplemented()