R/resi.R

Defines functions resi.glmmTMB resi.lmerMod resi.lme resi.gee resi.glmgee resi.geeglm resi.hurdle resi.coxph resi.survreg resi.nls resi.glmrob resi.lmrob resi.lm resi.glm resi.default resi

Documented in resi resi.coxph resi.default resi.gee resi.geeglm resi.glm resi.glmgee resi.glmmTMB resi.glmrob resi.hurdle resi.lm resi.lme resi.lmerMod resi.lmrob resi.nls resi.survreg

#' Robust Effect Size Index (RESI) point and interval estimation for models
#'
#' This function will estimate the robust effect size (RESI) from Vandekar, Tao, & Blume (2020) and its confidence interval in various ways for a fitted model object. The overall RESI is estimated via a Wald test. RESI is (optionally) estimated for each factor in coefficients-style table. RESI is (optionally) estimated for each variable/interaction in an Anova-style table for models with existing Anova methods. CIs can be calculated using either non-parametric or Bayesian bootstrapping.
#' @param model.full \code{lm, glm, nls, survreg, coxph, hurdle, zeroinfl, gee, geeglm} or \code{lme} model object.
#' @param model.reduced Fitted model object of same type as model.full. By default `NULL`; the same model as the full model but only having intercept.
#' @param data Data.frame or object coercible to data.frame of model.full data (required for some model types).
#' @param vcovfunc The variance estimator function for constructing the Wald test statistic. By default, \link[sandwich]{vcovHC} (the robust (sandwich) variance estimator).
#' @param coefficients Logical, whether to produce a coefficients (summary) table with the RESI columns added. By default = `TRUE`.
#' @param anova Logical, whether to produce an Anova table with the RESI columns added. By default = `TRUE`.
#' @param overall Logical, whether to produce an overall Wald test comparing full to reduced model with RESI columns added. By default = `TRUE`.
#' @param ci.method Character, the method used to compute confidence intervals.
#'   One of `"boot"` (bootstrap), `"qf"` (quadratic-form inversion, default for lm and glm),
#'   `"normal"` (normal approximation), or `"cf"` (Cornish-Fisher inversion).
#'   See `resi_pe_asymptotic` for details on the asymptotic methods.
#' @param nboot Numeric, the number of bootstrap replicates. Used only when
#'   `ci.method = "boot"`. By default, 1000.
#' @param boot.method String, which type of bootstrap to use: `nonparam` = non-parametric bootstrap (default); `bayes` = Bayesian bootstrap.
#' @param alpha Numeric, significance level of the constructed CIs. By default, 0.05.
#' @param store.boot Logical, whether to store all the bootstrapped estimates. By default, `FALSE`.
#' @param Anova.args List, additional arguments to be passed to \link[car]{Anova} function.
#' @param vcov.args List, additional arguments to be passed to vcovfunc.
#' @param unbiased Logical, whether to use the unbiased or alternative T/Z statistic to RESI conversion. By default, `TRUE`. See details.
#' @param parallel See documentation for \link[boot]{boot}.
#' @param ncpus See documentation for \link[boot]{boot}.
#' @param long Logical, whether the data is longitudinal/clustered. By default, `FALSE`.
#' @param clvar Character, the name of the cluster/id variable if data is clustered. By default, `NULL`.
#' @param ... Ignored.
#' @importFrom aod wald.test
#' @importFrom boot boot
#' @importFrom car Anova
#' @importFrom lmtest waldtest
#' @importFrom sandwich vcovHC
#' @importFrom stats anova as.formula coef complete.cases formula glm hatvalues lm nobs pchisq pf predict quantile rbinom residuals rnorm runif update vcov
#' @importFrom utils capture.output
#' @export
#' @details The RESI, denoted as S, is applicable across many model types. It is a unitless
#' index and can be easily be compared across models. The RESI can also be
#' converted to Cohen's \emph{d} (\code{\link{S2d}}) under  model homoskedasticity.
#'
#' This function computes the RESI point estimates and bootstrapped confidence
#' intervals based on Chi-square, F, T, or Z statistics. The robust (sandwich)
#' variance is used by default, allowing for consistency under
#' model-misspecification. The RESI is related to the non-centrality parameter
#' of the test statistic. The RESI estimate is consistent for all four
#' (Chi-square, F, T, and Z) types of statistics used. The Chi-square and F-based
#' calculations rely on asymptotic theory, so they may be biased in small samples.
#' When possible, the T and Z statistics are used. There are two formulas for both
#' the T and Z statistic conversion. The first (default, unbiased = TRUE)
#' are based on solving the expected value of the T or Z statistic for the RESI.
#' The alternative is based on squaring the T or Z statistic and using the
#' F or Chi-square statistic conversion. Both of these methods are consistent, but
#' the alternative exhibits a notable amount of finite sample bias. The alternative
#' may be appealing because its absolute value will be equal to the RESI based on
#' the F or Chi-square statistic. The RESI based on the Chi-Square and F statistics
#' is always greater than or equal to 0. The type of statistic
#' used is listed with the output. See \code{\link{f2S}}, \code{\link{chisq2S}},
#' \code{\link{t2S}}, and \code{\link{z2S}} for more details on the formulas.
#'
#' For GEE (\link[geepack]{geeglm}, \link[glmtoolbox]{glmgee}) models, a longitudinal RESI (L-RESI) and a cross-sectional,
#' per-measurement RESI (CS-RESI) is estimated. The longitudinal RESI takes the
#' specified clustering into account, while the cross-sectional RESI is designed to
#' estimate the effect size if a random observation for each participant were collected cross-sectionally.
#'
#' For most \code{lm} and \code{nls} model types, there is a Bayesian bootstrap
#' option available as an alternative to the default, standard non-parametric
#' bootstrap. The interpretation of a Bayesian bootstrapped interval is similar to
#' that of a credible interval.
#'
#' Certain model types require the data used for the model be entered as an argument.
#' These are: \code{nls, survreg,} and \code{coxph}. Additionally, if a model
#' includes certain functions (splines, factor, I), the data needs to be provided.
#'
#' If running into convergence issues with nls models, it is advised to refit the
#' nls model with starting values equal to the estimates provided by the model
#' and then try rerunning \code{resi}.
#'
#' @return Returns a list that includes function arguments, RESI point estimates,
#' and confidence intervals in coefficients/anova-style tables
#' @family RESI functions
#' @seealso \code{\link{resi_pe}}, \link[sandwich]{vcovHC},
#' \code{\link{f2S}}, \code{\link{chisq2S}}, \code{\link{z2S}}, \code{\link{t2S}}
#'
#' @examples
#' ## for timing purposes, a small number of bootstrap replicates is used in the
#' ## examples. Run them with a higher or default `nboot` argument for better performance
#'
#' ## RESI on a linear model
#' # fit linear model
#' mod = lm(charges ~ region * age + bmi + sex, data = RESI::insurance)
#'
#' # run resi on fitted model with desired number of bootstrap replicates
#' # store bootstrap results for calculating different CIs later
#' resi_obj = resi(mod, nboot = 50, store.boot = TRUE)
#' # print output
#' resi_obj
#'
#' # fit a reduced model for comparison
#' mod_red = lm(charges ~ bmi, data = RESI::insurance)
#'
#' # running resi and including the reduced model will provide almost the exact same
#' # output as not including a reduced model. The difference is that the "overall"
#' # portion of the output will compare the full model to the reduced model.
#' # The "summary" and "anova" RESI estimates will be the same. (The bootstrapped
#' # confidence intervals may differ.)
#' resi(model.full = mod, model.reduced = mod_red, nboot = 10)
#'
#' # used stored bootstrap results to get a different alpha-level confidence interval
#' summary(resi_obj, alpha = c(0.01, 0.1))
#' car::Anova(resi_obj, alpha = c(0.01, 0.1))
#'
#' # the result of resi, as well as the summary or Anova of a `resi` object can be plotted
#' # if the resi object was created with the store.boot = `TRUE` option, any alpha
#' # can be specified
#' plot(resi_obj, alpha = 0.01)
#' # if the variable names on the y-axis are too long, you can reduce their size with
#' # the ycex.axis argument (or use regular common solutions like changing the margins)
#' plot(resi_obj, alpha = 0.01, ycex.axis = 0.5)
#'
#' # for some model types and formula structures, data argument is required
#' if(requireNamespace("splines")){
#'   # fit logistic regression model with splines
#'   mod = glm(smoker ~ splines::ns(age, df = 3) + region, data = RESI::insurance,
#'     family = "binomial")
#'
#'   # specify additional arguments to the variance-covariance function via vcov.args
#'   resi_obj = resi(mod, data = RESI::insurance, alpha = 0.01,
#'     vcov.args = list(type = "HC0"), nboot = 25)
#'   summary(resi_obj)
#'   car::Anova(resi_obj)}
#'
#'
#' ## RESI on a survival model with alternate Z2S
#' if(requireNamespace("survival")){
#'   # fit coxph model on example data from survival package
#'   # Note: for survival models, you need to specify robust variance in the model
#'   # creation. resi will ignore the vcovfunc argument for this reason.
#'   mod.coxph =  survival::coxph(survival::Surv(time, status) ~ age + sex + wt.loss,
#'    data=survival::lung, robust = TRUE)
#'
#'   # run resi on the model
#'   # to use the alternative Z to RESI formula (which is equal in absolute value to the
#'   # chi-square to RESI (S) formula), specify unbiased = FALSE.
#'   resi(mod.coxph, data = survival::lung, unbiased = FALSE, nboot = 10)}
#'
#' @references Vandekar S, Tao R, Blume J. A Robust Effect Size Index. \emph{Psychometrika}. 2020 Mar;85(1):232-246. doi: 10.1007/s11336-020-09698-2.
#'
#' Kang, K., Armstrong, K., Avery, S., McHugo, M., Heckers, S., & Vandekar, S. (2021). Accurate confidence interval estimation for non-centrality parameters and effect size indices. \emph{arXiv preprint arXiv:2111.05966}.
#'
#' Jones, M., Kang, K., Vandekar, S. (2025). \emph{Journal of Statistical Software}. RESI: An R Package for Robust Effect Sizes.<doi:10.18637/jss.v112.i03>

