Worksheet 04 — Testing Our Hypothesis
The two-sample Welch’s t-test from assumptions to scientific report
How to do a T-Test and other r items.
In-class Activity 4: The Welch’s Two-Sample t-Test
In-class Activity 4: The Welch’s Two-Sample t-Test
Recap from Worksheet 03
- Loaded our Excel leaf data with
read_excel() - 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() - Prediction: shady leaves appeared heavier, taller, and wider than sunny leaves
Today’s Objectives
- State null and alternate hypotheses formally before running any test
- Check the normality assumption — histogram, QQ plot, and Shapiro-Wilk test
- Check the variance assumption — Levene’s test
- Run a Welch’s two-sample t-test (
var.equal = FALSE, two-tailed) - Read and decode every line of the
t.test()output - Make a formal statistical decision and state a conclusion
- Write a results sentence in proper scientific format
How to use this worksheet
- Work through each part in order — the steps build on each other. Type the code into your script in Positron and run it line by line.
- Code blocks marked ▶ Run this should be executed as written.
- Blocks marked ✏️ Your turn ask you to write, modify, or interpret something.
- The Going further section is optional if you finish early.
🔮 Predict before you run — and type, don’t paste
Before you run any ▶ Run this block, cover the output and predict what R will print. Jot your guess down, then run it and compare. Two reasons it is worth the extra few seconds:
- Predicting makes it stick. Guessing forces you to retrieve what you already know, and the little jolt of being wrong is when real learning happens — especially for p-values, which are easy to misread.
- Typing beats pasting. Typing every line builds muscle memory and trains your eye to catch small errors (
=vs==, a missing)or%>%) that you would otherwise spend real time hunting.
🧩 Chunk 1 — Setup, hypotheses & recap (after lecture Chunk 1)
Parts 1–3: load the data, commit to your hypotheses, and recompute the descriptive stats.
Part 1 · Load libraries and data
Libraries
▶ Run this at the top of your script — in this exact order:
# Load all packages at the top of the script -----------
library(readxl) # reading Excel files
library(tidyverse) # data manipulation + ggplot2
library(skimr) # fast dataset summaries
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 sessionlibrary(car)
Load the data
▶ Run this:
# Load the leaf data from the data folder --------------
tree_df <- read_excel("data/2026_06_25_tree_experiment_raw_data.xlsx")
head(tree_df)✏️ Your turn: Before anything else — do you remember the structure of this data frame? Fill in from memory, then check with glimpse(tree_df).
Number of rows:
Number of columns:
Column holding the grouping variable (sunny/shady):
Column we are testing today:
Part 2 · 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 weight. 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 3 · Quick descriptive stats review
🔮 Predict first: Before running, guess the mean weight for each side (shady vs sunny) and the gap between them. Write your guesses, then check.
▶ Run this:
# Recap stats from Lecture 03 — both groups at once ----
recap_df <- tree_df %>%
group_by(side) %>%
summarize(
n = sum(!is.na(weight_g)),
mean_wt = round(mean(weight_g, na.rm = TRUE), 2),
sd_wt = round(sd(weight_g, na.rm = TRUE), 2),
se_wt = round(sd_wt / sqrt(n), 2)
)
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 — Checking assumptions (after lecture Chunk 2)
Parts 4–7: histogram, QQ plot, Shapiro-Wilk, and Levene’s test — check before you test.
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 bell-shaped distribution
hist_norm_plot <- tree_df %>%
ggplot(aes(x = weight_g, fill = side)) +
geom_histogram(binwidth = 1, color = "white", alpha = 0.8) +
facet_wrap(~side, ncol = 2) +
labs(
title = "Leaf Weight Distribution by Side",
x = "Leaf Weight (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
✏️ Your turn: With only 10 values per group, histograms will look lumpy even if the data are normal. Does this mean we should reject normality? Why or why not?
Your answer:
Part 5 · Check normality — 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.
▶ Run this:
# Q-Q plot — points should fall along the diagonal ----
qq_norm_plot <- tree_df %>%
ggplot(aes(sample = weight_g, color = side)) +
stat_qq() +
stat_qq_line(color = "black", linewidth = 0.8) +
facet_wrap(~side, scales = "free") +
labs(
title = "Normal Q-Q Plots by Side",
x = "Theoretical Quantiles",
y = "Sample Quantiles"
) +
theme_minimal() +
theme(legend.position = "none")
qq_norm_plot✏️ Your turn: Do the points for each group fall approximately along the diagonal line? What would it mean if they curved strongly away at the ends?
Sunny side — on the line? Y / mostly / no
Shady side — on the line? Y / mostly / no
What strong curvature at the ends would indicate:
Part 6 · Check normality — Shapiro-Wilk test
🔮 Predict first: Shapiro-Wilk’s H₀ is “data are normal.” From your histogram and QQ plot, predict whether p will be above or below 0.05 for each side before you run it.
The Shapiro-Wilk test formally tests whether a sample could have come from a normal distribution.
- 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 --------------------
tree_df %>%
filter(side == "sunny") %>%
pull(weight_g) %>%
shapiro.test()# Shapiro-Wilk for the shady side -------------------
tree_df %>%
filter(side == "shady") %>%
pull(weight_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):
✏️ Your turn: With n = 10 per group, the Shapiro-Wilk test has low statistical power. What does that mean in plain language for interpreting this result?
Your answer:
⚠️ Watch out! Even if Shapiro-Wilk returns p < 0.05 with very small samples, the t-test may still be appropriate — the QQ plot and histogram give important visual context. None of these checks alone is definitive.
Part 7 · Check variance — Levene’s test
🔮 Predict first: Look back at the two SD values from Part 3. Predict whether Levene’s will call the variances equal (p > 0.05) or not, before you run it.
Levene’s test checks whether the two groups have the same population variance.
- 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(weight_g ~ side, data = tree_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:
🧩 Chunk 3 — Run & interpret (after lecture Chunk 3)
Parts 8–11: run the test, decode the output, reproduce t by hand, and decide.
Part 8 · Run the Welch’s t-test
🔮 Predict first: Write down a guess for the p-value now — above or below 0.05? Then run
t.test()and see how close you were.
Now that we have checked our assumptions, we can run the test.
▶ Run this:
# Welch's two-sample t-test --------------------------
leaf_ttest_model <- t.test(
weight_g ~ side, # response ~ grouping variable
data = tree_df,
var.equal = FALSE, # Welch's — no pooled variance
alternative = "two.sided" # two-tailed test
)
leaf_ttest_model✏️ Your turn: Copy the full output below:
Paste or write the t.test() output here:
Part 9 · Decode the output line by line
▶ Run this to extract individual values:
# Pull specific values from the model object ---------
cat("t-statistic:", round(leaf_ttest_model$statistic, 3), "\n")
cat("df (Welch): ", round(leaf_ttest_model$parameter, 2), "\n")
cat("p-value: ", signif(leaf_ttest_model$p.value, 3), "\n")
cat("95% CI: ", round(leaf_ttest_model$conf.int[1],2),
"to", round(leaf_ttest_model$conf.int[2], 2), "g\n")
cat("Mean shady: ", round(leaf_ttest_model$estimate[1],2), "g\n")
cat("Mean sunny: ", round(leaf_ttest_model$estimate[2],2), "g\n")✏️ Your turn: Match each piece of output to its meaning:
t-statistic = _____
→ This is large/small (circle one) because the difference in means
is large/small relative to the variability (circle one).
df = _____
→ This is a non-integer. Why? (hint: Welch-Satterthwaite equation)
p-value = _____
→ In plain language, this means: if H₀ were true, the probability
of seeing a t-statistic this extreme just by chance is _____.
95% CI = _____ to _____ g
→ This CI is for the ________________ (the true difference in means).
It does / does not include zero (circle one).
What does it mean when the CI excludes zero?
Mean shady = _____ g
Mean sunny = _____ g
Part 10 · Calculate t by hand
▶ Run this:
# Reproduce the t-statistic manually -----------------
m_sha <- recap_df$mean_wt[recap_df$side == "shady"]
m_sun <- recap_df$mean_wt[recap_df$side == "sunny"]
s_sha <- recap_df$sd_wt[recap_df$side == "shady"]
s_sun <- recap_df$sd_wt[recap_df$side == "sunny"]
n_sha <- recap_df$n[recap_df$side == "shady"]
n_sun <- recap_df$n[recap_df$side == "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, 2), "g\n")
cat("SE of difference: ", round(se_diff, 4), "\n")
cat("Manual t: ", round(t_manual, 3), "\n")✏️ Your turn: Does your manually calculated t match the value from t.test()? (They may differ very slightly due to rounding in recap_df.)
Manual t = _____
t.test() t = _____
Match? Y / close / no
✏️ Your turn: Write the Welch’s t-test formula below and label each part:
t = _________________
Numerator means:
Denominator means:
Part 11 · Make a formal decision
✏️ Your turn: State your decision and conclusion. Use the formal language of hypothesis testing.
Our p-value:
Our α:
Decision (circle one): REJECT H₀ / FAIL TO REJECT H₀
In one sentence, what does this mean biologically?
💡 Key idea: We never say “H₀ is true” or “H₀ is false.” We say “we reject H₀” (evidence was strong enough) or “we fail to reject H₀” (evidence was not strong enough). Statistics gives us a decision rule, not certainty.
🧩 Chunk 4 — Visualize & report (after lecture Chunk 4)
Parts 12–13: build the figure with the statistics embedded, then write the results sentence.
Part 12 · Visualize the result
Boxplot with test statistics in the subtitle
▶ Run this:
# Boxplot with t and p embedded in subtitle ----------
leaf_box_04_plot <- tree_df %>%
ggplot(aes(x = side, y = weight_g, fill = side)) +
geom_boxplot(alpha = 0.5, outlier.shape = NA) +
geom_point(
position = position_jitter(width = 0.15, seed = 42),
alpha = 0.6, size = 2.5
) +
labs(
title = "Leaf Weight by Tree Side",
subtitle = paste0("Welch's t-test: t = ",
round(leaf_ttest_model$statistic, 2),
", p = ",
signif(leaf_ttest_model$p.value, 2)),
x = "Side of Tree",
y = "Leaf Weight (g)"
) +
theme_minimal() +
theme(legend.position = "none")
leaf_box_04_plot✏️ Your turn: The subtitle line uses paste0() to build the label from the model object. What happens to the subtitle if you re-run this with different data? Why is this better than typing the numbers yourself?
Your answer:
Save the plot
▶ Run this:
ggsave("figures/leaf_weight_boxplot_ttest.png",
plot = leaf_box_04_plot,
width = 5,
height = 5,
units = "in",
dpi = 300)Part 13 · Write a scientific results sentence
✏️ Your turn: Write a complete results sentence as you would in a lab report or paper. Include:
- Which group was larger
- The word “significantly” (if appropriate)
- Test name, t-statistic, df, and p-value in parentheses
- Mean ± SE for both groups with units
Use this template:
"[Group] leaves were significantly [heavier / lighter] than [group] leaves
(Welch's two-sample t-test: t(___) = ___, p = ___).
Mean ± SE: [side 1] = ___ ± ___ g, [side 2] = ___ ± ___ g."
Write your sentence here:
Your results sentence:
💡 Key idea: Never write
p = 0.000. Writep < 0.001instead. A p-value is never exactly zero — it is just too small to display at three decimal places.
Part 14 · 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. Does it run without errors?
Ran cleanly? Y / N
If not, what error appeared:
Part 15 · Going further
This section is optional — work through it if you finish early or want to push deeper.
Test a second variable: height_cm
▶ Try this:
# Run the same t-test pipeline for height_cm ---------
# 1. Stats recap
tree_df %>%
group_by(side) %>%
summarize(
n = sum(!is.na(height_cm)),
mean_ht = round(mean(height_cm, na.rm = TRUE), 2),
sd_ht = round(sd(height_cm, na.rm = TRUE), 2),
se_ht = round(sd_ht / sqrt(n), 2)
)# 2. Shapiro-Wilk for height_cm
tree_df %>% filter(side == "sunny") %>% pull(height_cm) %>% shapiro.test()
tree_df %>% filter(side == "shady") %>% pull(height_cm) %>% shapiro.test()# 3. Levene's test for height_cm
leveneTest(height_cm ~ side, data = tree_df)# 4. Welch's t-test for height_cm
height_ttest_model <- t.test(
height_cm ~ side,
data = tree_df,
var.equal = FALSE,
alternative = "two.sided"
)
height_ttest_model✏️ Your turn: Is leaf height also significantly different between sides? Fill in the results:
t = _____ df = _____ p = _____
Decision:
One-sentence conclusion:
Change the confidence level
By default t.test() reports a 95% CI (conf.level = 0.95). Try 99%:
▶ Try this:
# Run the weight t-test with 99% confidence interval --
t.test(
weight_g ~ side,
data = tree_df,
var.equal = FALSE,
alternative = "two.sided",
conf.level = 0.99 # 99% CI instead of 95%
)✏️ Your turn: How did the 99% CI compare to the 95% CI? Why is the 99% CI wider?
95% CI was: _____ to _____
99% CI was: _____ to _____
Why wider:
What if the groups were not significantly different?
✏️ Your turn: Think about what would happen if both sides had leaves with weight_g around 5.5 ± 2.0 g. Would the t-test be significant? What would change in the output?
Your prediction:
What your finished figures/ folder should contain
project/
├── data/
│ └── 2026_06_25_tree_experiment_raw_data.xlsx
├── figures/
│ ├── leaf_weight_boxplot.png <- from Worksheet 03
│ ├── leaf_weight_mean_se.png <- from Worksheet 03
│ └── leaf_weight_boxplot_ttest.png <- from this worksheet
└── scripts/
└── 04_leaf_ttest.R <- your script
Getting unstuck
- Read the error message out loud. R usually names the line and the problem.
- Check the usual suspects:
- Loaded
library(car),library(readxl),library(tidyverse)? - Spelling of column names? Check with
names(tree_df) - Missing
)or%>%?
- Loaded
leveneTestnot found? You needlibrary(car)— not justcar::leveneTest.- Shapiro-Wilk needs > 3 values. If you filtered to a tiny subset, you may get an error.
t.test()formula: the grouping variable goes on the right of~and must have exactly two levels. Check withunique(tree_df$side).- Cheat sheets — https://posit.co/resources/cheatsheets/
- Bring the exact error (copy-paste it) to class, Canvas, or office hours.
💡 Key idea: The five-step framework (hypotheses → normality → variance → test → report) is the same for nearly every parametric test you will ever run. Master it here and it transfers directly to ANOVA and regression.
End of Worksheet 04. Next: Worksheet 05 — one-way ANOVA for comparing more than two groups. ### Recap from Worksheet 03
- Loaded our Excel leaf data with
read_excel() - 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() - Prediction: shady leaves appeared heavier, taller, and wider than sunny leaves
Today’s Objectives
- State null and alternate hypotheses formally before running any test
- Check the normality assumption — histogram, QQ plot, and Shapiro-Wilk test
- Check the variance assumption — Levene’s test
- Run a Welch’s two-sample t-test (
var.equal = FALSE, two-tailed) - Read and decode every line of the
t.test()output - Make a formal statistical decision and state a conclusion
- Write a results sentence in proper scientific format
How to use this worksheet
- Work through each part in order — the steps build on each other. Type the code into your script in Positron and run it line by line.
- Code blocks marked ▶ Run this should be executed as written.
- Blocks marked ✏️ Your turn ask you to write, modify, or interpret something.
- The Going further section is optional if you finish early.
Part 1 · Load libraries and data
Libraries
▶ Run this at the top of your script — in this exact order:
# Load all packages at the top of the script -----------
library(readxl) # reading Excel files
library(tidyverse) # data manipulation + ggplot2
library(skimr) # fast dataset summaries
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 sessionlibrary(car)
Load the data
▶ Run this:
# Load the leaf data from the data folder --------------
tree_df <- read_excel("data/2026_06_25_tree_experiment_raw_data.xlsx")
head(tree_df)✏️ Your turn: Before anything else — do you remember the structure of this data frame? Fill in from memory, then check with glimpse(tree_df).
Number of rows:
Number of columns:
Column holding the grouping variable (sunny/shady):
Column we are testing today:
Part 2 · 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 weight. 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 3 · Quick descriptive stats review
▶ Run this:
# Recap stats from Lecture 03 — both groups at once ----
recap_df <- tree_df %>%
group_by(side) %>%
summarize(
n = sum(!is.na(weight_g)),
mean_wt = round(mean(weight_g, na.rm = TRUE), 2),
sd_wt = round(sd(weight_g, na.rm = TRUE), 2),
se_wt = round(sd_wt / sqrt(n), 2)
)
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:
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 bell-shaped distribution
hist_norm_plot <- tree_df %>%
ggplot(aes(x = weight_g, fill = side)) +
geom_histogram(binwidth = 1, color = "white", alpha = 0.8) +
facet_wrap(~side, ncol = 2) +
labs(
title = "Leaf Weight Distribution by Side",
x = "Leaf Weight (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
✏️ Your turn: With only 10 values per group, histograms will look lumpy even if the data are normal. Does this mean we should reject normality? Why or why not?
Your answer:
Part 5 · Check normality — 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.
▶ Run this:
# Q-Q plot — points should fall along the diagonal ----
qq_norm_plot <- tree_df %>%
ggplot(aes(sample = weight_g, color = side)) +
stat_qq() +
stat_qq_line(color = "black", linewidth = 0.8) +
facet_wrap(~side, scales = "free") +
labs(
title = "Normal Q-Q Plots by Side",
x = "Theoretical Quantiles",
y = "Sample Quantiles"
) +
theme_minimal() +
theme(legend.position = "none")
qq_norm_plot✏️ Your turn: Do the points for each group fall approximately along the diagonal line? What would it mean if they curved strongly away at the ends?
Sunny side — on the line? Y / mostly / no
Shady side — on the line? Y / mostly / no
What strong curvature at the ends would indicate:
Part 6 · Check normality — Shapiro-Wilk test
The Shapiro-Wilk test formally tests whether a sample could have come from a normal distribution.
- 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 --------------------
tree_df %>%
filter(side == "sunny") %>%
pull(weight_g) %>%
shapiro.test()# Shapiro-Wilk for the shady side -------------------
tree_df %>%
filter(side == "shady") %>%
pull(weight_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):
✏️ Your turn: With n = 10 per group, the Shapiro-Wilk test has low statistical power. What does that mean in plain language for interpreting this result?
Your answer:
⚠️ Watch out! Even if Shapiro-Wilk returns p < 0.05 with very small samples, the t-test may still be appropriate — the QQ plot and histogram give important visual context. None of these checks alone is definitive.
Part 7 · Check variance — Levene’s test
Levene’s test checks whether the two groups have the same population variance.
- 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(weight_g ~ side, data = tree_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 8 · Run the Welch’s t-test
Now that we have checked our assumptions, we can run the test.
▶ Run this:
# Welch's two-sample t-test --------------------------
leaf_ttest_model <- t.test(
weight_g ~ side, # response ~ grouping variable
data = tree_df,
var.equal = FALSE, # Welch's — no pooled variance
alternative = "two.sided" # two-tailed test
)
leaf_ttest_model✏️ Your turn: Copy the full output below:
Paste or write the t.test() output here:
Part 9 · Decode the output line by line
▶ Run this to extract individual values:
# Pull specific values from the model object ---------
cat("t-statistic:", round(leaf_ttest_model$statistic, 3), "\n")
cat("df (Welch): ", round(leaf_ttest_model$parameter, 2), "\n")
cat("p-value: ", signif(leaf_ttest_model$p.value, 3), "\n")
cat("95% CI: ", round(leaf_ttest_model$conf.int[1],2),
"to", round(leaf_ttest_model$conf.int[2], 2), "g\n")
cat("Mean shady: ", round(leaf_ttest_model$estimate[1],2), "g\n")
cat("Mean sunny: ", round(leaf_ttest_model$estimate[2],2), "g\n")✏️ Your turn: Match each piece of output to its meaning:
t-statistic = _____
→ This is large/small (circle one) because the difference in means
is large/small relative to the variability (circle one).
df = _____
→ This is a non-integer. Why? (hint: Welch-Satterthwaite equation)
p-value = _____
→ In plain language, this means: if H₀ were true, the probability
of seeing a t-statistic this extreme just by chance is _____.
95% CI = _____ to _____ g
→ This CI is for the ________________ (the true difference in means).
It does / does not include zero (circle one).
What does it mean when the CI excludes zero?
Mean shady = _____ g
Mean sunny = _____ g
Part 10 · Calculate t by hand
▶ Run this:
# Reproduce the t-statistic manually -----------------
m_sha <- recap_df$mean_wt[recap_df$side == "shady"]
m_sun <- recap_df$mean_wt[recap_df$side == "sunny"]
s_sha <- recap_df$sd_wt[recap_df$side == "shady"]
s_sun <- recap_df$sd_wt[recap_df$side == "sunny"]
n_sha <- recap_df$n[recap_df$side == "shady"]
n_sun <- recap_df$n[recap_df$side == "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, 2), "g\n")
cat("SE of difference: ", round(se_diff, 4), "\n")
cat("Manual t: ", round(t_manual, 3), "\n")✏️ Your turn: Does your manually calculated t match the value from t.test()? (They may differ very slightly due to rounding in recap_df.)
Manual t = _____
t.test() t = _____
Match? Y / close / no
✏️ Your turn: Write the Welch’s t-test formula below and label each part:
t = _________________
Numerator means:
Denominator means:
Part 11 · Make a formal decision
✏️ Your turn: State your decision and conclusion. Use the formal language of hypothesis testing.
Our p-value:
Our α:
Decision (circle one): REJECT H₀ / FAIL TO REJECT H₀
In one sentence, what does this mean biologically?
💡 Key idea: We never say “H₀ is true” or “H₀ is false.” We say “we reject H₀” (evidence was strong enough) or “we fail to reject H₀” (evidence was not strong enough). Statistics gives us a decision rule, not certainty.
Part 12 · Visualize the result
Boxplot with test statistics in the subtitle
▶ Run this:
# Boxplot with t and p embedded in subtitle ----------
leaf_box_04_plot <- tree_df %>%
ggplot(aes(x = side, y = weight_g, fill = side)) +
geom_boxplot(alpha = 0.5, outlier.shape = NA) +
geom_point(
position = position_jitter(width = 0.15, seed = 42),
alpha = 0.6, size = 2.5
) +
labs(
title = "Leaf Weight by Tree Side",
subtitle = paste0("Welch's t-test: t = ",
round(leaf_ttest_model$statistic, 2),
", p = ",
signif(leaf_ttest_model$p.value, 2)),
x = "Side of Tree",
y = "Leaf Weight (g)"
) +
theme_minimal() +
theme(legend.position = "none")
leaf_box_04_plot✏️ Your turn: The subtitle line uses paste0() to build the label from the model object. What happens to the subtitle if you re-run this with different data? Why is this better than typing the numbers yourself?
Your answer:
Save the plot
▶ Run this:
ggsave("figures/leaf_weight_boxplot_ttest.png",
plot = leaf_box_04_plot,
width = 5,
height = 5,
units = "in",
dpi = 300)Part 13 · Write a scientific results sentence
✏️ Your turn: Write a complete results sentence as you would in a lab report or paper. Include:
- Which group was larger
- The word “significantly” (if appropriate)
- Test name, t-statistic, df, and p-value in parentheses
- Mean ± SE for both groups with units
Use this template:
"[Group] leaves were significantly [heavier / lighter] than [group] leaves
(Welch's two-sample t-test: t(___) = ___, p = ___).
Mean ± SE: [side 1] = ___ ± ___ g, [side 2] = ___ ± ___ g."
Write your sentence here:
Your results sentence:
💡 Key idea: Never write
p = 0.000. Writep < 0.001instead. A p-value is never exactly zero — it is just too small to display at three decimal places.
Part 14 · 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. Does it run without errors?
Ran cleanly? Y / N
If not, what error appeared:
Part 15 · Going further
This section is optional — work through it if you finish early or want to push deeper.
Test a second variable: height_cm
▶ Try this:
# Run the same t-test pipeline for height_cm ---------
# 1. Stats recap
tree_df %>%
group_by(side) %>%
summarize(
n = sum(!is.na(height_cm)),
mean_ht = round(mean(height_cm, na.rm = TRUE), 2),
sd_ht = round(sd(height_cm, na.rm = TRUE), 2),
se_ht = round(sd_ht / sqrt(n), 2)
)# 2. Shapiro-Wilk for height_cm
tree_df %>% filter(side == "sunny") %>% pull(height_cm) %>% shapiro.test()
tree_df %>% filter(side == "shady") %>% pull(height_cm) %>% shapiro.test()# 3. Levene's test for height_cm
leveneTest(height_cm ~ side, data = tree_df)# 4. Welch's t-test for height_cm
height_ttest_model <- t.test(
height_cm ~ side,
data = tree_df,
var.equal = FALSE,
alternative = "two.sided"
)
height_ttest_model✏️ Your turn: Is leaf height also significantly different between sides? Fill in the results:
t = _____ df = _____ p = _____
Decision:
One-sentence conclusion:
Change the confidence level
By default t.test() reports a 95% CI (conf.level = 0.95). Try 99%:
▶ Try this:
# Run the weight t-test with 99% confidence interval --
t.test(
weight_g ~ side,
data = tree_df,
var.equal = FALSE,
alternative = "two.sided",
conf.level = 0.99 # 99% CI instead of 95%
)✏️ Your turn: How did the 99% CI compare to the 95% CI? Why is the 99% CI wider?
95% CI was: _____ to _____
99% CI was: _____ to _____
Why wider:
What if the groups were not significantly different?
✏️ Your turn: Think about what would happen if both sides had leaves with weight_g around 5.5 ± 2.0 g. Would the t-test be significant? What would change in the output?
Your prediction:
What your finished figures/ folder should contain
project/
├── data/
│ └── 2026_06_25_tree_experiment_raw_data.xlsx
├── figures/
│ ├── leaf_weight_boxplot.png <- from Worksheet 03
│ ├── leaf_weight_mean_se.png <- from Worksheet 03
│ └── leaf_weight_boxplot_ttest.png <- from this worksheet
└── scripts/
└── 04_leaf_ttest.R <- your script
Getting unstuck
- Read the error message out loud. R usually names the line and the problem.
- Check the usual suspects:
- Loaded
library(car),library(readxl),library(tidyverse)? - Spelling of column names? Check with
names(tree_df) - Missing
)or%>%?
- Loaded
leveneTestnot found? You needlibrary(car)— not justcar::leveneTest.- Shapiro-Wilk needs > 3 values. If you filtered to a tiny subset, you may get an error.
t.test()formula: the grouping variable goes on the right of~and must have exactly two levels. Check withunique(tree_df$side).- Cheat sheets — https://posit.co/resources/cheatsheets/
- Bring the exact error (copy-paste it) to class, Canvas, or office hours.
💡 Key idea: The five-step framework (hypotheses → normality → variance → test → report) is the same for nearly every parametric test you will ever run. Master it here and it transfers directly to ANOVA and regression.
End of Worksheet 04. Next: Worksheet 05 — one-way ANOVA for comparing more than two groups.