Activity: T-Tests: One, Two, Paired

Comparing means

Hands-on worksheet: hypothesis testing framework, assumption checks, and one-sample, two-sample (Student’s and Welch’s), and paired t-tests on the class pine needle dataset.
Author

Bill Perry

Worksheet: T-Tests — One, Two, Paired

How to use this worksheet

Work through each part in order, at your own pace. Type every line of code yourself into a plain R script — do not copy-paste. Blocks marked ▶ Run this are code you should type and execute. Blocks marked ✏️ Your turn ask you to write, modify, or answer something. Boxes marked 🚀 If you finish early are optional bonus material that goes a bit further than what we covered in lecture.


Part 1 · Setup — Load and De-Pseudoreplicate the Data

▶ Run this in your Script:

library(readxl)
library(tidyverse)
library(patchwork)
library(car)   # For diagnostic tests

pine_df        <- read_excel("data/class_pine needle length.xlsx")
pine_switch_df <- read_excel("data/class_pine needle length switched.xlsx")

head(pine_df)

✏️ Your turn: What is our true sample unit here — needles? branches? trees? sides? Why does that matter for how we analyze this data?

________________________________________________________

We need to average the sunny and shady sides so the data is not pseudoreplicated — multiple needles per tree side are not independent observations.

▶ Run this:

p_df <- pine_df %>%
  group_by(group, tree_no, tree_char, side) %>%
  summarise(length_mm = mean(length_mm, na.rm = TRUE), .groups = "drop")

ps_df <- pine_switch_df %>%
  group_by(group, tree_no, tree_char, side) %>%
  summarise(length_mm = mean(length_mm, na.rm = TRUE), .groups = "drop")

ps_shady_df <- ps_df %>% filter(side == "shady")
ps_sunny_df <- ps_df %>% filter(side == "sunny")

ps_shady_df %>% arrange(tree_no) %>% head()

⚠️ Watch out! class_pine needle length switched.xlsx has corrected sunny/shady labels compared to the original file. From here on we use ps_df — it’s the version that matches the field notes.

Tip

🚀 If you finish early: Write a group_by() + summarize() pipeline that gets n, mean_length, sd_length, and se_length for ps_df, grouped by side — the same summary the lecture built as stats_df.


Part 2 · Hypothesis Testing Framework

Every t-test starts the same way: state H₀ and Hₐ, and pick α before you look at the result.

✏️ Your turn: For the question “Do needle lengths differ between the sunny and shady sides?”, write out:

H0 = ________________________________________________________
Ha = ________________________________________________________
alpha = ________

✏️ Your turn: In your own words, what does a p-value actually tell you? (Hint: it is a statement about the data given H₀ — not a statement about whether H₀ is true.)

________________________________________________________
Tip

🚀 If you finish early: A two-sample t-test on 14 degrees of freedom produces an observed t-statistic of 2.31. Use 2 * (1 - pt(2.31, df = 14)) to compute the two-tailed p-value by hand, then compare it to the p-value table from lecture (p < 0.05 = moderate-to-strong evidence). Is H₀ rejected at α = 0.05?

# Write your code here:

Part 3 · One-Sample t-Test

A one-sample t-test compares a sample mean to a specific hypothesized value. Let’s test whether the mean needle length on the shady side is 15mm.

✏️ Your turn: Before testing, write the hypotheses:

H0: mu = _____ (the mean needle length on the shady side is _____ mm)
Ha: mu ≠ _____ (the mean needle length on the shady side is not _____ mm)

Check assumptions first — normality:

▶ Run this:

# The test below is about the SHADY side, so check normality of that group
# -- not the two sides pooled together.
qqPlot(ps_shady_df$length_mm,
       main = "QQ Plot — shady side needle length",
       ylab = "Sample Quantiles")

shapiro.test(ps_shady_df$length_mm)

✏️ Your turn: For the Shapiro-Wilk test, what is the null hypothesis? Do we want a significant or a non-significant result here? ________________________

✏️ Your turn: Why do we check the shady side on its own rather than ps_df$length_mm (both sides pooled)? What would pooling two groups with different means do to the shape of the distribution? ________________________

Check assumptions — outliers:

▶ Run this:

ps_df %>%
  ggplot(aes(x = side, y = length_mm, fill = side)) +
  geom_boxplot() +
  labs(x = "side", y = "Length (mm)", fill = "side")

Run the one-sample t-test:

▶ Run this:

ps_shade_mean <- mean(ps_shady_df$length_mm, na.rm = TRUE)
cat("Mean:", round(ps_shade_mean, 1), "mm\n")

t_test_result <- t.test(ps_shady_df$length_mm, mu = 15)
t_test_result

✏️ Your turn: Interpret the test result:

  1. What was the null hypothesis?
  2. What was the alternative hypothesis?
  3. What does the p-value tell us?
  4. Should we reject or fail to reject the null hypothesis at α = 0.05?
  5. What is the practical interpretation of this result for botanists?
