
35 Scatter plots
theme_doom()
Graphs in this chapter use fmggp2::theme_doom(), built from Doom (1993): a scorched brown background, a hellfire red frame and title, red, orange, gunmetal, armor green, blue key, and yellow key marks, and an Impact title.
35.1 Description
Scatter plots are used to display two continuous variables.
If one of the continuous variables is assumed to affect the other (i.e., outcome vs. predictor), place the outcome on the y axis and the predictor on the x axis.
35.2 Set up
PACKAGES:
Install packages.
show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulationDATA:
When Doom came out in December 1993, the first question every PC owner asked was how well it would run on their machine. A faster processor drew more frames per second, but only up to a point: the Doom engine never drew more than 35 frames per second, no matter how fast the computer was. We’ll simulate 200 PCs with processors between 25 and 100 MHz (cpu_mhz, drawn with runif()). Each PC’s frame rate (fps) is 0.45 frames per megahertz, plus some noise from rnorm() (for the graphics card, the RAM, and whatever else was running), and pmin() caps it at 35. Setting the seed with set.seed() means you’ll get the same 200 PCs every time you run the code below.
show/hide
set.seed(1993) # same 200 PCs every time the code runs
doom_pcs <- tibble(
pc_id = 1:200, # 200 different PCs, one row each
cpu_mhz = round(runif(200, min = 25, max = 100)), # processor speeds spread evenly from 25 to 100 MHz
fps = round(
pmin(35, 0.45 * cpu_mhz + # faster processors, more frames (Doom caps out at 35)
rnorm(200, mean = 0, sd = 3)), # random PC to PC noise
1 # one decimal place
)
)
glimpse(doom_pcs)
#> Rows: 200
#> Columns: 3
#> $ pc_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1…
#> $ cpu_mhz <dbl> 48, 27, 51, 78, 64, 65, 44, 44, …
#> $ fps <dbl> 19.2, 10.8, 34.7, 35.0, 29.6, 26…35.3 Distribution
A scatter plot shows how one variable changes as the other changes. In the Doom data, the frame rate climbs in a straight line with the processor speed, wobbles around that line, and then hits a ceiling. Below we’ll move from the PCs, to a picture, to the formula.
COUNT IT:
Start with the first six PCs.
show/hide
head(doom_pcs)Faster processors tend to have higher frame rates. Below we fit a straight line to the PCs at or below 66 MHz (well under the cap) with lm(), and count how many PCs landed exactly on the 35 frame ceiling.
show/hide
slow_pcs <- doom_pcs |>
filter(cpu_mhz <= 66)
slow_fit <- lm(fps ~ cpu_mhz, data = slow_pcs)
coef(slow_fit)
#> (Intercept) cpu_mhz
#> 0.7081649 0.4389735
sigma(slow_fit)
#> [1] 3.272555
sum(doom_pcs$fps == 35)
#> [1] 60The fitted slope is about 0.44 frames per megahertz (close to the 0.45 we built in), and the typical distance from the line (sigma()) is about 3.3 frames. 60 of the 200 PCs are stuck at exactly 35 frames per second.
DRAW IT:
The graph below adds the line we built the data from (geom_abline()), a ribbon two standard deviations above and below it, and a dashed line at the 35 frame cap. Most of the points fall inside the ribbon until the line runs into the cap, and from there the points pile up in a flat row.
show/hide
ggplot(doom_pcs, aes(x = cpu_mhz, y = fps)) +
geom_ribbon(aes(ymin = 0.45 * cpu_mhz - 6, ymax = 0.45 * cpu_mhz + 6),
fill = "#8A8D8F", alpha = 1 / 4) +
geom_abline(intercept = 0, slope = 0.45, color = "#F2C14E",
linewidth = 1) +
geom_hline(yintercept = 35, linetype = "dashed", color = "#D63A1E") +
geom_point() +
labs(x = "CPU speed (MHz)", y = "Frames per second")
NAME IT:
Each PC’s frame rate is a draw from a normal distribution centered on a straight line (the simple linear regression model), and then capped:
\[ \text{fps}_i = \min\left(35, \; 0.45 \times \text{MHz}_i + \varepsilon_i\right), \quad \varepsilon_i \sim \text{Normal}(0, \; 3) \]
About 95% of the points land within \(2 \times 3 = 6\) frames of the line, which is the ribbon in DRAW IT. The line reaches the cap at \(35 / 0.45 \approx 78\) MHz, so above that speed most of the PCs sit on the ceiling. A cap like this is called censoring, and it bends the relationship: the correlation for all 200 PCs is 0.92, but a straight line fit to all of them would underestimate the slope for the slow PCs and overestimate it for the fast ones. Try removing pmin(35, ...) and the points keep climbing in a straight line.
35.4 Grammar
CODE:
Create labels with
labs()Initialize the graph with
ggplot()and providedataMap
cpu_mhzto thexaxisMap
fpsto theyaxisAdd
geom_point()
show/hide
labs_scatter <- labs(
title = "Doom frame rates",
subtitle = "Frames per second by processor speed",
caption = "Simulated data",
x = "CPU speed (MHz)", y = "Frames per second")
ggp2_scatter <- doom_pcs |>
ggplot(
aes(x = cpu_mhz,
y = fps)) +
geom_point()
ggp2_scatter +
labs_scatterGRAPH:
If multiple points occupy the same value position, use transparency (alpha) to improve visibility.
