stan-dev / stan-dev/projpred

Projection onto the full model for multilevel Gaussian models

Open
#323 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
R
Stars
114
Forks
31
PR merge metrics
No merged PRs in 30d

Description

For multilevel Gaussian models, the projection onto the full model could be instable, if not even incorrect:


# Setup -------------------------------------------------------------------

default_post_warmup <- 1000
ndraws_ref <- 30
thin_ref <- ceiling(default_post_warmup / ndraws_ref)
set.seed(856824715)

# For simulating a dataset:
dataconstructor <- function() {
  nobsv <- 750L
  icpt <- -0.2

  npreds_cont <- 5L
  coefs_cont <- seq(-4, 4, length.out = npreds_cont)

  ngrPL <- 3L
  coefs_grPL <- c(0, seq(-2, 2, length.out = ngrPL - 1L))

  # Number of observations per group:
  ngrGL <- 375L
  coefs_grGL <- list("icpt" = rnorm(ngrGL, sd = 1.5))

  # Continuous predictors:
  dat_sim <- setNames(replicate(npreds_cont, {
    rnorm(nobsv, sd = 0.5)
  }, simplify = FALSE), paste0("Xcont", seq_len(npreds_cont)))
  dat_sim <- as.data.frame(dat_sim)
  eta <- icpt + drop(as.matrix(dat_sim) %*% coefs_cont)

  # Population-level (PL) categorical predictor:
  dat_sim$XgrPL1 <- sample(
    gl(n = ngrPL, k = floor(nobsv / ngrPL), length = nobsv,
       labels = paste0("gr", seq_len(ngrPL)))
  )
  eta <- eta + coefs_grPL[dat_sim$XgrPL1]

  # Group-level (GL) categorical predictor:
  dat_sim$XgrGL1 <- sample(
    gl(n = ngrGL, k = floor(nobsv / ngrGL), length = nobsv,
       labels = paste0("gr", seq_len(ngrGL)))
  )
  eta <- eta + coefs_grGL$icpt[dat_sim$XgrGL1]

  # Response:
  dat_sim$Y <- rnorm(nobsv, mean = eta, sd = 1.6)

  # Formula:
  voutc <- "Y"
  vpreds <- grep("^Xcont|^XgrPL", names(dat_sim), value = TRUE)
  vpreds_GL <- grep("^XgrGL", names(dat_sim), value = TRUE)
  vpreds_GL <- paste0("(1 | ", vpreds_GL, ")")
  fml_sim <- as.formula(paste(
    voutc, "~", paste(c(vpreds, vpreds_GL), collapse = " + ")
  ))

  # Output:
  return(list(true_GLEs = coefs_grGL$icpt,
              dat = dat_sim,
              fml = fml_sim))
}

# For fitting the reference model and projecting it onto itself, i.e., onto the
# full model:
ref_prj <- function(dat, fml, ...) {
  refm_fit <- rstanarm::stan_glmer(
    formula = fml,
    family = gaussian(),
    data = dat,
    chains = 1,
    thin = thin_ref,
    QR = TRUE,
    refresh = 0
  )
  soltrms <- labels(terms(fml))
  soltrms[grep("\\|", soltrms)] <- paste0("(", soltrms[grep("\\|", soltrms)], ")")
  ### For projecting onto a submodel, not the full model:
  # soltrms <- setdiff(soltrms, grep("^Xcont", soltrms, value = TRUE))
  ###
  prj <- projpred::project(refm_fit, solution_terms = soltrms, ...)
  sd_colnm <- "Sigma[XgrGL1:(Intercept),(Intercept)]"
  return(list(
    sd_ref = as.matrix(refm_fit)[, sd_colnm],
    sd_prj = as.matrix(prj)[, sd_colnm]
  ))
}

# Run ---------------------------------------------------------------------

sim_dat_etc <- dataconstructor()
# Debug so that the lme4::lmer() call inside of projpred:::fit_glmer_callback()
# can be run without suppressed warnings and messages:
debug(projpred:::fit_glmer_callback)
simres <- ref_prj(dat = sim_dat_etc$dat,
                  fml = sim_dat_etc$fml,
                  ndraws = ndraws_ref)

The instability can be seen from the warnings

Warning messages:
1: In optwrap(optimizer, devfun, getStart(start, rho$pp), lower = rho$lower,  :
  convergence code -4 from nloptwrap: NLOPT_ROUNDOFF_LIMITED: Roundoff errors led to a breakdown of the optimization algorithm. In this case, the returned minimum may still be useful. (e.g. this error occurs in NEWUOA if one tries to achieve a tolerance too close to machine precision.)
2: In checkConv(attr(opt, "derivs"), opt$par, ctrl = control$checkConv,  :
  unable to evaluate scaled gradient
3: In checkConv(attr(opt, "derivs"), opt$par, ctrl = control$checkConv,  :
  Model failed to converge: degenerate  Hessian with 1 negative eigenvalues

shown when debugging as outlined in the reprex above and from comparing simres$sd_prj to simres$sd_ref.

This instability does not seem to occur when projecting onto an actual submodel (e.g., by uncommenting soltrms <- setdiff(soltrms, grep("^Xcont", soltrms, value = TRUE)) in the reprex above).

The reason for the instability could be that in the projection onto the full model (i.e., in the lme4::lmer() fit), the residual SD is zero. So far, I did not encounter a similar issue for non-multilevel Gaussian models fitted via projpred:::fit_glm_ridge_callback() or via projpred:::fit_glm_callback(). So this could indeed be restricted to multilevel models.

This issue shows that—at least as a first step—the convergence of the algorithms used for fitting the submodels needs to be checked. For this, see the code started in PR #259. As a longer-term objective, I think we need to investigate this issue via simulation to find out if it's really an issue and—if yes—fix it.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the supplied R reprex and inspect projpred:::fit_glmer_callback(), including its lme4::lmer() call, while comparing simres$sd_prj with simres$sd_ref and the reported convergence warnings. Review the convergence-check work started in PR #259, then use simulations to determine whether full-model projections are unstable and define the expected corrected behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
r
Domain
data
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.