________________________________________________________

✏️ Your turn: Write a properly formatted results sentence: “A one-sample t-test at α = 0.05 showed that the mean needle length (shady side) (M = …, SD = …) [was/was not] significantly different from the expected 15mm, t(…) = …, p = …”

________________________________________________________
Tip

🚀 If you finish early — calculate the 95% confidence interval by hand:

\[95\% \text{ CI} = \bar{x} \pm t_{\alpha/2, n-1} \times \frac{s}{\sqrt{n}}\]

shady_mean <- mean(ps_shady_df$length_mm, na.rm = TRUE)
shady_se   <- sd(ps_shady_df$length_mm, na.rm = TRUE) / sum(!is.na(ps_shady_df$length_mm))^.5
shady_n    <- sum(!is.na(ps_shady_df$length_mm))

t_critical <- qt(0.975, df = shady_n - 1)
shady_ci_lower <- shady_mean - t_critical * shady_se
shady_ci_upper <- shady_mean + t_critical * shady_se

cat("95% CI for shady mean length:", round(shady_ci_lower, 1), "to", round(shady_ci_upper, 1), "mm\n")

Does 15mm fall inside or outside this interval? How does that relate to your t-test conclusion above?


Part 4 · Two-Sample t-Test

A two-sample t-test compares means from two independent groups — here, needle length on the sunny vs. shady sides.

✏️ Your turn: Write the hypotheses for this comparison:

H0 = ________________________________________________________
Ha = ________________________________________________________

Summary statistics and a first look:

▶ Run this:

group_summary <- ps_df %>%
  group_by(side) %>%
  summarize(
    mean_length = mean(length_mm, na.rm = TRUE),
    sd_length   = sd(length_mm, na.rm = TRUE),
    n           = sum(!is.na(length_mm)),
    se_length   = sd_length / sqrt(n),
    .groups = "drop"
  )
group_summary

group_summary %>%
  summarize(difference = mean_length[side == "shady"] - mean_length[side == "sunny"])

▶ Run this — plot the mean ± SE directly:

ggplot(ps_df, aes(x = side, y = length_mm, color = side)) +
  stat_summary(fun = mean, geom = "point") +
  stat_summary(fun.data = mean_se, geom = "errorbar", width = 0.2) +
  labs(x = "side", y = "Mean Length (mm)") +
  theme_classic()

Check assumptions — normality by group:

▶ Run this:

normality_results <- ps_df %>%
  group_by(side) %>%
  summarize(
    shapiro_stat    = shapiro.test(length_mm)$statistic,
    shapiro_p_value = shapiro.test(length_mm)$p.value,
    normal_distribution = if_else(shapiro_p_value > 0.05, "Normal", "Non-normal"))
normality_results

Check assumptions — equal variances:

▶ Run this:

levene_result <- leveneTest(length_mm ~ factor(side), data = ps_df)
levene_result

✏️ Your turn: What is the null hypothesis of Levene’s test? Do we want it to be significant or non-significant? Based on your result, should you use the standard t-test or Welch’s t-test? ________________________

Run both versions of the two-sample t-test:

▶ Run this:

# Standard t-test (equal variances assumed)
t_test_result <- t.test(length_mm ~ side, data = ps_df, var.equal = TRUE)
print("Standard two-sample t-test:")
print(t_test_result)

# Welch's t-test (unequal variances allowed)
welch_test_result <- t.test(length_mm ~ side, data = ps_df, var.equal = FALSE)
print("Welch's two-sample t-test:")
print(welch_test_result)

✏️ Your turn: Compare the two results — the t-statistic, degrees of freedom, and p-value. Are your conclusions the same either way? ________________________

✏️ Your turn: Write a properly formatted results sentence: “A two-tailed, two-sample t-test at α = 0.05 showed [a significant/no significant] difference in needle length between sunny (M = …, SD = …) and shady (M = …, SD = …) sides, t(…) = …, p = …”

________________________________________________________
Tip

🚀 If you finish early — calculate the two-sample t-statistic by hand and confirm it matches t.test():

\[t = \frac{\bar{x}_1 - \bar{x}_2}{S_p\sqrt{\frac{1}{n_1} + \frac{1}{n_2}}}\]

n1 <- sum(!is.na(ps_shady_df$length_mm)); n2 <- sum(!is.na(ps_sunny_df$length_mm))
s1 <- sd(ps_shady_df$length_mm); s2 <- sd(ps_sunny_df$length_mm)

sp <- sqrt(((n1 - 1) * s1^2 + (n2 - 1) * s2^2) / (n1 + n2 - 2))
t_by_hand <- (mean(ps_shady_df$length_mm) - mean(ps_sunny_df$length_mm)) /
  (sp * sqrt(1/n1 + 1/n2))

t_by_hand

Does t_by_hand match the t value from your standard t.test() output above?


