Skip to main content

Compare Two Means with t-tests

One-sample, Welch two-sample, and paired t-tests on the chick-weight and sleep data from lecture

In Coding 7 you checked whether one sample was normal enough for a t-test. Today you run all three t-tests from lecture A009 on the same data the slides used: chickwts (71 chicks, six feeds) and sleep (ten patients, two sleeping drugs). Both are built into R, so there is nothing to download. The numbers you get should match the slides exactly.

The recipe is the same every time:

  1. Write H₀ and Hₐ as math.
  2. Choose the test and set α before you look.
  3. Look at the data, then check the assumptions.
  4. Compute the statistic and its p-value.
  5. p ≤ α: reject H₀. Otherwise: fail to reject. Never “accept”.

The three tables below are a reference for this activity and for Coding Assignment 4, Parts A and B.

Design Test H₀ as math Check normality on
One group vs. a number from outside the data One-sample μ = μ₀ the group
Two independent groups Welch two-sample μ₁ = μ₂ each group separately
The same unit measured twice, or matched pairs Paired μ_diff = 0 the within-unit differences

Welch’s test does not assume equal variances, so you do not need a variance check before it. A paired t-test is a one-sample t-test on the differences, with mu = 0.

Argument Meaning Example
x A numeric vector (one sample, or the first of two) t.test(linseed_chicks$weight, mu = 200)
mu The hypothesized mean for a one-sample test mu = 200
alternative "two.sided" (default), "greater", or "less" alternative = "greater"
y ~ group, data = Formula form: a continuous column split by a two-level column t.test(weight ~ feed, data = two_feeds)
x, y, paired = TRUE Two vectors of the same length, matched row by row t.test(drug2, drug1, paired = TRUE)

One-tailed tests put all of α in one tail. Name the direction before you see the data, and only when the other direction would change nothing you do.

Every test in this activity returns the same kind of object. Save it, print it, and pull pieces out with $.

Piece What it holds Example
$statistic The t value: signal ÷ noise t_one_sample$statistic
$parameter Degrees of freedom t_one_sample$parameter
$p.value Probability of a t this extreme if H₀ were true t_one_sample$p.value
$conf.int 95% confidence interval for the mean (or difference) t_one_sample$conf.int
$estimate The sample mean(s) or mean difference t_one_sample$estimate

shapiro.test() returns the same kind of object. Its H₀ is normality, so a small p-value is evidence against normality. A large one is not proof of normality.

Part A: One-sample t-test

Research question: Do linseed-fed chicks weigh more than 200 g on average?

H₀: μ = 200 g. Hₐ: μ > 200 g. One-sample t-test, one-tailed, α = 0.05. The direction is fixed now, before we look.

1. Load the tidyverse

chickwts and sleep ship with R, so the only package you need is the tidyverse. A conflicts message after loading it is expected, not an error.

NoteHint

From ?library: library(package) attaches a package so its functions become available.

library(package name here)
TipSolution
library(tidyverse)

2. Glimpse the chicks

Run glimpse() on chickwts.

NoteHint

From ?glimpse: glimpse(x) prints rows, columns, and types.

glimpse(mtcars)
TipSolution
glimpse(chickwts)
  1. How many factors are in this dataset, and how many levels does it have?
  2. What type is weight?
  3. Which column tells you which feed a chick ate?

Answers: one factor (feed) with six levels; weight is <dbl>; feed.

3. Boxplot of every feed

Before testing one group, look at all of them. Make a boxplot of weight by feed with ggplot() and geom_boxplot(). Save it as box_feed and display it.

NoteHint

Add geom_boxplot() with +, then type the plot’s name on its own line so it displays.

box_iris <- ggplot(iris, aes(x = Species, y = Sepal.Length)) +
  geom_boxplot()
box_iris
TipSolution
box_feed <- ggplot(chickwts, aes(x = feed, y = weight)) +
  geom_boxplot()
box_feed

This is the figure from the lecture slides. Linseed sits near the middle; horsebean is lowest, sunflower and casein highest.

4. Keep only the linseed chicks

