
22 Beeswarm plots
theme_tetris()
Graphs in this chapter use fmggp2::theme_tetris(), modeled on Tetris: a black well, the tetromino block colors as marks, and the pixel font Press Start 2P (bundled with the package) for the title.
22.1 Description
A beeswarm plot shows each number as its own point, so we can see where the numbers fall in each group.1
The points for each group stay close together and fall to the left and right of the group’s place on the x axis. The form the points make in each group is called a swarm.2
We can make a beeswarm plot with the ggbeeswarm code.3
22.2 Set up
PACKAGES:
Bring in the code we’ll use.4
show/hide
# pak::pak("eclarke/ggbeeswarm")
library(ggbeeswarm)
library(ggplot2)
library(dplyr) # for tibble() and data manipulationDATA:
The Game Boy is a small computer for playing games that you hold in your hands. Tetris is a game where small pieces fall from the top, and you fit them together to make a full line from side to side and clear it. Tetris came with every Game Boy,5 and by the 90s everyone had a best game to talk about. We’ll make up6 the number of lines cleared in 80 games by each of three players. rnbinom() picks how many lines each game clears from a rule for numbers that can fall far from their average,7 with a different average (mu) for each player: 25 lines for a casual player, 60 for a regular, and 120 for an expert. Starting with set.seed()8 means you’ll get the same 240 games every time you run the code below.
show/hide
set.seed(1989) # same 240 games every time the code runs
tetris_games <- tibble(
game_id = 1:240, # 240 different games of Tetris, one row each
player = rep(c("Casual", "Regular", "Expert"), each = 80) # 80 games for each player type
) |>
mutate(
player = factor(player, levels = c("Casual", "Regular", "Expert")), # least to most skilled
lines_cleared = rnbinom(n(),
size = 5, # small size spreads the counts out
mu = c(Casual = 25, Regular = 60, # average lines cleared
Expert = 120)[as.character(player)] # (looked up for each player type)
)
)
glimpse(tetris_games)
#> Rows: 240
#> Columns: 3
#> $ game_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9,…
#> $ player <fct> Casual, Casual, Casual, Ca…
#> $ lines_cleared <dbl> 42, 6, 21, 30, 15, 36, 31,…22.3 Distribution
A beeswarm plot shows every game as its own point, so we can see how far the numbers in each group fall from each other.9 A count is how many times something happens. Lines cleared are counts, but they fall much farther from their average than the counts we saw before: one bad drop can end a game early, and one good run can keep it going for a long time. Below we’ll move from the games, to a graph, to the rule behind it.10
COUNT IT:
Start with the first ten games (all from the casual player).
show/hide
head(tetris_games$lines_cleared, 10)
#> [1] 42 6 21 30 15 36 31 9 26 30The casual player clears about 25 lines on average, but these games run from 6 to 42. The variance is a number for how far the numbers fall from the mean.11 The numbers below show the variance for each player.12 For counts that come at a set rate (like the number of books kids read, which we saw before),13 the variance would be close to the mean. Here the variance is many times larger than the mean, and the difference grows as the player gets better.
show/hide
tetris_games |>
group_by(player) |>
summarise(mean = mean(lines_cleared), variance = var(lines_cleared),
min = min(lines_cleared), max = max(lines_cleared))DRAW IT:
The beeswarms below compare our regular player’s 80 games to 80 games picked from the set rate rule with the same average (60 lines).14 The Poisson swarm stays close together around 60. The Negative binomial swarm runs from under 10 to more than two times the average, with a long line of great games at the top.15
show/hide
regular_games <- tibble(
model = rep(c("Negative binomial", "Poisson"), each = 80),
lines_cleared = c(
tetris_games$lines_cleared[tetris_games$player == "Regular"],
rpois(80, lambda = 60)))
ggplot(regular_games, aes(x = model, y = lines_cleared)) +
ggbeeswarm::geom_beeswarm(size = 1.6, color = "#00F0F0") +
geom_hline(yintercept = 60, linetype = "dashed", color = "#F5F5F5") +
annotate("text", x = 0.5, y = 60, label = "Average: 60 lines",
hjust = 0, vjust = -0.5, color = "#F5F5F5") +
labs(title = "Same average, wider spread",
x = NULL, y = "Lines cleared")
NAME IT:
The rule we used16 is often used for counts that fall farther from their average than the set rate rule allows.17 It has two numbers in it,18 an average (\(\mu\)) and a size (\(r\)), and its variance is:
\[ X \sim \text{NB}(\mu, \; r = 5), \quad \text{Var}(X) = \mu + \frac{\mu^2}{r} \]
The first part (\(\mu\)) is the variance of the set rate rule, and the second part (\(\mu^2 / r\)) adds more on top.19 For the regular player that’s \(60 + 3600 / 5 = 780\), but only 60 for the set rate rule. This extra part grows with \(\mu^2\), so the expert’s games (\(\mu = 120\)) fall much farther from their average than the casual player’s (\(\mu = 25\)). As \(r\) gets larger, the extra part gets smaller and our rule turns into the set rate rule.20 Try size = 500 in rnbinom() and the three swarms come in close.
22.4 Grammar
CODE:
Make the words around the graph21 with
labs()Start the graph with
ggplot()and give itdataPut
playeron thexaxisPut
lines_clearedon theyaxisAdd
ggbeeswarm::geom_beeswarm()on top22 (withalpha, so we can see through the points)
show/hide
labs_beeswarm <- labs(
title = "Tetris",
caption = "Simulated data",
subtitle = "Lines cleared in 80 games by each player",
x = "Player",
y = "Lines cleared")
ggp2_beeswarm <- ggplot(data = tetris_games,
aes(x = player,
y = lines_cleared)) +
ggbeeswarm::geom_beeswarm(alpha = 2 / 3, color = "#00F0F0")
ggp2_beeswarm +
labs_beeswarmGRAPH:
Below we change the form of the swarm with method. The ggbeeswarm code also has geom_quasirandom(), another way to place the points.23

