Rolling Stone’s greatest albums: the workbook

Session 2 · TidyTuesday 2024-05-07 · Music sales and styles

Published

October 16, 2026

TipSession 2 · 2026-10-16 · facilitated by Jon Minton

No one is named, because no consent record was taken. The room chose the dataset. This page is the session workbook, prepared by Claude Opus 5.5 from the session folder: shortlist.md, notes.md, and the specs and figures under specs/ and figures/. It starts with the preparation and records each figure in the order it was made. The page was approved before publishing. How these sessions work.

WarningWhere this workbook stands

The room chose the dataset. The spec trail records no graphic design decisions by the room: spec 01 was proposed by the agent before the session, and figure 02 was requested by the facilitator. Later specs will be added below as new numbered sections, quoted verbatim, each with its figure and what looking at the render showed.

The brief for this session was music sales and musical styles. TidyTuesday has no dedicated music-sales week, so the shortlist offered four weeks that measure success in other ways: chart positions, critics’ ranks and Spotify popularity. The room chose Rolling Stone’s “500 Greatest Albums of All Time”: the 2003, 2012 and 2020 lists, merged into one table by The Pudding.

Runs from one CSV cached in data/. To refresh from source:

download.file(
  "https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2024/2024-05-07/rolling_stone.csv",
  "data/tt-2024-05-07-rolling_stone.csv")

Or use the repo’s helper, which caches every file in the week: sh scripts/workshop r scripts/tt_fetch.R 2024-05-07.

Packages: tidyverse. Source: The Pudding, What makes an album the greatest of all time?, from Chris Eckert’s spreadsheet, via TidyTuesday 2024-05-07. No licence stated.

Code
library(tidyverse)

rs <- read_csv("../../data/tt-2024-05-07-rolling_stone.csv", show_col_types = FALSE)

on_list <- rs |>
  transmute(`2003` = !is.na(rank_2003), `2012` = !is.na(rank_2012), `2020` = !is.na(rank_2020))
n_all_three <- sum(on_list$`2003` & on_list$`2012` & on_list$`2020`)
n_only_2020 <- sum(!on_list$`2003` & !on_list$`2012` & on_list$`2020`)
n_only_2003 <- sum(on_list$`2003` & !on_list$`2012` & !on_list$`2020`)

rs_2020 <- rs |> filter(!is.na(rank_2020))

Step 1: choosing a dataset

The shortlist (shortlist.md) was scouted on the day of the session. Each week is listed with the grammar layer it stresses most. The series arc puts geoms and stats next, but which layer to cover was left for the room to decide. The room chose Rolling Stone, which stresses scales and position most.

Week What it is Layer it stresses best What it can’t say
2025-08-26 Billboard Hot 100 #1s Every US #1 single, 1958–2025, hand-coded for genre, tempo and 54 instrument flags Geoms and stats Only the songs that reached #1; the codes are one listener’s judgement
2024-05-07 Rolling Stone Album Rankings 691 albums on the 2003, 2012 or 2020 “500 Greatest” lists Scales and position Critics’ rankings, not sales; the voter panel changed between lists
2020-09-29 Beyoncé and Taylor Swift Copies sold per album and country, plus national chart peaks Scales Two artists, patchy country coverage, worldwide rows that include the country rows
2020-01-21 Spotify Songs About 28,000 tracks with audio features, in 6 genres Facets and stats Genre belongs to the playlist, not the track

The only real sales figures are in the 48-row Beyoncé and Taylor Swift table. Every other week stands in for sales with a proxy.

Step 2: describing the data

This step follows the tt-fetch skill’s description of the week (the notes.md summary). The numbers here are recomputed from the cached CSV.

Grain: one row per album. The table has 691 rows and 21 columns. An album appears once, whichever lists it was on. A missing rank means “not on that list”, so each rank column is about 28% missing. That missingness carries information; it is not lost data.

Code
on_list |>
  count(`2003`, `2012`, `2020`, name = "albums") |>
  mutate(across(`2003`:`2020`, \(x) if_else(x, "✓", ""))) |>
  arrange(desc(albums))
