Activity 09 — T-Tests I: Setting Up the Test

Hypotheses, the t-statistic, and checking assumptions before we test

r-basics
project-setup

Hands-on companion to T-Tests I: state hypotheses, check normality and variance, and see where the t-statistic comes from.

Author

Bill Perry

Published

September 10, 2026

In-class Activity 9: Setting Up the Test

Recap from Activities 05–06

  • Loaded the leaf data with read_excel() and clean_names() into leaf_df
  • Used group_by() + summarize() to compute mean, SD, and SE per group
  • Checked for missing values with sum(!is.na())
  • Made boxplots with jittered points and mean ± SE plots with stat_summary() (Activity 03)
  • Prediction: shady-side leaves would be heavier than sunny-side leaves

Today’s Objectives

  1. See where the t-statistic comes from and why we default to Welch’s
  2. State null and alternate hypotheses formally before running any test
  3. Check the normality assumption — histogram and Shapiro-Wilk test
  4. Check the variance assumption — Levene’s test

How this activity works

You are building one R script this whole class, and you turn it in. Create it now: scripts/09_t_tests_1.R.

  • Every line you run goes in that script — not the Console, not this page. Type it, don’t paste it.
  • Start every code chunk in your script with a short # comment saying what it does. The comments are part of the grade.
  • The top of your script, in order: a title comment, then your library() calls, then the line that loads the data into leaf_df.
  • Code marked ▶ Run this is typed into your script exactly as shown. Code marked ✏️ Your turn is a change you make and run.
  • The Going further section is optional if you finish early.

🔮 Predict before you run

Before you run any ▶ Run this block, cover the output and predict what R will print. A t-test lives or dies on the p-value — committing to “above or below 0.05” before you see the real number is what forces you to reason about the test instead of skimming the output.


🧩 Chunk 1 — Setup & recap (after lecture Chunk 1)

Parts 1–2: load the data and recompute the descriptive stats.

Part 1 · Load libraries and data

Tip📂 Get the data

Same leaf file you have used since Activity 02 — it should already be in your project’s data/ folder. If not: 2026_09_03_data_sci_leaf_area.xlsx → put it in data/.

▶ Type this at the top of scripts/09_t_tests_1.R — in this exact order:

# ---- Activity 09: T-Tests I -----------------------------
# your name, today's date

# ---- Libraries -----------------------------------------
library(readxl)      # read Excel files
library(tidyverse)   # dplyr + ggplot2
library(janitor)     # clean_names()
library(car)         # Levene's test for variance

⚠️ Watch out! If R says "could not find function 'leveneTest'" you forgot library(car). Install it once with install.packages("car"), then load it every session.

# ---- Load data ----------------------------------------
leaf_df <- read_excel("data/2026_09_03_data_sci_leaf_area.xlsx") %>%
  clean_names()

glimpse(leaf_df)

✏️ Your turn: From the glimpse() output, fill in:

Number of rows:
Column holding the grouping variable (sunny/shady):
Column we are testing today (leaf mass):

Part 2 · Quick descriptive stats review

🔮 Predict first: Before running, guess the mean mass for each side (shady vs sunny) and the gap between them. Write your guesses, then check.

▶ Run this:

# Recap stats from Activity 06 — both groups at once ----
recap_df <- leaf_df %>%
  group_by(shade) %>%
  summarize(
    n         = sum(!is.na(mass_g)),
    mean_mass = round(mean(mass_g, na.rm = TRUE), 3),
    sd_mass   = round(sd(mass_g,   na.rm = TRUE), 3),
    se_mass   = round(sd_mass / sqrt(n), 3)
  )

recap_df

✏️ Your turn: Record the values you will need for the t-test formula:

Shady:  mean = _____ g   SD = _____   n = _____
Sunny:  mean = _____ g   SD = _____   n = _____
Difference in means = _____ g

✏️ Your turn: Looking at the SD values, do the two groups appear to have similar or different variance? (Does one group have much more spread than the other?)

Your observation:

🧩 Chunk 2 — Hypotheses & assumptions (after lecture Chunk 2)

Parts 3–6: state your hypotheses, then check normality and variance — before you test.

Part 3 · State your hypotheses FIRST

💡 Key idea: Always write down your hypotheses before you look at the data or run any test. This is how you stay scientifically honest.

✏️ Your turn: Write the null and alternate hypotheses for leaf mass. Use words first, then symbols.

In words:
  H₀ (null hypothesis):

  Hₐ (alternate hypothesis):

In symbols (use μ for "mean"):
  H₀:
  Hₐ:

Test type (circle one):   one-tailed   /   two-tailed

Significance level α:

✏️ Your turn: Why do we use a two-tailed test here even though we predicted shady leaves would be heavier?

Your answer:

Part 4 · Check normality — histogram

The t-test assumes that data within each group are approximately normally distributed.

▶ Run this:

