R/hypotheses_joint.R

Defines functions joint_test

joint_test <- function(
    object,
    joint_index = NULL,
    hypothesis = 0,
    joint_test = "f",
    df = NULL,
    vcov = TRUE
) {
    checkmate::assert_choice(joint_test, c("f", "chisq"))

    if (joint_test == "f") {
        checkmate::assert_numeric(df, len = 2, null.ok = TRUE)
    } else {
        checkmate::assert_numeric(df, len = 1, null.ok = TRUE)
    }

    # theta_hat: P x 1 vector of estimated parameters
    if (inherits(object, c("slopes", "comparisons"))) {
        nam <- object$term
        if ("contrast" %in% names(object)) {
            nam <- paste(nam, object$contrast)
        }
        theta_hat <- stats::setNames(object$estimate, nam)
    } else {
        theta_hat <- get_coef(object)
    }

    # index
    checkmate::assert(
        checkmate::check_integerish(
            joint_index,
            lower = 1,
            upper = length(theta_hat)
        ),
        checkmate::check_character(joint_index),
        checkmate::check_true(joint_index)
    )

    if (isTRUE(joint_index)) {
        joint_index <- seq_along(theta_hat)
    } else if (isTRUE(checkmate::check_string(joint_index))) {
        joint_index <- grep(joint_index, names(theta_hat), perl = TRUE)
    }

    # V_hat: estimated covariance matrix
    V_hat <- get_vcov(object, vcov = vcov)

    # R: Q x P matrix for testing Q hypotheses on P parameters
    # build R matrix based on joint_index
    R <- matrix(0, nrow = length(joint_index), ncol = length(theta_hat))
    for (i in seq_along(joint_index)) {
        if (is.numeric(joint_index)) {
            R[i, joint_index[i]] <- 1
        } else {
            R[i, which(names(theta_hat) == joint_index[i])] <- 1
        }
    }

    # null hypothesis
    checkmate::assert(
        checkmate::check_number(hypothesis),
        checkmate::check_numeric(hypothesis, len = nrow(R)),
        checkmate::check_null(hypothesis)
    )
    if (is.null(hypothesis)) hypothesis <- 0
    r <- matrix(hypothesis, nrow = nrow(R), ncol = 1)

    # Calculate the difference between R*theta_hat and r
    diff <- R %*% theta_hat - r

    # Calculate the inverse of R*(V_hat/n)*R'
    inv <- solve(R %*% V_hat %*% t(R))

    # Calculate the Wald test statistic
    if (joint_test == "f") {
        wald_statistic <- t(diff) %*% inv %*% diff / dim(R)[1] # Q is the number of rows in R
    } else if (joint_test == "chisq") {
        wald_statistic <- t(diff) %*% inv %*% diff # Not normalized for chi-squared joint_test
    }

    # Degrees of freedom
    if (is.null(df)) {
        df1 <- dim(R)[1] # Q

        if (joint_test == "f") {
            df2 <- tryCatch(
                insight::get_df(attr(object, "model")),
                error = function(e) NULL
            )
            if (is.null(df2)) tryCatch(insight::get_df(object), error = function(e) NULL)
            if (is.null(df2)) {
                # n: sample size
                n <- tryCatch(stats::nobs(object), error = function(e) NULL)
                if (is.null(n))
                    n <- tryCatch(
                        stats::nobs(attr(object, "model")),
                        error = function(e) NULL
                    )
                if (is.null(n))
                    insight::format_error(
                        "Could not extract sample size from model object."
                    )

                if (inherits(object, "lme")) {
                    msg <- "The `hypotheses()` functions uses simple heuristics to select degrees of freedom for this test. See the relevant section in `?hypotheses`. These rules are likely to yield inappropriate results for models of class `%s`. Please supply degrees of freedom values explicitly via the `df` argument."
                    msg <- sprintf(msg, class(object)[1])
                    insight::format_warning(msg)
                }

                df2 <- n - length(theta_hat) # n - P
            }
        }
    } else {
        df1 <- df[1]

        if (joint_test == "f") {
            df2 <- df[2]
        }
    }

    # Calculate the p-value
    if (joint_test == "f") {
        p_value <- 1 - stats::pf(wald_statistic, df1, df2)
    } else if (joint_test == "chisq") {
        p_value <- 1 - stats::pchisq(wald_statistic, df1)
        df2 <- NULL
    }

    # Return the Wald joint_test statistic and p-value
    out <- data.frame(statistic = drop(wald_statistic), p.value = drop(p_value))
    class(out) <- c("hypotheses", "data.frame")
    if (joint_test == "f") {
        attr(out, "statistic_label") <- "F"
    } else if (joint_test == "chisq") {
        attr(out, "statistic_label") <- "ChiSq"
    }

    # degrees of freedom print
    if (joint_test == "f") {
        out$df1 <- df1
        out$df2 <- df2
    } else {
        out$df <- df1
    }

    # Create the print_head string
    print_head <- "\nJoint hypothesis test:\n"
    if (is.character(joint_index)) {
        for (i in joint_index) {
            print_head <- paste0(print_head, i, sprintf(" = %s\n", hypothesis))
        }
    } else if (
        inherits(
            object,
            c("marginaleffects", "comparisons", "slopes", "marginal_means")
        )
    ) {
        tmp <- paste0(
            get_labels(object, idx = joint_index),
            sprintf(" = %s\n", hypothesis)
        )
        print_head <- c(print_head, tmp)
    } else {
        tmp <- paste0(
            get_labels(stats::coef(object), idx = joint_index),
            sprintf(" = %s\n", hypothesis)
        )
        print_head <- c(print_head, tmp)
    }
    attr(out, "print_head") <- print_head

    return(out)
}

Try the marginaleffects package in your browser

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

marginaleffects documentation built on June 8, 2025, 12:44 p.m.