2003 2012 2020 albums
✓ ✓ ✓ 319
✓ 154
✓ ✓ 145
✓ 29
✓ ✓ 20
✓ 16
✓ ✓ 7
1

319 albums are on all three lists. 154 appear only in 2020 and 29 only in 2003. One row is on none of the three lists.

Variable Type (grammar terms) Notes
rank_2003, rank_2012, rank_2020 continuous, reversed 1 is best, so the scale must be reversed. 2003 has 2 tied ranks
differential derived 2003 rank minus 2020 rank, with a missing rank counted as 501. Recompute it before using it
release_year continuous (or temporal) 1955–2019
peak_billboard_position continuous, with a code 201 means “never charted” (117 albums), not a position
weeks_on_billboard continuous, long right tail up to 741; 17% missing
spotify_popularity continuous a snapshot from when the data was collected
genre discrete (16 levels) 24% missing; “Blues/Blues ROck” is a misspelled duplicate
type discrete (5 levels) 88% Studio
artist_gender discrete (3 levels) Male, Female, Male/Female
artist_member_count discrete or continuous whole numbers 1–12; how to treat it is the room’s decision
album, clean_name, album_id, spotify_url identifiers labels only

The agent proposed these candidate mappings. None of them is a choice:

  • a slope or bump chart of rank across the three lists, which has to handle albums that join or leave a list
  • release year against 2020 rank, coloured by genre or gender, to see which eras the 2020 voters rewarded
  • critics against the market: peak Billboard position or weeks on the chart against rank, which needs decisions about the 201 code and a log axis
  • small multiples by type or genre

Step 3: spec 01, a starting point

ImportantProposed by the agent, not decided by the room

The facilitator asked for a first figure to have on screen before the session. Claude Opus 5.5 filled every slot in this spec. The room has not chosen any of it, and it is there to be revised or thrown away.

The question the agent chose was: which release eras did the 2020 list reward, and where in the ranking?

# sessions/2026-10-16/specs/01-rs-era-2020.yml
data: tt-2024-05-07-rolling_stone
filter: "!is.na(rank_2020)"   # grain: one row per album on the 2020 list
mapping:
  x: release_year
  y: rank_2020
  colour: artist_gender
layers:
  - geom: point
    alpha: 0.7
scales:
  y: scale_y_reverse(breaks = c(1, 100, 200, 300, 400, 500))
  colour: okabe-ito
labs:
  title: "Rolling Stone's 500 greatest albums (2020 list)"
  x: "Release year"
  y: "Rank in 2020 (1 = greatest)"
  colour: "Artist gender"
theme: minimal
Code
ggplot(rs_2020, aes(x = release_year, y = rank_2020, colour = artist_gender)) +
  geom_point(alpha = 0.7) +
  scale_y_reverse(breaks = c(1, 100, 200, 300, 400, 500)) +
  scale_colour_manual(values = unname(palette.colors(3, palette = "Okabe-Ito"))) +
  labs(
    title = "Rolling Stone's 500 greatest albums (2020 list)",
    x = "Release year",
    y = "Rank in 2020 (1 = greatest)",
    colour = "Artist gender"
  ) +
  theme_minimal(base_size = 13)
Scatter of the 500 albums on Rolling Stone's 2020 list: release year on x, rank on a reversed y axis with 1 at the top, points coloured by artist gender. Points are densest from the mid-1960s to the late 1970s and spread evenly from top to bottom.
Figure 1

