
38 Grouped bubble graph
theme_powerrangers()
Graphs in this chapter use fmggp2::theme_powerrangers(), built from the Mighty Morphin Power Rangers: a light gray background, a lightning gold frame, a red Copperplate title, and red, blue, yellow, pink, black, and green Ranger marks.
38.1 Description
Grouped bubble graphs use position, color, and size to display the relationships between four variables (three numerical/quantitative, one categorical).
The position on the x and y axis captures the relationship between two numerical variables. The size and color of the points display a third numerical variable across the levels of a (fourth) categorical variable.
38.2 Set up
PACKAGES:
Install packages.
show/hide
library(ggplot2)
library(dplyr) # for data manipulation
library(tidyr) # for expand_grid()DATA:
Every episode of Mighty Morphin Power Rangers had the same rhythm: Rita Repulsa sent down a squad of Putties, and the Rangers kicked their way through them. We’ll simulate 24 fights for each of the five original Rangers with four variables:
fight_minutes: how long the fight lasted, drawn from a gamma distribution withrgamma()putties_defeated: a count fromrpois(). Each Ranger has their ownputty_rate(putties per minute), and the longer the fight, the more putties they defeathits_taken: anotherrpois()count, at 0.6 hits per minute for every Rangerranger: the Ranger, ordered red, blue, yellow, pink, and black so the theme colors match the suits
Setting the seed with set.seed() means you’ll get the same 120 fights every time you run the code below.
show/hide
set.seed(1993) # same 120 fights every time the code runs
ranger_fights <- expand_grid(
ranger = c("Red", "Blue", "Yellow", "Pink", "Black"), # five Rangers
fight = 1:24 # 24 fights for each Ranger
) |>
mutate(
ranger = factor(ranger,
levels = c("Red", "Blue", "Yellow", "Pink", "Black")), # keep the order above
putty_rate = c(Red = 2.0, Blue = 1.2, Yellow = 1.6, # Putties defeated per minute
Pink = 1.4, Black = 1.8)[as.character(ranger)], # (looked up for each Ranger)
fight_minutes = round(rgamma(n(), shape = 4, rate = 1), 1), # fights last about 4 minutes
putties_defeated = rpois(n(), lambda = putty_rate * fight_minutes), # longer fights, more Putties
hits_taken = rpois(n(), lambda = 0.6 * fight_minutes) # longer fights, more hits
)
glimpse(ranger_fights)
#> Rows: 120
#> Columns: 6
#> $ ranger <fct> Red, Red, Red, Red, Red…
#> $ fight <int> 1, 2, 3, 4, 5, 6, 7, 8,…
#> $ putty_rate <dbl> 2, 2, 2, 2, 2, 2, 2, 2,…
#> $ fight_minutes <dbl> 2.6, 4.6, 3.6, 2.4, 4.1…
#> $ putties_defeated <int> 5, 8, 7, 4, 11, 13, 9, …
#> $ hits_taken <int> 1, 2, 2, 3, 7, 1, 3, 4,…38.3 Distribution
In this grouped bubble chart, the y variable is a count that piles up over time. Longer fights don’t just give more putties on average; they also give a wider range of possible counts, so each Ranger’s points fan out as they move to the right. Below we’ll move from the fights, to a picture, to the formula.
COUNT IT:
Start with the Red Ranger’s first five fights.
show/hide
ranger_fights |>
filter(ranger == "Red") |>
select(fight, fight_minutes, putties_defeated, hits_taken) |>
head(5)Below we divide each Ranger’s total putties by their total fight minutes to estimate their rate (putties_per_min), and compare it to the rate we built in (putty_rate).
show/hide
ranger_rates <- ranger_fights |>
group_by(ranger) |>
summarise(putty_rate = first(putty_rate),
putties_per_min = sum(putties_defeated) / sum(fight_minutes),
hits_per_min = sum(hits_taken) / sum(fight_minutes))
ranger_ratesEvery putty estimate lands within a few tenths of the rate we built in. The hits per minute range from about 0.4 to 0.8 around the true rate of 0.6 (hits are rarer than putties, so their estimates bounce around more).
DRAW IT:
The graph below shows the Red and Blue Rangers’ fights. Each line is the putty rate times the fight length, and each ribbon covers two standard deviations above and below the line. Both ribbons get wider as the fights get longer.
show/hide
red_blue <- ranger_fights |>
filter(ranger %in% c("Red", "Blue"))
ggplot(red_blue, aes(x = fight_minutes, fill = ranger, color = ranger)) +
geom_ribbon(aes(ymin = pmax(0, putty_rate * fight_minutes -
2 * sqrt(putty_rate * fight_minutes)),
ymax = putty_rate * fight_minutes +
2 * sqrt(putty_rate * fight_minutes)),
alpha = 1 / 5, color = NA) +
geom_line(aes(y = putty_rate * fight_minutes), linewidth = 1) +
geom_point(aes(y = putties_defeated), size = 2) +
labs(x = "Fight length (minutes)", y = "Putties defeated",
color = "Ranger", fill = "Ranger")
NAME IT:
The putties in each fight follow a Poisson distribution whose average is the Ranger’s rate (\(\lambda\)) times the length of the fight (\(t\)):
\[ \text{putties} \sim \text{Poisson}(\lambda_{\text{ranger}} \times t) \]
A Poisson count has a variance equal to its mean, so its standard deviation is \(\sqrt{\lambda t}\). For the Red Ranger (\(\lambda = 2\)), a 2 minute fight averages 4 putties give or take about 2, while an 8 minute fight averages 16 putties give or take about 4. The spread grows with the square root of the fight length, which is why the points fan out (and why the ribbons in DRAW IT get wider). The hits_taken that set the bubble sizes work the same way with \(\lambda = 0.6\), so bigger bubbles show up in longer fights for every Ranger. Try changing rate = 1 to rate = 4 in rgamma() and the fights get shorter, the points crowd to the left, and the fan almost disappears.
38.4 Grammar
CODE:
Create labels with
labs()Initialize the graph with
ggplot()and providedataMap
fight_minutesto thexaxis andputties_defeatedto theyaxisInside
geom_point()map
hits_takentosizeandrangertocolorset the
alphato2/3
In the
scale_size()- set
rangetoc(.04, 6)
- set
show/hide
labs_grp_bubble <- labs(
title = "Power Rangers vs. the Putties",
subtitle = "Putties defeated by fight length",
caption = "Simulated data",
x = "Fight length (minutes)",
y = "Putties defeated",
size = "Hits taken",
color = "Ranger",
fill = "Ranger")
ggp2_grp_bubble <- ranger_fights |>
ggplot(aes(
x = fight_minutes, y = putties_defeated)) +
geom_point(
aes(size = hits_taken,
color = ranger),
alpha = 2/3) +
scale_size(range = c(.04, 6))
ggp2_grp_bubble +
labs_grp_bubbleGRAPH:

38.5 More info
We can change the shape of the points for more control of the color and fill.
38.5.1 Shape
Inside geom_point(), we can set shape to 21, and color to "#000000" to outline the points in black
show/hide
ggp2_grp_bubble_s21 <- ranger_fights |>
ggplot(aes(
x = fight_minutes, y = putties_defeated)) +
geom_point(
aes(size = hits_taken, fill = ranger),
alpha = 2/3, shape = 21, color = "#000000") +
scale_size(range = c(.04, 6))
ggp2_grp_bubble_s21 +
labs_grp_bubble
#> Ignoring unknown labels:
#> • colour : "Ranger"
38.5.2 Color
Inside the aes() of geom_point(), we can set shape to 21, and map color to ranger to outline the points in the same colors as fill
show/hide
ggp2_grp_bubble_s21_color <- ranger_fights |>
ggplot(aes(
x = fight_minutes, y = putties_defeated)) +
geom_point(
aes(size = hits_taken,
fill = ranger,
color = ranger),
alpha = 2/3, shape = 21) +
scale_size(range = c(.04, 6))
ggp2_grp_bubble_s21_color +
labs_grp_bubble