22.5 More info
Below are a few more things we can set24 and ways to make beeswarm plots.
22.5.1 method
Use method to change the form of the beeswarm (swarm, compactswarm, hex, square, center, or centre)
Set the point shape to 21 so you can choose both its fill and its color
show/hide
ggp2_compact_swarm <- ggplot(data = tetris_games,
mapping = aes(x = player,
y = lines_cleared)) +
ggbeeswarm::geom_beeswarm(
aes(fill = player),
method = 'compactswarm',
shape = 21,
color = "#ffffff",
alpha = 2/3, size = 1.7,
show.legend = FALSE)
ggp2_compact_swarm +
# add labels
labs_beeswarm
22.5.2 side
To put the points on one side of each group only, use side.25
show/hide
ggp2_rside_swarm <- ggplot(data = tetris_games,
mapping = aes(x = player,
y = lines_cleared)) +
ggbeeswarm::geom_beeswarm(
aes(fill = player),
side = 1, # right side only
shape = 21,
color = "#ffffff",
alpha = 2/3,
size = 1.7,
show.legend = FALSE)
ggp2_rside_swarm +
# add labels
labs_beeswarm
22.5.3 cex
The cex setting26 controls the “scaling for adjusting point spacing”
show/hide
ggp2_beeswarm_cex <- ggplot(data = tetris_games,
mapping = aes(x = player,
y = lines_cleared)) +
ggbeeswarm::geom_beeswarm(
aes(fill = player),
cex = 1.6,
shape = 21,
color = "#ffffff",
alpha = 2/3,
size = 1.7,
show.legend = FALSE)
ggp2_beeswarm_cex +
# add labels
labs_beeswarm
22.5.4 geom_jitter()
geom_jitter() does not make a beeswarm. It moves each point to the side by a small amount picked by chance, so points can still land on top of each other. We set width to move the points to the side, and height = 0 so no count moves up or down.27
show/hide
ggp2_jitter_swarm <- ggplot(data = tetris_games,
mapping = aes(x = player,
y = lines_cleared)) +
geom_jitter(
aes(fill = player),
height = 0,
width = 0.11,
shape = 21,
color = "#ffffff",
alpha = 2 / 3,
size = 1.7,
show.legend = FALSE)
ggp2_jitter_swarm +
# add labels
labs_beeswarm +
labs(caption = "Simulated data; points jittered to the side at random")
The beeswarm plot displays the distribution of a continuous variable across the levels of a categorical variable.↩︎
The points are grouped by level and spread sideways along the categorical (
player) axis, on both sides of each group by default, so the swarm takes the shape of the distribution (similar to a violin plot).↩︎The
ggbeeswarmpackage.↩︎Install and load the packages.↩︎
Tetris came bundled with the Game Boy.↩︎
Simulate.↩︎
A negative binomial distribution.↩︎
Setting the random seed.↩︎
The spread of each group, shown directly.↩︎
The formula.↩︎
The variance: the average squared distance from the mean (
var()).↩︎Summary statistics: the mean, variance, minimum, and maximum for each player.↩︎
Poisson counts, like the Goosebumps books in Chapter 21.↩︎
A Poisson distribution with \(\lambda = 60\) (
rpois(80, lambda = 60)).↩︎The negative binomial swarm stretches from single digits to more than twice the average, with a long right tail.↩︎
The negative binomial distribution.↩︎
Overdispersion: counts more spread out than a Poisson distribution allows.↩︎
Its parameters: the mean \(\mu\) and the size (or dispersion) parameter \(r\).↩︎
The first term is the Poisson variance; the second term is the extra variance.↩︎
As \(r \to \infty\), the negative binomial converges to the Poisson.↩︎
Labels.↩︎
Add the
geom_beeswarm()layer.↩︎geom_quasirandom()spreads the points with a quasirandom sequence instead of placing them one by one; we don’t show it here.↩︎Additional arguments.↩︎
The
sideargument:0(the default) uses both sides,1the right side only, and-1the left side only.↩︎The
cexargument.↩︎geom_jitter()adds random noise to each point’s position (widthsideways,heightup and down). It is a contrast to a beeswarm, not a beeswarm, and the jitter is noted in the caption.↩︎