# Histogram per group — look for a bell-shaped distribution
hist_norm_plot <- leaf_df %>%
  ggplot(aes(x = mass_g, fill = shade)) +
  geom_histogram(binwidth = 0.05, color = "white", alpha = 0.8) +
  facet_wrap(~shade, ncol = 2) +
  labs(
    title = "Leaf Mass Distribution by Shade",
    x     = "Leaf Mass (g)",
    y     = "Count"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

hist_norm_plot

✏️ Your turn: Describe the shape of each histogram:

Sunny side shape (bell-shaped, skewed, flat, bimodal?):
Shady side shape:
Any obvious outliers?  Y / N

Part 5 · Check normality — Shapiro-Wilk test

🔮 Predict first: Shapiro-Wilk’s H₀ is “data are normal.” From your histogram, predict whether p will be above or below 0.05 for each side before you run it.

  • H₀ (Shapiro-Wilk): the data are normally distributed
  • p > 0.05 → fail to reject normality → proceed with t-test
  • p < 0.05 → evidence of non-normality → consider alternatives

▶ Run this:

# Shapiro-Wilk for the sunny side --------------------
leaf_df %>%
  filter(shade == "sunny") %>%
  pull(mass_g) %>%
  shapiro.test()
# Shapiro-Wilk for the shady side -------------------
leaf_df %>%
  filter(shade == "shady") %>%
  pull(mass_g) %>%
  shapiro.test()

✏️ Your turn: Record the results and state your decision:

Sunny:  W = _____   p = _____   Decision (normal / not normal):
Shady:  W = _____   p = _____   Decision (normal / not normal):

⚠️ Watch out! One side may come back p < 0.05 while the other does not. A borderline Shapiro result with a roughly bell-shaped histogram is not a reason to abandon the t-test — the test is fairly robust to mild non-normality. Look at the histogram and the test together.


Part 6 · Check variance — Levene’s test

🔮 Predict first: Look back at the two SD values from Part 2. Predict whether Levene’s will call the variances equal (p > 0.05) or not, before you run it.

  • H₀ (Levene’s): the two groups have equal variance
  • p > 0.05 → variances are not significantly different
  • p < 0.05 → variances are significantly different

▶ Run this:

# Levene's test for equal variance -------------------
leveneTest(mass_g ~ shade, data = leaf_df)

✏️ Your turn: Record the result and decide:

F-statistic = _____   p = _____
Decision (variances equal / not equal):

✏️ Your turn: We are going to use Welch’s t-test (var.equal = FALSE) regardless of Levene’s result. In your own words, why is Welch’s the safe default even when Levene’s says variances are equal?

Your answer:

Part 7 · Review and checkpoint

At this point you should be able to:

✏️ Your turn: Run your entire script from top to bottom with Ctrl/Cmd + Shift + Enter (Source). Does it run without errors?

Ran cleanly?  Y / N
If not, what error appeared:

Part 8 · Going further

Optional — work through it if you finish early. Next lecture assumes you’ve completed Parts 1–7 only, not this section.

An alternative normality check: the QQ plot

A QQ (quantile-quantile) plot compares your data’s quantiles to what a perfect normal distribution would look like. Points on the diagonal line = normal.

▶ Try this:

# Q-Q plot — points should fall along the diagonal ----
qq_norm_plot <- leaf_df %>%
  ggplot(aes(sample = mass_g, color = shade)) +
  stat_qq() +
  stat_qq_line(color = "black", linewidth = 0.8) +
  facet_wrap(~shade, scales = "free") +
  labs(
    title = "Normal Q-Q Plots by Shade",
    x     = "Theoretical Quantiles",
    y     = "Sample Quantiles"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

qq_norm_plot
Do the points for each group fall approximately along the diagonal?
Does this agree with the histogram and Shapiro-Wilk?

Calculate t by hand

The lecture showed you where the t-statistic comes from. Try reproducing it yourself:

# Reproduce the t-statistic manually -----------------
m_sha <- recap_df$mean_mass[recap_df$shade == "shady"]
m_sun <- recap_df$mean_mass[recap_df$shade == "sunny"]
s_sha <- recap_df$sd_mass[recap_df$shade == "shady"]
s_sun <- recap_df$sd_mass[recap_df$shade == "sunny"]
n_sha <- recap_df$n[recap_df$shade == "shady"]
n_sun <- recap_df$n[recap_df$shade == "sunny"]

# Welch's SE of the difference
se_diff <- sqrt((s_sha^2 / n_sha) + (s_sun^2 / n_sun))

# t-statistic
t_manual <- (m_sha - m_sun) / se_diff

cat("Difference in means:", round(m_sha - m_sun, 3), "g\n")
cat("SE of difference:   ", round(se_diff, 4), "\n")
cat("Manual t:           ", round(t_manual, 3), "\n")
Manual t = _____   (write it down — you'll compare to t.test() next lecture)

Getting unstuck

  1. Read the error message out loud. R usually names the line and the problem.
  2. Check the usual suspects: loaded library(car), library(readxl), library(tidyverse), library(janitor)? Column spelling? Check names(leaf_df).
  3. leveneTest not found? You need library(car).
  4. Shapiro-Wilk needs > 3 values. If you filtered to a tiny subset, you may get an error.
  5. Cheat sheetshttps://posit.co/resources/cheatsheets/
  6. Bring the exact error (copy-paste it) to class, Canvas, or office hours.

End of Activity 09. Next: Activity 10 — T-Tests II: running the test, decoding the output, and writing a results sentence.