Part 5 · Paired t-Test

A paired t-test compares two measurements taken from the same subject — here, the sunny and shady side of the same tree. It uses the differences between pairs as the data, and is generally more powerful than a two-sample test because it removes tree-to-tree variation from the noise.

▶ Run this:

ps_wide_df <- ps_df %>%
  pivot_wider(
    names_from = "side",
    values_from = length_mm
  )

paired_t_test_result <- t.test(ps_wide_df$sunny, ps_wide_df$shady, paired = TRUE)
paired_t_test_result

✏️ Your turn: What does pairing control for here that the two-sample test in Part 4 does not? ________________________

Tip

🚀 If you finish early — compare the paired and two-sample p-values side by side:

cat("Two-sample p-value:", t_test_result$p.value, "\n")
cat("Paired p-value:     ", paired_t_test_result$p.value, "\n")

Which p-value is smaller? Does that match the lecture’s claim that a paired design is “generally more powerful because it controls for individual variation”?


Part 6 · Communicating Results and Reflection

In scientific writing, statistical results are reported clearly and consistently. Standard formats:

  • One-sample: “A one-sample t-test showed that the mean needle length on the shady side (M = [mean], SD = [sd]) was [significantly/not significantly] different from 15mm, t([df]) = [t-value], p = [p-value].”
  • Two-sample: “A two-sample t-test revealed that pine needle lengths on the sunny side (M = [mean1], SD = [sd1]) were [significantly/not significantly] [longer/shorter] than on the shady side (M = [mean2], SD = [sd2]), t([df]) = [t-value], p = [p-value].”

✏️ Your turn: Write all three results statements — one-sample, two-sample (or Welch’s), and paired — using your own numbers from Parts 3–5.

________________________________________________________
________________________________________________________
________________________________________________________

Reflection Questions

  1. How does the t-distribution differ from the normal distribution, and why does this matter for small samples?
  2. What assumptions must be met to use a t-test, and what alternatives exist if these assumptions are violated?
  3. What is the difference between statistical significance and practical importance?
  4. How would the confidence interval change if we used a 99% confidence level instead of 95%?
  5. How would you explain the concept of a p-value to someone with no statistical background?

Review and checkpoint

At this point you can:

Note

📤 What to turn in before next class

Upload both of these to the course management system:

  1. Your code — the scripts/ folder (or just 06_t_tests.R)
  2. This worksheet, with your written answers

Part 7 · Take-Home Extension — Mouse Body Mass and the Island Rule

Due Monday, September 28 — before the Power & Error class.

Everything below uses the exact same tests you just ran in Parts 3 and 4 — a one-sample and a two-sample t-test — on a new dataset and a new question. This time you decide which version of each test to run and defend that choice yourself; nobody walks you through it step by step.

Background

The Island Rule predicts that when mainland species colonize islands, small-bodied species — like rodents — tend to evolve larger body size, released from mainland predation and competition pressure. Below are body mass measurements for deer mice trapped on Sidney Island (an island population) and near Vancouver, BC (a mainland population).

▶ Run this:

mice_df <- read_csv("data/mice_weights.csv")
head(mice_df)

Task A · One-Sample Question

Question: Do island mice (Sidney Island) differ from a “typical” continental deer mouse body mass of 19 g?

✏️ Your turn: Write your hypotheses, decide which test is appropriate and justify your choice, then write and run the code, and interpret the result.

H0 =
________________________________________________________
Ha =
________________________________________________________
Test I will use and why:
________________________________________________________
# Write your code here:
Interpretation:
________________________________________________________
________________________________________________________

Task B · Two-Sample Question

Question: Does mouse body mass differ between the island (Sidney Island) and mainland (Vancouver) populations?

✏️ Your turn: Same process — hypotheses, test choice and justification, code, and interpretation. (Hint: in Part 4 you used Levene’s test to decide between the standard and Welch’s two-sample t-test — do that check again here, on your own, before picking one.)

H0 =
________________________________________________________
Ha =
________________________________________________________
Test I will use and why:
________________________________________________________
# Write your code here:
Interpretation:
________________________________________________________
________________________________________________________

Final Figure

Produce one publication-quality figure summarizing island vs. mainland body mass — proper axis labels, no default ggplot grey background — and export it with ggsave().

# Write your code here:
Note

📤 What to turn in — due Sept 28

Your write-up should include, for both Task A and Task B:


Getting unstuck

When code breaks — and it will, that is normal:

  1. Read the error message out loud. R usually names the line and the problem.
  2. Check the usual suspects: did you run library(tidyverse), library(car), and library(readxl)? Spelling? A missing ) or |>/%>% at the start of a line?
  3. ?function_name opens the built-in help page.
  4. Bring the exact error (copy-paste it) to class or office hours.

💡 Key idea: Every working scientist googles error messages daily. Getting stuck is not failing — it is the job.