For this project, I decided to explore different aspects of successful songs on the Billboard Hot 100 chart. My dataset is from Chris Dalla Riva and was compiled for the book “Uncharted Territory: What Numbers Tell Us about the Biggest Hit Songs and Ourselves.” It contains information on all songs that reached #1 on the Billboard Hot 100 chart between August 4, 1958 and January 11, 2025. The dataset contains extensive information for each individual song that made it to #1. It includes basic information like song title and artist but also advanced information such as BPM, song structure, and songwriter demographics. The variables relevant for my analysis are: weeks in the #1 position (this included nonconsecutive weeks), parent label, musical key, and overall rating. Three judges gave their subjective rating of a song on a scale from 1 to 10, and the mean of these scores was the song’s overall rating.
For my analysis, I did not look at each song individually but instead grouped songs either by parent label or musical key and then investigated the weeks at #1 and overall rating between the groups using treemaps. By nature of using treemaps to visualize the data, I also explored which parent labels and musical keys had the most #1 songs.
Setting up R
# Using the library function to load tidyverse and treemap. Using the read_csv function to load my data as a tibble. library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.2.1 ✔ readr 2.2.0
✔ forcats 1.0.1 ✔ stringr 1.6.0
✔ ggplot2 4.0.3 ✔ tibble 3.3.1
✔ lubridate 1.9.5 ✔ tidyr 1.3.2
✔ purrr 1.2.2
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(treemap)
Warning: package 'treemap' was built under R version 4.6.1
billboard <- readr::read_csv("billboard.csv")
Rows: 1177 Columns: 105
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (33): Song, Artist, Date, Label, Parent Label, CDR Genre, CDR Style, Dis...
dbl (72): Weeks at Number One, Non-Consecutive, Rating 1, Rating 2, Rating 3...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Cleaning and Exploring the Data
# Storing my clean data under a new tibble ("clean_billboard").clean_billboard <- billboard %>%# Renaming some columns that have spaces or capital letters so that they are easier to work with.rename(parent_label=`Parent Label`) %>%rename(simple_key=`Simplified Key`) %>%# Filtering out any songs with a ";" charater in their "Parent Label" because those are unique collabrations with too few datapoints to be of interest. filter(!grepl(";", parent_label))
# Grouping my clean data by parent label for exploration. label_billboard <- clean_billboard %>%group_by(parent_label) %>%# Using the summarize function to make columns for the number of songs each label had, the mean number of weeks the label's songs spent at number one, and the mean overall rating of each label's songs. Note that I am choosing to use column names that include spaces and capital letters so that the treemaps have clear labels. summarize(`Number of Hit Songs`=n(), `Mean Number of Weeks at Number One`=mean(`Weeks at Number One`), `Mean Overall Rating`=mean(`Overall Rating`)) %>%# Using the slice_max function to only look at the top 20 labels by number of songs, since there are many labels that only have a small number of songs. slice_max(order_by=`Number of Hit Songs`, n=20)label_billboard
# A tibble: 20 × 4
parent_label `Number of Hit Songs` `Mean Number of Weeks at Number One`
<chr> <int> <dbl>
1 Warner Bros. 136 2.62
2 Sony 135 3.81
3 Vivendi 119 4.16
4 EMI 85 2.46
5 CBS 81 2.63
6 BMG 63 3.73
7 Motown 53 2.13
8 Polygram 53 3
9 RCA 38 2.45
10 MCA 34 2.38
11 TimeWarner 26 3
12 Universal 24 2.67
13 ABC 20 2.4
14 A&M 17 2.53
15 RSO 17 2.71
16 MGM 14 2.5
17 Apple 13 2.54
18 Chrysalis 11 2.09
19 Virgin 11 2.55
20 Atlantic 10 2.7
# ℹ 1 more variable: `Mean Overall Rating` <dbl>
# Grouping my clean data by musical key for exploration. keys_billboard <- clean_billboard %>%group_by(simple_key) %>%# Using the summarize function to make columns for the number of songs each key had, the mean number of weeks the key's songs spent at number one, and the mean overall rating of each key's songs. Note that I am choosing to use column names that include spaces and capital letters so that the treemaps have clear labels.summarize(`Number of Hit Songs`=n(), `Mean Number of Weeks at Number One`=mean(`Weeks at Number One`), `Mean Overall Rating`=mean(`Overall Rating`))keys_billboard
# A tibble: 25 × 4
simple_key `Number of Hit Songs` Mean Number of Weeks…¹ `Mean Overall Rating`
<chr> <int> <dbl> <dbl>
1 A 67 2.69 6.04
2 Ab 49 2.88 6.18
3 Abm 20 4 6.40
4 Am 39 3.21 6.03
5 B 29 2.66 6.26
6 Bb 48 2.73 5.81
7 Bbm 27 3.37 6.15
8 Bm 25 2.84 6.05
9 C 120 2.61 6.12
10 Cm 31 2.97 6.05
# ℹ 15 more rows
# ℹ abbreviated name: ¹`Mean Number of Weeks at Number One`
Making Treemaps
# Making a treemap grouped by label.label_weeks_treemap <-treemap( label_billboard,index="parent_label",# The size of the boxes will be determined by how many songs each label had.vSize="Number of Hit Songs",# The color of the boxes will be determined the mean weeks at number one for each label.vColor="Mean Number of Weeks at Number One",type="manual",palette="PRGn",title ="Billboard Hot 100 #1 Songs by Parent Label and Mean Weeks at Number One")
Data on all Billboard Hot 100 #1 songs between 08/4/1958 and 01/11/2025 from Chris Dalla Riva
# Making a treemap grouped by label.label_rating_treemap <-treemap( label_billboard,index="parent_label",# The size of the boxes will be determined by how many songs each label had.vSize="Number of Hit Songs",# The color of the boxes will be determined the mean overall rating for each label.vColor="Mean Overall Rating",type="manual",palette="PRGn",title ="Billboard Hot 100 #1 Songs by Parent Label and Mean Overall Rating")
Data on all Billboard Hot 100 #1 songs between 08/4/1958 and 01/11/2025 from Chris Dalla Riva
# Making a treemap grouped by musical key.key_weeks_treemap <-treemap( keys_billboard,index="simple_key",# The size of the boxes will be determined by how many songs each key had.vSize="Number of Hit Songs",# The color of the boxes will be determined the mean weeks at number one for each key.vColor="Mean Number of Weeks at Number One",type="manual",palette="PRGn",title ="Billboard Hot 100 #1 Songs by Musical Key and Mean Weeks at Number One")
Data on all Billboard Hot 100 #1 songs between 08/4/1958 and 01/11/2025 from Chris Dalla Riva
# Making a treemap grouped by musical key.key_rating_treemap <-treemap( keys_billboard,index="simple_key",# The size of the boxes will be determined by how many songs each key had.vSize="Number of Hit Songs",# The color of the boxes will be determined the mean overall rating for each key.vColor="Mean Overall Rating",type="manual",palette="PRGn",title ="Billboard Hot 100 #1 Songs by Musical Key and Mean Overall Rating")
Data on all Billboard Hot 100 #1 songs between 08/4/1958 and 01/11/2025 from Chris Dalla Riva
Conclusion
Cleaning this dataset was relatively straightforward. I renamed some columns to remove spaces and capital letters and used the grepl function to filter out songs with multiple labels by filtering out the “;” character. I then grouped by label and key (separately) and used the summarize function to create new columns for number of songs, mean weeks at number one, and mean overall rating for each label/key. Because there were so many labels with a small number of songs, I decided to use the slice_max function when I grouped by label to take only the top 20 labels by number of songs.
My treemaps grouped by label show several major leaders by number of hit songs: Warner Bros, Sony, Vivendi, and EMI. Quite a few of the slightly smaller labels have about the same number of #1 songs. The treemap colored by weeks at number one shows that only a three labels exceeded the middle of the weeks at number one range (3 weeks): Sony, Vivendi, and BMG. These three are some of the labels with the most hit songs. The remaining large labels did about the same as the smaller labels: they all had an average of 2-3 weeks at #1. The treemap colored by overall rating shows that most of the large labels had mean overall ratings in the middle of the data range. The lowest and highest mean ratings were reserved for the smaller labels.
My treemaps grouped by musical key show a slow taper from keys with the most to the fewest #1 songs. There are clear leaders: C, G, F, and D (and multiple keys in the same song), but the remaining keys slowly get a smaller and smaller share of the #1 songs, instead of the dramatic difference in label size seen in the previous two treemaps. The treemap colored by weeks at number one shows that the low and middle of the data range are evenly distributed across the different keys. However, the keys at the high end of the data range, with 3.6-4 weeks at number 1, are all uncommon keys with some of the fewest #1 songs. The treemap colored by overall rating also shows the middle of the data range evenly distributed across the different keys. However, both extreme ends of the data range (5.4-5.6 and 6.6-6.8) are only seen among the uncommon keys.
I was also interested in making treemaps grouped by number of band members or colored by lead singer age, but I did not have time to do so for this project.