Looking at the render:

  • The y axis carries no density. Each rank from 1 to 500 is used once, so the points are spread evenly from top to bottom by construction. Everything the figure shows is in how release years spread along each band of ranks. The eye reads clusters left to right, not top to bottom.
  • The pattern is in the x distribution, and points are a weak way to show it. The 1970s alone hold 155 of the 500 albums, and at alpha = 0.7 the points from 1965 to 1975 merge into a block. The correlation between release year and rank is only 0.13. If there is an era effect, this geom does not make it visible.
  • Colour shows counts, not shares. 358 of the 500 albums are by male artists, so the female and mixed points are scattered among them. Whether women’s share rises in later decades is a proportion per decade, which is a stat the spec does not compute.
  • Two defaults nobody decided on. Okabe-Ito’s first colour is black, and levels are assigned alphabetically, so “Female” gets the heaviest ink by accident. The legend also has an NA entry: the 2 albums on the 2020 list with no artist gender are both “Various Artists” compilations.
  • The title fits at 9 inches, and the reversed axis with 1 at the top reads correctly.

All of these point at the geoms and stats layer: the same mapping could be drawn as something other than points. Some candidate first moves are listed below. They are proposals, at most three per slot, as in gg-spec’s interactive mode.

Slot Options to put to the room
Geom/stat for the era question histogram or density of release_year, faceted or filled by rank band (top 100 / 101–250 / 251–500); boxplot or ggdist interval of release_year per rank band; keep points and add a smoother
The gender question bars of the proportion of each gender per release decade (position = "fill"); drop colour and answer the era question first; keep colour but put Male in grey
The NA gender rows filter them out and say so in a caption; label them “Compilation”; leave them as they are

Step 4: figure 02, the genre of each list’s top five

ImportantRequested by the facilitator, with one assumption by the agent

The facilitator asked for “the assumed genre of the top five over time, as something like a Sankey chart”. Claude Opus 5.5 made the remaining choices: an alluvial layout, an “Outside top 5” block, the colours and the album labels. It also made the genre assumption described below. This figure is outside the gg-spec schema, because its geoms come from the ggalluvial extension rather than from ggplot2 itself. So it is recorded as a script (specs/02-top5-genre-sankey.R), not as YAML.

Code
library(ggalluvial)

assumed_genre <- "Rock/Pop (assumed)"
outside       <- "Outside top 5"

top5_albums <- rs |>
  filter(pmin(rank_2003, rank_2012, rank_2020, na.rm = TRUE) <= 5)

top5 <- top5_albums |>
  mutate(
    genre = replace_na(genre, assumed_genre),
    album = recode(album, "Sgt. Pepper's Lonely Hearts Club Band" = "Sgt. Pepper's")
  ) |>
  pivot_longer(starts_with("rank_"), names_to = "list", names_prefix = "rank_",
               values_to = "rank") |>
  mutate(
    position = if_else(rank <= 5, genre, outside),
    position = fct_relevel(factor(position), outside, after = Inf),
    genre    = fct_relevel(factor(genre), assumed_genre)
  )

in_top5    <- \(yr) top5 |> filter(list == yr, rank <= 5)
n_assumed  <- sum(is.na(top5_albums$genre))
rock_2003  <- sum(in_top5("2003")$genre == assumed_genre)
rock_2020  <- sum(in_top5("2020")$genre == assumed_genre)
soul_2020  <- sum(in_top5("2020")$genre == "Soul/Gospel/R&B")
same_03_12 <- setequal(in_top5("2003")$album, in_top5("2012")$album)
rank_20    <- \(a) top5 |> filter(list == "2020", album == a) |> pull(rank)

The assumption. 9 albums have been in at least one list’s top five. 5 of them have no genre in the data, and all of them are by the Beatles or the Beach Boys. None of the dataset’s 16 genre groups is plain rock or pop, which suggests the coders left mainstream rock and pop blank. So these albums are labelled Rock/Pop (assumed), and the label says it is an assumption. The data says nothing about why the genre is missing.

Code
genre_cols <- c(
  set_names(unname(palette.colors(4, palette = "Okabe-Ito"))[2:4], levels(top5$genre)),
  set_names("grey85", outside)
)