resi = function(model.full, ...){
  UseMethod("resi")
}

#' @describeIn resi RESI point and interval estimation for models
#' @export
resi.default = function(model.full, model.reduced = NULL, data, anova = TRUE,
                         coefficients = TRUE, overall = TRUE, nboot = 1000,
                         boot.method = "nonparam", vcovfunc = sandwich::vcovHC,
                         alpha = 0.05, store.boot = FALSE, Anova.args = list(),
                         vcov.args = list(), unbiased = TRUE,
                         parallel = c("no", "multicore", "snow"),
                         ncpus = getOption("boot.ncpus", 1L), long = FALSE,
                         clvar = NULL,
                         ci.method = c("boot", "qf", "cf", "normal"),
                         ...){

  ci.method <- match.arg(ci.method)

  # check for supported model type
  if(!(any(class(model.full) %in%
    sapply(as.character(utils::methods(RESI::resi)), function(x) substr(x, 6, nchar(x)))))){
    warning("Model type not implemented in RESI package, attempting default")
  }
  dots = list(...)

  data_from_model_frame <- missing(data)
  if (data_from_model_frame){
    data = model.full$model
    if (ci.method == "boot") {
      tryCatch(update(model.full, data = data), error = function(e){
        message("Updating model fit failed. Try running with providing data argument")})
    }
  }
  else{
    if (!(is.null(model.full$na.action))){
      data = data[which(!(1:nrow(data)%in% model.full$na.action)),]
    }
    data = as.data.frame(data)
  }

  # For clustered/GEE models, strip rows with NA in any model variable from data
  # before constructing model.reduced or bootstrapping. geeglm may not populate
  # na.action when the model was fitted on already-complete data but the user
  # passes a data frame with NAs. Use only the columns that appear in model.frame()
  # to avoid dropping rows with NAs in unrelated columns.
  if (long) {
    model_vars <- intersect(names(model.frame(model.full)), names(data))
    data <- data[complete.cases(data[, model_vars, drop = FALSE]), , drop = FALSE]
  }

  if (is.null(model.reduced) & overall){
    if(!("skip.red" %in% names(dots))){
      form.reduced = as.formula(paste(format(formula(model.full)[[2]]), "~ 1"))
      if (!(form.reduced == formula(model.full))){
        # How we fit the intercept-only reduced model depends on the data source:
        #
        # (a) data was supplied by the user (raw columns, e.g. 'charges'): use the
        #     expression formula (e.g. log10(charges) ~ 1) so that bootstrap
        #     replicates (which also have raw columns) can update() successfully.
        #
        # (b) data came from model.full$model (the model frame, e.g. column named
        #     'log10(charges)'): use the backtick-quoted pre-computed column name
        #     so that update() finds the response without re-evaluating the
        #     expression on columns that don't exist in the model frame.
        #     This path is only reached for asymptotic CIs (ci.method != "boot"),
        #     since bootstrap with a model-frame data warns and is expected to fail.
        if (data_from_model_frame && !is.null(model.full$model) && !long) {
          resp_name <- colnames(model.full$model)[1]
          form.reduced_fit <- as.formula(paste0("`", resp_name, "` ~ 1"))
          model.reduced = try(update(model.full, formula = form.reduced_fit,
                                      data = data), silent = T)
        } else {
          model.reduced = try(update(model.full, formula = form.reduced,
                                      data = data), silent = T)
        }
        if(inherits(model.reduced, "try-error")){
          warning("Fitting intercept-only model failed. No overall test computed")
          overall = FALSE
        }
    }}}

  # point estimation
  output = list(alpha = alpha, nboot = nboot, boot.method = boot.method)
  output = c(output, resi_pe(model.full = model.full, model.reduced = model.reduced,
                             data = data, anova = anova, coefficients = coefficients,
                             overall = overall, vcovfunc = vcovfunc,
                             Anova.args = Anova.args, vcov.args = vcov.args,
                             unbiased = unbiased, ...))

  if (long){
    data = unique(data[, clvar])
  }

  # ---- asymptotic CIs (skip bootstrap) ----
  if (ci.method != "boot") {
    asym_out <- tryCatch(
      resi_pe_asymptotic(model.full   = model.full,
                         data         = data,
                         vcovfunc     = vcovfunc,
                         coefficients = coefficients,
                         anova        = anova,
                         alpha        = alpha,
                         ci.method    = ci.method,
                         unbiased     = unbiased,
                         Anova.args   = Anova.args,
                         vcov.args    = vcov.args),
      error = function(e) {
        warning("Asymptotic CI computation failed: ", conditionMessage(e),
                "\nFalling back to bootstrap.")
        NULL
      }
    )
    if (!is.null(asym_out)) {
      alpha.order <- sort(c(alpha / 2, 1 - alpha / 2))
      ci_cols     <- paste0(alpha.order * 100, "%")
      if (coefficients && !is.null(output$coefficients) && !is.null(asym_out$coefficients)) {
        # Match row names and add CI columns
        rn <- rownames(output$coefficients)
        rn_asym <- rownames(asym_out$coefficients)
        for (col in ci_cols) {
          if (col %in% colnames(asym_out$coefficients)) {
            output$coefficients[rn %in% rn_asym, col] <-
              asym_out$coefficients[rn_asym %in% rn, col]
          }
        }
      }
      if (anova && !is.null(output$anova) && !is.null(asym_out$anova)) {
        rn <- rownames(output$anova)
        rn_asym <- rownames(asym_out$anova)
        for (col in ci_cols) {
          if (col %in% colnames(asym_out$anova)) {
            output$anova[rn %in% rn_asym, col] <-
              asym_out$anova[rn_asym %in% rn, col]
          }
        }
      }
      output$ci.method  <- ci.method
      # Store model and vcov settings so S3 methods can recompute CIs at a
      # different alpha without re-running resi().
      output$model.full <- model.full
      output$vcovfunc   <- vcovfunc
      output$vcov.args  <- vcov.args
      output$Anova.args <- Anova.args
      output$unbiased   <- unbiased
      class(output) <- "resi"
      return(output)
    }
    # if we fell through (error), continue to bootstrap
    ci.method <- "boot"
  }

  # bootstrapping
  suppressMessages(capture.output(boot_out <- boot::boot(data = data, R = nboot, statistic = resi_stat, parallel = parallel,
                  ncpus = ncpus, mod.full = model.full, mod.reduced = model.reduced,
                  anova = anova, coefficients = coefficients, overall = overall, vcovfunc = vcovfunc,
                  Anova.args = Anova.args, vcov.args = vcov.args, unbiased = unbiased,
                  boot.method = boot.method, clvar = clvar, nest = length(output$estimates), ...), file = nullfile()))

  # bootstrapped estimates
  boot.results = boot_out$t
  colnames(boot.results) = names(boot_out$t0)

  if (all(is.na(boot.results))){
    stop("\nBootstrapping failed. Run resi_pe for point estimates")
  }

  alpha.order = sort(c(alpha/2, 1-alpha/2))

  if (!long){
    if (overall & !is.null(output$overall)){
      if(nrow(output$overall) == 1){
        names.overall = colnames(output$overall)
        for (i in 1:length(alpha.order)){
          output$overall = cbind(output$overall, quantile(boot.results[,1],
                                                          probs = alpha.order[i], na.rm = TRUE))
        }
        colnames(output$overall) = c(names.overall, paste(alpha.order*100, "%", sep=""))
      } else{
        output$overall[nrow(output$overall),paste(alpha.order*100, "%", sep="")] =
          quantile(boot.results[,1], probs = alpha.order, na.rm = TRUE)}
    }

    if (coefficients){
      co = boot.results[,(1+!(is.null(output$overall))):((!is.null(output$overall))+nrow(output$coefficients))]
      if (is.null(dim(co))){
        CIs = quantile(co, probs = alpha.order, na.rm = TRUE)
      } else{
        CIs = apply(co, 2,  quantile, probs = alpha.order, na.rm = TRUE)
      }
      CIs = t(CIs)
      output$coefficients[1:nrow(CIs), paste(alpha.order*100, "%", sep="")] = CIs
    }

    if (anova){
      an = boot.results[,(ncol(boot.results)-length(which(rownames(output$anova) != "Residuals"))+1):ncol(boot.results)]
      if (is.null(dim(an))){
        CIs = quantile(an, probs = alpha.order, na.rm = TRUE)
      } else{
        CIs = apply(an, 2,  quantile, probs = alpha.order, na.rm = TRUE)
      }
      CIs = t(CIs)
      output$anova[1:nrow(CIs), paste(alpha.order*100, "%", sep="")] = CIs
    }
  } else {
    r = 1
    if (!is.null(output$overall)){
      # L-RESI for geeglm model overall Wald test
      output$overall[nrow(output$overall),paste("L ",alpha.order*100, "%", sep="")] =
        quantile(boot.results[,1], probs = alpha.order, na.rm = TRUE)
      r = 2
    }
    # L-RESI and CS-RESI for longitudinal models
    if (coefficients){
      lco = boot.results[,r:(nrow(output$coefficients) + r -1)]
      if (is.null(dim(lco))){
        lCIs = quantile(lco, probs = alpha.order, na.rm = TRUE)
      } else{
        lCIs = apply(lco, 2,  quantile,
                     probs = alpha.order, na.rm = TRUE)
      }
      lCIs = t(lCIs)
      output$coefficients[1:nrow(lCIs), paste("L ",alpha.order*100, "%", sep="")] = lCIs
      cco = boot.results[,(nrow(output$coefficients)+r):(2*nrow(output$coefficients) + r - 1)]
      if (is.null(dim(cco))){
        cCIs = quantile(cco, probs = alpha.order, na.rm = TRUE)
      } else{
        cCIs = apply(cco, 2,  quantile, probs = alpha.order, na.rm = TRUE)
      }
      cCIs = t(cCIs)
      output$coefficients[1:nrow(cCIs), paste("CS ",alpha.order*100, "%", sep="")] = cCIs
    }

    if (anova){
      lan = boot.results[,(ncol(boot.results)-
                             2*length(rownames(output$anova))+1):
                           (ncol(boot.results)-length(rownames(output$anova)))]
      if (is.null(dim(lan))){
        lCIs = quantile(lan, probs = alpha.order, na.rm = TRUE)
      } else{
        lCIs = apply(lan, 2,  quantile, probs = alpha.order, na.rm = TRUE)
      }
      lCIs = t(lCIs)
      output$anova[1:nrow(lCIs), paste("L ", alpha.order*100, "%", sep="")] = lCIs
      can = boot.results[,(ncol(boot.results)-length(rownames(output$anova))+1):
                           ncol(boot.results)]
      if (is.null(dim(can))){
        cCIs = quantile(can, probs = alpha.order, na.rm = TRUE)
      } else{
        cCIs = apply(can, 2,  quantile, probs = alpha.order, na.rm = TRUE)
      }
      cCIs = t(cCIs)
      output$anova[1:nrow(cCIs), paste("CS ", alpha.order*100, "%", sep="")] = cCIs
    }
  }

  if(store.boot){
    #output$boot.results = boot.results
    output$boot.results = boot_out
  }
  class(output) = "resi"
  return(output)
}

