tests/testthat/test-rank.R

cat(crayon::yellow("\ntest singular design matrices:\n"))

# Singular matrix from ?Matrix::qr :
singX <- cbind(int = 1,
           b1=rep(1:0, each=3), b2=rep(0:1, each=3),
           c1=rep(c(1,0,0), 2), c2=rep(c(0,1,0), 2), c3=rep(c(0,0,1),2))
rownames(singX) <- paste0("r", seq_len(nrow(singX)))
donn <- as.data.frame(singX)
set.seed(123)
donn$y <- runif(6)
fv1 <- fitted(singlm <- lm(y~int+ b1+b2+c1+c2+c3,data=donn)) # (glmmTMB returns a full vector with numeric values instead of NA's)... vcov will be full of NaN's
spaMM.options(rankMethod="qr")
fv2 <- (singfit <- fitme(y~int+ b1+b2+c1+c2+c3,data=donn))$fv
spaMM.options(rankMethod=".rankinfo")
fv3 <- fitme(y~int+ b1+b2+c1+c2+c3,data=donn)$fv ## fixef is not unique, but fitted values must be equivalent
spaMM.options(rankMethod="qr")
testthat::expect_true( max(apply(cbind(fv1,fv2,fv3),1L,var))<1e-16)
crit <- diff(range(anova(singlm) -anova(singfit), na.rm=TRUE))
testthat::test_that("whether anova.lm and spaMM:::.anova.lm give equivalent results for singular X",
                    testthat::expect_true(crit<1e10))

# the way singular matrices are handled differ for LM vs GLMs: for the latter there are rows with 0 df in the Table
fv1 <- fitted(singglm <- glm(y~int+ b1+b2+c1+c2+c3,data=donn, family=Gamma(log)))
spaMM.options(rankMethod="qr")
fv2 <- (singfit <- fitme(y~int+ b1+b2+c1+c2+c3,data=donn, family=Gamma(log)))$fv
spaMM.options(rankMethod=".rankinfo")
fv3 <- fitme(y~int+ b1+b2+c1+c2+c3,data=donn, family=Gamma(log))$fv ## fixef is not unique, but fitted values must be equivalent
spaMM.options(rankMethod="qr")
testthat::expect_true( max(apply(cbind(fv1,fv2,fv3),1L,var))<1e-9)
# stats::anova(singglm) new default is to return the F test, as by spaMM::anova(singfit, test="F"), but with different result as doc'ed.
crit <- diff(range(anova(singglm, test=FALSE) -anova(singfit), na.rm=TRUE)) 
testthat::test_that("whether anova.glm and spaMM:::.anova.glm give equivalent results for singular X",
                    testthat::expect_true(crit<1e10))

donn$dummy<- c(0,0,0,1,1,1) # completely équivalent to b1 or b2!
chk <- try(nlme::lme(y~int+ b1+b2+c1+c2+c3, data = donn, random = ~ 1 | dummy), silent=TRUE)
testthat::test_that("whether nlme::lme does not handle singular X, as stated in the Description of spaMM::rankinfo",
                    testthat::expect_true(inherits(chk,"try-error"))
)

