Este relatório apresenta a aplicação de técnicas de redução de dimensionalidade e clusterização sobre dados do conjunto MillionSong, com integração opcional ao dataset Billboard Rank. O objetivo é identificar padrões de áudio que diferenciem músicas em grupos distintos e avaliar se tais características estão associadas ao sucesso comercial (hit).
O estudo de músicas a partir de características de áudio permite
compreender tendências, padrões de consumo e fatores associados ao
sucesso.
Neste trabalho, seguimos as seguintes etapas:
Nesta etapa:
library(tidyverse)
library(data.table)
library(cluster)
library(factoextra)
library(GGally)
set.seed(42)
million_path <- "MillionSong.csv"
billboard_path <- "billboard_rank.csv"
ms_raw <- fread(million_path, sep = ",", encoding = "UTF-8", quote = "")
audio_cols <- c("Danceability","Duration","Tempo","Energy","Loudness",
"KeySignatureConfidence","TimeSignature","TimeSignatureConfidence",
"end_of_fade_in","key","keyConfidence","mode","mode_confidence","start_of_fade_out")
audio_cols <- intersect(audio_cols, names(ms_raw))
ms_num <- ms_raw %>% select(all_of(c("Title","ArtistName","Year", audio_cols))) %>% drop_na()
sds <- sapply(ms_num %>% select(all_of(audio_cols)), sd, na.rm = TRUE)
audio_cols <- setdiff(audio_cols, names(sds[sds == 0 | is.na(sds)]))
ms_num <- ms_num %>% select(all_of(c("Title","ArtistName","Year", audio_cols)))
ms_features <- ms_num %>% select(all_of(audio_cols)) %>% as.data.frame()
ms_scaled <- scale(ms_features)
is_finite_row <- apply(ms_scaled, 1, function(r) all(is.finite(r)))
ms_scaled <- ms_scaled[is_finite_row, , drop = FALSE]
ms_num <- ms_num[is_finite_row, , drop = FALSE]
O K-Means é usado para agrupar músicas com base em
suas características normalizadas.
Para definir o número de clusters:
fviz_nbclust(ms_scaled, kmeans, method = "wss", k.max = 10) +
ggtitle("Método do Cotovelo (WSS)")
## Warning: did not converge in 10 iterations
fviz_nbclust(ms_scaled, kmeans, method = "silhouette", k.max = 10) +
ggtitle("Coeficiente de Silhueta")
sil_values <- sapply(2:10, function(k){
km <- kmeans(ms_scaled, centers = k, nstart = 10)
ss <- silhouette(km$cluster, dist(ms_scaled))
mean(ss[, 3])
})
## Warning: did not converge in 10 iterations
## Warning: Quick-TRANSfer stage steps exceeded maximum (= 500050)
k_star <- which.max(sil_values) + 1
km_fit <- kmeans(ms_scaled, centers = k_star, nstart = 25)
## Warning: Quick-TRANSfer stage steps exceeded maximum (= 500050)
## Warning: Quick-TRANSfer stage steps exceeded maximum (= 500050)
ms_num$cluster <- factor(km_fit$cluster)
ms_num %>% group_by(cluster) %>% summarise(across(all_of(audio_cols), mean, na.rm = TRUE))
## Warning: There was 1 warning in `summarise()`.
## ℹ In argument: `across(all_of(audio_cols), mean, na.rm = TRUE)`.
## ℹ In group 1: `cluster = 1`.
## Caused by warning:
## ! The `...` argument of `across()` is deprecated as of dplyr 1.1.0.
## Supply arguments directly to `.fns` through an anonymous function instead.
##
## # Previously
## across(a:b, mean, na.rm = TRUE)
##
## # Now
## across(a:b, \(x) mean(x, na.rm = TRUE))
A Análise de Componentes Principais reduz a
dimensionalidade preservando a maior variância dos dados.
Interpretamos o PC1 observando as cargas mais altas e
mais baixas.
pca_fit <- prcomp(ms_scaled, center = FALSE, scale. = FALSE)
var_explained <- (pca_fit$sdev^2) / sum(pca_fit$sdev^2)
cumsum(var_explained)[1:5]
## [1] 0.2304163 0.4057352 0.5140983 0.6163331 0.7088633
loadings <- as.data.frame(pca_fit$rotation)
head(loadings[order(-loadings$PC1), , drop=FALSE], 3)
tail(loadings[order(-loadings$PC1), , drop=FALSE], 3)
scores <- as.data.frame(pca_fit$x[, 1:2])
colnames(scores) <- c("PC1","PC2")
scores <- bind_cols(ms_num %>% select(Title, ArtistName, Year, cluster), scores)
ggplot(scores, aes(x = PC1, y = PC2, color = cluster)) +
geom_point(alpha = 0.6) +
theme_minimal() +
ggtitle("Dispersão PC1 vs PC2 por cluster")
Podemos unir com billboard_rank.csv e marcar as músicas de sucesso (hit=1).
bb <- fread(billboard_path, sep = ",", encoding = "UTF-8", quote = "") %>%
mutate(Title = tolower(Title), Year = as.integer(Year))
scores_hit <- scores %>%
mutate(Title_join = tolower(Title)) %>%
left_join(bb %>% select(Title, Year, hit) %>% rename(Title_join = Title),
by = c("Title_join","Year"))
scores_hit$hit <- ifelse(is.na(scores_hit$hit), 0, scores_hit$hit)
ggplot(scores_hit, aes(x = PC1, y = PC2, shape = factor(hit))) +
geom_point(alpha = 0.6) +
scale_shape_discrete(name = "Hit", labels = c("0 = não-hit","1 = hit")) +
theme_minimal() +
ggtitle("PC1 vs PC2 — separação por Hit (Billboard)")