
41 2D histograms
theme_geocities()
Graphs in this chapter use fmggp2::theme_geocities(), built from a GeoCities home page: a black background, a web gray frame, a dark navy panel, lime green text, a yellow Comic Sans title, web safe marks, and a blue to yellow continuous scale.
41.1 Description
Standard histograms separate a variable’s values into discrete groups, or ‘bins,’ which are arranged in increasing order across the x axis. The y axis displays the frequency (or count) of values within each bin.
Vertical bars capture the variable’s distribution using the height of the bar to represent the number of values per ‘bin’, and the number of bars corresponds with the bin value (or ‘bin-width’).
When we extend this display to two numerical/quantitative variables, the bins are used to divide the total graph area into a grid, and color is used to display the variation in frequency (or count) of both variable values that fall within each intersecting square.
41.2 Set up
PACKAGES:
Install packages.
show/hide
install.packages("colorspace")
library(colorspace)
library(ggplot2)
library(dplyr) # for tibble() and data manipulationDATA:
In the late 90s, anyone could build a home page on GeoCities, and most people loaded those pages (animated GIFs, MIDI files, and all) over a dial-up modem. We’ll simulate 5,000 page loads with three variables:
modem: a 28.8k or 56k modem, drawn withsample()page_kb: the size of the page in kilobytes. Most pages are small and a few are huge, so we draw these from a lognormal distribution withrlnorm()load_sec: the seconds it takes to load the page. Each load takes 2 seconds to connect, plus the page size in kilobits (page_kb * 8) divided by the modem’s real world speed (24 kilobits per second for a 28.8k modem, 44 for a 56k modem), plus some noise fromrnorm()
Setting the seed with set.seed() means you’ll get the same 5,000 page loads every time you run the code below.
show/hide
set.seed(1997) # same 5,000 pages every time the code runs
geocities_pages <- tibble(
page_id = 1:5000, # 5,000 different GeoCities home pages
modem = sample(
c("28.8k", "56k"), # modem used to load each page
size = 5000, # one modem for each page
replace = TRUE # modems repeat (with equal probability)
),
page_kb = round(
rlnorm(5000, meanlog = log(60), sdlog = 0.45), # page sizes (most near 60 KB, a few much larger)
1 # one decimal place
)
) |>
mutate(
kbps = if_else(modem == "28.8k", 24, 44), # real world modem speeds (kilobits per second)
load_sec = round(
2 + page_kb * 8 / kbps + # 2 second wait plus kilobits divided by speed
rnorm(n(), mean = 0, sd = 1.5), # random network noise
1 # one decimal place
)
) |>
select(-kbps) # drop the helper column
glimpse(geocities_pages)
#> Rows: 5,000
#> Columns: 4
#> $ page_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, …
#> $ modem <chr> "56k", "28.8k", "56k", "28.8k",…
#> $ page_kb <dbl> 50.2, 173.7, 28.8, 28.2, 37.9, …
#> $ load_sec <dbl> 12.8, 61.3, 8.4, 12.7, 8.5, 17.…41.3 Distribution
With 5,000 points, a scatter plot turns into a solid blob. A 2D histogram counts how many points land in each square of a grid and colors the squares by those counts. The counts reveal a pattern the blob would hide: there are two kinds of page loads, one for each modem. Below we’ll move from the page loads, to a picture, to the formula.
COUNT IT:
Start with the first five page loads.
show/hide
head(geocities_pages, 5)Below we build a small 2D histogram by hand. We cut the page sizes into 25 KB bins and the load times into 5 second bins with cut_width(), then count the page loads in each pair of bins (only the most crowded cells are shown).
show/hide
geocities_cells <- geocities_pages |>
mutate(kb_bin = cut_width(page_kb, width = 25, boundary = 0),
sec_bin = cut_width(load_sec, width = 5, boundary = 0)) |>
count(kb_bin, sec_bin, sort = TRUE)
head(geocities_cells, 6)The busiest cell holds 683 page loads, and 11 of the 52 cells hold only a single page load. Each of these counts becomes one colored square in the graph.
DRAW IT:
The graph below shows the same 5,000 page loads with geom_bin2d(), split into two panels by modem. Each modem draws its own straight band: the band for the slow 28.8k modem climbs almost twice as steeply as the band for the 56k modem.
show/hide
ggplot(geocities_pages, aes(x = page_kb, y = load_sec)) +
geom_bin2d(bins = 30) +
facet_wrap(~ modem) +
labs(x = "Page size (KB)", y = "Seconds to load")
NAME IT:
The page sizes follow a lognormal distribution, and the load times follow a normal distribution centered on a straight line whose slope depends on the modem:
\[ \log(\text{KB}) \sim \text{Normal}(\log(60), \; 0.45), \quad \text{seconds} \mid \text{KB}, \text{modem} \sim \text{Normal}\left(2 + \frac{8 \times \text{KB}}{\text{kbps}_{\text{modem}}}, \; 1.5\right) \]
The slopes are \(8 / 24 \approx 0.33\) seconds per kilobyte for a 28.8k modem and \(8 / 44 \approx 0.18\) for a 56k modem, which is why one band is about twice as steep as the other. With both modems together, the data come from a mixture of two of these distributions. Each square in a 2D histogram counts how many of our 5,000 page loads fell inside it, so its count is about \(5000 \times p\), where \(p\) is the chance a page load lands in that square. Smaller squares have smaller counts, and counts that small bounce around a lot from square to square (the noise in a count is about its square root). Try changing bins = 30 to bins = 80 in the graph above and the bands break up into speckles.
41.4 Grammar
CODE:
Create labels with
labs()Initialize the graph with
ggplot()and providedataMap
page_kbto thexandload_secto theyAdd the
geom_bin2d()layer
show/hide
labs_2dhist <- labs(
title = "GeoCities page loads",
subtitle = "Page size vs. seconds to load on a dial-up modem",
caption = "Simulated data",
x = "Page size (KB)",
y = "Seconds to load")
ggp2_2dhist <- ggplot(data = geocities_pages,
mapping = aes(x = page_kb,
y = load_sec)) +
geom_bin2d(bins = 30)
ggp2_2dhist +
labs_2dhistGRAPH:

41.5 More info
41.5.1 Bins
The value for bins will vary depending on the variable values (there is no correct number):
If the number of
binsis too low, the density may hide important nuances between the variables.If the number of
binsis too high, the noise might drown out the signal.Below we change the
binsto15,45and60to compare the display:
show/hide
ggp2_base <- ggplot(data = geocities_pages,
mapping = aes(x = page_kb,
y = load_sec))
ggp2_2dhist_bins15 <- ggp2_base +
geom_bin2d(bins = 15)
ggp2_2dhist_bins45 <- ggp2_base +
geom_bin2d(bins = 45)
ggp2_2dhist_bins60 <- ggp2_base +
geom_bin2d(bins = 60)
ggp2_2dhist_bins15 +
labs_2dhist
ggp2_2dhist_bins45 +
labs_2dhist
ggp2_2dhist_bins60 +
labs_2dhist


41.5.2 Scale
scale_fill_continuous_sequential() from the colorspace package comes with a variety of palettes to choose from (run hcl_palettes(type = "sequential") to view the full list).
We can also reverse the order of the fill color scale with rev (TRUE or FALSE).
show/hide
ggp2_2dhist_bins45 +
scale_fill_continuous_sequential(
palette = "Mako",
rev = TRUE) +
labs_2dhist
ggp2_2dhist_bins45 +
scale_fill_continuous_sequential(
palette = "Mako",
rev = FALSE) +
labs_2dhist

41.5.3 Options
If you set the point shape to 21, you have control over both color and fill.
In the previous example we showed how to reverse the color scale for the palette in scale_fill_continuous_sequential().
Below we reverse the color scale, but also manually set which colors on the scale we want to
beginwith (i.e., smallest data value) and which color we want toendwith (i.e., the largest data value). Possible values range from0-1.We also add a
geom_point()layer (with a smallsizeandalpha, because there are 5,000 points).
show/hide
ggp2_2dhist_bins45 +
scale_fill_continuous_sequential(
palette = "SunsetDark",
rev = TRUE,
begin = 1.0,
end = 0.3) +
geom_point(
fill = "#00FFFF",
color = "#000033",
shape = 21,
size = 0.8,
alpha = 0.25) +
labs_2dhist