#' @describeIn resi RESI point and interval estimation for models
#' @export
resi.glm = function(model.full, model.reduced = NULL, data, anova = TRUE,
                    coefficients = TRUE, overall = TRUE, nboot = 1000,
                    vcovfunc = sandwich::vcovHC, alpha = 0.05, store.boot = FALSE,
                    Anova.args = list(), vcov.args = list(), unbiased = TRUE,
                    parallel = c("no", "multicore", "snow"), ncpus = getOption("boot.ncpus", 1L),
                    ci.method = "qf",
                    ...){  dots = list(...)
  if ("boot.method" %in% names(dots)){    stop("\nOnly nonparametric bootstrap supported for model type")
  }
  resi.default(model.full = model.full, model.reduced = model.reduced, data = data,
               anova = anova, coefficients = coefficients, overall = overall,
               nboot = nboot, vcovfunc = vcovfunc, store.boot = store.boot,
               Anova.args = Anova.args, vcov.args = vcov.args,
               unbiased = unbiased, alpha = alpha, parallel = parallel,
               ncpus = ncpus, ci.method = ci.method, ...)

}

#' @describeIn resi RESI point and interval estimation for lm models
#' @export
resi.lm = function(model.full, model.reduced = NULL, data, anova = TRUE,
                    coefficients = TRUE, overall = TRUE, nboot = 1000,
                    boot.method = "nonparam", vcovfunc = sandwich::vcovHC,
                    alpha = 0.05, store.boot = FALSE, Anova.args = list(),
                    vcov.args = list(), unbiased = TRUE,
                    parallel = c("no", "multicore", "snow"),
                    ncpus = getOption("boot.ncpus", 1L),
                    ci.method = "qf",
                    ...){  
  boot.method = match.arg(tolower(boot.method), choices = c("nonparam", "bayes"))

  resi.default(model.full = model.full, model.reduced = model.reduced, data = data,
               anova = anova, coefficients = coefficients, overall = overall,
               nboot = nboot, vcovfunc = vcovfunc, store.boot = store.boot,
               Anova.args = Anova.args, vcov.args = vcov.args, unbiased = unbiased,
               alpha = alpha, parallel = parallel, ncpus = ncpus,
               boot.method = boot.method, ci.method = ci.method, ...)

}

