
9 Summary bar graphs
theme_happymeal()
Graphs in this chapter use fmggp2::theme_happymeal(), modeled on a McDonald’s Happy Meal box: red box, golden yellow marks, a cream plot area, and an Arial Rounded title.
9.1 Description
Bar graphs can summarize data by showing a statistic for each category. The x-axis lists the categories, and the y-axis shows the values. They can include error bars to show variation or confidence intervals.
In ggplot2, we can create summary bar graphs with geom_col().
9.2 Set up
PACKAGES:
Install packages.
show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulation
library(tidyr) # for uncount()DATA:
McDonald’s ran a different Happy Meal toy promotion every few weeks in the late 90s, and some toys (like the 1997 Teenie Beanie Babies) sold out almost everywhere. We’ll simulate 28 days of Happy Meal sales at one restaurant for four promotions. Each promotion gets its own daily sales rate (lambda), tidyr::uncount() creates one row per day, and rpois() draws the number sold. Then we average the daily sales for each promotion (avg_sold). Setting the seed with set.seed() means you’ll get the same 112 days every time you run the code below.
show/hide
set.seed(1997) # same 28 days of sales every time the code runs
happy_meals <- tibble(
promotion = c("Teenie Beanie Babies", "Furby", # four Happy Meal promotions
"Hot Wheels", "Inspector Gadget"),
lambda = c(420, 310, 220, 180) # average Happy Meals sold per day
) |>
tidyr::uncount(28, .id = "day") |> # 28 days of each promotion
mutate(
promotion = factor(promotion, levels = unique(promotion)), # keep the order above
sold = rpois(n(), lambda = lambda) # daily sales around each average
) |>
select(promotion, day, sold)
hm_sum_col <- happy_meals |>
group_by(promotion) |> # one group per promotion
summarise(avg_sold = mean(sold)) |> # average daily sales
ungroup()
dplyr::glimpse(hm_sum_col)
#> Rows: 4
#> Columns: 2
#> $ promotion <fct> Teenie Beanie Babies, Furby, H…
#> $ avg_sold <dbl> 417.6071, 309.3929, 229.5714, …9.3 Distribution
A summary bar graph shows one statistic per group (here, the mean). Each bar hides 28 days of sales behind a single height, so it’s worth knowing how much those days vary before we trust the differences between bars. Below we’ll move from the daily sales, to a picture, to the formula.
COUNT IT:
Start with the first week of the Teenie Beanie Babies promotion.
show/hide
head(happy_meals, 7)Daily sales bounce around between about 400 and 450, but they never drift far from the rate we gave rpois() (420). The summary below shows the mean and standard deviation for each promotion. It also shows the standard error of the mean (se), which measures how much each average would move if we simulated another 28 days.
show/hide
hm_stats <- happy_meals |>
group_by(promotion) |>
summarise(
mean = mean(sold),
sd = sd(sold),
se = sd(sold) / sqrt(n()))
hm_statsDRAW IT:
The graph below puts each promotion’s 28 days back behind its bar. The bar is the average, the points are the individual days, and the error bars stretch two standard errors above and below the average. geom_jitter() nudges the points sideways so they don’t pile up. The points spread out more for the busier promotions, but the error bars stay short because each average is built from 28 days.
show/hide
ggplot(data = hm_stats, aes(x = promotion, y = mean)) +
geom_col(aes(fill = promotion), show.legend = FALSE) +
geom_jitter(data = happy_meals, aes(y = sold),
width = 0.2, height = 0, alpha = 0.5, size = 1) +
geom_errorbar(aes(ymin = mean - 2 * se, ymax = mean + 2 * se),
width = 0.3, linewidth = 0.8) +
labs(x = "Toy promotion", y = "Happy Meals sold per day")
NAME IT:
Each day’s sales come from a Poisson distribution with the promotion’s rate:
\[ X \sim \text{Poisson}(\lambda) \]
For a Poisson distribution, the variance equals \(\lambda\), so the standard deviation is \(\sqrt{\lambda}\). For Teenie Beanie Babies that’s \(\sqrt{420} \approx 20.5\) Happy Meals a day, and our simulated days have a standard deviation of 21.8. The average of \(n\) days has a much smaller spread, \(\sqrt{\lambda / n}\), which is the standard error:
\[ \bar{X} \approx \text{Normal}\left(\lambda, \; \sqrt{\lambda / n}\right) \]
With \(n = 28\) days, the Teenie Beanie Babies average should land within about \(2 \times \sqrt{420 / 28} \approx 7.7\) Happy Meals of 420. Ours is 417.6. That’s why the bars are trustworthy here: the gaps between promotions (about 50 or more Happy Meals a day) are much larger than the error bars. Try cutting the 28 in tidyr::uncount() down to 3 days and watch the error bars grow.
9.4 Grammar
CODE:
Create labels with
labs()Initialize the graph with
ggplot()and providedataMap
promotiontoxandavg_soldtoyInside the
aes()ofgeom_col(), mappromotiontofillOutside the
aes()ofgeom_col(), remove the legend withshow.legend = FALSE
show/hide
labs_sum_col <- labs(
title = "Happy Meals",
caption = "Simulated data",
subtitle = "Average Happy Meals sold per day\nduring each toy promotion",
x = "Toy promotion",
y = "Average sold per day")
ggp2_sum_col <- ggplot(data = hm_sum_col,
aes(x = promotion,
y = avg_sold)) +
geom_col(aes(fill = promotion),
show.legend = FALSE)
ggp2_sum_col +
labs_sum_colGRAPH:

9.5 More info
Below is more information on geom_bar() vs. geom_col().
9.5.1 Identity vs. Count
- The
geom_bar()geom will also create summary bar graphs, but we have to switch thestatargument to"identity".
show/hide
ggplot(data = hm_sum_col,
aes(x = promotion,
y = avg_sold)) +
geom_bar(aes(fill = promotion),
show.legend = FALSE,
stat = "identity") +
labs_sum_col
9.5.2 geom_bar() vs. geom_col()
geom_bar()will map a categorical variable to thexoryand display counts for the discrete levels (seestat_count()for more info)geom_col()will map bothxandyaesthetics, and is used when we want to display numerical (quantitative) values across the levels of a categorical variable.geom_col()assumes these values have been created in their own column (seestat_identity()for more info)