A one-sample t-test uses one group. Filter chickwts to the rows where feed is "linseed" and save the result as linseed_chicks.

NoteHint

From ?filter: keep rows where a condition is TRUE. Text goes in quotes.

mtcars |>
  filter(cyl == 4)
TipSolution
linseed_chicks <- chickwts |>
  filter(feed == "linseed")
linseed_chicks

12 chicks. Their mean weight is 218.75 g.

5. Histogram of linseed weights

Make a histogram of weight in linseed_chicks. Save it as hist_linseed and display it.

NoteHint

Add geom_histogram(). With 12 values, binwidth = 25 gives a readable picture.

hist_mpg <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(binwidth = 5)
hist_mpg
TipSolution
hist_linseed <- ggplot(linseed_chicks, aes(x = weight)) +
  geom_histogram(binwidth = 25)
hist_linseed

Twelve values spread from about 140 to 310 g. No obvious skew, no gap, nothing impossible.

6. Shapiro-Wilk on the linseed weights

Run shapiro.test() on weight in linseed_chicks. Save the result as shapiro_linseed and print it.

NoteHint

Pass one numeric column with $.

shapiro.test(mtcars$mpg)
TipSolution
shapiro_linseed <- shapiro.test(linseed_chicks$weight)
shapiro_linseed

W = 0.969, p = 0.90. No evidence against normality. The same output is on the lecture slide.

7. One-tailed one-sample t-test

Test whether the mean linseed weight is greater than 200 g. Use t.test() with mu = 200 and alternative = "greater". Save the result as t_one_sample and print it.

NoteHint

From ?t.test: for one sample, pass a numeric vector, the hypothesized mean, and the direction.

t.test(mtcars$mpg, mu = 20, alternative = "greater")
TipSolution
t_one_sample <- t.test(linseed_chicks$weight, mu = 200, alternative = "greater")
t_one_sample

t = 1.24, df = 11, p = 0.12. Mean = 218.75 g; the one-sided 95% CI runs from 191.7 g upward and includes 200.

8. Make the call

Does the mean weight of linseed-fed chicks exceed 200 g at α = 0.05? Assign linseed_above_200 to TRUE or FALSE (no quotes, all caps).

NoteHint

Compare t_one_sample$p.value with 0.05. The sample mean being above 200 is not enough on its own.

TipSolution
linseed_above_200 <- FALSE

p = 0.12 is above α = 0.05, so we fail to reject H₀. The sample mean (218.75 g) is above 200, but a difference this size is easy to get by chance with 12 chicks. We do not “accept” that the mean is 200; we lack evidence that it is higher.

Part B: Welch two-sample t-test

Research question: Do linseed and meatmeal chicks differ in mean weight?

H₀: μ_linseed = μ_meatmeal. Hₐ: μ_linseed ≠ μ_meatmeal. Welch two-sample t-test, two-tailed, α = 0.05. Different chicks ate each feed, so the groups are independent.

9. Keep two feeds

Filter chickwts to the linseed and meatmeal rows. Save the result as two_feeds.

NoteHint

%in% asks whether a value is one of several. Either of these works:

mtcars |>
  filter(cyl %in% c(4, 6))

mtcars |>
  filter(cyl == 4 | cyl == 6)
TipSolution
two_feeds <- chickwts |>
  filter(feed %in% c("linseed", "meatmeal"))
two_feeds

12 linseed + 11 meatmeal = 23 rows.

10. Histogram of each group

Welch’s test assumes each group is roughly normal, so look at each group separately. Make a histogram of weight in two_feeds with one panel per feed using facet_wrap(~ feed). Save it as hist_two_feeds and display it.

NoteHint

Add geom_histogram(), then facet_wrap(~ feed) so each feed gets its own panel.

ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(binwidth = 5) +
  facet_wrap(~ cyl)
TipSolution
hist_two_feeds <- ggplot(two_feeds, aes(x = weight)) +
  geom_histogram(binwidth = 25) +
  facet_wrap(~ feed)
hist_two_feeds

