Code
install.packages(c("tidyverse", "nullabor", "MASS"))Assessing significance of patterns
Prof. Di Cook
This worksheet requires R version 4.5.0 or later (so that penguins is available without any extra package) plus three packages. Install them once from CRAN if you have not already done so:
Then load them at the start of your session:
| Package | Role in this worksheet | Version used |
|---|---|---|
| tidyverse [@Wickham2019] | Data wrangling (dplyr, tidyr) and plotting (ggplot2) |
≥ 2.0 |
| nullabor [@Buja2009] | Lineup and Rorschach protocols, null-generating functions, pvisual() |
≥ 0.3.10 |
| MASS [@Venables2002] | mvrnorm() for bivariate normal simulation in Exercise 3 |
≥ 7.3 |
The penguins dataset is part of the datasets package that ships with base R (≥ 4.5.0) [@Kaye2025], so no additional install is needed for the data.
“The most important maxim for data analysis … is that it is better to look at the data than to stare at the output of a statistical test.” — John W. Tukey
A lineup is a way of making this idea rigorous. You hide the real data among a set of plots generated under the assumption that nothing interesting is happening (the null hypothesis), then ask an observer — you, your classmates, or anyone — to identify which plot is real. If the real data is easy to find, that is evidence something interesting is happening. If it blends in with the rest, the data are consistent with the null hypothesis.
Before we put real data in a lineup, we need to know what “nothing interesting” looks like. That is what the Rorschach protocol is for: it shows a set of plots that are all generated under the null, with no real data at all. This calibrates your eye.
This worksheet introduces both protocols using the penguins dataset, which has been part of base R since version 4.5.0. No extra packages are needed to load the data.
R version note. The base R
penguinsdataset uses shorter variable names than thepalmerpenguinspackage version. The names used in this worksheet are:bill_len,bill_dep,flipper_len, andbody_mass(without unit suffixes).
By the end of this worksheet you should be able to:
null_permute, null_dist, or null_lm) given a description of the plot and the hypothesis being tested.nullabor::lineup() and display it with facet_wrap().pvisual() and state a conclusion.Rows: 344
Columns: 8
$ species <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Ad…
$ island <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgersen, Tor…
$ bill_len <dbl> 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, 42.0, …
$ bill_dep <dbl> 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, 20.2, …
$ flipper_len <int> 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186, 180,…
$ body_mass <int> 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, 4250, …
$ sex <fct> male, female, female, NA, female, male, female, male, NA, …
$ year <int> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…
There are three species of penguin — Adélie, Chinstrap, and Gentoo — measured on four physical variables (bill_len, bill_dep, flipper_len, body_mass) along with sex, island, and year.
Before you can judge whether a pattern in your data is real, you need to know what patterns arise by chance alone. The Rorschach protocol generates several plots that are all pure noise — no real data anywhere. Looking at them trains your eye before you see a lineup.
The question for this exercise is: do the three penguin species have different flipper lengths? The plot we will use is a boxplot of flipper_len by species.
The null hypothesis is: species labels are exchangeable — knowing which species a penguin belongs to tells you nothing about its flipper length. We generate null data by randomly shuffling (permuting) the species column, so each penguin keeps its actual flipper length but gets a randomly assigned species label.
null_permute("species") generates one dataset where the species labels have been randomly shuffled. We call lineup() in Rorschach mode (no real data) to get 20 such shuffled datasets.
a. In every panel, the species labels are randomly shuffled, so any difference between the three boxes is pure chance. Do the three boxes look roughly the same height in most panels, or do they vary quite a bit?
b. Find a panel where one species looks noticeably higher or lower than the others. Write down the panel number. Would you have been suspicious of that panel if you had seen it alone, without the other 19?
c. Now think about what the real data would have to look like to stand out from these panels. Describe in one sentence what you would need to see in the real data plot for it to be clearly distinguishable from the null panels.
Now we embed the real penguin data among 19 null panels generated by the same permutation mechanism. This is the lineup protocol.
a. Look carefully at all 20 panels. Write down which panel you think contains the real data and the specific feature that identified it.
b. The null hypothesis being tested is: species labels are exchangeable — flipper length does not differ between species. What does it mean for the real panel to stand out clearly from the null panels, in terms of this hypothesis?
c. Decrypt the lineup to reveal the true position. Were you correct?
d. Suppose this lineup was shown to 16 students and 14 of them correctly identified the real panel. Compute the visual p-value and state your conclusion. The lineup has \(m = 20\) panels.
e. Describe what the p-value means in plain language, without using the word “probability”. Use the lineup panels to explain your answer.
When we plot two continuous variables against each other in a scatter plot, we are often implicitly asking whether their joint distribution follows a particular shape. A bivariate normal distribution is the two-dimensional generalisation of the familiar bell curve: it produces elliptically shaped scatter plots with no outliers or curved structure.
The code below will make a plot of bill length (bill_len) against bill depth (bill_dep) for Adélie penguins.
To test whether this scatter plot is consistent with a bivariate normal distribution, we need to generate null datasets that are bivariate normal with the same means, variances, and correlation as the observed data.
The null_dist() function in nullabor can generate data from a named distribution, but it works on one variable at a time. For a bivariate normal we use null_lm() — not because we are fitting a regression, but because regressing bill_dep on bill_len and rotating the residuals generates null scatter plots that preserve the linear relationship’s strength while removing any non-normal structure.
Alternatively, and more directly, we can write our own null-generating function that simulates from a bivariate normal with parameters estimated from the data.
library(MASS) # for mvrnorm()
adelie <- penguins |>
filter(species == "Adelie",
!is.na(bill_len), !is.na(bill_dep))
# Estimate bivariate normal parameters from the data
bvn_params <- list(
mu = c(mean(adelie$bill_len), mean(adelie$bill_dep)),
Sigma = cov(cbind(adelie$bill_len, adelie$bill_dep))
)
# Null-generating function: simulate one bivariate normal dataset
null_bvn <- function(data) {
n <- nrow(data)
sims <- mvrnorm(n, mu = bvn_params$mu, Sigma = bvn_params$Sigma)
data.frame(bill_len = sims[, 1],
bill_dep = sims[, 2])
}a. Why is it important that the simulated null data have the same means, variances, and correlation as the observed data? What would go wrong if we simulated from a standard bivariate normal with mean 0 and variance 1?
b. The lineup below uses null_bvn as the null-generating function. Before looking at the panels, predict: if the real data are consistent with a bivariate normal, how hard should it be to find the real panel?
c. Write down which panel you think is the real data and what feature (if any) made it stand out. If you genuinely cannot tell, write that down too — that is also a valid and informative result.
d. Decrypt.
e. Suppose 6 out of 18 students found the real panel. Compute the visual p-value. What does the result tell you about whether the bivariate normal is an adequate model for Adélie bill measurements?
f. The null here is null_bvn — simulation from a bivariate normal. Contrast this with Exercise 2, where the null was null_permute("species"). In your own words, what is the key difference in what these two null mechanisms destroy or preserve?
A scatter plot of two continuous variables with a smooth line through it is implicitly asking: is there a linear trend, or is the relationship flat? The null hypothesis is that there is no linear relationship — any apparent slope is just random variation.
The standard null-generating mechanism for this question is null_lm(). It fits a linear model to the data, then generates null datasets by rotating the residuals — a technique that preserves the marginal distributions of both variables and the overall spread, while destroying any systematic linear relationship between them.
gentoo <- penguins |>
filter(species == "Gentoo",
!is.na(body_mass), !is.na(flipper_len))
ggplot(gentoo,
aes(x = flipper_len, y = body_mass)) +
geom_point(colour = "#009ACD", alpha = 0.6, size = 2) +
geom_smooth(method = "lm", se = TRUE,
colour = "#009ACD", fill = "#009ACD", alpha = 0.2) +
labs(
title = "Gentoo penguins: flipper length vs body mass",
x = "Flipper length (mm)",
y = "Body mass (g)"
)For this question the null-generating mechanism is:
method = "rotate" rotates the residuals from the fitted model, which creates new response values that are close to the fitted values but with no systematic linear trend remaining.
a. Before generating the lineup: look at the scatter plot above. Does the relationship between flipper length and body mass look roughly linear? Are there any obvious outliers or curves?
b. Why is null_permute("flipper_len") not a good null for this question, even though it would also destroy the linear relationship?
Hint: think about what permuting flipper_len does to its marginal distribution and whether values of 180 mm and 230 mm are equally plausible for the same penguin.
Now generate the lineup.
c. Look at the 20 panels. Write down which panel you think is real and the visual feature that led you there.
d. Decrypt.
e. Suppose 15 out of 17 students found the real panel. Compute the visual p-value and state your conclusion.
f. The lineup includes a geom_smooth(method = "lm") line in every panel, including the null panels. Explain why this is useful rather than misleading. What does the slope of the line in the null panels tell you?
g. Review all three lineups in this worksheet and complete the table below:
| Exercise | Question | Null mechanism | What the null destroys |
|---|---|---|---|
| 2 | Do species differ in flipper length? | null_permute("species") |
|
| 3 | Are bill dims jointly normal for Adélie? | Custom null_bvn() |
|
| 4 | Is there a linear relationship (Gentoo)? | null_lm(..., "rotate") |
| Protocol | What it is | When to use it |
|---|---|---|
| Rorschach | All panels are null — no real data | Before a lineup, to calibrate your eye to what chance looks like |
| Lineup | One real panel hidden among null panels | To test whether a pattern in your data is surprising |
| Null mechanism | Use when… | What it does |
|---|---|---|
null_permute("x") |
Testing whether group differences or correlations are real | Shuffles one column, breaking its relationship with other variables |
null_dist("x", dist, params) |
Testing whether one variable follows a specific distribution | Simulates new values from the named distribution |
| Custom function | Testing a bivariate or multivariate distributional assumption | You define how to simulate from the null model |
null_lm(y ~ x, "rotate") |
Testing whether a linear trend is real | Rotates residuals to remove the linear signal while preserving marginal distributions |
The three steps of a lineup test:
pvisual(x, K, m).Wickham H, Averick M, Bryan J, Chang W, McGowan LD, François R, Grolemund G, Hayes A, Henry L, Hester J, Kuhn M, Pedersen TL, Miller E, Bache SM, Müller K, Ooms J, Robinson D, Seidel DP, Spinu V, Takahashi K, Vaughan D, Wilke C, Woo K, Yutani H (2019). “Welcome to the tidyverse.” Journal of Open Source Software, 4(43), 1686. https://doi.org/10.21105/joss.01686
Buja A, Cook D, Hofmann H, Lawrence M, Lee E, Swayne DF, Wickham H (2009). “Statistical inference for exploratory data analysis and model diagnostics.” Philosophical Transactions of the Royal Society A, 367(1906), 4361–4383. https://doi.org/10.1098/rsta.2009.0120
Cook D, Wickham H, Roy Chowdhury N, Hofmann H, Thulin M (2024). nullabor: Tools for Graphical Inference. R package version 0.3.15. https://dicook.github.io/nullabor/
Venables WN, Ripley BD (2002). Modern Applied Statistics with S, 4th ed. Springer, New York. ISBN 0-387-95457-0. https://www.stats.ox.ac.uk/pub/MASS4/
Kaye E, Turner H, Gorman KB, Horst AM, Presmanes Hill A (2025). “Preparing the Palmer Penguins data for the datasets package in R.” Zenodo. https://doi.org/10.5281/zenodo.14902740
Horst AM, Presmanes Hill A, Gorman KB (2022). “Palmer Archipelago penguins data in the palmerpenguins R package — an alternative to Anderson’s irises.” The R Journal, 14(1), 244–254. https://doi.org/10.32614/RJ-2022-020
Gorman KB, Williams TD, Fraser WR (2014). “Ecological sexual dimorphism and environmental variability within a community of Antarctic penguins (genus Pygoscelis).” PLOS ONE, 9(3), e90081. https://doi.org/10.1371/journal.pone.0090081
Wickham H, Cook D, Hofmann H, Buja A (2010). “Graphical inference for infovis.” IEEE Transactions on Visualization and Computer Graphics, 16(6), 973–979. https://doi.org/10.1109/TVCG.2010.161
Majumder M, Hofmann H, Cook D (2013). “Validation of visual statistical inference, applied to linear models.” Journal of the American Statistical Association, 108(503), 942–956. https://doi.org/10.1080/01621459.2013.808157
Hofmann H, Follett L, Majumder M, Cook D (2012). “Graphical tests for power comparison of competing designs.” IEEE Transactions on Visualization and Computer Graphics, 18(12), 2441–2448. https://doi.org/10.1109/TVCG.2012.230
This worksheet is released under a Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) licence.
You are free to share (copy and redistribute in any medium or format) and adapt (remix, transform, and build upon) this material for any purpose, even commercially, provided you: