Unidad 6 - Práctica: la fábrica de playlists

Aplicación guiada en clase · Semana 13 · 06278-ECO

Autor/a

PhD. Eduard F. Martínez-González

1 Reglas de juego

Hoy nadie tiene la respuesta correcta — ni siquiera yo

Tres semanas prediciendo con hoja de respuestas; hoy se acabaron. Vas a descubrir grupos que nadie ha etiquetado y a defenderlos con perfilamiento. Esta práctica asume la teoría de la semana (k-means, escalamiento, codo, silueta) — nada se re-explica. El dplyr y el ggplot2 de la semana 4 hoy trabajan tiempo completo.

  • 👥 En parejas: uno escribe, el otro audita cifras y lee los perfiles en voz alta. Roten en el Momento 4.
  • 🖥️ RStudio con tu proyecto del curso. Paquetes: dplyr, ggplot2 y clusterninguno es nuevo; cluster viene instalado con R.
  • 📥 El dato de hoy: spotify_canciones.csv — 320 canciones del catálogo urbano de una plataforma de streaming, con sus características de audio medidas por el sistema. Guárdalo en data/.
  • 🎯 La misión de la gerencia de producto: “Quiero listas de reproducción automáticas con personalidad: ‘para el gimnasio’, ‘tarde tranquila’… Y no me sirve agrupar por el género que declara la disquera — eso es marketing. Agrupen por cómo suenan.”

2 El setup ≈ 5 min

Script nuevo: scripts/13_playlists.R.

## ============================================
## Práctica semana 13 — La fábrica de playlists
## Nombre(s): _______________
## Fecha:     _______________
## ============================================

## Librerías / Libraries
pacman::p_load(dplyr, ggplot2, cluster)

## Cargar datos / Load data
canciones <- read.csv("data/spotify_canciones.csv")

## Radiografía / First look
dim(canciones)
str(canciones)

Checkpoint 🎯: 320 filas y 8 columnas. Tres columnas de identidad (cancion, artista, genero) y cinco numéricas: cuatro características de audiodanceability (qué tan bailable, 0–1), energy (intensidad, 0–1), acousticness (probabilidad de ser acústica, 0–1), tempo (velocidad en BPM) — y streams (reproducciones).

3 Momento 1 — Elegir las variables del sonido ≈ 5 min

El pipeline de la teoría, paso 2: la pregunta decide las variables. La misión dice “agrupar por cómo suenan” — entonces entran las cuatro variables de audio y quedan fuera dos tentaciones:

  • streams mide popularidad, no estilo: una canción de gimnasio y un bolero pueden tener los mismos streams.
  • genero es justamente la etiqueta declarada que la gerencia no se cree — la guardamos para el final, como control de calidad.
## Variables de clustering / Clustering features
audio <- select(.data = canciones,
                danceability, energy, acousticness, tempo)

head(audio, 3)

## Las escalas de cada variable / Each variable's scale
round(sapply(audio, sd), 3)

Checkpoint 🎯: las desviaciones estándar: danceability 0.183, energy 0.234, acousticness 0.281… y tempo 20.239. Una de estas variables no es como las otras — habla en BPM mientras las demás susurran entre 0 y 1. Guarda ese dato: es la bomba del Momento 2.

4 Momento 2 — El experimento del ingenuo ≈ 8 min

La teoría advierte que escalar es obligatorio. Pero una advertencia no duele — un experimento sí. Corramos k-means sin escalar, como lo haría alguien de afán, y perfilemos lo que sale:

## k-means SIN escalar (a propósito) / k-means on raw features (on purpose)
set.seed(13)   # ¡en la línea inmediatamente anterior a kmeans()!
km_crudo <- kmeans(audio, centers = 4, nstart = 25)

## Perfil de los grupos resultantes / Profile the raw clusters
canciones_crudo <- mutate(.data = canciones, cluster = factor(km_crudo$cluster))

perfil_crudo <- summarise(.data = canciones_crudo,
                          danceability = mean(danceability),
                          energy       = mean(energy),
                          acousticness = mean(acousticness),
                          tempo        = mean(tempo),
                          n_canciones  = n(),
                          .by = cluster)
arrange(perfil_crudo, cluster)

Checkpoint 🎯: cuatro grupos con tempos 88.6 / 101.0 / 75.3 / 130.4 — franjas de BPM perfectamente ordenadas. Ahora mira el resto de la tabla: los clusters 1 y 3 son casi la misma canción en todo lo demás (danceability 0.60 vs 0.58, energy 0.50 vs 0.50)… separados solo por velocidad. ¿Y la lista acústica que pedía la gerencia? No existe: el acousticness más alto de la tabla es 0.44 — las canciones de guitarra quedaron regadas entre las franjas de tempo.