Two panels, 12 and 11 chicks. Neither is badly skewed. Meatmeal sits higher on the axis.

11. Shapiro-Wilk on each group

Run shapiro.test() on the linseed weights and, separately, on the meatmeal weights. Save them as shapiro_linseed and shapiro_meatmeal and print both.

NoteHint

Two routes to one group’s weights. Pick either.

# square brackets: keep the mpg values where cyl is 4
shapiro.test(mtcars$mpg[mtcars$cyl == 4])

# filter then pull
cyl4 <- mtcars |> filter(cyl == 4) |> pull(mpg)
shapiro.test(cyl4)
TipSolution
shapiro_linseed <- shapiro.test(two_feeds$weight[two_feeds$feed == "linseed"])
shapiro_meatmeal <- shapiro.test(two_feeds$weight[two_feeds$feed == "meatmeal"])
shapiro_linseed
shapiro_meatmeal

Linseed p = 0.90, meatmeal p = 0.96. No evidence against normality in either group.

12. Welch two-sample t-test

Run a Welch two-sample t-test of weight by feed in two_feeds. Save it as t_two_sample and print it.

NoteHint

Formula form: the continuous column, a tilde, the grouping column, then data =. Welch is the default.

t.test(mpg ~ am, data = mtcars)
TipSolution
t_two_sample <- t.test(weight ~ feed, data = two_feeds)
t_two_sample

t = −2.35, df = 19.2, p = 0.029. The difference (linseed − meatmeal) is −58 g, 95% CI −109.8 to −6.5 g. The lecture passed two vectors, t.test(weights_meatmeal, weights_linseed), so its t is +2.35: same test, same p-value, opposite subtraction order. Coding Assignment 4 uses the formula form.

13. Make the call

Do the two feeds differ in mean chick weight at α = 0.05? Assign feeds_differ to TRUE or FALSE.

NoteHint

Compare t_two_sample$p.value with 0.05, and check whether t_two_sample$conf.int contains zero.

TipSolution
feeds_differ <- TRUE

p = 0.029 < 0.05, and the 95% CI (−109.8 to −6.5 g) excludes zero. Reject H₀: meatmeal chicks are heavier than linseed chicks by about 58 g on average.

Part C: Paired t-test

Research question: Do two sleeping drugs differ in the extra sleep they give?

The sleep data record extra hours of sleep for ten patients, each of whom tried drug 1 and drug 2. The same person measured twice is a paired design. H₀: μ_diff = 0. Hₐ: μ_diff ≠ 0, where diff = drug 2 − drug 1 within each patient. Paired t-test, two-tailed, α = 0.05.

14. Glimpse the sleep data

Run glimpse() on sleep.

NoteHint

Same function as step 2, different table.

TipSolution
glimpse(sleep)
  1. There are 20 rows. How many patients are there?
  2. Which column tells you which drug a row belongs to, and which tells you which patient?
  3. Why is this a paired design and not two independent groups?

Answers: 10 patients, each appearing twice; group is the drug (1 or 2) and ID is the patient; every patient tried both drugs, so each drug-2 value has a matching drug-1 value from the same person.

15. Spaghetti plot

Draw each patient’s two values joined by a line. Start from ggplot(sleep, aes(x = group, y = extra, group = ID)), add geom_line() and geom_point(). Save it as spaghetti_sleep and display it.

NoteHint

group = ID inside aes() tells geom_line() which two points belong to the same patient. Two layers, each added with +; the starter already has the ggplot() call.

TipSolution
spaghetti_sleep <- ggplot(sleep, aes(x = group, y = extra, group = ID)) +
  geom_line(color = "grey50") +
  geom_point(size = 2.5)
spaghetti_sleep

Nine of ten lines rise from drug 1 to drug 2. Side-by-side boxplots hide this, because patients differ from each other far more than the two drugs differ within a patient.

16. One row per patient

A paired test needs each patient’s two values side by side. pivot_wider() is the reverse of the pivot_longer() you used in Coding 6. The code is written for you: run it and look at the result.

NoteHint