ggplot(top5, aes(x = list, stratum = position, alluvium = album, y = 1, fill = genre)) +
  geom_alluvium(alpha = 0.6, width = 1/4, colour = "white", linewidth = 0.3) +
  geom_stratum(aes(fill = position), width = 1/4, colour = "white") +
  geom_text(stat = "alluvium", hjust = 1, nudge_x = -0.16, size = 3.4, na.rm = TRUE,
            aes(label = after_stat(if_else(x == 1, as.character(alluvium), NA)))) +
  geom_text(stat = "alluvium", hjust = 0, nudge_x = 0.16, size = 3.4, na.rm = TRUE,
            aes(label = after_stat(if_else(x == 3, as.character(alluvium), NA)))) +
  scale_x_discrete(expand = expansion(add = c(1.1, 1.1))) +
  scale_fill_manual(values = genre_cols, breaks = names(genre_cols)) +
  labs(
    title = "Genre of Rolling Stone's top five albums, 2003–2020",
    subtitle = "Each band is one album.\nAlbums with no genre in the data are labelled Rock/Pop (assumed).",
    x = "List", y = NULL, fill = NULL
  ) +
  theme_minimal(base_size = 13) +
  theme(axis.text.y = element_blank(), panel.grid = element_blank(),
        legend.position = "bottom") +
  guides(fill = guide_legend(nrow = 2))
Alluvial chart of the nine albums that were ever in a Rolling Stone top five, across the 2003, 2012 and 2020 lists. The 2003 and 2012 top fives are identical: four assumed Rock/Pop albums and Highway 61 Revisited. In 2020 Revolver, Rubber Soul, Sgt. Pepper's and Highway 61 Revisited flow out, and Blue, Abbey Road, Songs in the Key of Life and What's Going On flow in.
Figure 2

Looking at the render:

  • Nothing moves between 2003 and 2012. The two lists have the same top five, so the left half of the chart is flat bands. All the change happens in 2020, the list with the new panel of voters.
  • In 2020 the assumed Rock/Pop albums fall from 4 of the top five to 2, and Soul/Gospel/R&B goes from none to 2. Sgt. Pepper’s falls to 24, Revolver to 11 and Rubber Soul to 35. Abbey Road rises into the top five. The Rock/Pop assumption decides how that shift looks: label those albums differently and the story changes.
  • “Outside top 5” is not “dropped off the list”. All 9 albums are on all three lists. The grey block hides how far each one fell or rose. A slope chart of rank would show it, and this layout cannot.
  • The first render had the labels on the wrong ribbons. It used geom_flow() for the bands and stat = "alluvium" for the labels. The two stats sort albums differently inside a block, so in 2003 the “Blue” label sat on an orange ribbon. The code gave no warning; only looking at the render caught it. Drawing each album as one continuous geom_alluvium() puts the bands and labels in the same order.
  • With only nine albums, every band is one album (y = 1). The chart is closer to a bump chart drawn as ribbons than to a Sankey of quantities.

Decisions and pain points so far

  • No sales week exists. The brief asked for sales, but only the 48-row Beyoncé and Taylor Swift table has sales figures. Every candidate uses a proxy, and the choice of proxy (critics, charts or streams) shapes the question before any mapping is made.
  • No licence is stated for any shortlisted week. These weeks are fine as live teaching material, but derived data should not be republished on this page until the source has been checked.
  • Missing means “not on the list”. Treating a missing rank as zero, or dropping those rows silently, would change what any cross-list figure says. Spec 01 avoids the problem by filtering to the 2020 list, which also means it can’t say anything about the albums that dropped off.
  • differential is derived and inconsistent (a missing rank is counted as 501). Any figure about change between lists should recompute it.

What the data can’t say

These are critics’ rankings, not sales or listening figures. For 2020 Rolling Stone polled a new panel of voters, so a change in an album’s rank mixes changing taste with a change of voters. Even a clear rise in recent albums would not show that tastes had changed. The Billboard and Spotify columns are US-centric, and Spotify popularity is a single snapshot from when the data was collected. A “500 greatest” list is also censored at the top: it says nothing about the albums ranked 501st and below, and the 29 albums that appear only in 2003 simply drop out of a 2020-only figure.