Check Assumptions Before a t-test

Is our data normally distributed? If not, what should we do about it?

So you want to test whether the mean of one sample is no different than a hypothesized value. There are many statistical paths to test this null hypothesis and a one-sample t-test is one of the most common. Why? It’s powerful (i.e., can detect differences when they exist) and they are simple to calculate. In part, that power comes from an assumption that the data is normally distributed. If we assume normality then all we need is a mean and a standard deviation to give us a complete theoretical picture of how the underlying population is structured. But, we have to make sure our data is normal enough before we can make the assumption and run the t-test.

How can we do this?

  1. Clean up our data so missing values don’t get in the way.
    • Use na.rm = TRUE in summaries (mean, quantile, IQR, Shapiro)
  2. Identify and trim/filter out impossible values (e.g., negative counts or bill depths)
  3. Flag distributional outliers (1.5 × IQR)
  4. Look at your data using ggplot and geom_histogram()
  5. Test for normality using shapiro.test()
  6. Decide if your data is non-normal enough to warrant a transformation
    • If your data is normal, decide what to do, if anything, about your outliers
  7. If the data is not normally distributed, transform it. We will use a log transformation in this activity
  8. Look at your transformed data with a histogram and test for normality again using shapiro.test()
  9. Decide which test you will run
    • If the data is still not normally distributed, even after appropriate transformation, conduct a non-parametric test
    • If the data is normal enough then run a one-sample t-test

1. Load packages

Coding Assignment 3 starts with the tidyverse; this activity also needs the Palmer Penguins dataset. Run library(tidyverse) and library(palmerpenguins). A conflicts message after tidyverse is expected, not an error.

NoteHint

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

library(package name here)
library(another package name here)

Load both packages — tidyverse for dplyr and ggplot2, palmerpenguins for the penguins dataset.

TipSolution
library(tidyverse)
library(palmerpenguins)

On DataHub you will load tidyverse for the graded assignment; here you also need palmerpenguins for practice data.

2. Build the study group

A one-sample t-test asks whether the mean of one group differs from a hypothesized value. Filter to Adelie penguins measured in 2007 and save the result as penguin_data. You will check assumptions on bill_depth_mm.

NoteHint

From ?filter: keep rows where conditions are TRUE.

penguins |>
  filter(species == "Gentoo")

Filter to Adelie and year 2007. No new columns needed — bill_depth_mm is already there.

TipSolution
penguin_data <- penguins |>
  filter(species == "Adelie", year == 2007)

You should have 50 rows — one Adelie cohort from a single field season.

3. Glimpse the data

Run glimpse() on penguin_data to see row count, column names, and types.

NoteHint

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

glimpse(mtcars)

Apply it to the dataset you just built.

TipSolution
glimpse(penguin_data)

You should see 50 rows. bill_depth_mm is <dbl> — and one penguin has a missing bill measurement.

4. Step 1 — missing and impossible values

Two different data problems show up in real datasets: missing values (NA) and impossible values (e.g. negative bill depths). They need different handling.

  • Missing means no measurement was recorded — keep those rows for now.
  • Impossible means a sign error or bad entry — remove those rows.

Use filter(is.na(bill_depth_mm) | bill_depth_mm >= 0) to drop impossible depths while keeping missing ones. After this step, every numeric summary uses na.rm = TRUE so R skips the remaining NAs.

Bill depth cannot be negative. Remove rows where bill_depth_mm is negative. Keep rows where bill_depth_mm is missing. Save the result as penguin_clean.

Note| means “or”

Inside filter(), the vertical bar | means “or”. The condition is.na(bill_depth_mm) | bill_depth_mm >= 0 keeps a row if either the depth is missing or it is zero or positive.

Writing filter(bill_depth_mm >= 0) alone silently drops the missing rows — because NA >= 0 evaluates to NA, not TRUE.

NoteHint

From ?filter: keep rows where conditions are TRUE.

mtcars |>
  filter(is.na(mpg) | mpg >= 0)

Keep missing bill depths or non-negative depths.

TipSolution
penguin_clean <- penguin_data |>
  filter(is.na(bill_depth_mm) | bill_depth_mm >= 0)

50 rows — no negative values in this dataset, so nothing was removed. The one missing measurement is still there. Confirming there are no impossible values is itself a reportable result.

Count how many bill depths are still missing, then preview the mean using na.rm = TRUE — the same pattern you will use in every summary from here on.

NoteHint

From ?sum: sum(is.na(x)) counts missing values. From ?mean: set na.rm = TRUE to skip them.

Example on mtcars:

n_missing <- sum(is.na(mtcars$mpg))
mean_preview <- mean(mtcars$mpg, na.rm = TRUE)

Apply the same pattern to penguin_clean$bill_depth_mm.