#' @describeIn resi RESI point and interval estimation for lmrob models (robustbase)
#' @export
resi.lmrob = function(model.full, model.reduced = NULL, data, anova = TRUE,
                      coefficients = TRUE, overall = TRUE, nboot = 1000,
                      boot.method = "nonparam", vcovfunc = stats::vcov,
                      alpha = 0.05, store.boot = FALSE, Anova.args = list(),
                      vcov.args = list(), unbiased = TRUE,
                      parallel = c("no", "multicore", "snow"),
                      ncpus = getOption("boot.ncpus", 1L),
                      ci.method = "boot", ...) {
  # lmrob has a built-in robust sandwich variance; stats::vcov is the natural
  # default. sandwich::vcovHC also works but is redundant and can be unstable
  # on bootstrap samples with near-degenerate design matrices.
  boot.method = match.arg(tolower(boot.method), choices = c("nonparam", "bayes"))
  resi.default(model.full = model.full, model.reduced = model.reduced, data = data,
               anova = anova, coefficients = coefficients, overall = overall,
               nboot = nboot, vcovfunc = vcovfunc, store.boot = store.boot,
               Anova.args = Anova.args, vcov.args = vcov.args, unbiased = unbiased,
               alpha = alpha, parallel = parallel, ncpus = ncpus,
               boot.method = boot.method, ci.method = ci.method, ...)
}

