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.

NoteAssignment vs this activity

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.

NoteChoosing a t-test
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

NoteHint

Load tidyverse (ggplot2, dplyr) and palmerpenguins (the penguins dataset).

TipSolution
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.

NoteHint

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()
TipSolution
box_species <- ggplot(penguin_bills, aes(x = species, y = bill_length_mm)) +
  geom_boxplot()
box_species

3. Filter to two groups

Keep only Adelie and Chinstrap rows. Save as two_group.

NoteHint

Use filter() on penguin_bills to keep Adelie or Chinstrap rows.

Example:

penguins |>
  filter(species == "Gentoo")
TipSolution
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.

NoteHint

Run shapiro.test() separately on each group’s bill lengths. Subset with $ and a logical condition.

Example:

shapiro.test(mtcars$mpg[mtcars$cyl == 4])
TipSolution
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_chinstrap

Both 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).

NoteHint

Compare both Shapiro p-values to 0.05. Assign TRUE if normality looks acceptable in each group, otherwise FALSE.

TipSolution
assumptions_ok <- TRUE

6. 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.

NoteHint

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.

TipSolution
t_two_sample <- t.test(bill_length_mm ~ species, data = two_group)
t_two_sample

p ≪ 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.

NoteOne-tailed vs two-tailed

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.

NoteHint

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.

TipSolution
t_two_sample_one_tailed <- t.test(
  bill_length_mm ~ species,
  data = two_group,
  alternative = "less"
)
t_two_sample_one_tailed

One-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.

NoteHint

Compare t_two_sample$p.value to 0.05, then check which species had the larger mean in t_two_sample$estimate.

TipSolution
chinstrap_longer <- TRUE

p ≪ 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).

NoteHint

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)
TipSolution
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_pairs

72 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().

NoteHint

Add a column with mutate(): male mass minus female mass.

Example:

mtcars |>
  mutate(log_mpg = log(mpg))
TipSolution
adelie_pairs <- adelie_pairs |>
  mutate(mass_delta = male_mass - female_mass)
adelie_pairs

Most 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.

NoteHint

Use ggplot() + geom_histogram() on mass_delta in adelie_pairs.

Example:

ggplot(mtcars, aes(x = mpg)) +
  geom_histogram()
TipSolution
hist_delta <- ggplot(adelie_pairs, aes(x = mass_delta)) +
  geom_histogram()
hist_delta

Most 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.

NoteHint

Pass the difference vector to shapiro.test().

Example:

shapiro.test(mtcars$mpg)
TipSolution
shapiro_delta <- shapiro.test(adelie_pairs$mass_delta)
shapiro_delta

p ≈ 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.

NoteHint

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)
TipSolution
t_paired <- t.test(adelie_pairs$male_mass, adelie_pairs$female_mass, paired = TRUE)
t_paired

p ≪ 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.

NoteHint

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.

TipSolution
t_paired_greater <- t.test(
  adelie_pairs$male_mass,
  adelie_pairs$female_mass,
  paired = TRUE,
  alternative = "greater"
)
t_paired_greater

15. 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.

NoteHint

Run both lines above — the p-values and t statistics should be identical.

TipSolution

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.

NoteHint

Check t_paired$p.value against 0.05 and compare the male and female means in t_paired$estimate (male column was first).

TipSolution
males_heavier <- TRUE

Mean 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.

NoteHint

Filter penguins to Gentoo rows with non-missing body_mass_g.

Example:

penguins |>
  filter(species == "Adelie", !is.na(bill_length_mm))
TipSolution
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.

NoteHint

Histogram of body_mass_g from gentoo_mass using ggplot() + geom_histogram().

Example:

ggplot(mtcars, aes(x = wt)) +
  geom_histogram()
TipSolution
hist_gentoo_mass <- ggplot(gentoo_mass, aes(x = body_mass_g)) +
  geom_histogram()
hist_gentoo_mass

Roughly 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.

NoteHint

One-sample normality check: shapiro.test() on gentoo_mass$body_mass_g.

Example:

shapiro.test(mtcars$mpg)
TipSolution
shapiro_gentoo_mass <- shapiro.test(gentoo_mass$body_mass_g)
shapiro_gentoo_mass

p ≈ 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.

NoteReading one-sample output

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.

NoteHint

One-sample t.test() on a numeric vector with mu = 4500 and alternative = "greater".

Example:

t.test(mtcars$mpg, mu = 20, alternative = "greater")
TipSolution
t_one_sample_greater <- t.test(
  gentoo_mass$body_mass_g,
  mu = 4500,
  alternative = "greater"
)
t_one_sample_greater

p ≪ 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.

NoteHint

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.

TipSolution
gentoo_above_4500 <- TRUE

Sample 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.