Activity: T-Tests: One, Two, Paired
Comparing means
Worksheet: T-Tests — One, Two, Paired
📥 Data (.csv) 📥 Data (.xlsx) 📥 Tools script (.R) 📥 Script skeleton (.R)
pine_data.csv / pine_data.xlsx
Both files hold the same six columns, with exactly these names. Type them exactly as written — R is case-sensitive, and Needle_Length or needle_length will not work:
| Column | What it is |
|---|---|
source_file |
the raw team file the row came from |
team |
field team — one team = one tree |
replicate |
needle number within a tree side (1-8) |
side |
sun exposure — "shady" or "sunny" |
needle_length_mm |
needle length, millimetres |
needle_width_mm |
needle width, millimetres |
After you read the file in, click pine_df in the Variables pane in Positron and check the names on your screen against this table before you write any other code.
Section 1 · Read In the Data and Look at It
1.1 · Setup
Download the tools script from the button at the top. In your project folder, make a new folder called themes_functions and save the file into it, so the path is themes_functions/r_themes_and_functions.R.
That is a different folder from scripts/ — scripts/ holds the code you write, themes_functions/ holds tools you source() and do not edit.
One source() line then gives you two things you will use for the rest of the semester:
theme_regular()— the publication theme from the Graphing lecturesummary_stats()— hand it a column of numbers, get backn/mean/sd/seand a 95% CI
▶ Run this:
# ── Part 1.1 · Setup ────────────────────────────────────────────────────
library(tidyverse)
library(janitor) # round_half_up(), used inside summary_stats()
library(car) # leveneTest()
source("themes_functions/r_themes_and_functions.R")
pine_df <- read_csv("data/pine_data.csv")⚠️ Watch out!
source()needs the path to be right. If R says cannot open file, that is exactly what it is telling you — check that the folder is calledthemes_functions, spelled exactly that way, and that it sits next to your.Rprojfile.
💡 You do not need
glimpse()orhead(). Clickpine_dfin the Variables pane in Positron and look at it there.
1.2 · Look at all the data — spread
Before we do anything clever, look at every needle we measured — all 64 rows, exactly as they came out of the field.
▶ Run this:
# ── Part 1.2 · Boxplot of all the raw needles ───────────────────────────
raw_box_plot <- pine_df %>%
ggplot(aes(side, needle_length_mm)) +
geom_boxplot() +
geom_point(position = position_jitter(width = 0.08)) +
theme_regular(base_size = 10)
raw_box_plot💡 Why
position_jitter()here? 32 needles land on a single x value, so a plaingeom_point()would stack them into a line and hide the duplicates. Jitter nudges them sideways so you can see how many there are.
✏️ Your turn: How many points are on this plot, and how many trees did we actually climb?
points = ________ trees = ________
1.3 · Look at all the data — mean and standard error
▶ Run this:
# ── Part 1.3 · Mean and SE of all the raw needles ───────────────────────
raw_mean_se_plot <- pine_df %>%
ggplot(aes(side, needle_length_mm)) +
stat_summary(fun = mean, geom = "point") +
stat_summary(fun.data = mean_se, geom = "errorbar", width = 0.2) +
theme_regular(base_size = 10)
raw_mean_se_plot✏️ Your turn: Do these two error bars overlap? Just from this picture, would you expect a t-test to find a difference?
____________________________________________________________________
____________________________________________________________________
⚠️ Watch out — those error bars are a lie. They are computed as SD / √n with n = 32 per side. But we did not measure 32 independent things. We measured 4 trees, eight needles each. The 32 is fake, so the precision is fake. Hold this picture in your head — we fix it next and then plot it again.
1.4 · Average out the pseudoreplication
The 8 needles from one side of one tree are pseudoreplicates — they are not 8 independent measurements, they are 8 looks at the same tree side. Average them so each tree side is one data point.
▶ Run this:
# ── Part 1.4 · Average out pseudoreplication ────────────────────────────
p_df <- pine_df %>%
group_by(team, side) %>%
summarise(needle_length_mm = mean(needle_length_mm, na.rm = TRUE),
.groups = "drop")
p_df⚠️ Watch out! 64 rows became 8. That is not data loss — it is the correct sample size. We measured 8 needles per side, but only 4 trees.
✏️ Your turn: What is our true sample unit here — needles? branches? trees? tree sides? Why does that matter for the rest of this worksheet?
____________________________________________________________________
____________________________________________________________________
____________________________________________________________________
1.5 · Summary statistics, the long way
▶ Run this:
# ── Part 1.5 · Summary stats the long way ───────────────────────────────
stats_df <- p_df %>%
group_by(side) %>%
summarize(
n = sum(!is.na(needle_length_mm)),
mean = mean(needle_length_mm, na.rm = TRUE),
sd = sd(needle_length_mm, na.rm = TRUE),
se = sd / sqrt(n),
.groups = "drop"
)
stats_df1.6 · Summary statistics, the short way
The exact same numbers, from the function you source()d in Part 1.1:
▶ Run this:
# ── Part 1.6 · Summary stats with summary_stats() ───────────────────────
summary_stats(p_df$needle_length_mm) # all 8 trees together
p_df %>% group_by(side) %>% reframe(summary_stats(needle_length_mm))💡 Why
reframe()?summarize()wants one value per group.summary_stats()hands back a whole row — six columns at once — soreframe()is the verb that allows it. Thegroup_by()in front is the same one you already know.
✏️ Your turn: Write down n, mean, sd and se for each side, and check them against stats_df.
shady: n = ______ mean = ______ sd = ______ se = ______
sunny: n = ______ mean = ______ sd = ______ se = ______
Do they match stats_df? ______________________________________
✏️ Your turn: summary_stats() also gives you ci_lower and ci_upper — a 95% confidence interval. Look at the n column. How many degrees of freedom did it use to build that interval, and why that number?
____________________________________________________________________
____________________________________________________________________
1.7 · The same two plots, after averaging
Exactly the same two plots as Parts 1.2 and 1.3 — the only thing that changed is pine_df became p_df.
▶ Run this:
# ── Part 1.7 · Boxplot and mean SE of the tree means ────────────────────
box_plot <- p_df %>%
ggplot(aes(side, needle_length_mm)) +
geom_boxplot() +
geom_point() +
theme_regular(base_size = 10)
box_plot
mean_se_plot <- p_df %>%
ggplot(aes(side, needle_length_mm)) +
stat_summary(fun = mean, geom = "point") +
stat_summary(fun.data = mean_se, geom = "errorbar", width = 0.2) +
theme_regular(base_size = 10)
mean_se_plot💡 No jitter needed now. Four points per side, so plain
geom_point()is enough.
✏️ Your turn: Fill this in from your Part 1.3 and Part 1.6 output. This is the most important table in the worksheet.
raw needles tree means
n per side __________ __________
shady mean __________ __________
sunny mean __________ __________
shady se __________ __________
sunny se __________ __________
✏️ Your turn: What happened to the means when you averaged? What happened to the standard errors?
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: Compare raw_mean_se_plot to mean_se_plot side by side on your screen. Which one looks like stronger evidence? Which one is honest? Explain in one sentence why the raw version was not actually more precise.
____________________________________________________________________
____________________________________________________________________
____________________________________________________________________
1.8 · One line per tree
▶ Run this:
# ── Part 1.8 · One line per tree ────────────────────────────────────────
slope_plot <- p_df %>%
ggplot(aes(side, needle_length_mm, group = team)) +
geom_line() +
geom_point() +
theme_regular(base_size = 10)
slope_plot✏️ Your turn: The boxes in Part 1.7 overlapped a lot. Now look at the lines. Do they all slope the same way? Write down what you see — this is the whole point of Section 4.
____________________________________________________________________
____________________________________________________________________
df = the number of values free to vary once you have estimated a mean. You lose one df for every mean you estimate. Our data is 4 trees, one shady side and one sunny side each — so n is always trees, never needles.
| Test | What n counts | df formula | Our df |
|---|---|---|---|
| One-sample (Section 2) | trees in the one group | n − 1 | 3 |
| Two-sample (Section 3) | trees in both groups | n₁ + n₂ − 2 | 6 |
| Welch’s (Section 3) | both groups, penalised | Welch–Satterthwaite | ≈ 5.66 |
| Paired (Section 4) | differences | npairs − 1 | 3 |
Check the df = line in every bit of t.test() output below against this table. If it does not match, you ran a different test than you thought you did.
Section 2 · One-Sample t-Test
A one-sample t-test compares one group’s mean to a fixed number — not to another group. Here: is the mean needle length on the shady side different from the 15 mm reported for this species?
2.1 · Just the shady side
▶ Run this:
# ── Part 2.1 · Just the shady side ──────────────────────────────────────
shady_df <- p_df %>% filter(side == "shady")
shady_df2.2 · Question, prediction, hypotheses
✏️ Your turn:
Question (the biology, in plain English):
____________________________________________________________________
Prediction (what you expect, and WHY — the biology):
____________________________________________________________________
____________________________________________________________________
H0 (null hypothesis):
____________________________________________________________________
Ha (alternative hypothesis):
____________________________________________________________________
alpha = ____________
✏️ Your turn: How many trees are in this test, and how many means did you estimate? So what is the df?
n = ________ means estimated = ________ df = ________
2.3 · Check normality
▶ Run this:
# ── Part 2.3 · Normality of the shady side ──────────────────────────────
qqnorm(shady_df$needle_length_mm)
qqline(shady_df$needle_length_mm)
shapiro.test(shady_df$needle_length_mm)✏️ Your turn: Why do we check the shady side on its own rather than p_df$needle_length_mm (both sides pooled)? What would pooling two groups with different means do to the shape of the distribution?
____________________________________________________________________
____________________________________________________________________
2.4 · Run the two-tailed test
▶ Run this:
# ── Part 2.4 · One-sample t-test, two-tailed ────────────────────────────
one_sample_model <- t.test(shady_df$needle_length_mm, mu = 15)
one_sample_model✏️ Your turn: Copy the numbers out, and check the df against what you predicted in Part 2.2.
t = ________ df = ________ p-value = ________
sample mean = ________ mm 95% CI: ________ to ________
Does the df match your answer above? ______________________________
✏️ Your turn: Does 15 mm fall inside or outside that confidence interval? How does that relate to your p-value?
____________________________________________________________________
____________________________________________________________________
2.5 · The one-tailed variation
Your prediction in Part 2.2 probably had a direction — shady needles are shorter than 15 mm. If you committed to that direction before collecting data, you can put all 5% of α into one tail.
✏️ Your turn: Write the one-tailed hypotheses.
H0: mu ______ 15
Ha: mu ______ 15
▶ Run this — alternative is the only thing that changes:
# ── Part 2.5 · One-sample t-test, one-tailed ────────────────────────────
one_tailed_model <- t.test(shady_df$needle_length_mm, mu = 15, alternative = "less")
one_tailed_model✏️ Your turn: Fill in the comparison from your own output.
t df p-value
Two-tailed ________ ______ ________
One-tailed ________ ______ ________
✏️ Your turn: What happened to t? To df? To the p-value? (Look hard at the relationship between the two p-values.)
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: Look at the confidence interval in the one-tailed output. What is the lower bound, and why does that make sense?
____________________________________________________________________
____________________________________________________________________
2.6 · Why the one-tailed p is smaller
▶ Run this — the critical values behind that change, at df = 3:
# ── Part 2.6 · Critical values, one vs two tailed ───────────────────────
qt(0.975, df = 3) # two-tailed, 2.5% in each tail
qt(0.95, df = 3) # one-tailed, 5% in one tail✏️ Your turn: Which cutoff is closer to zero? So which test is easier to get a significant result from, and why?
____________________________________________________________________
____________________________________________________________________
2.7 · Point it the wrong way on purpose
▶ Run this:
# ── Part 2.7 · The wrong tail ───────────────────────────────────────────
t.test(shady_df$needle_length_mm, mu = 15, alternative = "greater")✏️ Your turn: Same data, same t. What is the p-value now? What does that tell you about when you are allowed to choose the tail?
p-value = ________
____________________________________________________________________
____________________________________________________________________
⚠️ Watch out! Choosing the tail after looking at your means is not a one-tailed test. It is a two-tailed test with the p-value secretly halved, and it is one of the most common ways people accidentally cheat. Pick the direction from the biology, before you collect data, or use two-tailed.
🚀 If you finish early: A one-tailed test is defensible when a result in the opposite direction would lead to the same action as no result at all — a drug that harms patients and a drug that does nothing are both “do not approve.” For our pine needles, is “sunny is longer” and “shady is longer” both interesting biology? So which should we use?
____________________________________________________________________
2.8 · Interpretation
✏️ Your turn:
Do you reject or fail to reject H0 at alpha = 0.05?
____________________________________________________________________
In biological terms, what does this say about shady-side needles?
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: Write the result the way it would appear in a paper.
"A two-tailed, one-sample t-test at alpha = 0.05 showed ___________
____________________________________________________________________
____________________________________________________________________
t(______) = ________, p = ________."
Section 3 · Two-Sample t-Test
A two-sample t-test compares the means of two independent groups — here, needle length on the sunny side versus the shady side.
3.1 · Question, prediction, hypotheses
Write all five down before you run anything.
✏️ Your turn:
Question (the biology, in plain English):
____________________________________________________________________
Prediction (what you expect to see, and WHY — the biology):
____________________________________________________________________
____________________________________________________________________
H0 (null hypothesis):
____________________________________________________________________
Ha (alternative hypothesis):
____________________________________________________________________
alpha = ____________
✏️ Your turn: Your prediction probably had a direction (“sunny needles are longer”). Is your Hₐ directional too, or is it just “they differ”? Which one does t.test() run by default?
____________________________________________________________________
____________________________________________________________________
3.2 · Check normality within each group
▶ Run this:
# ── Part 3.2 · Normality by group ───────────────────────────────────────
p_df %>%
group_by(side) %>%
summarize(
shapiro_p = shapiro.test(needle_length_mm)$p.value,
.groups = "drop")✏️ Your turn: What is the null hypothesis of the Shapiro-Wilk test? Do you want a significant or a non-significant result here?
H0 of Shapiro-Wilk: ________________________________________________
I want p to be: ________________________________________________
My result: shady p = ____________ sunny p = ____________
⚠️ Watch out! With n = 4, Shapiro-Wilk has almost no power — it will pass nearly anything. A high p-value here is not evidence that the data are normal; it is evidence that we do not have enough data to tell. Run it, report it, but lean on the design and the biology.
3.3 · Check equal variances
▶ Run this:
# ── Part 3.3 · Equal variances ──────────────────────────────────────────
leveneTest(needle_length_mm ~ factor(side), data = p_df)✏️ Your turn: What is the null hypothesis of Levene’s test? Based on your p-value, should you use the standard (Student’s) t-test or Welch’s?
H0 of Levene's test: _______________________________________________
My p-value: ____________
Test I will use, and why: __________________________________________
____________________________________________________________________
3.4 · Run both versions
▶ Run this:
# ── Part 3.4 · Two-sample t-test ────────────────────────────────────────
two_sample_model <- t.test(needle_length_mm ~ side, data = p_df, var.equal = TRUE)
two_sample_model
welch_model <- t.test(needle_length_mm ~ side, data = p_df, var.equal = FALSE)
welch_model✏️ Your turn: Copy the numbers out of the output by hand.
t df p-value
Standard (Student's) ________ ________ ________
Welch's ________ ________ ________
✏️ Your turn: Welch’s df is not a whole number, and it is smaller than the standard df. Why? What did Welch’s buy you in exchange?
____________________________________________________________________
____________________________________________________________________
3.5 · Interpretation
✏️ Your turn: Answer in full sentences.
Do you reject or fail to reject H0 at alpha = 0.05?
____________________________________________________________________
What does the p-value actually mean here? (Careful — it is a statement
about the data GIVEN H0, not about whether H0 is true.)
____________________________________________________________________
____________________________________________________________________
In biological terms, what does this result say about pine needles?
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: Write the result the way it would appear in a paper. The number in parentheses after t is the degrees of freedom — always report it.
"A two-tailed, two-sample t-test at alpha = 0.05 showed __________
____________________________________________________________________
____________________________________________________________________
t(______) = ________, p = ________."
🚀 If you finish early — compute the 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}}}\]
# ── Part 3.6 · Two-sample t by hand ─────────────────────────────────────
sunny_df <- p_df %>% filter(side == "sunny") # shady_df is from Part 2.1
n1 <- length(shady_df$needle_length_mm)
n2 <- length(sunny_df$needle_length_mm)
s1 <- sd(shady_df$needle_length_mm)
s2 <- sd(sunny_df$needle_length_mm)
sp <- sqrt(((n1 - 1) * s1^2 + (n2 - 1) * s2^2) / (n1 + n2 - 2))
(mean(shady_df$needle_length_mm) - mean(sunny_df$needle_length_mm)) /
(sp * sqrt(1/n1 + 1/n2))Look at the denominator of sp: n1 + n2 - 2. That is the degrees of freedom. df is not a rule to memorise — it is whatever you divided by.
Section 4 · Paired t-Test
A paired t-test compares two measurements taken from the same subject — here, the sunny side and the shady side of the same tree. It uses the differences between pairs as the data.
4.1 · Question, prediction, hypotheses
Notice that H₀ and Hₐ are now about one number: the mean difference.
✏️ Your turn:
Question (the biology, in plain English):
____________________________________________________________________
Prediction (what you expect, and WHY):
____________________________________________________________________
____________________________________________________________________
H0 (in terms of the mean difference between the two sides of a tree):
____________________________________________________________________
Ha:
____________________________________________________________________
alpha = ____________
4.2 · Reshape and run the test
t.test() needs the two sides side by side, one row per tree.
▶ Run this:
# ── Part 4.2 · Paired t-test ────────────────────────────────────────────
p_wide_df <- p_df %>%
pivot_wider(names_from = side, values_from = needle_length_mm)
p_wide_df
paired_model <- t.test(p_wide_df$shady, p_wide_df$sunny, paired = TRUE)
paired_model⚠️ Watch out — order matters for the sign.
t.test(shady, sunny, paired = TRUE)computes shady − sunny. Swap the two andtflips sign. The p-value is identical, but the direction you report is backwards. Always write down which way you subtracted.
✏️ Your turn: Copy the numbers out, and note the df.
t = ________ df = ________ p-value = ________
mean difference = ________ mm (this is ________ minus ________)
4.3 · Same data, two different answers
▶ Run this:
# ── Part 4.3 · Compare the two tests ────────────────────────────────────
two_sample_model$p.value
paired_model$p.value✏️ Your turn: Fill in the table from your own output.
Test t df p-value
Two-sample ________ ______ ________
Paired ________ ______ ________
✏️ Your turn: Both tests used the same 8 numbers and both estimated the same difference in means. So why are the p-values so different? (Think about where tree-to-tree variation ends up in each test.)
____________________________________________________________________
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: The paired test has half the degrees of freedom — 3 instead of 6. Fewer df makes significance harder to reach, not easier. So how did it still do better?
____________________________________________________________________
____________________________________________________________________
4.4 · A paired test is a one-sample test
Treat the shady side as each tree’s own control and compute one difference per tree.
▶ Run this:
# ── Part 4.4 · Paired = one-sample on the differences ───────────────────
diff_df <- p_wide_df %>%
mutate(difference = sunny - shady)
diff_df
diff_model <- t.test(diff_df$difference, mu = 0)
diff_model✏️ Your turn: Compare diff_model to paired_model. Write down the t, the df and the p-value for each. What do you notice?
paired_model: t = ________ df = ______ p = ________
diff_model: t = ________ df = ______ p = ________
What I notice: _____________________________________________________
✏️ Your turn: So what is paired = TRUE actually doing for you?
____________________________________________________________________
✏️ Your turn: This is the answer to the degrees of freedom question. The two-sample test on these same measurements had df = 6. This one has df = 3. Explain why, in terms of how many numbers diff_model is actually working with.
____________________________________________________________________
____________________________________________________________________
4.5 · Interpretation
✏️ Your turn:
Do you reject or fail to reject H0 at alpha = 0.05?
____________________________________________________________________
Which test — two-sample or paired — is the CORRECT one for how we
collected this data, and why?
____________________________________________________________________
____________________________________________________________________
In biological terms, what does the paired result say about pine needles?
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: Write the result the way it would appear in a paper.
"A two-tailed, paired t-test at alpha = 0.05 showed _______________
____________________________________________________________________
____________________________________________________________________
t(______) = ________, p = ________."
🚀 If you finish early: The shady side “controls for” everything about that particular tree — its genetics, its soil, its water, its team’s measuring technique. Subtracting removes all of it at once. Which assumption from Section 3 does this design let you stop worrying about? And which variable’s normality should you now be checking?
____________________________________________________________________
Reflection Questions
- How does the t-distribution differ from the normal distribution, and why does that matter when you only have 4 trees?
- What assumptions must be met to use a t-test, and what alternatives exist if they are violated?
- What is the difference between statistical significance and practical importance?
- How would your confidence interval change at 99% instead of 95%?
- How would you explain 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
Take-Home Extension — Mouse Body Mass and the Island Rule
Due Monday, September 28 — before the Power & Error class.
This uses the same machinery you just ran, on a new dataset and a new question. This time you decide which test to run and defend that choice; nobody walks you through it.
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. Below are body mass measurements for deer mice trapped on Sidney Island (island) and near Vancouver, BC (mainland).
▶ Run this:
# ── Part 5.1 · Take-home extension ──────────────────────────────────────
mice_df <- read_csv("data/mice_weights.csv")
mice_df %>% group_by(location) %>% reframe(summary_stats(mass_g))Task A · A one-sample question
Question: Do island mice (Sidney Island) differ from a “typical” continental deer mouse body mass of 19 g?
💡 This is Section 2 again, with a different
muand a different dataset.
✏️ Your turn:
Question: ______________________________________________________
Prediction: ______________________________________________________
______________________________________________________
H0 = ______________________________________________________
Ha = ______________________________________________________
Test I will use, and why: __________________________________________
______________________________________________________
# ── Part 5.2 · Task A ───────────────────────────────────────────────────
# Write your code in the R script:Result: t(______) = ________, p = ________
Interpretation:
____________________________________________________________________
____________________________________________________________________
Task B · A two-sample question
Question: Does mouse body mass differ between the island (Sidney Island) and mainland (Vancouver) populations?
💡 In Section 3 you used Levene’s test to choose between the standard and Welch’s two-sample t-test. Do that check again here, on your own, before you pick one.
✏️ Your turn:
Question: ______________________________________________________
Prediction: ______________________________________________________
______________________________________________________
H0 = ______________________________________________________
Ha = ______________________________________________________
Levene's p = ____________
Test I will use, and why: __________________________________________
______________________________________________________
# ── Part 5.3 · Task B ───────────────────────────────────────────────────
# Write your code in the R script:Result: t(______) = ________, p = ________
Interpretation:
____________________________________________________________________
____________________________________________________________________
✏️ Your turn: Could you have run a paired t-test on these mice? Why or why not?
____________________________________________________________________
____________________________________________________________________
Final Figure
Produce one figure summarizing island versus mainland body mass. This is the one place in this worksheet where you should dress it up — proper axis labels, no default grey background — and export it with ggsave().
# ── Part 5.4 · Final figure ─────────────────────────────────────────────
# Write your code in the R script:📤 What to turn in — due Sept 28
For both Task A and Task B:
Homework
One- and two-sample t-tests on mice from the San Juan Islands and the mainland, testing the island rule: do island rodents evolve larger bodies? You test each population against a stated population mean of 19 g, and then test island against mainland.
Data: mice_weights.csv — a course dataset; the biology behind it is Foster’s rule (1964).