#' @describeIn resi RESI point and interval estimation for glmrob models (robustbase)
#' @export
resi.glmrob = function(model.full, model.reduced = NULL, data, anova = TRUE,
                       coefficients = TRUE, overall = TRUE, nboot = 1000,
                       vcovfunc = stats::vcov, alpha = 0.05, store.boot = FALSE,
                       Anova.args = list(), vcov.args = list(), unbiased = TRUE,
                       parallel = c("no", "multicore", "snow"),
                       ncpus = getOption("boot.ncpus", 1L),
                       ci.method = "boot", ...) {
  dots = list(...)
  if ("boot.method" %in% names(dots)){
    stop("\nOnly nonparametric bootstrap supported for model type")
  }
  if (identical(vcovfunc, sandwich::vcovHC)) {
    vcovfunc <- stats::vcov
    warning("sandwich::vcovHC is not supported for glmrob objects. ",
            "Using the model's built-in robust variance (stats::vcov) instead.")
  }
  resi.default(model.full = model.full, model.reduced = model.reduced, data = data,
               anova = anova, coefficients = coefficients, overall = overall,
               nboot = nboot, vcovfunc = vcovfunc, store.boot = store.boot,
               Anova.args = Anova.args, vcov.args = vcov.args,
               unbiased = unbiased, alpha = alpha, parallel = parallel,
               ncpus = ncpus, ci.method = ci.method, ...)
}

