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
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)
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")
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())
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