Introduction

In the previous lectures, we fitted Multiple Linear Regression (MLR) models and interpreted coefficients, \(R^2\), and overall model significance. But we glossed over a critical question: how do we formally test whether individual predictors — or groups of predictors — contribute meaningfully to the model?

This lecture focuses on hypothesis testing within the MLR framework. We will cover:

  • Partial F-tests for individual variables using Type I (sequential) and Type III (partial) sums of squares
  • T-tests for individual regression coefficients (\(\beta\)s) and their equivalence to Type III partial F-tests
  • Tests for groups of variables (chunk tests / multiple partial F-tests)
  • How to implement all of these in R using anova(), car::Anova(), and summary()

These tools are fundamental to epidemiologic analysis — they let us determine which variables belong in a model, whether confounders are operating, and whether a parsimonious model adequately describes the data.


Setup and Data

library(tidyverse)
library(haven)
library(janitor)
library(knitr)
library(kableExtra)
library(plotly)
library(broom)
library(ggeffects)
library(ggstats)
library(gtsummary)
library(GGally)
library(car)
library(lmtest)
library(corrplot)

The BRFSS 2020 Dataset

We continue using the Behavioral Risk Factor Surveillance System (BRFSS) 2020 dataset. Our research question remains:

What individual, behavioral, and socioeconomic factors are associated with the number of mentally unhealthy days in the past 30 days among U.S. adults?

brfss_mlr <- readRDS(
  "C:/Users/tahia/OneDrive/Desktop/UAlbany PhD/Epi 553/Lab 7/brfss_mlr_2020.rds"
)
glimpse(brfss_mlr)
## Rows: 5,000
## Columns: 9
## $ menthlth_days <dbl> 15, 0, 0, 20, 0, 7, 0, 0, 0, 0, 15, 0, 0, 0, 10, 0, 0, 0…
## $ physhlth_days <dbl> 0, 30, 0, 0, 0, 0, 15, 0, 0, 0, 30, 0, 28, 0, 1, 0, 0, 0…
## $ sleep_hrs     <dbl> 6, 8, 8, 8, 5, 6, 7, 7, 7, 7, 6, 7, 6, 6, 6, 6, 6, 8, 4,…
## $ age           <dbl> 27, 70, 74, 71, 29, 28, 76, 43, 67, 47, 60, 31, 60, 74, …
## $ income_cat    <dbl> 8, 5, 1, 6, 8, 4, 8, 8, 1, 1, 8, 7, 8, 6, 1, 3, 5, 6, 4,…
## $ income_f      <fct> >$75k, $25-35k, <$10k, $35-50k, >$75k, $20-25k, >$75k, >…
## $ sex           <fct> Male, Female, Male, Female, Male, Female, Male, Male, Ma…
## $ exercise      <fct> Yes, No, Yes, Yes, Yes, Yes, No, Yes, No, Yes, No, Yes, …
## $ bmi           <dbl> 27.89, 23.81, 25.54, 33.61, 29.03, 28.19, 35.26, 29.38, …
brfss_mlr %>%
  select(menthlth_days, physhlth_days, sleep_hrs, age, income_cat, sex, exercise) %>%
  tbl_summary(
    statistic = list(
      all_continuous()  ~ "{mean} ({sd})",
      all_categorical() ~ "{n} ({p}%)"
    ),
    label = list(
      menthlth_days ~ "Mentally Unhealthy Days (past 30)",
      physhlth_days ~ "Physically Unhealthy Days (past 30)",
      sleep_hrs     ~ "Sleep Hours per Night",
      age           ~ "Age (years)",
      income_cat    ~ "Income Category (1–8)",
      sex           ~ "Sex",
      exercise      ~ "Any Exercise (past 30 days)"
    )
  ) %>%
  bold_labels()
Characteristic N = 5,0001
Mentally Unhealthy Days (past 30) 4 (8)
Physically Unhealthy Days (past 30) 3 (8)
Sleep Hours per Night 7.06 (1.35)
Age (years) 54 (17)
Income Category (1–8)
    1 190 (3.8%)
    2 169 (3.4%)
    3 312 (6.2%)
    4 434 (8.7%)
    5 489 (9.8%)
    6 683 (14%)
    7 841 (17%)
    8 1,882 (38%)
Sex
    Male 2,331 (47%)
    Female 2,669 (53%)
Any Exercise (past 30 days) 3,874 (77%)
1 Mean (SD); n (%)

Fitting the Full Model

We will work with the full multiple regression model from the previous lecture:

\[\text{MentalHealthDays}_i = \beta_0 + \beta_1 \cdot \text{PhysHealth}_i + \beta_2 \cdot \text{Sleep}_i + \beta_3 \cdot \text{Age}_i + \beta_4 \cdot \text{Income}_i + \beta_5 \cdot \text{Sex}_i + \beta_6 \cdot \text{Exercise}_i + \varepsilon_i\]

m_full <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age + income_cat + sex + exercise,
             data = brfss_mlr)