if (spaMM.getOption("example_maxtime")>0.7) { # a check accumulating possible causes of problems
  { # check on minimal pathological case
    ## b1 is redundant with | dummy ...
    trivml <- fitme(y~b1 + (1 | dummy), data = donn, #verbose=c(TRACE=interactive()), 
                    method="ML") # There is info about lambda, which is estimated as zero
    numInfo(trivml) # the nonzero gradient at the boundary is detected. lambda is removed from target params, so that num derivation does not generate negative lambdas
    trivreml <- fitme(y~b1 + (1 | dummy), data = donn, #verbose=c(TRACE=interactive()), 
                      method="REML") # There is NO info kept in Re.L about lambda
    numInfo(trivreml,which=c("lambda","phi","beta")) # the haphazard 'estimate' of lambda is far from 0, so is included, with vanishing 2nd derivative. 
                      # The phi result is consistent with the next one
                      # The beta result is different from the next one as expected from the different lambda.
    trivreml <- fitme(y~b1 + (1 | dummy), data = donn, control=list(refit=TRUE), method="REML") 
    numInfo(trivreml, which=c("lambda","phi","beta")) # values at boundary are excluded from computation to avoid negative lambda
  }
  
  ## Each of b1 and b2 are redundant with | dummy ...
  # Here lambda appears unidentifiable by *RE*ML: RE.L is 'completely' flat wrt it (marginal L is not). 
  (singfitF <- fitme(y~int+ b1+b2+c1+c2+c3 + (1 | dummy), data = donn, method="REML", 
                    # verbose=c(TRACE=TRUE), # quite useful to check what's going on in numInfo computation too
                    init=list(lambda=0.01), 
                    control=list(refit=FALSE))) ## Final value of lambda dependent on initial value
  # The unit SE for the phi coef is also surprizing but may also be a far-fetched consequence of idiosyncrasies of data and model,
  # combined with the approxs of the SmythHV method (by contrast, cond.SE is not 1 when formula= y~int + (1 | dummy)).
  suppressWarnings(anova(singfitF, type="2")) # lambda not low enough for removal, but numInfo singular since no info on lambda => warning (suppressed here)
  ranef(singfitF) #  =0, which is consistent with the refit below. The latter de facto replaces an undefined lambda estimate ((v=0)/(1-lev=0)) by v/(tiny value) = 0
  (singfitT <- fitme(y~int+ b1+b2+c1+c2+c3 + (1 | dummy), data = donn, method="REML", 
                    # verbose=c(TRACE=TRUE), # quite useful to check what's going on
                    init=list(lambda=0.01), 
                    control=list(refit=TRUE))) # Note effect on lambda
  suppressWarnings(anova(singfitT, type="2")) # Distinct warning for removal of lambda at boundary
  suppressWarnings(chk <- lme4::lmer(y~int+ b1+b2+c1+c2+c3 + (1 | dummy), data = donn)) # warnings, but this fits contrary to lme.
  suppressWarnings(anova(lmerTest::as_lmerModLmerTest(chk), type="II")) 
  # Trying to get the same result as lme4... not completely successful, but there are good reasons for that: 
  (singfit <- fitme(y~int+ b1+b2+c1+c2+c3 + (1 | dummy), data = donn, method="REML", 
                    init=list(lambda=0.0164^2), 
                    lower=list(lambda=0.0163^2),
                    upper=list(lambda=0.0165^2),
                    control=list(refit=FALSE))) ## Final value of lambda = initial value
  suppressWarnings(anova(singfit, type="2")) # Again, lambda not low enough for removal, but numInfo singular since no info on lambda => warning
  suppressWarnings(anova(singfit, type="2",transf=FALSE)) # Note the difference in DenDf hence in p-value
  # Since lambda is not quite low, there is no check of the num derivs, despite all the identifiability issues. The difference is on the b1 coeff, redundant with the ranef...
}

if (spaMM.getOption("example_maxtime")>0.7) { # Test of correct handling of singular fits in anova lead to an odd F test| cX2 test comparison on non-singular fits.
  # Here for devel purposes a single collinearity is retained ('int' is equiv to the Intercept). 
  #
  ## Also b1 was redundant with | dummy and was removed here (this does not prevent lambda -> 0 unless we modify y: see commented-out line. 
  donn$yy <- donn$y
  # donn$yy[donn$dumm==1] <- donn$y[donn$dumm==1]+1 # if we want to avoid the zero lambda estimate
  ##
  okchk <- lme4::lmer(yy~int+c1+c2 + (1 | dummy), data = donn, REML=FALSE) 
  (okfit <- fitme(yy~c1+c2 + (1 | dummy), data = donn, method="ML")) 
  (singfit <- fitme(yy~int+ c1+c2 + (1 | dummy), data = donn, method="ML")) 
  # library("lmerTest")
  a1 <- anova(lmerTest::as_lmerModLmerTest(okchk), type="1") # consistent (despite possibly different treatment of lambda) with:
  suppressWarnings(a2 <- anova(okfit, type="1")) 
  suppressWarnings(a3 <- anova(singfit, type="1")) 
  crit <- max(abs(c(a1[1:2,6]-a2[1:2,6],a1[1:2,6]-a3[1:2,6])))
  testthat::test_that("whether LMM type 1 anova for singular X is consistent with trimmed X",
                      testthat::expect_true(crit<1e8))
  #
  anova(okfit, type="1", method="t.Chisq")
  # Remarkably, the X2 stats are identical to the previous F stats. 
  # The X2 stat comes from .anova_fallback -> Wald's method, hard to mess with.
  # The F stat is the square of the t-stat computed by lmerTest:::anova.lmerModlmerTest() -> single_anova() -> contest1D() -> [locally defined]mk_ttable()
  
  # same compar for type 2, quite different from type 1 ...
  a1 <- anova(lmerTest::as_lmerModLmerTest(okchk), type="2")
  suppressWarnings(a2 <- anova(okfit, type="2")) # 
  suppressWarnings(a3 <- anova(singfit, type="2"))
  crit <- max(abs(c(a1[1:2,6]-a2[1:2,6],a1[1:2,6]-a3[1:2,6])))
  testthat::test_that("whether LMM type 2 anova for singular X is consistent with trimmed X",
                      testthat::expect_true(crit<1e8))
  #
  anova(okfit, type="2", method="t.Chisq")
  
}

Try the spaMM package in your browser

Any scripts or data that you put into this service are public.

spaMM documentation built on Aug. 30, 2023, 1:07 a.m.