#' @describeIn resi RESI point and interval estimation for nls models
#' @export
resi.nls = function(model.full, model.reduced = NULL, data, coefficients = TRUE,
                     overall = TRUE, nboot = 1000, boot.method = "nonparam",
                     anova = FALSE, vcovfunc = r_nlshc, alpha = 0.05,
                     store.boot = FALSE, vcov.args = list(), unbiased = TRUE,
                     parallel = c("no", "multicore", "snow"), ncpus = getOption("boot.ncpus", 1L),
                     ...){

  boot.method = match.arg(tolower(boot.method), choices = c("nonparam", "bayes"))

  if (missing(data)){
    stop("\nData argument is required for nls model")
  }
  if (identical(vcovfunc, sandwich::vcovHC)){
    vcovfunc = r_nlshc
    warning("Sandwich vcov function not applicable for nls model type, vcovfunc set to regtools::nlshc")
  }

  output = resi.default(model.full = model.full, model.reduced = model.reduced,
                        data = data, anova = FALSE, coefficients = coefficients,
                        overall = overall, nboot = nboot, vcovfunc = vcovfunc,
                        store.boot = TRUE, unbiased = unbiased, alpha = alpha,
                        vcov.args = vcov.args, parallel = parallel,
                        ncpus = ncpus, boot.method = boot.method, skip.red = TRUE, ...)


  # number of bootstrap replicates with failed updating
  output$nfail = length(which(is.na(output$boot.results$t[,1])))

  if (!store.boot){
    output = output[names(output) != "boot.results"]
  }
  class(output) = "resi"
  return(output)
}

#' @describeIn resi RESI point and interval estimation for survreg models
#' @export
resi.survreg = function(model.full, model.reduced = NULL, data, anova = TRUE,
                         coefficients = TRUE, overall = TRUE, nboot = 1000,
                         vcovfunc = vcov, alpha = 0.05, store.boot = FALSE,
                         Anova.args = list(), unbiased = TRUE,
                         parallel = c("no", "multicore", "snow"),
                         ncpus = getOption("boot.ncpus", 1L),...){
  dots = list(...)
  if (missing(data)){
    stop("\nData argument is required for model type")
  }
  if (!identical(vcovfunc, vcov)){
    warning("vcovfunc argument ignored for survreg objects")
  }
  if ("vcov.args" %in% names(dots)){
    warning("vcov.args ignored for model type")
  }
  if ("boot.method" %in% names(dots)){
    stop("\nOnly nonparametric bootstrap supported for model type")
  }

  resi.default(model.full = model.full, model.reduced = model.reduced, data = data,
               anova = anova, coefficients = coefficients, overall = overall,
               nboot = nboot, vcovfunc = vcov, store.boot = store.boot,
               Anova.args = Anova.args, unbiased = unbiased, alpha = alpha,
               boot.method = "nonparam", parallel = parallel, ncpus = ncpus, ...)
}