TipSolution
n_missing <- sum(is.na(penguin_clean$bill_depth_mm))
mean_preview <- mean(penguin_clean$bill_depth_mm, na.rm = TRUE)
n_missing
mean_preview

One missing depth; mean ≈ 18.8 mm computed from the 49 complete cases only.

5. Flag distributional outliers

Using only the non-missing bill depths in penguin_clean, calculate the upper fence of the 1.5 × IQR rule: Q3 + 1.5 × IQR. Save it as upper_fence and print it. Also count how many non-missing bill depths exceed that fence and save the count as n_raw_outliers. Print both values. Do not remove any rows.

The 1.5 × IQR rule is a screening tool. Points above the fence deserve a look; they are not automatically errors.

NoteHint

From ?quantile: quantile(x, probs, na.rm = FALSE). From ?IQR: IQR(x, na.rm = FALSE).

q3 <- quantile(mtcars$mpg, 0.75, na.rm = TRUE)
fence <- q3 + 1.5 * IQR(mtcars$mpg, na.rm = TRUE)
n_out <- sum(mtcars$mpg > fence, na.rm = TRUE)

Use the 75th percentile and IQR of non-missing bill depths, then count values above the fence.

TipSolution
upper_fence <- quantile(penguin_clean$bill_depth_mm, 0.75, na.rm = TRUE) +
  1.5 * IQR(penguin_clean$bill_depth_mm, na.rm = TRUE)
n_raw_outliers <- sum(penguin_clean$bill_depth_mm > upper_fence, na.rm = TRUE)
upper_fence
n_raw_outliers

The upper fence is about 21.4 mm — one penguin has a bill depth above it.

6. Histogram on the raw scale

Make a histogram of the non-missing bill depths in penguin_clean using ggplot() and geom_histogram(). Save the plot as hist_raw and display it.

NoteHint

From ?ggplot: add a layer with +.

hist_raw <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram()
hist_raw

Map bill_depth_mm to the x-axis and assign the whole plot to hist_raw.

TipSolution
hist_raw <- ggplot(penguin_clean, aes(x = bill_depth_mm)) +
  geom_histogram()
hist_raw

The distribution is right-skewed — a long tail toward deep bills.

7. Shapiro-Wilk on the raw scale

Run shapiro.test() on the non-missing bill depths in penguin_clean. Save the result as shapiro_raw and print it.

NoteReading Shapiro-Wilk output

The null hypothesis is normality. A small p-value (conventionally below 0.05) means the data look inconsistent with a normal distribution. A large p-value means you do not have evidence against normality — not proof that the data are perfectly normal.

Do not treat p > 0.05 as a green light or p < 0.05 as an automatic veto — combine this test with your histogram and sample size.

NoteHint

Pass the bill-depth vector directly.

shapiro.test(mtcars$mpg)

Use non-missing values from penguin_clean$bill_depth_mm.

TipSolution
shapiro_raw <- shapiro.test(penguin_clean$bill_depth_mm)
shapiro_raw

p ≈ 0.029 — evidence against normality on the raw scale.

8. Make the call on normality

Based on your histogram and Shapiro-Wilk result, are the raw bill depths normally distributed enough for a one-sample t-test? Assign data_is_normal to TRUE or FALSE (no quotes, all caps).

NoteHint

Look at shapiro_raw$p.value. Is it above or below 0.05?

TipSolution
data_is_normal <- FALSE

p ≈ 0.029 — below 0.05, and the histogram is right-skewed. Follow checklist steps 7–9 and try a log transform.

9. Log transform

The raw depths are not normally distributed. Apply a log transformation: use mutate() to add a column called log_depth equal to the natural log of bill_depth_mm. Save the result as penguin_transformed and inspect it with glimpse().

NoteHint

From ?mutate: mutate(.data, ...) adds or changes columns. From ?log: log(x) returns the natural logarithm.

mtcars |>
  mutate(log_mpg = log(mpg))

Add log_depth = log(bill_depth_mm) to penguin_clean.

TipSolution
penguin_transformed <- penguin_clean |>
  mutate(log_depth = log(bill_depth_mm))
glimpse(penguin_transformed)

Missing bill depths stay missing; negative rows were never present.

10. Histogram on the log scale

Make a histogram of log_depth from penguin_transformed, using only non-missing values. Save the plot as hist_log and display it.

NoteHint

Same pattern as the raw histogram — map the transformed column to x.

hist_log <- ggplot(mtcars, aes(x = log(mpg))) +
  geom_histogram()
hist_log
TipSolution
hist_log <- ggplot(penguin_transformed, aes(x = log_depth)) +
  geom_histogram()
hist_log

The distribution looks much more symmetric after the log transform.

11. Shapiro-Wilk on the log scale

Run shapiro.test() on the non-missing log_depth values in penguin_transformed. Save the result as shapiro_log and print it.

NoteHint

Same function as before — pass a numeric vector to shapiro.test().

Example:

shapiro.test(mtcars$mpg)

Use non-missing values from penguin_transformed$log_depth.

TipSolution
shapiro_log <- shapiro.test(penguin_transformed$log_depth)
shapiro_log

p ≈ 0.081 — no evidence against normality after the log transform.

12. Make the call on log

Based on your histogram and Shapiro-Wilk result for the log-transformed data, are the log bill depths normally distributed enough for a one-sample t-test? Assign log_is_normal to TRUE or FALSE (no quotes, all caps).

NoteHint

Look at shapiro_log$p.value. Is it above or below 0.05?

TipSolution
log_is_normal <- TRUE

p ≈ 0.081 — no evidence against normality on the log scale.

13. Re-flag outliers after transformation

Using only the non-missing log_depth values in penguin_transformed, calculate the upper fence of the 1.5 × IQR rule on the log scale. Save it as upper_fence_log and print it. Count how many non-missing log_depth values exceed that fence and save the count as n_log_outliers. Print both values. Do not remove any rows yet.

NoteHint

Apply the same Q3 + 1.5 × IQR formula to log_depth instead of bill_depth_mm.

Example on mtcars:

q3 <- quantile(mtcars$mpg, 0.75, na.rm = TRUE)
upper_fence_log <- q3 + 1.5 * IQR(mtcars$mpg, na.rm = TRUE)
n_log_outliers <- sum(mtcars$mpg > upper_fence_log, na.rm = TRUE)
TipSolution
upper_fence_log <- quantile(penguin_transformed$log_depth, 0.75, na.rm = TRUE) +
  1.5 * IQR(penguin_transformed$log_depth, na.rm = TRUE)
n_log_outliers <- sum(penguin_transformed$log_depth > upper_fence_log, na.rm = TRUE)
upper_fence_log
n_log_outliers

Zero penguins sit above the log-scale fence — the one raw outlier was pulled inside the fence by the transformation.

14. Decide whether to trim/Winsorize outliers

Based on your log-scale outlier count, decide whether to remove any rows. Assign remove_outliers to TRUE if you would filter out IQR outliers, or FALSE if you would keep all rows.

NoteHint

The raw outlier was a Torgersen male with a 21.5 mm bill depth — large but biologically plausible. After the log transform, n_log_outliers is 0, so there is nothing left to filter out. Assign TRUE or FALSE accordingly.

TipSolution
remove_outliers <- FALSE

Keep all rows. The one bird flagged on the raw scale is no longer an outlier after transformation, and it was never an impossible value like a negative count.

15. Run a one-sample t-test

Assumptions look good on the log scale: no impossible values, no log-scale IQR outliers, and log_depth is normal enough. Test whether mean log bill depth differs from log(18.5).

Use t.test() on penguin_transformed$log_depth with mu = log(18.5). Save the result as t_test_result and print it.

NoteReading one-sample t-test output

The null hypothesis is that the true mean equals mu. A small p-value (conventionally below 0.05) means the sample mean looks inconsistent with the hypothesized value.

Logging then testing means your inference applies to log(depth), not the original millimeter scale. That is acceptable if you report it clearly in your write-up.

NoteHint

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

t.test(mtcars$mpg, mu = 20)

Use penguin_transformed$log_depth and mu = log(18.5).

TipSolution
t_test_result <- t.test(penguin_transformed$log_depth, mu = log(18.5))
t_test_result

t ≈ 1.33, df = 48, p ≈ 0.19. Mean log bill depth in these 2007 Adelie penguins is not significantly different from log(18.5).

16. Recap — functions you practiced

Function Role
library(tidyverse) Loads dplyr, ggplot2, and related packages
library(palmerpenguins) Loads the penguins dataset
filter() Keeps rows matching a condition (species == "Adelie", is.na(...) \| ... >= 0)
mutate() Adds transformed columns (log_depth)
glimpse() Quick look at rows, columns, and types
mean(..., na.rm = TRUE) Average of a numeric column, skipping missing values
quantile(..., 0.75, na.rm = TRUE) 75th percentile for the IQR fence
IQR(..., na.rm = TRUE) Interquartile range for the 1.5×IQR rule
sum(x > fence, na.rm = TRUE) Counts values above an outlier fence
ggplot() + geom_histogram() Visual check of distribution shape
shapiro.test() Formal test of normality (H₀: data are normal)
log() Natural-log transformation for right-skewed data
t.test(x, mu = ...) One-sample t-test against a hypothesized mean

If you can explain each row in your own words, you have covered the core of Coding Assignment 3.

Keep playing

What happens if you filter to male Adelie penguins only? Run Shapiro-Wilk on that smaller group and compare it to the full 2007 cohort.

p ≈ 0.12 — males alone pass the normality check on the raw scale. The full 2007 cohort failed because males and females have different bill depths stacked together. That is why you check assumptions on one group, not a pile of groups.