
25 Grouped box plots
theme_oregontrail()
Graphs in this chapter use fmggp2::theme_oregontrail(), modeled on The Oregon Trail on an Apple II emulator: a gray window frame, a black screen, phosphor green text and marks in three shades, and an Andale Mono title.
25.1 Description
A box plot shows where a set of numbers fall, using five numbers that stand for all of them.1 Grouped box plots put one box plot for each group side by side in the same graph,2 so we can compare the five numbers from one group to the next.
Put the numbers in order, and three of the five cut them into four groups of the same size:3 the number with 25 out of 100 of the numbers below it, the middle number, and the number with 75 out of 100 below it. The median is the middle number when you put all the numbers in order. The other two are the smallest and largest numbers that are no farther past the ends of the box than 1.5 times how long the box is.4
25.2 Set up
PACKAGES:
Bring in the code we’ll use.5
show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulationDATA:
The Oregon Trail is a computer game where you lead a group of people across the country to Oregon, a place in the west of the United States. Going from one place to another is called a trip. In the game, you picked a job6 before you set out, and it decided how much money (and how many things for the trip)7 your group started with. We’ll make up8 how many days it took 100 groups9 for each job to reach Oregon. Each job gets its own median number of days for the trip (median_days),10 and rlnorm() picks the days for each trip from a rule that lets a few groups with bad luck take much longer than the rest11 (people got hurt, parts broke, or things went missing).12 Starting with set.seed()13 means you’ll get the same 300 trips every time you run the code below.
show/hide
set.seed(1985) # same 300 parties every time the code runs
oregon_trail <- tibble(
party_id = 1:300, # 300 different wagon parties, one row each
profession = rep(c("Banker", "Carpenter", "Farmer"), each = 100) # 100 parties per profession
) |>
mutate(
profession = factor(profession,
levels = c("Banker", "Carpenter", "Farmer")), # fastest to slowest
median_days = c(Banker = 140, Carpenter = 155, # typical days on the trail
Farmer = 170)[as.character(profession)], # (looked up for each profession)
days_to_oregon = round(
rlnorm(n(), meanlog = log(median_days), # trips cluster near the median
sdlog = 0.2) # with a longer tail of slow trips
)
) |>
select(party_id, profession, days_to_oregon)
glimpse(oregon_trail)
#> Rows: 300
#> Columns: 3
#> $ party_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9…
#> $ profession <fct> Banker, Banker, Banker, B…
#> $ days_to_oregon <dbl> 152, 112, 131, 144, 180, …25.3 Distribution
Grouped box plots compare five numbers that stand for all of them14 from one group to the next. When a few numbers run out much higher than the rest,15 those five numbers don’t have the same space between them: the median is closer to the low end of the box, and the line out from the high end of the box is longer than the line out from the low end.16 With only 100 trips in each group, chance can change this, so not every box below looks this way. Below we’ll move from the trips, to a graph, to the rule behind it.17
COUNT IT:
Start with the first eight trips (all bankers).
show/hide
head(oregon_trail$days_to_oregon, 8)
#> [1] 152 112 131 144 180 126 164 179These trips took between 112 and 180 days. The numbers below show the low end, middle, and high end of each job’s box.18 Only for bankers is the space from the median up to the high end of the box (upper_gap) larger than the space down to the low end of the box (lower_gap).19 For the other two jobs, the space down is about the same or larger.
show/hide
oregon_trail |>
group_by(profession) |>
summarise(q1 = quantile(days_to_oregon, 0.25),
median = median(days_to_oregon),
q3 = quantile(days_to_oregon, 0.75)) |>
mutate(lower_gap = median - q1, upper_gap = q3 - median)DRAW IT:
The box plots on the left use days. In the bankers’ box, one side is bigger than the other:20 the median line is low in the box, and the top line is longer than the low line.21 The farmers’ box is the other way around: the median line is high, and the low line is longer than the top line. The farmers also have a few points far above the rest.22 A log scale puts 0.1, 1, and 10 at even steps, so it shows how many times bigger one number is than another. The box plots on the right show the same trips on a log scale with scale_y_log10(). The log scale doesn’t make the two sides of each box match: 100 made up trips are too few to match the rule, and the bankers and farmers each still have one point far from the rest.
show/hide
ggplot(oregon_trail, aes(x = profession, y = days_to_oregon,
fill = profession)) +
geom_boxplot(alpha = 2/3, show.legend = FALSE) +
labs(title = "Days", x = NULL, y = "Days to reach Oregon")
ggplot(oregon_trail, aes(x = profession, y = days_to_oregon,
fill = profession)) +
geom_boxplot(alpha = 2/3, show.legend = FALSE) +
scale_y_log10() +
labs(title = "Log scale", x = NULL, y = "Days (log10 scale)")

NAME IT:
A hill is a line that is high in the middle and low on both sides. The trips for each job come from a hill on the log scale,23 the same rule as the sizes of pictures saved on a small card in another part of this book:24
\[ \log(X_{\text{profession}}) \sim \text{Normal}(\log(m_{\text{profession}}), \; \sigma = 0.2), \quad m = (140, 155, 170) \]
On the log scale, the low and high ends of the box25 are \(0.674\,\sigma\) below and above the middle, which is why, for the rule, the two sides of the box match on the log scale. Back in days, the low and high ends of the box become \(m\,e^{-0.674 \times 0.2}\) and \(m\,e^{0.674 \times 0.2}\): for bankers, about 122 and 160 days. That’s 18 days below the median but 20 days above it, and the space grows the farther out you go, which is why the rule makes the top lines longer.26 Try a larger sdlog in rlnorm() and one side of each box gets even bigger than the other.
25.4 Grammar
CODE:
Make the words around the graph27 with
labs()Start the graph with
ggplot()and give itdataPut
professionon thexaxis, and give eachprofessionits own color withfillPut
days_to_oregonon theyaxisAdd
geom_boxplot()on top28 and setalphato2/3(so we can see through the boxes)Set
outlier.shape = NAso points far from the rest29 aren’t shown two timesAdd each trip as a point with
geom_jitter()30Take away the key that tells what each color means31 with
show.legend = FALSE
show/hide
labs_grp_boxplots <- labs(
title = "The Oregon Trail",
caption = "Simulated data",
subtitle = "Days for 100 parties of each profession to reach Oregon",
x = "Profession", fill = "Profession",
y = "Days to reach Oregon")
ggp2_grp_boxplots <- ggplot(data = oregon_trail,
aes(x = profession,
y = days_to_oregon,
fill = profession)) +
geom_boxplot(alpha = 2/3,
outlier.shape = NA,
show.legend = FALSE) +
geom_jitter(width = 0.15, height = 0, alpha = 0.4,
show.legend = FALSE)
ggp2_grp_boxplots +
labs_grp_boxplotsGRAPH:
When we give it a column of groups,32 the graph shows one box for each group. Each point is one trip. We move each point a little to the left or right by chance so the points don’t cover each other, but how high each point is doesn’t change.

25.5 More info
25.5.1 Notches
Make each box less wide near the median33 with notch = TRUE and notchwidth.
show/hide
ggp2_grp_box_notch <- ggplot(data = oregon_trail,
aes(x = profession,
y = days_to_oregon,
fill = profession)) +
geom_boxplot(
notch = TRUE,
notchwidth = 0.85,
alpha = 2/3,
show.legend = FALSE)
ggp2_grp_box_notch +
labs_grp_boxplots
25.5.2 Outliers
Box plots show numbers that are far from the rest34 as points, and we can change the color of these points with outlier.colour. Inside geom_boxplot(), we give each profession its own color35 and set outlier.colour to NULL:
show/hide
ggp2_grp_box_outliers <- ggplot(data = oregon_trail,
aes(x = profession,
y = days_to_oregon,
fill = profession)) +
geom_boxplot(aes(color = profession),
outlier.colour = NULL,
outlier.size = 2,
notch = TRUE,
notchwidth = 0.85,
alpha = 2/3,
show.legend = FALSE)
ggp2_grp_box_outliers +
labs_grp_boxplots
The five-number summary.↩︎
The categories, or groups, of a categorical variable.↩︎
The first quartile (Q1), the second quartile (the median), and the third quartile (Q3).↩︎
The whiskers. Each one stops at the largest or smallest value no farther than 1.5 × the interquartile range (IQR) from the hinges (the ends of the box).↩︎
Install and load the packages.↩︎
A profession.↩︎
Supplies.↩︎
Simulate.↩︎
Parties.↩︎
Each profession’s typical trip length.↩︎
rlnorm()draws each trip from a log normal distribution.↩︎Dysentery, broken axles, and lost oxen.↩︎
Setting the random seed.↩︎
Summary statistics.↩︎
When the data are skewed (here, with a long tail to the right).↩︎
The upper whisker is longer than the lower one.↩︎
The formula.↩︎
Each profession’s quartiles:
q1,median, andq3.↩︎upper_gapis the third quartile minus the median, andlower_gapis the median minus the first quartile.↩︎The box is lopsided (asymmetric).↩︎
The upper whisker is longer than the lower one.↩︎
Outliers.↩︎
A log normal distribution.↩︎
The first and third quartiles.↩︎
The upper whiskers.↩︎
Labels.↩︎
Add the
geom_boxplot()layer.↩︎Outliers.↩︎
Jittered points:
width = 0.15moves each point at random along the x axis, andheight = 0keeps each point at its true number of days.↩︎Remove the legend.↩︎
A categorical variable.↩︎
Notches. Each notch spans the median ± 1.58 × IQR / √n, a rough confidence interval for the median, and
notchwidthsets how narrow the notch is compared to the box.↩︎Outliers.↩︎
Map
professiontocolor. Withoutlier.colour = NULL, the outlier points take the mapped color.↩︎