
8 Grouped bar graphs
theme_jurassic()
Graphs in this chapter use fmggp2::theme_jurassic(), modeled on the Jurassic Park (1993) Post Office souvenir stamp sheet: a black sheet with a logo red frame, a dark jungle plot area, stamp yellow titles, red, yellow, sunset orange, cream, and tan marks, and a Herculanum title.
8.1 Description
Grouped bar graphs compare multiple values across categories. Each category has a cluster of bars. X-axis labels show categories and y-axis values. Colors or patterns differentiate subsets, and a legend is shown on the side or top of the graph.
geom_col() allows us to display ‘grouped’ numerical values across levels (or groups) of a categorical variable.
8.2 Set up
PACKAGES:
Install packages.
show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulation
library(tidyr) # for uncount()DATA:
In Jurassic Park (1993), every species lives in its own paddock. We’ll simulate the weight of every dinosaur in the park: each species gets a head count (n) and its own normal distribution of weights (mean_tons and sd_tons). tidyr::uncount() repeats each row n times (one row per dinosaur), and rnorm() draws each dinosaur’s weight. Then we add up the weights in each paddock (total_tons). Setting the seed with set.seed() means you’ll get the same dinosaurs every time you run the code below.
show/hide
set.seed(1993) # same dinosaur weights every time the code runs
jp_dinos <- tibble(
species = c("Brachiosaurus", "Triceratops", "Tyrannosaurus", # five species in the park
"Gallimimus", "Velociraptor"),
n = c(3, 5, 2, 20, 8), # number of animals of each species
mean_tons = c(40, 9, 8, 0.45, 0.15), # average weight (tons)
sd_tons = c(5, 1.2, 1, 0.08, 0.03) # spread of each species' weights
) |>
tidyr::uncount(n) |> # one row per animal
mutate(
weight_tons = round(rnorm(n(), mean = mean_tons, sd = sd_tons), 2) # each animal's weight
) |>
select(species, weight_tons)
jp_paddocks <- jp_dinos |>
group_by(species) |> # one group per species
summarise(total_tons = sum(weight_tons)) |> # total weight in each paddock
ungroup()
glimpse(jp_paddocks)
#> Rows: 5
#> Columns: 2
#> $ species <chr> "Brachiosaurus", "Gallimimus"…
#> $ total_tons <dbl> 115.83, 8.88, 43.08, 15.27, 1…8.3 Distribution
A grouped bar graph shows one number per group, and here that number is a total. Totals depend on two things: how many dinosaurs live in a paddock and how much each one weighs. Twenty Gallimimus still weigh less than two Tyrannosaurs. Below we’ll move from the dinosaurs, to a picture, to the formula.
COUNT IT:
Start with the first six dinosaurs.
show/hide
head(jp_dinos, 6)Each Brachiosaurus weighs close to 40 tons, and each Triceratops weighs close to 9. For every paddock, the total is the number of dinosaurs times their average weight.
show/hide
jp_dinos |>
group_by(species) |>
summarise(
dinosaurs = n(),
mean_tons = mean(weight_tons),
total_tons = sum(weight_tons))DRAW IT:
Each bar in a grouped bar graph is a bar model: a total made of parts. Below we draw every dinosaur as its own segment and stack the segments in each paddock. The height of each stack is the paddock’s total, and the number of segments is its head count. The Gallimimus bar has 20 thin segments, and the Brachiosaurus bar has three thick ones.
show/hide
ggplot(data = jp_dinos,
aes(x = species, y = weight_tons, fill = species)) +
geom_col(color = "#111111", linewidth = 0.3, show.legend = FALSE) +
scale_x_discrete(guide = guide_axis(n.dodge = 2)) +
labs(x = "Paddock", y = "Total weight (tons)")
NAME IT:
Each dinosaur’s weight comes from a normal distribution for its species, \(X_i \sim \text{Normal}(\mu, \sigma)\). A paddock’s total adds up \(n\) of these weights:
\[ T = X_1 + X_2 + \dots + X_n \sim \text{Normal}(n\mu, \; \sqrt{n}\,\sigma) \]
The expected total is \(n\mu\): \(3 \times 40 = 120\) tons for the Brachiosaurus paddock and \(20 \times 0.45 = 9\) tons for Gallimimus. The spread grows with \(\sqrt{n}\), not \(n\), because heavy and light dinosaurs partly cancel each other out. Our Brachiosaurus paddock weighs 115.8 tons, within one standard deviation (\(\sqrt{3} \times 5 \approx 8.7\) tons) of its expected 120.
Try changing the head counts in n. Doubling the Velociraptors doubles their bar, but the bar would still be the shortest in the park.
8.4 Grammar
CODE:
Grouped bar graphs assume the statistical measure (i.e., the value that the length of the bars will be derived from) is contained in a variable and mapped to the x or y aesthetic.
Create labels with
labs()Initialize the graph with
ggplot()and providedataMap
speciesto thexandtotal_tonsto theyMap
speciestofillinside theaes()ofgeom_col()Stagger the long paddock names over two lines with
scale_x_discrete(guide = guide_axis(n.dodge = 2))
show/hide
labs_grp_col <- labs(
title = "Jurassic Park",
caption = "Simulated data",
subtitle = "Total weight of the dinosaurs in each paddock",
x = "Paddock",
y = "Total weight (tons)")
ggp2_grp_col <- ggplot(data = jp_paddocks,
aes(x = species,
y = total_tons)) +
geom_col(aes(fill = species),
show.legend = FALSE) +
scale_x_discrete(guide = guide_axis(n.dodge = 2))
ggp2_grp_col +
labs_grp_colGRAPH:

8.5 More info
We didn’t have to calculate total_tons (displayed on the y axis) by species because ggplot2 does this for us!
If we pass a categorical variable to the x (like species) and a continuous variable to y (like weight_tons), geom_col() stacks the rows in each group, which adds up y for each level of x:
show/hide
jp_dinos |>
ggplot(aes(x = species, y = weight_tons)) +
geom_col(aes(fill = species),
show.legend = FALSE) +
scale_x_discrete(guide = guide_axis(n.dodge = 2)) +
labs_grp_col
We can see the underlying totals using dplyr’s group_by() and summarise() functions.
show/hide
jp_dinos |>
dplyr::group_by(species) |>
dplyr::summarise(
`Total weight (tons)` = sum(weight_tons)
) |>
dplyr::ungroup() |>
dplyr::select(`Paddock` = species,
`Total weight (tons)`)| Paddock | Total weight (tons) |
|---|---|
| Brachiosaurus | 115.83 |
| Gallimimus | 8.88 |
| Triceratops | 43.08 |
| Tyrannosaurus | 15.27 |
| Velociraptor | 1.33 |