#' @describeIn resi RESI point and interval estimation for coxph models
#' @export
resi.coxph = function(model.full, model.reduced = NULL, data, anova = TRUE,
                       coefficients = TRUE, overall = TRUE, nboot = 1000,
                       vcovfunc = vcov, alpha = 0.05, store.boot = FALSE,
                       Anova.args = list(), unbiased = TRUE,
                       parallel = c("no", "multicore", "snow"),
                       ncpus = getOption("boot.ncpus", 1L),...){
  dots = list(...)
  if (missing(data)){
    stop("\nData argument is required for model type")
  }
  if (!identical(vcovfunc, vcov)){
    warning("vcovfunc argument ignored for coxph objects")
  }
  if ("vcov.args" %in% names(dots)){
    warning("vcov.args ignored for model type")
  }
  if ("boot.method" %in% names(dots)){
    stop("\nOnly nonparametric bootstrap supported for model type")
  }
  if (!is.null(model.reduced)){
    warning("Reduced model argument ignored for coxph model")
  }
  resi.default(model.full = model.full, model.reduced = NULL, data = data,
               anova = anova, coefficients = coefficients, overall = overall,
               nboot = nboot, vcovfunc = vcov, store.boot = store.boot,
               Anova.args = Anova.args, unbiased = unbiased, alpha = alpha,
               boot.method = "nonparam", parallel = parallel, ncpus = ncpus,
               skip.red = TRUE, ...)
}


#' @describeIn resi RESI point and interval estimation for hurdle models
#' @export
resi.hurdle = function(model.full, model.reduced = NULL, data, coefficients = TRUE,
                        overall = TRUE, nboot = 1000, vcovfunc = sandwich::sandwich,
                        anova = FALSE, alpha = 0.05, store.boot = FALSE,
                        vcov.args = list(), unbiased = TRUE,
                        parallel = c("no", "multicore", "snow"),
                        ncpus = getOption("boot.ncpus", 1L), ...){

  dots = list(...)
  if ("boot.method" %in% names(dots)){
    stop("\nOnly nonparametric bootstrap supported for model type")
  }

  resi.default(model.full = model.full, model.reduced = model.reduced, data = data,
               coefficients = coefficients, overall = overall, nboot = nboot,
               anova = FALSE, vcovfunc = vcovfunc, store.boot = store.boot,
               vcov.args = vcov.args, unbiased = unbiased, alpha = alpha,
               boot.method = "nonparam", parallel = parallel, ncpus = ncpus, ...)

}

#' @describeIn resi RESI point and interval estimation for zeroinfl models
#' @export
resi.zeroinfl = resi.hurdle

#' @describeIn resi RESI point and interval estimation for GEE models
#' @export
resi.geeglm = function(model.full, model.reduced = NULL, data, anova = TRUE,
                        coefficients = TRUE, overall = TRUE, nboot = 1000,
                        alpha = 0.05, store.boot = FALSE,
                        unbiased = TRUE, parallel = c("no", "multicore", "snow"),
                        ncpus = getOption("boot.ncpus", 1L), ...){
  dots = list(...)
  if ("boot.method" %in% names(dots)){
    stop("\nOnly nonparametric bootstrap supported for model type")
  }

  if (missing(data)){
    data = model.full$data
    tryCatch(update(model.full, data = data), error = function(e){
      message("Updating model fit failed. Try rerunning with providing data argument")})
  }
  else{
    data = as.data.frame(data)
  }

  # Strip rows with NA in any model variable so that mod.dat passed to the
  # bootstrap contains only the complete cases used in model fitting.
  # This prevents cluster-size mismatches when geeglm is re-fitted in each
  # bootstrap replicate.
  model_vars_gee <- intersect(names(model.frame(model.full)), names(data))
  data <- data[complete.cases(data[, model_vars_gee, drop = FALSE]), , drop = FALSE]

  # id variable name
  id_var = as.character(model.full$call$id)

  # bootstrapping
  output = resi.default(model.full = model.full, model.reduced = model.reduced,
                        data = data, anova = anova, coefficients = coefficients,
                        overall = overall, nboot = nboot,
                        store.boot = TRUE,
                        unbiased = unbiased, alpha = alpha,
                        parallel = parallel, ncpus = ncpus, boot.method = "nonparam",
                        cluster = TRUE, clvar = id_var, mod.dat = data, long = TRUE, ...)

  # number of bootstrap replicates with failed updating
  output$nfail = length(which(is.na(output$boot.results$t[,1])))


  if (!store.boot){
    output = output[names(output) != "boot.results"]
  }

  class(output) = "resi"

  return(output)
}