Advertencia

Qué acaba de pasar: la distancia la secuestró el tempo. Su desviación (20.2) es ~100 veces la de las variables 0–1, así que para el algoritmo una diferencia de 5 BPM pesa más que la diferencia entre una canción 100% acústica y una 100% electrónica. El clustering refleja unidades, no sonido — exactamente el error #1 de la teoría, y acabas de verlo con tus propios datos.

5 Momento 3 — La misma cancha para todos ≈ 4 min

## Escalar: media 0, desviación 1 / Scale to mean 0, sd 1
audio_esc <- scale(audio)

## Verificar / Verify
round(colMeans(audio_esc), 3)
round(apply(audio_esc, 2, sd), 3)

Checkpoint 🎯: medias 0, 0, 0, 0 y desviaciones 1, 1, 1, 1. Las cuatro variables ahora pesan igual en la distancia. (Nota: scale() devuelve una matriz, no un data frame — para kmeans() da lo mismo.)

6 Momento 4 — ¿Cuántas listas? El codo y la silueta ≈ 12 min

Nadie sabe cuántos “sonidos” hay en el catálogo. Corremos k-means para k = 1…8 y dejamos que las dos métricas de la teoría acoten la respuesta.

El codo (WCSS por k):

## WCSS para k = 1..8 / Elbow: WCSS for k = 1..8
set.seed(13)   # una semilla para todo el barrido
wss  <- sapply(1:8, function(k) kmeans(audio_esc, centers = k, nstart = 25)$tot.withinss)
codo <- data.frame(k = 1:8, wss = wss)
codo

## Gráfico del codo / Elbow plot
ggplot(codo, aes(x = k, y = wss)) +
  geom_line(color = "#1F4E79") +
  geom_point(color = "#E87722", size = 3) +
  scale_x_continuous(breaks = 1:8) +
  theme_minimal() +
  labs(title = "Método del codo — catálogo urbano",
       x = "Número de clusters (k)", y = "WCSS")

Checkpoint 🎯: la WCSS cae 1276 → 499 → 255 → 137 … y de ahí en adelante gotea (123, 114, 107, 100). El cuarto cluster compró −118 de compactación; el quinto, apenas −13. El codo se dobla en k = 4.

La silueta (el desempate con máximo):

## Silueta promedio para k = 2..8 / Average silhouette for k = 2..8
distancias <- dist(audio_esc)   # matriz de distancias (se calcula UNA vez)

set.seed(13)
sil <- sapply(2:8, function(k) {
  km <- kmeans(audio_esc, centers = k, nstart = 25)
  mean(silhouette(km$cluster, distancias)[, 3])
})
silueta <- data.frame(k = 2:8, sil_prom = round(sil, 3))
silueta

Checkpoint 🎯: sil_prom sube 0.530 → 0.552 → 0.572 y a partir de k = 5 se derrumba (0.507, 0.414…). Máximo claro en k = 4: las dos métricas votaron lo mismo. La gerencia tendrá cuatro listas — si logramos nombrarlas (Momento 6).

Advertencia

Si tus números no coinciden con los checkpoints, casi seguro corriste un kmeans() sin ejecutar su set.seed(13) justo antes (recuerda: cada arranque es aleatorio; la semilla + nstart = 25 hacen el resultado reproducible). Ejecuta siempre el bloque completo, o corre el script con Source.

7 Momento 5 — Las cuatro listas y su perfil ≈ 12 min

## k-means final con k = 4 / Final k-means
set.seed(13)
km_final <- kmeans(audio_esc, centers = 4, nstart = 25)

## Pegar el cluster al dataset ORIGINAL (sin escalar) / Attach clusters
canciones <- mutate(.data = canciones, cluster = factor(km_final$cluster))
table(canciones$cluster)

Checkpoint 🎯: cuatro grupos de 74 / 70 / 96 / 80 canciones. Ojo al detalle profesional: el cluster se calcula con los datos escalados, pero el perfil se lee con los datos originales — los BPM y los 0–1 son los que el negocio entiende.

## Tabla de perfilamiento / Profiling table
perfil <- summarise(.data = canciones,
                    danceability = mean(danceability),
                    energy       = mean(energy),
                    acousticness = mean(acousticness),
                    tempo        = mean(tempo),
                    streams_prom = mean(streams),
                    n_canciones  = n(),
                    .by = cluster)