names_from = group turns the values of group (1 and 2) into column names; names_prefix = "drug" makes them drug1 and drug2; values_from = extra fills those columns.

TipSolution

Run the block as written. You get 10 rows with columns ID, drug1, and drug2. Coding Assignment 4 hands you the drought data already in this shape, with photo_before and photo_after columns.

17. The differences are the data

Add a column sleep_delta equal to drug2 - drug1 with mutate(). Save the result back as sleep_wide.

NoteHint

From ?mutate: add a column computed from existing ones.

mtcars |>
  mutate(hp_per_cyl = hp / cyl)
TipSolution
sleep_wide <- sleep_wide |>
  mutate(sleep_delta = drug2 - drug1)
sleep_wide

Nine positive differences and one zero. Patient 9 gained 4.6 hours. The mean difference is 1.58 hours.

18. Histogram of the differences

Make a histogram of sleep_delta in sleep_wide. Save it as hist_delta and display it.

NoteHint

Same pattern as step 5. With 10 values, binwidth = 0.5 works well.

TipSolution
hist_delta <- ggplot(sleep_wide, aes(x = sleep_delta)) +
  geom_histogram(binwidth = 0.5)
hist_delta

A cluster between 0 and 2.5 hours and one value out at 4.6. That one patient is about to matter.

19. Shapiro-Wilk on the differences

For a paired design you check the differences, not drug 1 and drug 2 separately. Run shapiro.test() on sleep_delta. Save it as shapiro_delta and print it.

NoteHint

Same function as step 6, applied to sleep_wide$sleep_delta.

TipSolution
shapiro_delta <- shapiro.test(sleep_wide$sleep_delta)
shapiro_delta

W = 0.83, p = 0.033. The differences fail the normality check, because of patient 9.

ImportantOne odd patient, n = 10: what to do

The check failed because one patient gained 4.6 hours. Work through the questions from Coding 7.

  • Impossible? No. Data-entry error? No reason to think so. Keep the point.
  • Transform? Adding a constant to the differences changes H₀ from “mean difference = 0” to something with no biological meaning. The “+ c” trick is for skewed positive data with zeros, not for paired differences.
  • Small n. With ten values the normality test is weak, and one value can tip it.

So: run the paired t-test, report the failed check honestly, and run a sensitivity check without patient 9. If the conclusion is the same either way, say so.

The differences fail the normality check because one patient gained 4.6 hours. What should you do?

  1. Remove patient 9 and rerun the check
  2. Run the paired t-test and report the caveat
  3. Switch to a two-sample t-test

Answer: B. Deleting a real, possible value to pass a check is not analysis. Switching to a two-sample test throws away the pairing, which is the whole point of the design.

20. Paired t-test

Run a paired t-test on drug2 and drug1 in sleep_wide, drug 2 first. Save it as t_paired and print it.

NoteHint

Two vectors of the same length and paired = TRUE. The first vector minus the second is the difference being tested.

after <- c(11, 13, 12)
before <- c(10, 12, 11)
t.test(after, before, paired = TRUE)
TipSolution
t_paired <- t.test(sleep_wide$drug2, sleep_wide$drug1, paired = TRUE)
t_paired

t = 4.06, df = 9, p = 0.0028. Mean difference 1.58 hours, 95% CI 0.70 to 2.46 hours. Same as the slide.

21. Same test, two ways

A paired t-test is a one-sample t-test on the differences with mu = 0. Run the block and compare the two t statistics and p-values.

NoteHint

Run the block as written. The only difference between the two calls is who does the subtraction: R, or you in step 17.

TipSolution

Both give t = 4.06 and p = 0.0028. That is why the normality check goes on the differences: they are the one sample the test actually sees.

22. Sensitivity check

Rerun the one-sample test on the differences without patient 9 and see whether the conclusion changes. The block is written for you.

NoteHint

ID is a factor, so the patient number goes in quotes. Nine differences remain.

TipSolution

With all ten patients, p = 0.0028. Without patient 9, t = 5.66, df = 8, p = 0.0005. Same conclusion either way, so the failed normality check does not change the answer. Report the test on all ten and say that.