#' @describeIn resi RESI point and interval estimation for GEE models in glmtoolbox
#' @export
resi.glmgee = function(model.full, model.reduced = NULL, data, anova = FALSE,
                                     coefficients = TRUE, overall = TRUE, nboot = 1000,
                                     alpha = 0.05, store.boot = FALSE,
                                     unbiased = TRUE, parallel = c("no", "multicore", "snow"),
                                     ncpus = getOption("boot.ncpus", 1L), ...){
  # Capture the call to my_function
  call <- match.call()

  # Get the argument names and their default values
  formal_args <- formals(resi.glmgee)

  # Create a list of all arguments including defaults
  args_list <- as.list(formal_args)
  # remove ellipses
  args_list = args_list[-length(args_list)]

  # Update the list with the user's inputs from the call (if provided)
  for (name in names(call)[-1]) {  # Exclude the function name
    args_list[[name]] <- call[[name]]
  }
  do.call(resi.geeglm, args_list)
}

 #' @describeIn resi RESI point and interval estimation for GEE models
#' @export
resi.gee = function(model.full, data, nboot = 1000, alpha = 0.05,
                     store.boot = FALSE, unbiased = TRUE,
                     parallel = c("no", "multicore", "snow"),
                     ncpus = getOption("boot.ncpus", 1L), ...){
  dots = list(...)
  if ("boot.method" %in% names(dots)){
    stop("\nOnly nonparametric bootstrap supported for model type")
  }
  if (missing(data)){
    stop("\nData argument is required for GEE models from gee package")
  }
  else{
    data = as.data.frame(data)
  }

  id_var = as.character(model.full$call$id)

  # bootstrapping
  output = resi.default(model.full = model.full, model.reduced = NULL, data = data,
                        coefficients = TRUE, nboot = nboot,
                        store.boot = TRUE, anova = FALSE,
                        unbiased = unbiased, alpha = alpha, parallel = parallel,
                        ncpus = ncpus, boot.method = "nonparam", cluster = TRUE,
                        clvar = id_var, mod.dat = data, skip.red = TRUE, long = TRUE,
                        overall = FALSE, ...)

  # number of bootstrap replicates with failed updating
  output$nfail = length(which(is.na(output$boot.results$t[,1])))

  if (!store.boot){
    output = output[names(output) != "boot.results"]
  }

  class(output) = "resi"

  return(output)
}

#' @describeIn resi RESI point and interval estimation for LME (nlme) models
#' @importFrom nlme getGroups
#' @export
resi.lme = function(model.full, alpha = 0.05, nboot = 1000, anova = TRUE, vcovfunc = clubSandwich::vcovCR,
                     vcov.args = list(), ...){
  warning("\nConfidence Interval procedure not developed for lme, returning point estimates only")
  output = list(alpha = alpha, nboot = 0)
  # RESI point estimates
  output = c(output, resi_pe(model.full = model.full, vcovfunc = vcovfunc, vcov.args = vcov.args,
                             anova = anova, ...))
  output$boot.method = "nonparam"
  class(output) = "resi"
  return(output)
}

#' @describeIn resi RESI point and interval estimation for lmerMod models
#' @export
resi.lmerMod = function(model.full, alpha = 0.05, nboot = 1000, anova = TRUE,
                         vcovfunc = clubSandwich::vcovCR, vcov.args = list(), ...){
  warning("\nConfidence Interval procedure not developed for lmerMod, returning point estimates only")
  output = list(alpha = alpha, nboot = 0)
  output = c(output, resi_pe(model.full, vcovfunc = vcovfunc, vcov.args = vcov.args, anova = anova, ...)) # RESI point estimates
  output$boot.method = "nonparam"
  class(output) = "resi"
  return(output)
}


#' @describeIn resi RESI point and interval estimation for glmmTMB models - Gaussian only
#' @export
resi.glmmTMB = function(model.full, alpha = 0.05, nboot = 1000, anova = TRUE,
                        vcovfunc = clubSandwich::vcovCR, vcov.args = list(), ...){
  warning("\nConfidence Interval procedure not developed for glmmTMB, returning point estimates only")
  output = list(alpha = alpha, nboot = 0)
  output = c(output, resi_pe(model.full, vcovfunc = vcovfunc, vcov.args = vcov.args, anova = anova, ...)) # RESI point estimates
  output$boot.method = "nonparam"
  class(output) = "resi"
  return(output)
}

Try the RESI package in your browser

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

RESI documentation built on July 25, 2026, 1:07 a.m.