Compare Two Means with t-tests
One-sample, Welch two-sample, and paired t-tests on Palmer Penguins — check assumptions, then interpret
Everything on this page runs real R in your browser. You will practice the same workflow as Coding Assignment 4, Parts A and B, plus a one-sample t-test from lecture A008: explore the data, check assumptions, run the test, and interpret the result.
On DataHub, Assignment 4 uses brittlebush CSV files. Here we use Palmer Penguins so you can practice in the browser. In Part A, bill length is pre-filtered for you in step 2. Part B builds nest-mate pairs from penguins_raw and asks you to mutate() a delta column, just like Assignment 4. In Part C, the penguins table can still have missing body_mass_g — you will drop those rows with !is.na() before the one-sample test.
For a one-sample t-test with transformation when normality fails, see Coding Activity 5. Part C here runs a directional one-sample test on data that already looks normal enough.
| Design | Test | H₀ in words |
|---|---|---|
| One group vs a hypothesized mean | One-sample t-test | True mean equals mu |
| Two independent groups | Welch two-sample t-test | True means are equal |
| Two measurements on the same unit (pair) | Paired t-test | True mean difference is zero |
Welch’s two-sample test does not assume equal variances. Default t.test() is two-sided; you can set alternative = "greater" or "less" when the research question is directional — choose the tail before you look at the p-value.
Part A — Welch two-sample t-test
Research question: Is mean bill length different between Adelie and Chinstrap penguins?
1. Load packages
Load tidyverse (ggplot2, dplyr) and palmerpenguins (the penguins dataset).
library(tidyverse)
library(palmerpenguins)2. Boxplot all species
Make a boxplot of bill_length_mm by species for every penguin with a complete bill measurement. Save as box_species and display it.
Use ggplot(), aes(), and geom_boxplot() on penguin_bills. Map species to x and bill_length_mm to y.
Example on a different dataset:
ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
geom_boxplot()box_species <- ggplot(penguin_bills, aes(x = species, y = bill_length_mm)) +
geom_boxplot()
box_species3. Filter to two groups
Keep only Adelie and Chinstrap rows. Save as two_group.
Use filter() on penguin_bills to keep Adelie or Chinstrap rows.
Example:
penguins |>
filter(species == "Gentoo")two_group <- penguin_bills |>
filter(species == "Adelie" | species == "Chinstrap")151 Adelie + 68 Chinstrap = 219 rows.
4. Shapiro-Wilk per group
Run shapiro.test() on bill_length_mm separately for Adelie and Chinstrap in two_group. Save as shapiro_adelie and shapiro_chinstrap, and print both.
Run shapiro.test() separately on each group’s bill lengths. Subset with $ and a logical condition.
Example:
shapiro.test(mtcars$mpg[mtcars$cyl == 4])shapiro_adelie <- shapiro.test(two_group$bill_length_mm[two_group$species == "Adelie"])
shapiro_chinstrap <- shapiro.test(two_group$bill_length_mm[two_group$species == "Chinstrap"])
shapiro_adelie
shapiro_chinstrapBoth p-values are above 0.05 — normality within each group looks acceptable for a Welch t-test.
5. Assumptions met?
Assign assumptions_ok <- TRUE if the Shapiro tests support normality in each group (Welch does not require equal variance).
Compare both Shapiro p-values to 0.05. Assign TRUE if normality looks acceptable in each group, otherwise FALSE.
assumptions_ok <- TRUE6. Two-sided Welch t-test
Run a Welch two-sample t-test comparing bill_length_mm between species in two_group. Save as t_two_sample and print it.
Use t.test() with a formula: continuous variable ~ grouping factor, and data = two_group.
Example:
t.test(mpg ~ as.factor(cyl), data = mtcars)Default alternative is "two.sided" — what Assignment 4 uses.
t_two_sample <- t.test(bill_length_mm ~ species, data = two_group)
t_two_samplep ≪ 0.001 — reject H₀. Chinstrap mean bill length is about 10 mm longer than Adelie.
7. One-tailed Welch t-test
Directional question: Are Chinstrap bills longer than Adelie bills?
Re-run the test with alternative = "less". Adelie is the first factor level, so "less" tests whether Adelie mean < Chinstrap mean (i.e., Chinstrap is longer). Save as t_two_sample_one_tailed and print it.
When the effect goes in the direction you hypothesized, the one-tailed p-value is about half the two-tailed p-value. Assignment 4 uses two-sided tests unless a question says otherwise.
Re-run the two-sample test with alternative = "less" (Adelie is the first factor level).
Functions: t.test(). Objects: two_group, bill_length_mm, species.
t_two_sample_one_tailed <- t.test(
bill_length_mm ~ species,
data = two_group,
alternative = "less"
)
t_two_sample_one_tailedOne-tailed p is still ≪ 0.001 — Chinstrap bills are significantly longer.
8. Interpret the two-sample test
Using t_two_sample, decide whether Chinstrap mean bill length is significantly longer than Adelie (α = 0.05). Assign chinstrap_longer <- TRUE or FALSE.
Compare t_two_sample$p.value to 0.05, then check which species had the larger mean in t_two_sample$estimate.
chinstrap_longer <- TRUEp ≪ 0.05 and the Chinstrap mean exceeds the Adelie mean by about 10 mm.
Part B — Paired t-test
Research question: Within Adelie breeding pairs, is male body mass different from female body mass?
The raw Palmer Penguins file (penguins_raw) records individual birds with nest IDs like N1A1. You will build 72 Adelie nest-mate pairs — one male and one female measured at the same nest in the same breeding season — then add a paired-difference column before running the test.
9. Build Adelie nest-mate pairs
From penguins_raw, keep Adelie penguins with non-missing body mass and sex — after make.names(), use !is.na(Body.Mass..g.) in filter(). Extract nest ID and year, keep nests with exactly two birds, then pivot_wider() so each row has female_mass and male_mass. Save as adelie_pairs (72 rows).
Objects: penguins_raw. Functions you’ll likely need: make.names(), mutate(), str_extract(), count(), filter(), inner_join(), pivot_wider(), transmute().
Workflow: extract nest ID from Individual.ID, keep Adelie birds with mass and sex, find nests with exactly two birds in the same year, then pivot sex to columns.
Small pivot_wider() example:
tibble(
nest = c("A", "A", "B", "B"),
sex = c("female", "male", "female", "male"),
mass = c(3800, 3750, 3200, 3400)
) |>
pivot_wider(names_from = sex, values_from = mass)raw <- penguins_raw
names(raw) <- make.names(names(raw))
adelie_nests <- raw |>
mutate(
nest_id = str_extract(Individual.ID, "^N[0-9]+"),
year = as.integer(format(Date.Egg, "%Y")),
species_short = if_else(str_detect(Species, "Adelie"), "Adelie", "Other"),
sex_l = tolower(Sex)
) |>
filter(species_short == "Adelie", !is.na(Body.Mass..g.), sex_l %in% c("male", "female"))
two_bird_nests <- adelie_nests |>
count(nest_id, year, name = "n_birds") |>
filter(n_birds == 2) |>
select(nest_id, year)
adelie_pairs <- adelie_nests |>
inner_join(two_bird_nests, by = c("nest_id", "year")) |>
select(nest_id, year, sex_l, mass = Body.Mass..g.) |>
pivot_wider(names_from = sex_l, values_from = mass) |>
filter(!is.na(male), !is.na(female)) |>
transmute(
female_mass = female,
male_mass = male
)
adelie_pairs72 rows — one nest-mate pair per row.
10. Paired difference column
Assignment 4 asks you to compute a delta column. Add mass_delta = male_mass - female_mass to adelie_pairs with mutate().
Add a column with mutate(): male mass minus female mass.
Example:
mtcars |>
mutate(log_mpg = log(mpg))adelie_pairs <- adelie_pairs |>
mutate(mass_delta = male_mass - female_mass)
adelie_pairsMost values are positive — males tend to be heavier within a pair.
11. Histogram of paired differences
Make a histogram of mass_delta in adelie_pairs. Save as hist_delta and display it.
Use ggplot() + geom_histogram() on mass_delta in adelie_pairs.
Example:
ggplot(mtcars, aes(x = mpg)) +
geom_histogram()hist_delta <- ggplot(adelie_pairs, aes(x = mass_delta)) +
geom_histogram()
hist_deltaMost differences are positive — males tend to be heavier within a pair.
12. Shapiro-Wilk on the differences
Run shapiro.test() on mass_delta. Save as shapiro_delta and print it.
Pass the difference vector to shapiro.test().
Example:
shapiro.test(mtcars$mpg)shapiro_delta <- shapiro.test(adelie_pairs$mass_delta)
shapiro_deltap ≈ 0.26 — the paired differences look roughly normal.
13. Two-sided paired t-test
Run a paired t-test on male_mass and female_mass in adelie_pairs. Save as t_paired and print it.
Use t.test() with paired = TRUE on the two mass columns in adelie_pairs.
Example with made-up paired measurements:
before <- c(10, 12, 11)
after <- c(11, 13, 12)
t.test(after, before, paired = TRUE)t_paired <- t.test(adelie_pairs$male_mass, adelie_pairs$female_mass, paired = TRUE)
t_pairedp ≪ 0.001 — reject H₀ that the mean difference is zero.
14. One-tailed paired t-test
Directional question: Are males heavier than their nest-mates?
Re-run with alternative = "greater" (male column first). Save as t_paired_greater and print it.
Same paired setup as before, with alternative = "greater" and the male column first.
Functions: t.test(), paired = TRUE, alternative = "greater". Objects: adelie_pairs$male_mass, adelie_pairs$female_mass.
t_paired_greater <- t.test(
adelie_pairs$male_mass,
adelie_pairs$female_mass,
paired = TRUE,
alternative = "greater"
)
t_paired_greater15. Same result two ways
A paired t-test on two columns equals a one-sample t-test of the differences against mu = 0. Run both and confirm they match.
Run both lines above — the p-values and t statistics should be identical.
Both give the same t statistic and p-value. The paired test is really asking whether the mean difference is zero.
16. Interpret the paired test
Using t_paired, decide whether males are significantly heavier than females within Adelie breeding pairs (α = 0.05). Assign males_heavier <- TRUE or FALSE.
Check t_paired$p.value against 0.05 and compare the male and female means in t_paired$estimate (male column was first).
males_heavier <- TRUEMean male − female mass ≈ 682 g within pairs; p ≪ 0.05.
Part C — One-sample t-test (one-tailed)
Research question: Does mean body mass among Gentoo penguins exceed 4500 g?
This matches the lecture pattern: one continuous sample compared to a hypothesized mean (mu), with a directional alternative. H₀: true mean = 4500 g. Hₐ: true mean > 4500 g.
17. Build the Gentoo sample
Filter penguins to Gentoo rows with non-missing body mass: filter(species == "Gentoo", !is.na(body_mass_g)). Save as gentoo_mass.
Filter penguins to Gentoo rows with non-missing body_mass_g.
Example:
penguins |>
filter(species == "Adelie", !is.na(bill_length_mm))gentoo_mass <- penguins |>
filter(species == "Gentoo", !is.na(body_mass_g))123 Gentoo penguins — sample mean ≈ 5076 g.
18. Histogram
Make a histogram of body_mass_g in gentoo_mass. Save as hist_gentoo_mass and display it.
Histogram of body_mass_g from gentoo_mass using ggplot() + geom_histogram().
Example:
ggplot(mtcars, aes(x = wt)) +
geom_histogram()hist_gentoo_mass <- ggplot(gentoo_mass, aes(x = body_mass_g)) +
geom_histogram()
hist_gentoo_massRoughly bell-shaped — a reasonable first look before Shapiro-Wilk.
19. Shapiro-Wilk
Run shapiro.test() on body_mass_g in gentoo_mass. Save as shapiro_gentoo_mass and print it.
One-sample normality check: shapiro.test() on gentoo_mass$body_mass_g.
Example:
shapiro.test(mtcars$mpg)shapiro_gentoo_mass <- shapiro.test(gentoo_mass$body_mass_g)
shapiro_gentoo_massp ≈ 0.23 — normality looks acceptable for a one-sample t-test on this sample.
20. One-tailed one-sample t-test
Test whether mean Gentoo body mass is greater than 4500 g. Use alternative = "greater". Save as t_one_sample_greater and print it.
Pass a numeric vector and a hypothesized mean mu. With alternative = "greater", H₀ is that the true mean equals mu; Hₐ is that the true mean is above mu.
One-sample t.test() on a numeric vector with mu = 4500 and alternative = "greater".
Example:
t.test(mtcars$mpg, mu = 20, alternative = "greater")t_one_sample_greater <- t.test(
gentoo_mass$body_mass_g,
mu = 4500,
alternative = "greater"
)
t_one_sample_greaterp ≪ 0.001 — reject H₀. Mean Gentoo body mass is significantly above 4500 g.
21. Interpret the one-sample test
Using t_one_sample_greater, decide whether Gentoo mean body mass is significantly above 4500 g (α = 0.05). Assign gentoo_above_4500 <- TRUE or FALSE.
Compare t_one_sample_greater$p.value to 0.05 and check whether the sample mean in t_one_sample_greater$estimate exceeds 4500 g.
gentoo_above_4500 <- TRUESample mean is well above 4500 g and the one-tailed p-value is ≪ 0.05.
22. Recap — functions you practiced
| Function | Role |
|---|---|
ggplot() + geom_boxplot() |
Compare a continuous variable across groups |
filter() |
Keep rows for the groups you want to compare |
pivot_wider() |
Reshape long nest records to one row per pair |
mutate() |
Compute paired difference (mass_delta) |
shapiro.test() |
Test normality (H₀: data are normal) |
t.test(x, mu = ..., alternative = "greater") |
One-sample t-test against a hypothesized mean |
t.test(y ~ x, data = ...) |
Welch two-sample t-test (default two-sided) |
t.test(..., alternative = "less") |
One-tailed Welch test |
ggplot() + geom_histogram() |
Visual check on one sample or paired differences |
t.test(x, y, paired = TRUE) |
Paired t-test on two matched columns |
t.test(delta, mu = 0) |
Equivalent one-sample test on differences |
If you can explain each row, you have covered Coding Assignment 4, Parts A and B plus the lecture one-sample workflow.
Keep playing
What if you compare Gentoo male vs female body mass with a two-sample Welch test instead of a paired design? Load penguins, filter to Gentoo with complete sex and mass, and run t.test(body_mass_g ~ sex, data = gentoo).
Same biological question (sex difference in mass), different experimental design — independent samples vs matched pairs. The test you choose follows from how the data were collected, not from what you wish were true.