23. Make the call

Does drug 2 give more extra sleep than drug 1 at α = 0.05? Assign drug2_more_sleep to TRUE or FALSE.

NoteHint

Compare t_paired$p.value with 0.05 and look at the sign of t_paired$estimate.

TipSolution
drug2_more_sleep <- TRUE

p = 0.0028 < 0.05 and the mean difference is +1.58 hours. Reject H₀: drug 2 gives more extra sleep, by about 1.6 hours per patient (95% CI 0.7 to 2.5).

24. Recap: functions you practiced

Function Role
glimpse() Rows, columns, and types
filter() with == or %in% Keep one group, or two
ggplot() + geom_boxplot() Compare a continuous variable across groups
ggplot() + geom_histogram() + facet_wrap() Shape of each group, one panel each
ggplot() + geom_line() with group = ID Spaghetti plot: each pair joined by a line
pivot_wider() One row per patient, one column per drug
mutate() Compute the within-patient difference
shapiro.test() Normality check (H₀: data are normal)
t.test(x, mu = ..., alternative = "greater") One-sample, one-tailed
t.test(y ~ group, data = ...) Welch two-sample, two-tailed by default
t.test(x, y, paired = TRUE) Paired
t.test(delta, mu = 0) The same paired test, done by hand
$p.value, $estimate, $conf.int Pull one piece out of a test result

If you can explain each row in your own words, you have covered Coding Assignment 4, Parts A and B.

ImportantThree things to say in your write-up
  1. “Fail to reject” is not “accept”. The linseed test gave p = 0.12. That is not evidence that the mean is 200 g; it is a lack of evidence that it is higher. Twelve chicks is a small sample.
  2. Report the effect, not just the p-value. Meatmeal chicks were heavier by 58 g (95% CI 6.5 to 109.8 g). Drug 2 added 1.58 hours of sleep (95% CI 0.70 to 2.46). The interval tells the reader how big and how certain.
  3. Report failed checks and what you did about them. The sleep differences failed Shapiro-Wilk (p = 0.033) because of one patient. The sensitivity check without that patient gave the same conclusion. Say both.

25. Transfer to Coding Assignment 4

On DataHub you will repeat Parts B and C with the brittlebush data:

  1. Part A, two-sample. Boxplot of biomass_g by nitrogen for all four levels, filter() to control and high, shapiro.test() on each group, then t.test(biomass_g ~ nitrogen, data = two_group). Same formula form as step 12.
  2. Part B, paired. drought_data already has photo_before and photo_after side by side, like sleep_wide after step 16. Add drought_delta = photo_after - photo_before with mutate(), histogram and shapiro.test() on the delta, then t.test(photo_after, photo_before, paired = TRUE).

The assignment tells you to keep every row even when a boxplot shows a point past the whiskers. That is the same decision you made about patient 9.

Keep playing

Wrong test, wrong answer. What if you forgot the pairing and ran a two-sample test on the sleep data?

p = 0.079. Not significant. The paired test on the same twenty numbers gave p = 0.0028. Patients differ from each other by hours, and the two-sample test treats all of that as noise. Pairing removes it. The test you choose follows from how the data were collected.

Two tails instead of one. Rerun the linseed test without alternative = "greater".

p = 0.24, twice the one-tailed 0.12. A one-tailed test is a choice you make before looking. If a two-tailed test gives p = 0.08 and you switch to one-tailed afterward to get p = 0.04, that is not a result. It is a decision made after the data.

Which test? For each design, say which t-test you would run and what you would check for normality.

  1. Blood glucose measured in 20 rats, then again in the same 20 rats two hours after a glucose dose.
  2. Mean height of every BILD 5 student compared with a random sample of 300 UCSD students.
  3. Is the lead level in one well above the legal limit? Ten samples from that well.

Answers: (1) paired; check the 20 within-rat differences. (2) Welch two-sample; check each group separately. (3) one-sample, one-tailed, alternative = "greater"; check the ten samples. The direction is justified because a level below the limit changes nothing you do.