Hypotheses, the t-statistic, and checking assumptions before we test
2026-09-10
filter(), select(), mutate(), arrange() as the core tidyverse verbsgroup_by() + summarize()sum(!is.na()) is the safe way to count nstat_summary() (Lecture 03, GGPlot I)Note
✅ Key idea from Lecture 06
We described the pattern in our data. Over the next two lectures we formally test whether the sides really differ — today we set the test up, next time we run it.
Tip
🖐 Try it yourself
By the end of today your data will be fully checked and ready to test. Next lecture, we run it.
Tools added today:
car — Levene’s variance testReferences:
Both are in the course readings/ folder.
Today’s naming:
_model_plotBiological prediction:
Shady-side leaves will be larger and heavier — they need more surface area to capture the limited light filtering through the canopy.
| Formal statement | |
|---|---|
| H₀ — null | \(\mu_{shady} = \mu_{sunny}\) — no difference in mean leaf mass |
| Hₐ — alternate | \(\mu_{shady} \neq \mu_{sunny}\) — mean leaf mass differs by side |
We use a two-tailed test — we will detect a difference in either direction.
Why two-tailed?
α = 0.05 — our threshold for rejecting H₀
This lecture runs in two short chunks. After each chunk you switch to the activity and type the code yourself into your R script.
For every code block, do three things:
Note
✅ Why bother? (the evidence)
var.equal = FALSE yourself — rather than pasting it — is what makes you notice it’s there at all, instead of skating past the one argument that separates Welch’s from the standard t-test.Note
car is the Companion to Applied Regression package. It contains leveneTest(), which we use to check whether our two groups have similar variance.
Install once: install.packages("car")
We will cover: why comparing means isn’t enough, the five-step framework we’ll use for every test this semester, what a two-sample t-test is, and why we default to Welch’s.
🛑 After this chunk: Activity Parts 1–2 (load data, recap the descriptive stats).
# Remind ourselves what the data look like ---------------
recap_plot <- leaf_df %>%
ggplot(aes(x = shade, y = mass_g, color = shade)) +
geom_point(
position = position_jitter(width = 0.15, seed = 42),
alpha = 0.5,
size = 2
) +
stat_summary(fun = mean, geom = "point", size = 4) +
stat_summary(
fun.data = mean_se,
geom = "errorbar",
width = 0.15,
linewidth = 0.9
) +
labs(x = "Shade", y = "Leaf Mass (g)") +
theme_minimal() +
theme(legend.position = "none")
recap_plot
The two means look close — and with this much scatter, they could easily be the same:
A p-value tells us:
“If H₀ were true, how often would we see a gap at least this large just by chance?”
Large p → a gap this small is nothing unusual.
Every hypothesis test this semester — t-test, ANOVA, regression — follows the same sequence:
| Step | Action | When |
|---|---|---|
| 1 | State H₀ and Hₐ | Today |
| 2 | Check normality | Today |
| 3 | Check variance | Today |
| 4 | Run the test | Next lecture |
| 5 | Interpret and report | Next lecture |
Important
Check assumptions BEFORE running the test.
If your data seriously violate assumptions, the p-value from the t-test is unreliable.
With ~25 per group, our main concern is gross non-normality. The t-test is quite robust to mild departures.
A two-sample t-test compares the means of two independent groups.
It asks: Could both groups plausibly be drawn from populations with the same mean?
The t-statistic is:
\[t = \frac{\bar{x}_1 - \bar{x}_2}{\text{SE}_{\text{diff}}}\]
Large |t| → the difference is large relative to the noise → small p-value
| Component | Our context |
|---|---|
| Group 1 | Shady leaves |
| Group 2 | Sunny leaves |
| Measurement | mass_g |
| H₀ | \(\mu_{shady} = \mu_{sunny}\) |
| Hₐ | \(\mu_{shady} \neq \mu_{sunny}\) |
Standard t-test — pools the two variances into one estimate. Requires both groups to have the same population variance.
Welch’s t-test — uses each group’s variance separately. Does not require equal variance.
\[t_W = \frac{\bar{x}_1 - \bar{x}_2}{\sqrt{\dfrac{s_1^2}{n_1} + \dfrac{s_2^2}{n_2}}}\]
The denominator is the true SE of the difference — no pooling.
Why always use Welch’s?
Tip
Best practice: use Welch’s as your default for all two-group comparisons.
In R: var.equal = FALSE
Note
🔮 Predict first: t = difference ÷ SE-of-the-difference, and the two means differ by only about 0.02 g. Do you expect |t| to land nearer 0.5, 3, or 10? Predict before the numbers appear.
# Watch the instructor plug numbers into the formula ----
stats_df <- leaf_df %>%
group_by(shade) %>%
summarize(
n = sum(!is.na(mass_g)),
m = mean(mass_g, na.rm = TRUE),
s = sd(mass_g, na.rm = TRUE)
)
m_sha <- stats_df$m[stats_df$shade == "shady"]
m_sun <- stats_df$m[stats_df$shade == "sunny"]
s_sha <- stats_df$s[stats_df$shade == "shady"]
s_sun <- stats_df$s[stats_df$shade == "sunny"]
n_sha <- stats_df$n[stats_df$shade == "shady"]
n_sun <- stats_df$n[stats_df$shade == "sunny"]
se_diff <- sqrt((s_sha^2 / n_sha) + (s_sun^2 / n_sun))
t_manual <- (m_sha - m_sun) / se_diff
cat("Difference in means:", round(m_sha - m_sun, 3), "g\n")Difference in means: 0.018 g
SE of difference: 0.0505
t-statistic: 0.356
Step by step:
The observed difference is well under one standard error from zero — right in the range you’d expect if H₀ were true. You’ll try this yourself with a calculator in the activity’s optional section — today the goal is just to see where the number comes from.
🛑 Do Activity Parts 1–2 now
Load the data and recompute the group means, SD, and SE. Predict each output, type it, run it.
We will cover: stating H₀ / Hₐ formally, checking normality (histogram + Shapiro-Wilk), and checking equal variance (Levene’s).
🛑 After this chunk: Activity Parts 3–6 (hypotheses, histogram, Shapiro-Wilk, Levene’s).
Null hypothesis:
\[H_0: \mu_{shady} = \mu_{sunny}\]
Shady and sunny leaves have the same mean mass in the population.
Alternate hypothesis (two-tailed):
\[H_A: \mu_{shady} \neq \mu_{sunny}\]
The mean leaf masses differ between sides — in either direction.
Significance level: α = 0.05
We will reject H₀ if p < 0.05.
Why state hypotheses first?
Our commitment: two-tailed, α = 0.05, before running any test.
# Histogram per group to visually check normality -------
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
What to look for:
With ~25 per group these are readable, but still lumpy — that is normal for real ecological data.
Note
A QQ plot is another common visual check — same idea, different picture. You’ll find one in the activity’s optional section if you want to compare.
Note
🔮 Predict first: Shapiro-Wilk’s H₀ is “the data are normal.” From the histogram you just saw, predict for each side: will p be above or below 0.05?
Shapiro-Wilk normality test
data: .
W = 0.95266, p-value = 0.2876
Shapiro-Wilk H₀: the data are normally distributed.
Important
One side may fail, the other pass.
A borderline Shapiro result (p just under 0.05) with a roughly bell-shaped histogram is not a reason to abandon the t-test — it is robust to mild non-normality. Read the histogram and the test together.
Levene’s test H₀: the two groups have equal variance.
Our response regardless of the result:
We will use Welch’s t-test (var.equal = FALSE).
Welch’s is valid whether or not the variances are equal. We run Levene’s to understand our data, not to choose our test.
Suppose I forgot library(car) and run Levene’s:
R stops with:
Error in leveneTest(mass_g ~ shade, data = leaf_df) :
could not find function "leveneTest"
The fix — load the package that contains the function:
Tip
✅ Why show a broken run?
leveneTest() lives inside car, not base R — so R has no idea what the function is until the package is loaded. Seeing that exact error message once, on purpose, means you won’t waste ten minutes hunting for a typo the next time it happens to you.
🛑 Go to Activity 9 — Parts 3–6
Close the slides. State your hypotheses, then check normality (histogram + Shapiro-Wilk) and variance (Levene’s). Predict every p-value before you run it.
var.equal = FALSE as the safe defaultReferences:
Up next — Lecture 10, T-Tests II:
t.test()