Overview

This notebook reproduces the full analysis behind the project: whether profanity in Hip-Hop/Rap lyrics has increased over time, whether the “explicit” label is disproportionately applied to the genre, and whether swearing more is associated with more commercial success.

Data: MusicOSet (Silva et al., 2019) and the badwords profanity lexicon. Songs are filtered to those released 1990-2018.

prepared <- load_and_prepare_data()
songs_master <- prepared$songs_master
bad_words_list <- prepared$bad_words_list

nrow(songs_master)
## [1] 13841

Finding 1: Profanity in Hip-Hop/Rap has risen sharply since 1990

trend_hiphop <- songs_master %>%
  filter(!is.na(release_year) & genre_group == "Hip-Hop/Rap") %>%
  group_by(release_year) %>%
  summarise(avg_profanity = mean(profanity_count, na.rm = TRUE))

trend_other <- songs_master %>%
  filter(!is.na(release_year) & genre_group == "Other") %>%
  group_by(release_year) %>%
  summarise(avg_profanity = mean(profanity_count, na.rm = TRUE))

peak_point <- trend_hiphop %>% filter(avg_profanity == max(avg_profanity))

ggplot(trend_hiphop, aes(x = release_year, y = avg_profanity)) +
  geom_area(fill = "#E74C3C", alpha = 0.1) +
  geom_line(color = "#C0392B", size = 1) +
  geom_point(data = peak_point, size = 3, color = "#C0392B") +
  geom_text(data = peak_point,
            aes(label = paste0(round(avg_profanity, 1), " words (", release_year, ")")),
            vjust = -0.8, hjust = 1, nudge_x = -0.3,
            size = 3.5, fontface = "bold", color = "#C0392B") +
  scale_x_continuous(breaks = seq(1990, 2018, by = 4),
                      expand = expansion(mult = c(0.02, 0.08))) +
  labs(
    title = "The rise of profanity in Hip-Hop/Rap",
    subtitle = "Yearly average of explicit words per song",
    x = NULL, y = "Avg. profanity count"
  ) +
  theme_minimal(base_size = 13)

Line chart of yearly average profanity count in Hip-Hop/Rap lyrics, rising from about 1 word per song in 1990 to a peak of 14.7 words per song in 2018.

Hip-Hop/Rap profanity density rose from roughly 1 word/song around 1990 to 14.7 words/song by 2018. The comparison genres (Other) stayed essentially flat over the same period:

trend_both <- bind_rows(
  trend_hiphop %>% mutate(genre_group = "Hip-Hop/Rap"),
  trend_other  %>% mutate(genre_group = "Other")
)

# Direct end-of-line labels, so the two series aren't distinguished by colour alone (WCAG 1.4.1)
trend_end_labels <- trend_both %>%
  group_by(genre_group) %>%
  filter(release_year == max(release_year))

ggplot(trend_both, aes(x = release_year, y = avg_profanity, color = genre_group, linetype = genre_group)) +
  geom_line(size = 1) +
  geom_text_repel(data = trend_end_labels, aes(label = genre_group),
                   hjust = 0, nudge_x = 1, direction = "y", segment.color = NA,
                   fontface = "bold", size = 4, show.legend = FALSE) +
  scale_color_manual(values = c("Hip-Hop/Rap" = "firebrick", "Other" = "#4682B4")) +
  scale_linetype_manual(values = c("Hip-Hop/Rap" = "solid", "Other" = "dashed")) +
  scale_x_continuous(breaks = seq(1990, 2018, by = 4),
                      expand = expansion(mult = c(0.02, 0.16))) +
  labs(title = "Hip-Hop/Rap vs. other genres", x = "Release year", y = "Avg. profanity count") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

Line chart comparing Hip-Hop/Rap against other genres from 1990 to 2018. The Hip-Hop/Rap line (solid) climbs steadily while the Other genres line (dashed) stays close to flat throughout.

Finding 2: Hip-Hop/Rap accounts for the large majority of “explicit” tags

high_contrast_colors <- c("Hip-Hop/Rap" = "#800000", "Other" = "#003366")

songs_master %>%
  filter(!is.na(explicit)) %>%
  mutate(content_rating = if_else(explicit == TRUE, "Explicit Content", "Clean / Non-Explicit")) %>%
  count(content_rating, genre_group) %>%
  group_by(content_rating) %>%
  mutate(percentage = n / sum(n)) %>%
  ggplot(aes(x = content_rating, y = percentage, fill = genre_group)) +
  geom_col(position = "fill", width = .6, color = "white", linewidth = 1.2) +
  geom_text(aes(label = paste0(genre_group, "\n", percent(percentage, accuracy = 1))),
            position = position_fill(vjust = 0.5), color = "white", fontface = "bold", size = 4) +
  scale_y_continuous(labels = percent) +
  scale_fill_manual(values = high_contrast_colors) +
  labs(title = "Defining the 'Explicit' category", x = NULL, y = "Proportion of tracks") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

Stacked bar chart comparing clean versus explicit-tagged tracks by genre. Hip-Hop/Rap makes up 88 percent of all explicit tracks, versus a much smaller share of clean tracks.

