Activity: T-Tests: One, Two, Paired
Comparing means
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.xlsxhas correctedsunny/shadylabels compared to the original file. From here on we useps_df— it’s the version that matches the field notes.
🚀 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.)
________________________________________________________
🚀 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:
- What was the null hypothesis?
- What was the alternative hypothesis?
- What does the p-value tell us?
- Should we reject or fail to reject the null hypothesis at α = 0.05?
- 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 = …”
________________________________________________________
🚀 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_resultsCheck 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 = …”
________________________________________________________
🚀 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_handDoes 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? ________________________
🚀 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
- How does the t-distribution differ from the normal distribution, and why does this matter for small samples?
- What assumptions must be met to use a t-test, and what alternatives exist if these assumptions are violated?
- What is the difference between statistical significance and practical importance?
- How would the confidence interval change if we used a 99% confidence level instead of 95%?
- How would you explain the concept of a p-value to someone with no statistical background?
Review and checkpoint
At this point you can:
📤 What to turn in before next class
Upload both of these to the course management system:
- Your code — the
scripts/folder (or just06_t_tests.R) - 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:📤 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:
- Read the error message out loud. R usually names the line and the problem.
- Check the usual suspects: did you run
library(tidyverse),library(car), andlibrary(readxl)? Spelling? A missing)or|>/%>%at the start of a line? ?function_nameopens the built-in help page.- 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.