Resumo

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).


Introdução

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:

  1. Pré-processamento dos dados.
  2. Clusterização com K-Means.
  3. Redução de dimensionalidade via PCA.
  4. Integração opcional com Billboard para análise de hits.

1) Carregamento e Pré-processamento

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]

2) Clusterização de Músicas (K-Means)

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

3) Redução de Dimensionalidade (PCA)

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


4) Integração com Billboard (Opcional)

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

Conclusão