88% of all tracks tagged “explicit” in the dataset are Hip-Hop/Rap, despite the genre being a much smaller share of the overall catalogue.

Finding 3: Profanity is weakly negatively correlated with success

hiphop_data <- songs_master %>%
  filter(genre_group == "Hip-Hop/Rap", !is.na(total_success_score), !is.na(profanity_count))

other_data <- songs_master %>%
  filter(genre_group == "Other", !is.na(total_success_score), !is.na(profanity_count))

cor_hiphop <- cor.test(hiphop_data$profanity_count, hiphop_data$total_success_score)
cor_other  <- cor.test(other_data$profanity_count, other_data$total_success_score)

tibble(
  genre_group = c("Hip-Hop/Rap", "Other"),
  pearson_r = c(round(cor_hiphop$estimate, 3), round(cor_other$estimate, 3)),
  p_value = c(signif(cor_hiphop$p.value, 3), signif(cor_other$p.value, 3))
)
## # A tibble: 2 × 3
##   genre_group pearson_r  p_value
##   <chr>           <dbl>    <dbl>
## 1 Hip-Hop/Rap    -0.134 5.61e-14
## 2 Other          -0.01  3.14e- 1
ggplot(hiphop_data, aes(x = profanity_count, y = total_success_score)) +
  geom_jitter(alpha = 0.4, color = "midnightblue", width = 0.2) +
  geom_smooth(method = "lm", formula = 'y ~ x', color = "firebrick") +
  labs(
    title = "Hip-Hop/Rap: profanity vs. success",
    subtitle = paste0("Pearson's r = ", round(cor_hiphop$estimate, 3)),
    x = "Explicit terms", y = "Popularity score"
  ) +
  theme_minimal(base_size = 13)

Scatter plot of profanity count against popularity score for Hip-Hop/Rap songs, with a slightly downward-sloping trend line showing a weak negative correlation, Pearsons r equals negative 0.134.

Within Hip-Hop/Rap, profanity count is weakly negatively correlated with commercial success (r = -0.134). Other genres show essentially no relationship (r ≈ -0.01). The “shock value sells” hypothesis is not supported by this dataset.

Finding 4: Most common profanity, by root word

songs_master %>%
  filter(genre_group == "Hip-Hop/Rap") %>%
  select(song_id, lyrics) %>%
  unnest_tokens(word, lyrics) %>%
  filter(word %in% bad_words_list, word != "bum") %>%
  group_profanity_words() %>%
  mutate(word_display = str_replace_all(word_root, "[aeiou]", "*")) %>%
  count(word_display, sort = TRUE) %>%
  slice_max(n, n = 10) %>%
  ggplot(aes(x = reorder(word_display, n), y = n)) +
  geom_col(fill = "#800000", width = 0.7) +
  geom_text(aes(label = n), hjust = -0.2, size = 3.5) +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  labs(title = "Top 10 most frequent profane word roots", x = NULL, y = NULL) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_blank())

Horizontal bar chart of the ten most frequent profane word roots in Hip-Hop/Rap lyrics, each partially masked with asterisks, ranked by frequency count from about 500 to over 6000 mentions.

Session info

sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## time zone: Asia/Bangkok
## tzcode source: internal
## 
## attached base packages:
## [1] grid      stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] scales_1.4.0    patchwork_1.3.2 gridExtra_2.3   ggrepel_0.9.6  
##  [5] tidytext_0.4.3  lubridate_1.9.4 forcats_1.0.1   stringr_1.6.0  
##  [9] dplyr_1.1.4     purrr_1.2.0     readr_2.1.6     tidyr_1.3.2    
## [13] tibble_3.3.0    ggplot2_4.0.1   tidyverse_2.0.0
## 
## loaded via a namespace (and not attached):
##  [1] janeaustenr_1.0.0  utf8_1.2.6         sass_0.4.10        generics_0.1.4    
##  [5] stringi_1.8.7      lattice_0.22-7     hms_1.1.4          digest_0.6.39     
##  [9] magrittr_2.0.4     evaluate_1.0.5     timechange_0.3.0   RColorBrewer_1.1-3
## [13] fastmap_1.2.0      jsonlite_2.0.0     Matrix_1.7-4       mgcv_1.9-4        
## [17] jquerylib_0.1.4    cli_3.6.5          crayon_1.5.3       rlang_1.1.6       
## [21] tokenizers_0.3.0   splines_4.5.2      bit64_4.6.0-1      withr_3.0.2       
## [25] cachem_1.1.0       yaml_2.3.12        parallel_4.5.2     tools_4.5.2       
## [29] tzdb_0.5.0         vctrs_0.6.5        R6_2.6.1           lifecycle_1.0.4   
## [33] bit_4.6.0          vroom_1.6.7        pkgconfig_2.0.3    pillar_1.11.1     
## [37] bslib_0.9.0        gtable_0.3.6       glue_1.8.0         Rcpp_1.1.0        
## [41] xfun_0.55          tidyselect_1.2.1   rstudioapi_0.17.1  knitr_1.51        
## [45] farver_2.1.2       nlme_3.1-168       htmltools_0.5.9    SnowballC_0.7.1   
## [49] labeling_0.4.3     rmarkdown_2.30     compiler_4.5.2     S7_0.2.1