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:
- Write H₀ and Hₐ as math.
- Choose the test and set α before you look.
- Look at the data, then check the assumptions.
- Compute the statistic and its p-value.
- 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.
t.test() arguments (click to expand)
| 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.
From ?library: library(package) attaches a package so its functions become available.
library(package name here)library(tidyverse)2. Glimpse the chicks
Run glimpse() on chickwts.
From ?glimpse: glimpse(x) prints rows, columns, and types.
glimpse(mtcars)glimpse(chickwts)- How many factors are in this dataset, and how many levels does it have?
- What type is
weight? - 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.
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_irisbox_feed <- ggplot(chickwts, aes(x = feed, y = weight)) +
geom_boxplot()
box_feedThis 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.
From ?filter: keep rows where a condition is TRUE. Text goes in quotes.
mtcars |>
filter(cyl == 4)linseed_chicks <- chickwts |>
filter(feed == "linseed")
linseed_chicks12 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.
Add geom_histogram(). With 12 values, binwidth = 25 gives a readable picture.
hist_mpg <- ggplot(mtcars, aes(x = mpg)) +
geom_histogram(binwidth = 5)
hist_mpghist_linseed <- ggplot(linseed_chicks, aes(x = weight)) +
geom_histogram(binwidth = 25)
hist_linseedTwelve 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.
Pass one numeric column with $.
shapiro.test(mtcars$mpg)shapiro_linseed <- shapiro.test(linseed_chicks$weight)
shapiro_linseedW = 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.
From ?t.test: for one sample, pass a numeric vector, the hypothesized mean, and the direction.
t.test(mtcars$mpg, mu = 20, alternative = "greater")t_one_sample <- t.test(linseed_chicks$weight, mu = 200, alternative = "greater")
t_one_samplet = 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).
Compare t_one_sample$p.value with 0.05. The sample mean being above 200 is not enough on its own.
linseed_above_200 <- FALSEp = 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.
%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)two_feeds <- chickwts |>
filter(feed %in% c("linseed", "meatmeal"))
two_feeds12 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.
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)hist_two_feeds <- ggplot(two_feeds, aes(x = weight)) +
geom_histogram(binwidth = 25) +
facet_wrap(~ feed)
hist_two_feedsTwo 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.
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)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_meatmealLinseed 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.
Formula form: the continuous column, a tilde, the grouping column, then data =. Welch is the default.
t.test(mpg ~ am, data = mtcars)t_two_sample <- t.test(weight ~ feed, data = two_feeds)
t_two_samplet = −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.
Compare t_two_sample$p.value with 0.05, and check whether t_two_sample$conf.int contains zero.
feeds_differ <- TRUEp = 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.
Same function as step 2, different table.
glimpse(sleep)- There are 20 rows. How many patients are there?
- Which column tells you which drug a row belongs to, and which tells you which patient?
- 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.
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.
spaghetti_sleep <- ggplot(sleep, aes(x = group, y = extra, group = ID)) +
geom_line(color = "grey50") +
geom_point(size = 2.5)
spaghetti_sleepNine 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.
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.
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.
From ?mutate: add a column computed from existing ones.
mtcars |>
mutate(hp_per_cyl = hp / cyl)sleep_wide <- sleep_wide |>
mutate(sleep_delta = drug2 - drug1)
sleep_wideNine 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.
Same pattern as step 5. With 10 values, binwidth = 0.5 works well.
hist_delta <- ggplot(sleep_wide, aes(x = sleep_delta)) +
geom_histogram(binwidth = 0.5)
hist_deltaA 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.
Same function as step 6, applied to sleep_wide$sleep_delta.
shapiro_delta <- shapiro.test(sleep_wide$sleep_delta)
shapiro_deltaW = 0.83, p = 0.033. The differences fail the normality check, because of patient 9.
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?
- Remove patient 9 and rerun the check
- Run the paired t-test and report the caveat
- 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.
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)t_paired <- t.test(sleep_wide$drug2, sleep_wide$drug1, paired = TRUE)
t_pairedt = 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.
Run the block as written. The only difference between the two calls is who does the subtraction: R, or you in step 17.
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.
ID is a factor, so the patient number goes in quotes. Nine differences remain.
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.
Compare t_paired$p.value with 0.05 and look at the sign of t_paired$estimate.
drug2_more_sleep <- TRUEp = 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.
- “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.
- 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.
- 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:
- Part A, two-sample. Boxplot of
biomass_gbynitrogenfor all four levels,filter()tocontrolandhigh,shapiro.test()on each group, thent.test(biomass_g ~ nitrogen, data = two_group). Same formula form as step 12. - Part B, paired.
drought_dataalready hasphoto_beforeandphoto_afterside by side, likesleep_wideafter step 16. Adddrought_delta = photo_after - photo_beforewithmutate(), histogram andshapiro.test()on the delta, thent.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.
- Blood glucose measured in 20 rats, then again in the same 20 rats two hours after a glucose dose.
- Mean height of every BILD 5 student compared with a random sample of 300 UCSD students.
- 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.