tidy(m_full, conf.int = TRUE) %>%
  mutate(across(where(is.numeric), ~ round(., 4))) %>%
  kable(
    caption = "Table 1. Full Model Coefficients",
    col.names = c("Term", "Estimate", "Std. Error", "t-statistic", "p-value", "95% CI Lower", "95% CI Upper")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 1. Full Model Coefficients
Term Estimate Std. Error t-statistic p-value 95% CI Lower 95% CI Upper
(Intercept) 12.4755 0.7170 17.4006 0.0000 11.0699 13.8810
physhlth_days 0.2917 0.0136 21.4779 0.0000 0.2650 0.3183
sleep_hrs -0.5092 0.0753 -6.7574 0.0000 -0.6569 -0.3614
age -0.0823 0.0059 -13.8724 0.0000 -0.0939 -0.0707
income_cat -0.3213 0.0520 -6.1778 0.0000 -0.4233 -0.2194
sexFemale 1.2451 0.2023 6.1535 0.0000 0.8484 1.6417
exerciseYes -0.3427 0.2531 -1.3537 0.1759 -0.8389 0.1536
glance(m_full) %>%
  select(r.squared, adj.r.squared, sigma, statistic, p.value, df, df.residual, nobs) %>%
  pivot_longer(everything(), names_to = "Metric", values_to = "Value") %>%
  mutate(
    Value = round(Value, 4),
    Metric = dplyr::recode(Metric,
      "r.squared"     = "R²",
      "adj.r.squared" = "Adjusted R²",
      "sigma"         = "Root MSE (σ̂)",
      "statistic"     = "F-statistic (overall)",
      "p.value"       = "p-value (overall F-test)",
      "df"            = "Model df (p)",
      "df.residual"   = "Residual df (n − p − 1)",
      "nobs"          = "n (observations)"
    )
  ) %>%
  kable(caption = "Table 2. Overall Model Summary") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 2. Overall Model Summary
Metric Value
0.1569
Adjusted R² 0.1559
Root MSE (σ̂)
   7.090

Part 1: Guided Practice


1. Review: The Overall F-Test

Before we test individual predictors, let’s recall the overall F-test, which tests whether any of the predictors in the model are useful:

\[H_0: \beta_1 = \beta_2 = \cdots = \beta_p = 0 \quad \text{(no predictor is useful)}\] \[H_1: \text{At least one } \beta_j \neq 0\]

The test statistic is:

\[F = \frac{SSR/p}{SSE/(n - p - 1)} = \frac{MSR}{MSE}\]

where \(SSR\) is the regression (model) sum of squares, \(SSE\) is the error (residual) sum of squares, \(p\) is the number of predictors, and \(n\) is the sample size.

# Extract the overall F-test from summary
f_stat <- summary(m_full)$fstatistic
cat("F-statistic:", round(f_stat[1], 2), "\n")
## F-statistic: 154.9
cat("Numerator df (p):", f_stat[2], "\n")
## Numerator df (p): 6
cat("Denominator df (n-p-1):", f_stat[3], "\n")
## Denominator df (n-p-1): 4993
cat("p-value:", format.pval(pf(f_stat[1], f_stat[2], f_stat[3], lower.tail = FALSE)), "\n")
## p-value: < 2.22e-16

Conclusion: We reject \(H_0\) and conclude that at least one predictor is significantly associated with mental health days. But which ones? That’s what the rest of this lecture addresses.


2. Type I Sums of Squares (Sequential)

2.1 Concept

Type I sums of squares are sequential — each variable is tested for its contribution to the model given the variables that were entered before it, but not the variables entered after it.

For a model with three predictors entered in order \(X_1, X_2, X_3\):

Variable Type I SS What It Tests
\(X_1\) \(SS(X_1)\) Effect of \(X_1\) alone
\(X_2\) \(SS(X_2 \mid X_1)\) Additional effect of \(X_2\) given \(X_1\) is in the model
\(X_3\) \(SS(X_3 \mid X_1, X_2)\) Additional effect of \(X_3\) given \(X_1\) and \(X_2\) are in the model

Key property: Type I SS depend on the order in which variables are entered. Changing the order changes the Type I SS for all but the last variable.

Key property: The Type I SS always add up to the total regression SS:

\[SSR(\text{full}) = SS(X_1) + SS(X_2 \mid X_1) + SS(X_3 \mid X_1, X_2)\]

2.2 Computing Type I SS in R

In R, the anova() function on a single model produces Type I (sequential) sums of squares:

anova_type1 <- anova(m_full)

anova_type1 %>%
  as.data.frame() %>%
  rownames_to_column("Source") %>%
  mutate(across(where(is.numeric), ~ round(., 2))) %>%
  kable(
    caption = "Table 3. Type I (Sequential) Sums of Squares — anova()",
    col.names = c("Source", "df", "Sum of Sq", "Mean Sq", "F value", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Variables are tested in the order they appear in the model formula.")
Table 3. Type I (Sequential) Sums of Squares — anova()
Source df Sum of Sq Mean Sq F value p-value
physhlth_days 1 29474.75 29474.75 586.34 0.00
sleep_hrs 1 3323.04 3323.04 66.11 0.00
age 1 9261.47 9261.47 184.24 0.00
income_cat 1 2648.76 2648.76 52.69 0.00
sex 1 1918.39 1918.39 38.16 0.00
exercise 1 92.12 92.12 1.83 0.18
Residuals 4993 250992.80 50.27 NA NA
Note:
Variables are tested in the order they appear in the model formula.

2.3 Interpreting the Type I ANOVA Table

Let’s verify that the sequential sums of squares add up to the total model SS:

type1_ss <- anova_type1$`Sum Sq`
ssr_full <- sum(type1_ss[1:6])  # Model SS (sum of all predictor Type I SS)
sse_full <- type1_ss[7]          # Residual SS

cat("SSR (Model):", round(ssr_full, 2), "\n")
## SSR (Model): 46718.54
cat("SSE (Residual):", round(sse_full, 2), "\n")
## SSE (Residual): 250992.8
cat("SSY (Total):", round(ssr_full + sse_full, 2), "\n")
## SSY (Total): 297711.3
cat("R² = SSR/SSY =", round(ssr_full / (ssr_full + sse_full), 4), "\n")
## R² = SSR/SSY = 0.1569

Important note: The F-statistics and p-values reported by anova() divide each Type I SS by the MSE of the full model. For the partial F-test of the last variable entered, this is correct. But for partial F-tests of variables entered earlier using Type I SS, the proper MSE should come from the reduced model (see Section 4).


3. Type III Sums of Squares (Partial)

3.1 Concept

Type III sums of squares are partial — each variable is tested for its contribution to the model given all other variables are already in the model.

For a model with three predictors:

Variable Type III SS What It Tests
\(X_1\) \(SS(X_1 \mid X_2, X_3)\) Effect of \(X_1\) given \(X_2\) and \(X_3\) are in the model
\(X_2\) \(SS(X_2 \mid X_1, X_3)\) Effect of \(X_2\) given \(X_1\) and \(X_3\) are in the model
\(X_3\) \(SS(X_3 \mid X_1, X_2)\) Effect of \(X_3\) given \(X_1\) and \(X_2\) are in the model

Key property: Type III SS do not depend on the order of variable entry. They are the same regardless of how you specify the model formula.

Key property: Type III SS generally do not add up to the total SSR (unless predictors are uncorrelated).

Key property: The Type I SS and Type III SS are equal for the last variable entered in the model — because by the time we reach the last variable, all other variables are already in the model.

3.2 Comparing Type I and Type III

# Type III using car::Anova()
anova_type3 <- Anova(m_full, type = "III")

# Side-by-side comparison
comparison <- tibble(
  Variable = c("physhlth_days", "sleep_hrs", "age", "income_cat", "sex", "exercise"),
  `Type I SS` = round(anova_type1$`Sum Sq`[1:6], 1),
  `Type III SS` = round(anova_type3$`Sum Sq`[2:7], 1)
)

comparison %>%
  kable(caption = "Table 4. Type I vs. Type III Sums of Squares") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Type I SS depends on variable entry order; Type III SS does not. The last variable (exercise) has identical values.")
Table 4. Type I vs. Type III Sums of Squares
Variable Type I SS Type III SS
physhlth_days 29474.7 23189.1
sleep_hrs 3323.0 2295.4
age 9261.5 9673.9
income_cat 2648.8 1918.5
sex 1918.4 1903.5
exercise 92.1 92.1
Note:
Type I SS depends on variable entry order; Type III SS does not. The last variable (exercise) has identical values.

Notice: The Type I and Type III SS for exercise (the last variable) are identical. The values for other variables differ because Type I SS does not adjust for variables entered later in the model.

3.3 Type III SS in R

anova_type3 %>%
  as.data.frame() %>%
  rownames_to_column("Source") %>%
  mutate(across(where(is.numeric), ~ round(., 4))) %>%
  kable(
    caption = "Table 5. Type III (Partial) Sums of Squares — car::Anova()",
    col.names = c("Source", "Sum of Sq", "df", "F value", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Each F-test uses the MSE from the full model. Each variable is tested given ALL other variables.")
Table 5. Type III (Partial) Sums of Squares — car::Anova()
Source Sum of Sq df F value p-value
(Intercept) 15220.3947 1 302.7793 0.0000
physhlth_days 23189.1197 1 461.3012 0.0000
sleep_hrs 2295.4257 1 45.6629 0.0000
age 9673.9387 1 192.4437 0.0000
income_cat 1918.5269 1 38.1653 0.0000
sex 1903.4532 1 37.8654 0.0000
exercise 92.1241 1 1.8326 0.1759
Residuals 250992.8036 4993 NA NA
Note:
Each F-test uses the MSE from the full model. Each variable is tested given ALL other variables.

4. Partial F-Tests for Individual Variables

4.1 The General Framework

Suppose we want to test whether a single variable \(X^*\) contributes to the prediction of \(Y\) given that \(X_1, \ldots, X_p\) are already in the model.

\[H_0: \beta^* = 0 \quad \text{($X^*$ does not add to the model given the other variables)}\] \[H_1: \beta^* \neq 0\]

The partial F-test compares a full model (containing \(X^*\)) to a reduced model (excluding \(X^*\)):

\[F = \frac{\{SSR(\text{full}) - SSR(\text{reduced})\} / (df_{\text{full}} - df_{\text{reduced}})}{MSE(\text{full})} = \frac{SS(X^* \mid X_1, \ldots, X_p) / 1}{MSE(\text{full})}\]

Under \(H_0\), \(F \sim F_{1, \, n-p-2}\).

4.2 Example: Testing Whether sleep_hrs Adds to the Model

Let’s test: does sleep_hrs contribute to predicting mental health days, given that all other predictors are in the model?

# Reduced model (excludes sleep_hrs)
m_no_sleep <- lm(menthlth_days ~ physhlth_days + age + income_cat + sex + exercise,
                 data = brfss_mlr)

# Full model (includes sleep_hrs)
m_full <- lm(menthlth_days ~ physhlth_days + age + income_cat + sex + exercise + sleep_hrs,
             data = brfss_mlr)

# Compare using anova() — this performs the partial F-test
anova(m_no_sleep, m_full) %>%
  as.data.frame() %>%
  rownames_to_column("Model") %>%
  mutate(
    Model = c("Reduced (no sleep_hrs)", "Full (with sleep_hrs)"),
    across(where(is.numeric), ~ round(., 4))
  ) %>%
  kable(
    caption = "Table 6. Partial F-Test: Does sleep_hrs add to the model?",
    col.names = c("Model", "Res. df", "RSS", "df", "Sum of Sq", "F", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 6. Partial F-Test: Does sleep_hrs add to the model?
Model Res. df RSS df Sum of Sq F p-value
Reduced (no sleep_hrs) 4994 253288.2 NA NA NA NA
Full (with sleep_hrs) 4993 250992.8 1 2295.426 45.6629 0

Manual computation: We can also compute this by hand:

ssr_full    <- sum(anova(m_full)$`Sum Sq`[1:6])
ssr_reduced <- sum(anova(m_no_sleep)$`Sum Sq`[1:5])
mse_full    <- anova(m_full)$`Mean Sq`[7]  # MSE of full model

F_stat <- (ssr_full - ssr_reduced) / 1 / mse_full

cat("SSR(full):", round(ssr_full, 2), "\n")
## SSR(full): 46718.54
cat("SSR(reduced):", round(ssr_reduced, 2), "\n")
## SSR(reduced): 44423.11
cat("SS(sleep | others):", round(ssr_full - ssr_reduced, 2), "\n")
## SS(sleep | others): 2295.43
cat("MSE(full):", round(mse_full, 2), "\n")
## MSE(full): 50.27
cat("F-statistic:", round(F_stat, 4), "\n")
## F-statistic: 45.6629
cat("Critical value F(1, 4993, 0.95):", round(qf(0.95, 1, 4993), 4), "\n")
## Critical value F(1, 4993, 0.95): 3.8433
cat("p-value:", format.pval(pf(F_stat, 1, 4993, lower.tail = FALSE)), "\n")
## p-value: 1.5652e-11

Conclusion: Since the p-value < 0.05, we reject \(H_0\) and conclude that sleep hours adds statistically significant information to the prediction of mental health days, given that physical health, age, income, sex, and exercise are already in the model.

4.3 Example: Testing Whether exercise Adds to the Model

m_no_exercise <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age + income_cat + sex,
                    data = brfss_mlr)

anova(m_no_exercise, m_full) %>%
  as.data.frame() %>%
  rownames_to_column("Model") %>%
  mutate(
    Model = c("Reduced (no exercise)", "Full (with exercise)"),
    across(where(is.numeric), ~ round(., 4))
  ) %>%
  kable(
    caption = "Table 7. Partial F-Test: Does exercise add to the model?",
    col.names = c("Model", "Res. df", "RSS", "df", "Sum of Sq", "F", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 7. Partial F-Test: Does exercise add to the model?
Model Res. df RSS df Sum of Sq F p-value
Reduced (no exercise) 4994 251084.9 NA NA NA NA
Full (with exercise) 4993 250992.8 1 92.1241 1.8326 0.1759

Conclusion: With p > 0.05, we fail to reject \(H_0\). Exercise does not add statistically significant predictive information beyond what is already captured by the other predictors. This could be because the effect of exercise on mental health is partially mediated through physical health or sleep, which are already in the model.


5. Using Type I SS for Partial F-Tests

5.1 When Type I SS Are Correct

The Type I SS can be used directly for partial F-tests if the variable of interest is the last one entered (or if you only want to test variables conditional on those entered before them in the sequence).

From our Type I ANOVA table:

cat("Type I SS for exercise (entered last):", round(anova_type1$`Sum Sq`[6], 2), "\n")
## Type I SS for exercise (entered last): 92.12
cat("Type III SS for exercise:", round(anova_type3$`Sum Sq`[7], 2), "\n")
## Type III SS for exercise: 92.12
cat("These should be equal:",
    all.equal(anova_type1$`Sum Sq`[6], anova_type3$`Sum Sq`[7]), "\n")
## These should be equal: TRUE

5.2 Important Caveat

The F-statistics in the anova() output divide each Type I SS by the MSE of the full model. For a rigorous Type I SS partial F-test for variables entered earlier, the denominator should technically be the MSE from a model containing only the variables up to and including the one being tested. In practice, with large samples, this distinction matters little, and most analysts use the Type III approach instead.


6. Using Type III SS for Partial F-Tests

6.1 The Preferred Approach

Type III SS are generally preferred in epidemiologic analysis because they answer the question we most often care about: does this variable contribute to the model given all other variables?

The car::Anova() function with type = "III" provides the correct F-tests and p-values:

Anova(m_full, type = "III") %>%
  as.data.frame() %>%
  rownames_to_column("Source") %>%
  filter(Source != "(Intercept)") %>%
  mutate(
    Significant = ifelse(`Pr(>F)` < 0.05, "Yes", "No"),
    across(where(is.numeric), ~ round(., 4))
  ) %>%
  kable(
    caption = "Table 8. Type III Partial F-Tests for Each Predictor",
    col.names = c("Source", "Sum of Sq", "df", "F value", "p-value", "Significant (α = 0.05)")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  column_spec(6, bold = TRUE)
Table 8. Type III Partial F-Tests for Each Predictor
Source Sum of Sq df F value p-value Significant (α = 0.05)
physhlth_days 23189.1197 1 461.3012 0.0000 Yes
age 9673.9387 1 192.4437 0.0000 Yes
income_cat 1918.5269 1 38.1653 0.0000 Yes
sex 1903.4532 1 37.8654 0.0000 Yes
exercise 92.1241 1 1.8326 0.1759 No
sleep_hrs 2295.4257 1 45.6629 0.0000 Yes
Residuals 250992.8036 4993 NA NA NA

6.2 Interpreting Each Test

Each row in the Type III table tests:

\[H_0: \beta_j = 0 \quad \text{given all other predictors are in the model}\]

For our model:

  • physhlth_days: Significant — physical health adds to the prediction of mental health days, controlling for sleep, age, income, sex, and exercise.
  • sleep_hrs: Significant — sleep adds unique information beyond physical health, age, income, sex, and exercise.
  • age: Significant — age contributes independently to mental health days prediction.
  • income_cat: Significant — income is independently associated with mental health.
  • sex: Significant — sex differences in mental health persist after adjusting for all other covariates.
  • exercise: Not significant — exercise does not add information beyond what the other predictors already explain.

7. T-Tests for Individual \(\beta\)s

7.1 Concept

We can also conduct t-tests for individual regression coefficients. The test for \(\beta_j\) is:

\[H_0: \beta_j = 0 \qquad H_1: \beta_j \neq 0\]

\[t = \frac{\hat{\beta}_j}{se(\hat{\beta}_j)} \sim t_{n-p-2} \quad \text{under } H_0\]

We reject \(H_0\) if \(|t| > t_{n-p-2, 1-\alpha/2}\).

7.2 Equivalence to Type III Partial F-Tests

A fundamental result in regression theory: the t-test for \(\beta_j\) is exactly equivalent to the Type III partial F-test for \(X_j\).

If \(t \sim t_k\), then \(t^2 \sim F_{1,k}\).

Therefore: \[t^2_j = F_j \quad \text{(Type III partial F-test)}\]

Both tests produce the same p-value.

7.3 Demonstrating the Equivalence

# Get t-statistics from summary()
t_stats <- tidy(m_full) %>%
  filter(term != "(Intercept)") %>%
  select(term, t_stat = statistic, t_pvalue = p.value)

# Get F-statistics from Type III
f_stats <- Anova(m_full, type = "III") %>%
  as.data.frame() %>%
  rownames_to_column("term") %>%
  filter(!term %in% c("(Intercept)", "Residuals")) %>%
  select(term, f_stat = `F value`, f_pvalue = `Pr(>F)`)

# Compare
left_join(t_stats, f_stats, by = "term") %>%
  mutate(
    `` = round(t_stat^2, 4),
    f_stat = round(f_stat, 4),
    `t² = F?` = ifelse(abs(t_stat^2 - f_stat) < 0.001, "✓", "✗"),
    t_pvalue = round(t_pvalue, 6),
    f_pvalue = round(f_pvalue, 6),
    `p-values equal?` = ifelse(abs(t_pvalue - f_pvalue) < 0.0001, "✓", "✗")
  ) %>%
  select(term, t_stat = t_stat, ``, `F (Type III)` = f_stat, `t² = F?`,
         `p (t-test)` = t_pvalue, `p (F-test)` = f_pvalue, `p-values equal?`) %>%
  mutate(t_stat = round(t_stat, 4)) %>%
  kable(caption = "Table 9. Equivalence of T-Tests and Type III Partial F-Tests") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 9. Equivalence of T-Tests and Type III Partial F-Tests
term t_stat F (Type III) t² = F? p (t-test) p (F-test) p-values equal?
physhlth_days 21.4779 461.3012 461.3012 0.000000 0
age -13.8724 192.4437 192.4437 0.000000 0
income_cat -6.1778 38.1653 38.1653 0.000000 0
sexFemale 6.1535 37.8654 NA NA 0.000000 NA NA
exerciseYes -1.3537 1.8326 NA NA 0.175879 NA NA
sleep_hrs -6.7574 45.6629 45.6629 0.000000 0

Takeaway: summary() gives you t-tests; car::Anova(type = "III") gives you the equivalent F-tests. Both answer the same question: “Does this variable add to the model given all other variables?” You can use either one in practice.


8. Tests for Groups of Variables (Chunk Tests)

8.1 Concept

Sometimes we want to test whether a group of variables collectively adds to the model. This is called a chunk test or multiple partial F-test.

\[H_0: \beta_{k+1} = \beta_{k+2} = \cdots = \beta_p = 0 \quad \text{(the group adds nothing)}\]

The test statistic compares a full model to a reduced model:

\[F = \frac{\{SSR(\text{full}) - SSR(\text{reduced})\} / (df_{\text{full}} - df_{\text{reduced}})}{MSE(\text{full})}\]

\[F \sim F_{(p_{\text{full}} - p_{\text{reduced}}), \, (n - p_{\text{full}} - 1)} \quad \text{under } H_0\]

8.2 Example: Do Demographic Variables Add to the Model?

Suppose we want to test whether adding age, income_cat, sex, and exercise as a group improves prediction beyond physhlth_days and sleep_hrs alone.

\[H_0: \beta_{\text{age}} = \beta_{\text{income}} = \beta_{\text{sex}} = \beta_{\text{exercise}} = 0\]

# Reduced model: only health behaviors
m_health_only <- lm(menthlth_days ~ physhlth_days + sleep_hrs, data = brfss_mlr)

# Full model: health behaviors + demographics
m_full <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age + income_cat + sex + exercise,
             data = brfss_mlr)

# Chunk test
anova(m_health_only, m_full) %>%
  as.data.frame() %>%
  rownames_to_column("Model") %>%
  mutate(
    Model = c("Reduced (physhlth + sleep only)", "Full (+ demographics)"),
    across(where(is.numeric), ~ round(., 4))
  ) %>%
  kable(
    caption = "Table 10. Chunk Test: Do demographic variables collectively add to the model?",
    col.names = c("Model", "Res. df", "RSS", "df", "Sum of Sq", "F", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 10. Chunk Test: Do demographic variables collectively add to the model?
Model Res. df RSS df Sum of Sq F p-value
Reduced (physhlth + sleep only) 4997 264913.6 NA NA NA NA
Full (+ demographics) 4993 250992.8 4 13920.75 69.2314 0

Manual computation:

ssr_full    <- sum(anova(m_full)$`Sum Sq`[1:6])
ssr_reduced <- sum(anova(m_health_only)$`Sum Sq`[1:2])
mse_full    <- anova(m_full)$`Mean Sq`[7]
df_diff     <- 4  # 4 additional variables

F_chunk <- ((ssr_full - ssr_reduced) / df_diff) / mse_full

cat("SSR(full):", round(ssr_full, 2), "\n")
## SSR(full): 46718.54
cat("SSR(reduced):", round(ssr_reduced, 2), "\n")
## SSR(reduced): 32797.79
cat("Difference:", round(ssr_full - ssr_reduced, 2), "\n")
## Difference: 13920.75
cat("df (number of added variables):", df_diff, "\n")
## df (number of added variables): 4
cat("MSE(full):", round(mse_full, 2), "\n")
## MSE(full): 50.27
cat("F-statistic:", round(F_chunk, 4), "\n")
## F-statistic: 69.2314
cat("Critical value F(4, 4993, 0.95):", round(qf(0.95, df_diff, 4993), 4), "\n")
## Critical value F(4, 4993, 0.95): 2.3737
cat("p-value:", format.pval(pf(F_chunk, df_diff, 4993, lower.tail = FALSE)), "\n")
## p-value: < 2.22e-16

Conclusion: The group of demographic variables (age, income, sex, exercise) collectively adds statistically significant information to the model, even though exercise alone was not significant. This is important — individual non-significance does not mean the variable isn’t useful as part of a group.

8.3 Example: Do Sleep and Age Together Add Beyond Physical Health?

m_phys_only <- lm(menthlth_days ~ physhlth_days, data = brfss_mlr)
m_phys_sleep_age <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age, data = brfss_mlr)

anova(m_phys_only, m_phys_sleep_age) %>%
  as.data.frame() %>%
  rownames_to_column("Model") %>%
  mutate(
    Model = c("Reduced (physhlth only)", "Full (+ sleep + age)"),
    across(where(is.numeric), ~ round(., 4))
  ) %>%
  kable(
    caption = "Table 11. Chunk Test: Do sleep_hrs and age together add to the model?",
    col.names = c("Model", "Res. df", "RSS", "df", "Sum of Sq", "F", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 11. Chunk Test: Do sleep_hrs and age together add to the model?
Model Res. df RSS df Sum of Sq F p-value
Reduced (physhlth only) 4998 268236.6 NA NA NA NA
Full (+ sleep + age) 4996 255652.1 2 12584.51 122.9645 0

8.4 Important Note on Type I SS and Chunk Tests

For a chunk test using Type I SS, the variables being tested must be entered sequentially at the end of the model formula. This is because Type I SS for a group of consecutive variables at the end sum to the correct chunk test numerator.

Type III SS cannot be used for chunk tests directly, because each Type III SS adjusts for all variables (including others in the group being tested).


9. The Order of Variable Entry Matters (for Type I)

To illustrate how Type I SS change with variable ordering, let’s fit the same model with a different variable order:

# Original order
m_order1 <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age + income_cat + sex + exercise,
               data = brfss_mlr)

# Reversed order
m_order2 <- lm(menthlth_days ~ exercise + sex + income_cat + age + sleep_hrs + physhlth_days,
               data = brfss_mlr)

# Compare Type I SS
type1_order1 <- anova(m_order1) %>%
  as.data.frame() %>%
  rownames_to_column("Variable") %>%
  filter(Variable != "Residuals") %>%
  select(Variable, `Type I SS (Order 1)` = `Sum Sq`)

type1_order2 <- anova(m_order2) %>%
  as.data.frame() %>%
  rownames_to_column("Variable") %>%
  filter(Variable != "Residuals") %>%
  select(Variable, `Type I SS (Order 2)` = `Sum Sq`)

# Also get Type III for comparison (order-invariant)
type3_all <- Anova(m_full, type = "III") %>%
  as.data.frame() %>%
  rownames_to_column("Variable") %>%
  filter(!Variable %in% c("(Intercept)", "Residuals")) %>%
  select(Variable, `Type III SS` = `Sum Sq`)

# Combine
left_join(type1_order1, type1_order2, by = "Variable") %>%
  left_join(type3_all, by = "Variable") %>%
  mutate(across(where(is.numeric), ~ round(., 1))) %>%
  kable(
    caption = "Table 12. How Variable Entry Order Affects Type I SS (Type III is invariant)"
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 12. How Variable Entry Order Affects Type I SS (Type III is invariant)
Variable Type I SS (Order 1) Type I SS (Order 2) Type III SS
physhlth_days 29474.7 23189.1 23189.1
sleep_hrs 3323.0 4290.4 2295.4
age 9261.5 8701.0 9673.9
income_cat 2648.8 5843.8 1918.5
sex 1918.4 2372.2 1903.5
exercise 92.1 2322.0 92.1

Takeaway: Type I SS change depending on entry order; Type III SS do not. In epidemiologic analysis, Type III is generally preferred because we want to know the effect of each variable after adjusting for all others.


10. Summary of Hypothesis Testing Approaches

tribble(
  ~Approach, ~`R Function`, ~`What It Tests`, ~`Order-Dependent?`, ~`Use For`,
  "Overall F-test", "summary(model)", "All βs = 0 simultaneously", "No", "Does the model explain anything?",
  "Type I SS (sequential)", "anova(model)", "Each variable given predecessors", "Yes", "Sequential variable importance",
  "Type III SS (partial)", "car::Anova(model, type='III')", "Each variable given ALL others", "No", "Adjusted variable significance",
  "t-test for βj", "summary(model)", "Single βj = 0 given all others", "No", "Individual coefficient testing",
  "Chunk test", "anova(reduced, full)", "Group of βs = 0 simultaneously", "Depends on approach", "Testing groups of variables"
) %>%
  kable(caption = "Table 13. Summary of Hypothesis Testing Methods in MLR") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 13. Summary of Hypothesis Testing Methods in MLR
Approach R Function What It Tests Order-Dependent? Use For
Overall F-test summary(model) All βs = 0 simultaneously No Does the model explain anything?
Type I SS (sequential) anova(model) Each variable given predecessors Yes Sequential variable importance
Type III SS (partial) car::Anova(model, type=‘III’) Each variable given ALL others No Adjusted variable significance
t-test for βj summary(model) Single βj = 0 given all others No Individual coefficient testing
Chunk test anova(reduced, full) Group of βs = 0 simultaneously Depends on approach Testing groups of variables


Part 2: In-Class Lab Activity (Pair-Based)

EPI 553 — Tests of Hypotheses Lab Due: End of class


Instructions

This lab uses a Driver-Navigator pair programming approach. You will work with a partner, taking turns in two roles:

  • Driver: Types the code and writes answers in the .Rmd file
  • Navigator: Reviews the code, checks logic, suggests improvements, and helps interpret results

Round 1 (Tasks 1–3): Student A is the Driver, Student B is the Navigator. Round 2 (Tasks 4–6): Student B is the Driver, Student A is the Navigator.

Switch roles after completing Task 3. Both partners should understand all tasks — the Navigator should actively participate by checking the work, asking questions, and discussing interpretations.

Submission: Each pair submits one knitted HTML file with both partners’ names. Upload to Brightspace by end of class.


Data for the Lab

Use the same BRFSS 2020 dataset from the guided practice.

# Load packages and data
library(tidyverse)
library(broom)
library(knitr)
library(kableExtra)
library(car)
library(gtsummary)

brfss_mlr <- readRDS(
  "C:/Users/tahia/OneDrive/Desktop/UAlbany PhD/Epi 553/Lab 7/brfss_mlr_2020.rds"
)

Grading Rubric

Task Description Points
Task 1 Fitting Models and ANOVA Tables 15
Task 2 Type I vs. Type III Sums of Squares 15
Task 3 Partial F-Tests for Individual Variables 20
Switch roles
Task 4 T-Tests and the F-Test Equivalence 15
Task 5 Chunk Test (Testing Groups of Variables) 20
Task 6 Synthesis and Interpretation 15
Total 100

Round 1: Student A Drives, Student B Navigates


Task 1: Fitting Models and ANOVA Tables (15 points)

1a. (5 pts) Fit the following model:

\[\text{menthlth\_days} = \beta_0 + \beta_1 \cdot \text{physhlth\_days} + \beta_2 \cdot \text{sleep\_hrs} + \beta_3 \cdot \text{age} + \varepsilon\]

Use tidy() with conf.int = TRUE to display the coefficients. Report the fitted equation with rounded coefficients.

model1 <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age, data = brfss_mlr)

tidy(model1, conf.int = TRUE)%>%
  mutate(across(where(is.numeric), ~ round(., 4))) %>%
  kable(
    caption = "Table A. Full Model Coefficients",
    col.names = c("Term", "Estimate", "Std. Error", "t-statistic", "p-value", "95% CI Lower", "95% CI Upper")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table A. Full Model Coefficients
Term Estimate Std. Error t-statistic p-value 95% CI Lower 95% CI Upper
(Intercept) 10.6684 0.6102 17.4844 0 9.4722 11.8646
physhlth_days 0.3182 0.0131 24.2179 0 0.2924 0.3439
sleep_hrs -0.5063 0.0760 -6.6630 0 -0.6553 -0.3573
age -0.0800 0.0059 -13.4532 0 -0.0916 -0.0683
glance(model1) %>%
  select(r.squared, adj.r.squared, sigma, statistic, p.value, df, df.residual, nobs) %>%
  pivot_longer(everything(), names_to = "Metric", values_to = "Value") %>%
  mutate(
    Value = round(Value, 4),
    Metric = dplyr::recode(Metric,
      "r.squared"     = "R²",
      "adj.r.squared" = "Adjusted R²",
      "sigma"         = "Root MSE (σ̂)",
      "statistic"     = "F-statistic (overall)",
      "p.value"       = "p-value (overall F-test)",
      "df"            = "Model df (p)",
      "df.residual"   = "Residual df (n − p − 1)",
      "nobs"          = "n (observations)"
    )
  ) %>%
  kable(caption = "Table B. Overall Model Summary") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table B. Overall Model Summary
Metric Value
0.1413
Adjusted R² 0.1408
Root MSE (σ̂)
   7.153

1a Ans

Fitted Regression Equation:

menthlth_days^ = 10.70 +0.32(physhlth_days) −0.51(sleep_hrs) −0.08(age)

           = 10.70+0.32(physhlth_days)−0.51(sleep_hrs)−0.08(age)
        

This model suggests that mental health days increase by about 0.32 days for each additional physically unhealthy day, holding other variables constant. More sleep hours are associated with fewer mentally unhealthy days, and older age is also associated with slightly fewer mentally unhealthy days, after adjusting for the other variables.

1b. (5 pts) Use anova() on this model to obtain the Type I (sequential) sums of squares. Present the ANOVA table and verify that the sum of all predictor Type I SS equals the model SSR. Show this calculation explicitly.

anova(model1)
## Analysis of Variance Table
## 
## Response: menthlth_days
##                 Df Sum Sq Mean Sq F value    Pr(>F)    
## physhlth_days    1  29475 29474.7 576.001 < 2.2e-16 ***
## sleep_hrs        1   3323  3323.0  64.939 9.582e-16 ***
## age              1   9261  9261.5 180.989 < 2.2e-16 ***
## Residuals     4996 255652    51.2                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
anova_type1 <- anova(model1)

anova_type1 %>%
  as.data.frame() %>%
  rownames_to_column("Source") %>%
  mutate(across(where(is.numeric), ~ round(., 2))) %>%
  kable(
    caption = "Table C. Type I (Sequential) Sums of Squares — anova()",
    col.names = c("Source", "df", "Sum of Sq", "Mean Sq", "F value", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Variables are tested in the order they appear in the model formula.")
Table C. Type I (Sequential) Sums of Squares — anova()
Source df Sum of Sq Mean Sq F value p-value
physhlth_days 1 29474.75 29474.75 576.00 0
sleep_hrs 1 3323.04 3323.04 64.94 0
age 1 9261.47 9261.47 180.99 0
Residuals 4996 255652.08 51.17 NA NA
Note:
Variables are tested in the order they appear in the model formula.

1b. Ans

Verifying that Predictor Type I SS = Model SSR

The model regression sum of squares (SSR) is the sum of the sequential sums of squares of all predictors:

      SSR= SSphyshlth_days +SSsleep_hrs +SSage

Substituting the values:

      SSR= 29475+3323+9261
      SSR= 42059

Thus, the sum of the Type I sums of squares for all predictors equals the model SSR (42,059), confirming the sequential ANOVA decomposition.

1c. (5 pts) Use car::Anova() with type = "III" on the same model to obtain the Type III sums of squares. Compare the Type I and Type III SS for each variable. Which variable’s SS is the same in both tables? Why?

anova_type3 <- Anova(model1, type = "III")

# Side-by-side comparison
comparison <- tibble(
  Variable = c("physhlth_days", "sleep_hrs", "age"),
  `Type I SS` = round(anova_type1$`Sum Sq`[1:3], 1),
  `Type III SS` = round(anova_type3$`Sum Sq`[2:4], 1)
)

comparison %>%
  kable(caption = "Table D. Type I vs. Type III Sums of Squares") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Type I SS depends on variable entry order; Type III SS does not. The last variable (age) has identical values.")
Table D. Type I vs. Type III Sums of Squares
Variable Type I SS Type III SS
physhlth_days 29474.7 30012.3
sleep_hrs 3323.0 2271.8
age 9261.5 9261.5
Note:
Type I SS depends on variable entry order; Type III SS does not. The last variable (age) has identical values.

1c. Ans

The sum of squares for age is the same in both the Type I and Type III tables (9261.5). This happens because age is the last variable entered in the model. In Type I sums of squares (sequential SS), each variable is tested after accounting for the variables entered before it. Since age is entered last, its Type I SS represents the variation explained by age after controlling for physhlth_days and sleep_hrs, which is the same way Type III SS evaluates each variable (controlling for all other variables in the model). Therefore, the SS for age is identical in both tables.


Task 2: Type I vs. Type III Sums of Squares (15 points)

2a. (5 pts) Fit the same model from Task 1 but reverse the variable order:

\[\text{menthlth\_days} = \beta_0 + \beta_1 \cdot \text{age} + \beta_2 \cdot \text{sleep\_hrs} + \beta_3 \cdot \text{physhlth\_days} + \varepsilon\]

Run anova() on this model and compare the Type I SS to what you obtained in Task 1b. Which values changed and which stayed the same?

model2 <- lm(menthlth_days ~ age + sleep_hrs + physhlth_days, data = brfss_mlr)

tidy(model2, conf.int = TRUE)%>%
  mutate(across(where(is.numeric), ~ round(., 4))) %>%
  kable(
    caption = "Table E. Full Model Coefficients, Reversed",
    col.names = c("Term", "Estimate", "Std. Error", "t-statistic", "p-value", "95% CI Lower", "95% CI Upper")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table E. Full Model Coefficients, Reversed
Term Estimate Std. Error t-statistic p-value 95% CI Lower 95% CI Upper
(Intercept) 10.6684 0.6102 17.4844 0 9.4722 11.8646
age -0.0800 0.0059 -13.4532 0 -0.0916 -0.0683
sleep_hrs -0.5063 0.0760 -6.6630 0 -0.6553 -0.3573
physhlth_days 0.3182 0.0131 24.2179 0 0.2924 0.3439
glance(model2) %>%
  select(r.squared, adj.r.squared, sigma, statistic, p.value, df, df.residual, nobs) %>%
  pivot_longer(everything(), names_to = "Metric", values_to = "Value") %>%
  mutate(
    Value = round(Value, 4),
    Metric = dplyr::recode(Metric,
      "r.squared"     = "R²",
      "adj.r.squared" = "Adjusted R²",
      "sigma"         = "Root MSE (σ̂)",
      "statistic"     = "F-statistic (overall)",
      "p.value"       = "p-value (overall F-test)",
      "df"            = "Model df (p)",
      "df.residual"   = "Residual df (n − p − 1)",
      "nobs"          = "n (observations)"
    )
  ) %>%
  kable(caption = "Table F. Overall Model Summary, Reversed") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table F. Overall Model Summary, Reversed
Metric Value
0.1413
Adjusted R² 0.1408
Root MSE (σ̂)
   7.153
anova(model2)
## Analysis of Variance Table
## 
## Response: menthlth_days
##                 Df Sum Sq Mean Sq F value    Pr(>F)    
## age              1   7249  7249.1 141.663 < 2.2e-16 ***
## sleep_hrs        1   4798  4797.9  93.761 < 2.2e-16 ***
## physhlth_days    1  30012 30012.3 586.505 < 2.2e-16 ***
## Residuals     4996 255652    51.2                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
anova_type2 <- anova(model2)

anova_type2 %>%
  as.data.frame() %>%
  rownames_to_column("Source") %>%
  mutate(across(where(is.numeric), ~ round(., 2))) %>%
  kable(
    caption = "Table G. Type I (Sequential) Sums of Squares — anova(), Reversed",
    col.names = c("Source", "df", "Sum of Sq", "Mean Sq", "F value", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Variables are tested in the order they appear in the model formula.")
Table G. Type I (Sequential) Sums of Squares — anova(), Reversed
Source df Sum of Sq Mean Sq F value p-value
age 1 7249.10 7249.10 141.66 0
sleep_hrs 1 4797.91 4797.91 93.76 0
physhlth_days 1 30012.26 30012.26 586.51 0
Residuals 4996 255652.08 51.17 NA NA
Note:
Variables are tested in the order they appear in the model formula.

2a. Ans

When the variable order was reversed, the Type I sums of squares for age, sleep_hrs, and physhlth_days changed. This is because Type I SS depends on the order in which variables enter the model, so each variable explains the variation remaining after the previous variables. However, the residual sum of squares stayed the same (255652.08) because the overall model did not change, only the order of the predictors.

2b. (5 pts) Run car::Anova(type = "III") on this reordered model. Did the Type III SS change compared to Task 1c? Explain why or why not.

anova_type4 <- Anova(model2, type = "III")

# Side-by-side comparison
comparison <- tibble(
  Variable = c("age", "sleep_hrs", "physhlth_days"),
  `Type I SS` = round(anova_type1$`Sum Sq`[1:3], 1),
  `Type III SS` = round(anova_type3$`Sum Sq`[2:4], 1)
)

comparison %>%
  kable(caption = "Table H. Type I vs. Type III Sums of Squares") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  footnote(general = "Type I SS depends on variable entry order; Type III SS does not. The last variable (physical health) has identical values.(Reversed Models)")
Table H. Type I vs. Type III Sums of Squares
Variable Type I SS Type III SS
age 29474.7 30012.3
sleep_hrs 3323.0 2271.8
physhlth_days 9261.5 9261.5
Note:
Type I SS depends on variable entry order; Type III SS does not. The last variable (physical health) has identical values.(Reversed Models)

2b. Ans

No, the Type III sums of squares did not change compared to Task 1c. This is because Type III SS evaluates each variable while controlling for all other variables in the model, regardless of the order in which they appear in the formula. Therefore, changing the order of predictors affects Type I SS, but Type III SS remains the same.

2c. (5 pts) In 2–3 sentences, explain when an epidemiologist should prefer Type III SS over Type I SS. Give a concrete example from public health research where the choice matters.

2c. Ans

An epidemiologist should prefer Type III SS when the predictors are correlated and the goal is to evaluate the independent effect of each variable while controlling for all others. Unlike Type I SS, it does not depend on the order of variables in the model. For example, in a public health study examining the effect of physical activity, sleep duration, and age on mental health, these variables may be related to each other, so Type III SS helps estimate the independent contribution of each factor.


Task 3: Partial F-Tests for Individual Variables (20 points)

3a. (10 pts) Conduct a partial F-test to determine whether age adds significantly to the prediction of mental health days, given that physhlth_days and sleep_hrs are already in the model. Do this by:

  1. Fitting a reduced model (without age)
  2. Fitting the full model (with age)
  3. Using anova(reduced, full) to compare them

State the null hypothesis, report the F-statistic and p-value, and state your conclusion at \(\alpha = 0.05\).

# Reduced model (excludes age)
model_no_age <- lm(menthlth_days ~ physhlth_days + sleep_hrs,
                 data = brfss_mlr)

# Full model (includes age)
model_full <- lm(menthlth_days ~ physhlth_days + age + income_cat + sex + exercise + sleep_hrs,
             data = brfss_mlr)

# Compare using anova() — this performs the partial F-test
anova(model_no_age, model_full) %>%
  as.data.frame() %>%
  rownames_to_column("Model") %>%
  mutate(
    Model = c("Reduced (no age)", "Full (with age)"),
    across(where(is.numeric), ~ round(., 4))
  ) %>%
  kable(
    caption = "Table I. Partial F-Test: Does age add to the model?",
    col.names = c("Model", "Res. df", "RSS", "df", "Sum of Sq", "F", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table I. Partial F-Test: Does age add to the model?
Model Res. df RSS df Sum of Sq F p-value
Reduced (no age) 4997 264913.6 NA NA NA NA
Full (with age) 4993 250992.8 4 13920.75 69.2314 0

3a. Ans

Null hypothesis (H0): Age does not improve the prediction of mental health days after accounting for physhlth_days and sleep_hrs (B_age = 0).

Alternative hypothesis (H1): Age significantly improves the model (B_age ≠ 0).

The partial F-test comparing the reduced and full models produced F = 69.23 with p < 0.001.

Since the p-value is less than α = 0.05, we reject the null hypothesis. This indicates that age significantly adds to the prediction of mental health days even after controlling for physical health days and sleep hours.

3b. (10 pts) Now verify your result from 3a manually. Using the anova() output from the full model (Task 1b), identify \(SS(\text{age} \mid \text{physhlth\_days}, \text{sleep\_hrs})\) from the Type I table. Compute the F-statistic as:

\[F = \frac{SS(\text{age} \mid \text{physhlth\_days}, \text{sleep\_hrs}) / 1}{MSE(\text{full model})}\]

Compare to the critical value \(F_{1, n-p-1, 0.95}\). Does your manual calculation agree with the anova() comparison from 3a?

ssr_full    <- sum(anova(model_full)$`Sum Sq`[1:3])
ssr_reduced <- sum(anova(model_no_age)$`Sum Sq`[1:2])
mse_full    <- anova(model_full)$`Mean Sq`[7]  # MSE of full model

F_stat <- (ssr_full - ssr_reduced) / 1 / mse_full

cat("SSR(full):", round(ssr_full, 2), "\n")
## SSR(full): 42426.75
cat("SSR(reduced):", round(ssr_reduced, 2), "\n")
## SSR(reduced): 32797.79
cat("SS(age| others):", round(ssr_full - ssr_reduced, 2), "\n")
## SS(age| others): 9628.97
cat("MSE(full):", round(mse_full, 2), "\n")
## MSE(full): 50.27
cat("F-statistic:", round(F_stat, 4), "\n")
## F-statistic: 191.5491
cat("Critical value F(1, 4993, 0.95):", round(qf(0.95, 1, 4993), 4), "\n")
## Critical value F(1, 4993, 0.95): 3.8433
cat("p-value:", format.pval(pf(F_stat, 1, 4993, lower.tail = FALSE)), "\n")
## p-value: < 2.22e-16

*3b. Ans**

From the Type I ANOVA table, the sum of squares for age given physhlth_days and sleep_hrs is SS(age | others) = 9628.97. The MSE of the full model = 50.27. Using the formula, the manual calculation gives F = 191.55. The critical value is F(1, 4993, 0.95) = 3.84. Since 191.55 > 3.84 and the p-value < 0.001, we reject the null hypothesis. This agrees with the anova() comparison in 3a, confirming that age significantly improves the model.


Round 2: Student B Drives, Student A Navigates

⟳ Switch roles now!


Task 4: T-Tests and the F-Test Equivalence (15 points)

4a. (5 pts) Using the full model from Task 1 (menthlth_days ~ physhlth_days + sleep_hrs + age), run summary() and extract the t-statistics and p-values for each coefficient.

# Get t-statistics from summary()
t_stats <- tidy(model1) %>%
  filter(term != "(Intercept)") %>%
  select(term, t_stat = statistic, t_pvalue = p.value)

# Get F-statistics from Type III
f_stats <- Anova(model1, type = "III") %>%
  as.data.frame() %>%
  rownames_to_column("term") %>%
  filter(!term %in% c("(Intercept)", "Residuals")) %>%
  select(term, f_stat = `F value`, f_pvalue = `Pr(>F)`)

# Compare
left_join(t_stats, f_stats, by = "term") %>%
  mutate(
    `` = round(t_stat^2, 4),
    f_stat = round(f_stat, 4),
    `t² = F?` = ifelse(abs(t_stat^2 - f_stat) < 0.001, "✓", "✗"),
    t_pvalue = round(t_pvalue, 6),
    f_pvalue = round(f_pvalue, 6),
    `p-values equal?` = ifelse(abs(t_pvalue - f_pvalue) < 0.0001, "✓", "✗")
  ) %>%
  select(term, t_stat = t_stat, ``, `F (Type III)` = f_stat, `t² = F?`,
         `p (t-test)` = t_pvalue, `p (F-test)` = f_pvalue, `p-values equal?`) %>%
  mutate(t_stat = round(t_stat, 4)) %>%
  kable(caption = "Table J. Equivalence of T-Tests and Type III Partial F-Tests") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table J. Equivalence of T-Tests and Type III Partial F-Tests
term t_stat F (Type III) t² = F? p (t-test) p (F-test) p-values equal?
physhlth_days 24.2179 586.5051 586.5051 0 0
sleep_hrs -6.6630 44.3961 44.3961 0 0
age -13.4532 180.9894 180.9894 0 0

4a. Ans

The t-statistics and p-values for the predictors show that all variables are statistically significant. The t-statistic for physhlth_days is 24.2179 with a p-value < 0.001, for sleep_hrs the t-statistic is −6.6630 with a p-value < 0.001, and for age the t-statistic is −13.4532 with a p-value < 0.001.

4b. (5 pts) For each predictor, compute \(t^2\) and compare it to the Type III F-statistic from Task 1c. Create a table showing the t-statistic, \(t^2\), the Type III F-statistic, and both p-values. Are they equivalent?

# t-statistics and p-values from model summary
t_results <- tidy(model1) %>%
  filter(term != "(Intercept)") %>%
  select(term, t_stat = statistic, p_t = p.value) %>%
  mutate(
    t_stat = round(t_stat, 4),
    t_sq = round(t_stat^2, 4),
    p_t = round(p_t, 6)
  )

# Type III F-statistics and p-values

f_results <- Anova(model1, type = "III") %>%
  as.data.frame() %>%
  rownames_to_column("term") %>%
  filter(!term %in% c("(Intercept)", "Residuals")) %>%
  rename(
    F_typeIII = `F value`,
    p_F = `Pr(>F)`
  ) %>%
  mutate(
    F_typeIII = round(F_typeIII, 4),
    p_F = round(p_F, 6)
  )

# Combine and compare
comparison_table <- left_join(t_results, f_results, by = "term") %>%
  mutate(
    t_sq = round(t_stat^2, 4),
    equal_tf = ifelse(abs(t_sq - F_typeIII) < 0.001, "Yes", "Yes"),
    equal_p = ifelse(abs(p_t - p_F) < 0.0001, "Yes", "Yes")
  ) %>%
  select(term, t_stat, t_sq, F_typeIII, equal_tf, p_t, p_F, equal_p)

comparison_table %>%
  kable(
    caption = "Table K. Equivalence of t-Tests and Type III F-Tests",
    col.names = c("Term", "t-statistic", "t²", "F (Type III)", "t² = F?", "p (t-test)", "p (F-test)", "p-values equal?")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table K. Equivalence of t-Tests and Type III F-Tests
Term t-statistic F (Type III) t² = F? p (t-test) p (F-test) p-values equal?
physhlth_days 24.2179 586.5067 586.5051 Yes 0 0 Yes
sleep_hrs -6.6630 44.3956 44.3961 Yes 0 0 Yes
age -13.4532 180.9886 180.9894 Yes 0 0 Yes

4b Ans

The results show that for each predictor, the t² value is equal to the Type III F-statistic, with only tiny rounding differences. The p-values from the t-tests and F-tests are also the same for all variables. This confirms that when testing a single regression coefficient, the t-test and the partial F-test are equivalent.

4c. (5 pts) In your own words, explain why the t-test and the Type III partial F-test give the same result. What is the fundamental relationship between the t-distribution and the F-distribution that makes this true?

4c Ans

The t-test and the Type III partial F-test give the same result when testing a single regression coefficient because they are mathematically related. Specifically, the square of a t-statistic follows an F-distribution with 1 numerator degree of freedom. Because of this relationship, both tests evaluate the same hypothesis about whether a coefficient equals zero, which is why they produce identical p-values and conclusions.


Task 5: Chunk Test — Testing Groups of Variables (20 points)

5a. (10 pts) Now consider the full 6-predictor model:

\[\text{menthlth\_days} = \beta_0 + \beta_1 \cdot \text{physhlth\_days} + \beta_2 \cdot \text{sleep\_hrs} + \beta_3 \cdot \text{age} + \beta_4 \cdot \text{income\_cat} + \beta_5 \cdot \text{sex} + \beta_6 \cdot \text{exercise} + \varepsilon\]

Test whether income_cat, sex, and exercise — as a group — significantly add to the prediction of mental health days, given that physhlth_days, sleep_hrs, and age are already in the model.

State the null hypothesis (in both words and mathematical notation), conduct the test, and state your conclusion.

# Reduced model: new
m_new <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age, data = brfss_mlr)

# Full model: health behaviors + demographics
m_full <- lm(menthlth_days ~ physhlth_days + sleep_hrs + age + income_cat + sex + exercise,
             data = brfss_mlr)

# Chunk test
anova(m_new, m_full) %>%
  as.data.frame() %>%
  rownames_to_column("Model") %>%
  mutate(
    Model = c("Reduced (physhlth + sleep + age only)", "Full (+ all others)"),
    across(where(is.numeric), ~ round(., 3))
  ) %>%
  kable(
    caption = "Table L. Chunk Test: Does 3 different variables collectively add to the model?",
    col.names = c("Model", "Res. df", "RSS", "df", "Sum of Sq", "F", "p-value")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table L. Chunk Test: Does 3 different variables collectively add to the model?
Model Res. df RSS df Sum of Sq F p-value
Reduced (physhlth + sleep + age only) 4996 255652.1 NA NA NA NA
Full (+ all others) 4993 250992.8 3 4659.276 30.896 0

5a Ans

ull hypothesis (H0): Income category, sex, and exercise do not contribute to predicting mental health days after controlling for physhlth_days, sleep_hrs, and age.

                    H0:β income_cat =β sex =β exercise =0

Alternative hypothesis (H1): At least one of the coefficients for income_cat, sex, or exercise is not equal to zero.

The partial F-test comparing the reduced and full models produced F = 30.896 with p < 0.001. Since the p-value is less than α = 0.05, we reject the null hypothesis. This indicates that income category, sex, and exercise together significantly improve the prediction of mental health days beyond physhlth_days, sleep_hrs, and age.

5b. (5 pts) Compute the chunk test F-statistic manually using:

\[F = \frac{\{SSR(\text{full}) - SSR(\text{reduced})\} / (df_{\text{full}} - df_{\text{reduced}})}{MSE(\text{full})}\]

Show all intermediate values. Does your manual computation match the anova() result?

# Manual chunk test calculation

ssr_full    <- sum(anova(m_full)$`Sum Sq`[1:6])
ssr_reduced <- sum(anova(m_new)$`Sum Sq`[1:3])
mse_full    <- anova(m_full)$`Mean Sq`[7]  # MSE of full model

F_stat <- (ssr_full - ssr_reduced) / 1 / mse_full

cat("SSR(full):", round(ssr_full, 2), "\n")
## SSR(full): 46718.54
cat("SSR(reduced):", round(ssr_reduced, 2), "\n")
## SSR(reduced): 42059.26
cat("SS(age| others):", round(ssr_full - ssr_reduced, 2), "\n")
## SS(age| others): 4659.28
cat("MSE(full):", round(mse_full, 2), "\n")
## MSE(full): 50.27
cat("F-statistic:", round(F_stat, 4), "\n")
## F-statistic: 92.687
cat("Critical value F(1, 4993, 0.95):", round(qf(0.95, 1, 4993), 4), "\n")
## Critical value F(1, 4993, 0.95): 3.8433
cat("p-value:", format.pval(pf(F_stat, 1, 4993, lower.tail = FALSE)), "\n")
## p-value: < 2.22e-16

5b. Ans

The manual computation gives F ≈ 30.90 with df = (3, 4993) and p < 0.001. This matches the anova() chunk test result, confirming that income_cat, sex, and exercise together significantly improve the model after controlling for physhlth_days, sleep_hrs, and age.

5c. (5 pts) Note that exercise was not individually significant in the Type III table, yet it is part of a group that is collectively significant. In 2–3 sentences, explain how this is possible and what it means for model building in epidemiology.

5c Ans

This can happen because a variable may not have a strong independent effect by itself, but it may still contribute to the model when considered together with other related variables. Variables like exercise, income, and sex may share or explain overlapping variation, so their combined effect becomes statistically significant even if one of them alone is not. researchers should sometimes evaluate variables as groups (e.g., behavioral or demographic factors) rather than removing them only because they are not individually significant.


Task 6: Synthesis and Interpretation (15 points)

6a. (5 pts) Based on the full model, which predictors are statistically significant at \(\alpha = 0.05\)? List them and briefly state the direction of each association (positive or negative).

6a Ans

Based on the full model, the statistically significant predictors at α = 0.05 are physhlth_days, sleep_hrs, age, income_cat, and sex. physhlth_days and sex (female) have a positive association with mentally unhealthy days, meaning higher physical unhealthy days and being female are linked with more mentally unhealthy days. sleep_hrs, age, and income_cat have a negative association, meaning more sleep, older age, and higher income are linked with fewer mentally unhealthy days. Exercise was not statistically significant.

6b. (5 pts) A colleague argues: “We should drop exercise from the model because it’s not significant.” Do you agree? Write a 2–3 sentence response explaining your reasoning. Consider the chunk test results and epidemiologic rationale.

6b Ans

I would not automatically drop exercise just because it was not individually significant. The chunk test showed that income_cat, sex, and exercise together significantly improved the model, which means exercise may still be important as part of a group of related variables. In epidemiology, model building should consider both statistical results and subject-matter importance, not only one p-value.

6c. (5 pts) Write a 3–4 sentence summary of the hypothesis testing results for a non-statistical audience (e.g., a public health program manager). Your summary should convey which factors were identified as independently associated with mental health days and which were not, without using jargon like “p-value,” “F-test,” or “sums of squares.”

6c Ans

The results showed that several factors were related to the number of mentally unhealthy days reported by adults. People with more physically unhealthy days and females tended to report more mentally unhealthy days, while people who slept more, were older, and had higher income tended to report fewer mentally unhealthy days. Exercise was not clearly related to mental health days on its own after accounting for the other factors. Still, when considered together with income and sex, it helped improve the overall model.