arrange(perfil, cluster)

## La canción insignia de cada lista / Flagship song per cluster
top_por_cluster <- summarise(.data = canciones,
                             top_cancion = cancion[which.max(streams)],
                             top_artista = artista[which.max(streams)],
                             max_streams = max(streams),
                             .by = cluster)
arrange(top_por_cluster, cluster)

Checkpoint 🎯: la tabla de perfiles (redondeando):

cluster danceability energy acousticness tempo n
1 0.87 0.88 0.05 130 74
2 0.44 0.29 0.75 90 70
3 0.83 0.77 0.08 98 96
4 0.60 0.53 0.16 77 80

Y las canciones insignia: Alto Voltaje sin Frenos (Anuel AA) en el 1, Brisa y Vinilo (J Balvin) en el 2, Vibra Eterno (Farruko) en el 3 y Frío en el Alma (Maluma) en el 4. Esta vez sí apareció la lista acústica (cluster 2: acousticness 0.75, energy 0.29) — escalar la sacó del escondite.

Ahora los dos gráficos del perfilamiento — separación y dispersión:

## Mapa de los clusters / Cluster scatterplot
ggplot(canciones, aes(x = tempo, y = danceability, color = cluster)) +
  geom_point(alpha = 0.5, size = 2) +
  theme_minimal() +
  labs(title = "El mapa del catálogo: 4 sonidos",
       x = "Tempo (BPM)", y = "Danceability", color = "Cluster")

## Dispersión dentro de cada cluster / Within-cluster spread
ggplot(canciones, aes(x = cluster, y = acousticness, fill = cluster)) +
  geom_boxplot(show.legend = FALSE) +
  theme_minimal() +
  labs(title = "Acousticness por cluster",
       x = "Cluster", y = "Acousticness")

Cómo leer el scatter: muestra solo 2 de las 4 dimensiones del clustering — dos clusters que se “tocan” en este plano pueden estar bien separados en acousticness o energy (por eso el boxplot acompaña). Y recuerda la advertencia de la teoría: el centroide es el promedio de la lista, no una canción real.

8 Momento 6 — Bautizar las listas ≈ 8 min

k-means entregó números; el analista entrega nombres. Este es el paso que la gerencia de producto realmente compra.

En parejas (y luego compartimos): con la tabla de perfiles y las canciones insignia, completen para cada cluster:

Cluster Rasgo dominante del sonido Nombre de la lista ¿Cuándo suena?
1
2
3
4

Criterio de calidad: el nombre debe ser específico y evocar el momento de uso — “Cardio a 130”, “Domingo de guitarra”. Nombres penalizados por la vida real: “Grupo A”, “Lista 2”, “Música variada”. Si un cluster no se deja nombrar, esa es información: sospecha del k.

El producto de la semana — exporta la evidencia como siempre:

## Exportar perfil y mapa / Export deliverables
write.csv(perfil, "output/perfil_playlists.csv", row.names = FALSE)
ggsave("output/mapa_playlists.png", width = 7, height = 4.5)  # guarda el último gráfico

9 El reto (si te sobra tiempo) 🏆

Reto 1 — El careo con la disquera. La columna genero que dejamos en la banca es la etiqueta declarada. Confróntala con el sonido:

table(canciones$cluster, canciones$genero)

¿Los clusters coinciden con el género? Casi: cada lista tiene un género dominante… y decenas de canciones “traicionadas” (por ejemplo, mira cuántas canciones etiquetadas reggaeton suenan a otra cosa). Discutan: ¿por qué la etiqueta comercial y el sonido medido no coinciden? ¿A cuál le cree la playlist automática?

Reto 2 — La ruleta de la inicialización. Corre kmeans(audio_esc, centers = 4, nstart = 1) tres veces sin semilla y compara tot.withinss entre corridas. ¿Cambia? Esa es la lotería de la que nstart = 25 te protege (25 boletos en vez de 1).

10 Cierre

Hoy corriste el pipeline completo de segmentación con acompañamiento: variables → escala → codo/silueta → k-means → perfil → nombres. En el taller lo harás solo, con una vuelta de tuerca: un gimnasio te entrega su base de socios y la segmentación que dejó un practicante — tu primer trabajo será auditarla (pista: el practicante no leyó el Momento 2 de hoy). Misma disciplina de siempre: toda cifra sale de tu script.