# ── Librerías generales ──────────────────────────────────────────────────────
library(tidyverse)
library(caret)
library(class)
library(randomForest)
library(pROC)
library(ggplot2)
library(scales)
library(knitr)
library(kableExtra)
library(corrplot)
library(gridExtra)
library(dplyr)
library(rpart)
library(rpart.plot)

1 PARTE 1: Spotify Tracks Dataset — Modelo KNN


1.1 Contextualización

Las plataformas de streaming musical han transformado la industria discográfica y la experiencia de consumo musical a nivel global. El éxito de estas plataformas depende en gran medida de sus sistemas de recomendación, los cuales deben identificar patrones en las características sonoras de las canciones para conectar al oyente con contenido afín a sus preferencias.

El Spotify Tracks Dataset contiene 114,000 canciones distribuidas en 125 géneros musicales distintos, con características de audio extraídas directamente mediante la API oficial de Spotify. Cada pista está descrita por variables acústicas y estructurales cuantitativas:

Variable Descripción
danceability Aptitud para el baile (tempo, ritmo, beat) — escala 0–1
energy Intensidad y actividad perceptual — escala 0–1
key Tonalidad en notación Pitch Class (0–11)
loudness Volumen promedio en dB
speechiness Presencia de palabras habladas — escala 0–1
acousticness Probabilidad de que la pista sea acústica — escala 0–1
instrumentalness Ausencia de voz — escala 0–1
liveness Presencia de audiencia en vivo — escala 0–1
valence Positividad musical — escala 0–1
tempo Pulsos por minuto (BPM)
popularity Popularidad calculada por Spotify (0–100)

La variable objetivo de clasificación es el género musical (track_genre), que será binarizado o filtrado para construir el problema de clasificación.

1.2 Explicación del Modelo KNN y Justificación

1.2.1 ¿Cómo funciona KNN?

El algoritmo K-Nearest Neighbors (K Vecinos más Cercanos) es un método de aprendizaje supervisado no paramétrico que clasifica una nueva observación buscando los k ejemplos más similares en el conjunto de entrenamiento y asignando la clase más frecuente entre ellos.

El proceso sigue tres pasos fundamentales:

  1. Representación vectorial: cada canción se convierte en un punto en un espacio multidimensional donde cada dimensión corresponde a una característica acústica.
  2. Cálculo de distancias: para clasificar una canción nueva, se calcula su distancia respecto a todos los puntos del conjunto de entrenamiento. La métrica más común es la distancia euclídea:

\[d(x, y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2}\]

  1. Votación mayoritaria: se seleccionan los k vecinos con menor distancia y la clase predominante entre ellos se asigna como predicción.

1.2.2 ¿Por qué KNN para Spotify?

KNN es especialmente adecuado para esta base de datos por las siguientes razones:

  • Espacio vectorial natural: las características acústicas son todas numéricas y continuas, lo que permite calcular distancias significativas entre canciones.
  • Intuición musical: dos canciones “suenan parecidas” cuando sus atributos acústicos son próximos en el espacio vectorial, lo que replica la lógica de los sistemas de recomendación.
  • Sin supuestos distribucionales: KNN no asume ninguna distribución paramétrica, lo que lo hace robusto ante la heterogeneidad de géneros musicales.
  • Efecto del hiperparámetro k: al variar k se puede controlar el balance entre sesgo (k grande) y varianza (k pequeño), lo que permite optimizar el modelo de forma interpretable.

1.3 Hipótesis

H₁ (principal): Es posible clasificar el género musical de una canción a partir de sus características acústicas mediante un modelo KNN con una precisión significativamente superior al azar, dado que canciones del mismo género comparten patrones similares en el espacio de atributos de audio.

H₂ (variables discriminantes): Las variables energy, danceability y valence tendrán mayor capacidad discriminante entre géneros que variables como liveness o key, dado que capturan de forma más robusta las diferencias perceptuales entre estilos musicales.

H₃ (hiperparámetro k): Existe un valor óptimo de k que minimiza el error de clasificación; valores muy pequeños generarán sobreajuste y valores muy grandes producirán subajuste, evidenciando una curva de error en forma de U al variar k.


1.4 Código R — Análisis Spotify con KNN

1.4.1 Carga y Exploración de Datos

# ── Carga del dataset ────────────────────────────────────────────────────────

spotify <- read.csv("C:/Users/USUARIO 1/Downloads/spotify.csv", stringsAsFactors = FALSE)

# Vista general
dim(spotify)
## [1] 114000     21
str(spotify)
## 'data.frame':    114000 obs. of  21 variables:
##  $ X               : int  0 1 2 3 4 5 6 7 8 9 ...
##  $ track_id        : chr  "5SuOikwiRyPMVoIQDJUgSV" "4qPNDBW1i3p13qLCt0Ki3A" "1iJBSr7s7jYXzM8EGcbK5b" "6lfxq3CG4xtTiEg7opyCyx" ...
##  $ artists         : chr  "Gen Hoshino" "Ben Woodward" "Ingrid Michaelson;ZAYN" "Kina Grannis" ...
##  $ album_name      : chr  "Comedy" "Ghost (Acoustic)" "To Begin Again" "Crazy Rich Asians (Original Motion Picture Soundtrack)" ...
##  $ track_name      : chr  "Comedy" "Ghost - Acoustic" "To Begin Again" "Can't Help Falling In Love" ...
##  $ popularity      : int  73 55 57 71 82 58 74 80 74 56 ...
##  $ duration_ms     : int  230666 149610 210826 201933 198853 214240 229400 242946 189613 205594 ...
##  $ explicit        : chr  "False" "False" "False" "False" ...
##  $ danceability    : num  0.676 0.42 0.438 0.266 0.618 0.688 0.407 0.703 0.625 0.442 ...
##  $ energy          : num  0.461 0.166 0.359 0.0596 0.443 0.481 0.147 0.444 0.414 0.632 ...
##  $ key             : int  1 1 0 0 2 6 2 11 0 1 ...
##  $ loudness        : num  -6.75 -17.23 -9.73 -18.52 -9.68 ...
##  $ mode            : int  0 1 1 1 1 1 1 1 1 1 ...
##  $ speechiness     : num  0.143 0.0763 0.0557 0.0363 0.0526 0.105 0.0355 0.0417 0.0369 0.0295 ...
##  $ acousticness    : num  0.0322 0.924 0.21 0.905 0.469 0.289 0.857 0.559 0.294 0.426 ...
##  $ instrumentalness: num  1.01e-06 5.56e-06 0.00 7.07e-05 0.00 0.00 2.89e-06 0.00 0.00 4.19e-03 ...
##  $ liveness        : num  0.358 0.101 0.117 0.132 0.0829 0.189 0.0913 0.0973 0.151 0.0735 ...
##  $ valence         : num  0.715 0.267 0.12 0.143 0.167 0.666 0.0765 0.712 0.669 0.196 ...
##  $ tempo           : num  87.9 77.5 76.3 181.7 119.9 ...
##  $ time_signature  : int  4 4 4 3 4 4 3 4 4 4 ...
##  $ track_genre     : chr  "acoustic" "acoustic" "acoustic" "acoustic" ...
# ── Resumen estadístico ──────────────────────────────────────────────────────
summary(spotify[, c("danceability", "energy", "loudness",
                    "speechiness", "acousticness", "valence",
                    "tempo", "popularity")])
##   danceability        energy          loudness        speechiness     
##  Min.   :0.0000   Min.   :0.0000   Min.   :-49.531   Min.   :0.00000  
##  1st Qu.:0.4560   1st Qu.:0.4720   1st Qu.:-10.013   1st Qu.:0.03590  
##  Median :0.5800   Median :0.6850   Median : -7.004   Median :0.04890  
##  Mean   :0.5668   Mean   :0.6414   Mean   : -8.259   Mean   :0.08465  
##  3rd Qu.:0.6950   3rd Qu.:0.8540   3rd Qu.: -5.003   3rd Qu.:0.08450  
##  Max.   :0.9850   Max.   :1.0000   Max.   :  4.532   Max.   :0.96500  
##   acousticness       valence           tempo          popularity    
##  Min.   :0.0000   Min.   :0.0000   Min.   :  0.00   Min.   :  0.00  
##  1st Qu.:0.0169   1st Qu.:0.2600   1st Qu.: 99.22   1st Qu.: 17.00  
##  Median :0.1690   Median :0.4640   Median :122.02   Median : 35.00  
##  Mean   :0.3149   Mean   :0.4741   Mean   :122.15   Mean   : 33.24  
##  3rd Qu.:0.5980   3rd Qu.:0.6830   3rd Qu.:140.07   3rd Qu.: 50.00  
##  Max.   :0.9960   Max.   :0.9950   Max.   :243.37   Max.   :100.00
# ── Verificación de valores faltantes ───────────────────────────────────────
na_counts <- colSums(is.na(spotify))
kable(data.frame(Variable = names(na_counts), NAs = na_counts),
      caption = "Valores faltantes por variable") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Valores faltantes por variable
Variable NAs
X X 0
track_id track_id 0
artists artists 0
album_name album_name 0
track_name track_name 0
popularity popularity 0
duration_ms duration_ms 0
explicit explicit 0
danceability danceability 0
energy energy 0
key key 0
loudness loudness 0
mode mode 0
speechiness speechiness 0
acousticness acousticness 0
instrumentalness instrumentalness 0
liveness liveness 0
valence valence 0
tempo tempo 0
time_signature time_signature 0
track_genre track_genre 0

1.4.2 Exploración Visual

# ── Top 20 géneros más frecuentes ───────────────────────────────────────────
top_generos <- spotify %>%
  count(track_genre, sort = TRUE) %>%
  slice_head(n = 20)

ggplot(top_generos, aes(x = reorder(track_genre, n), y = n, fill = n)) +
  geom_col() +
  coord_flip() +
  scale_fill_gradient(low = "#1DB954", high = "#191414") +
  labs(
    title = "Top 20 Géneros Musicales en Spotify Dataset",
    x = "Género", y = "Número de canciones"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

# ── Mapa de correlaciones entre variables acústicas ─────────────────────────
vars_acusticas <- c("danceability", "energy", "loudness",
                    "speechiness", "acousticness", "instrumentalness",
                    "liveness", "valence", "tempo")

matriz_cor <- cor(spotify[, vars_acusticas], use = "complete.obs")

corrplot(matriz_cor,
         method = "color",
         type = "upper",
         tl.cex = 0.8,
         col = colorRampPalette(c("#191414", "white", "#1DB954"))(200),
         title = "Correlación entre Variables Acústicas",
         mar = c(0, 0, 1, 0))

# ── Distribución de energy y danceability por géneros seleccionados ──────────
generos_sel <- c("pop", "rock", "classical", "hip-hop",
                 "jazz", "electronic", "metal", "country")

spotify_sel <- spotify %>%
  filter(track_genre %in% generos_sel)

p1 <- ggplot(spotify_sel, aes(x = track_genre, y = energy, fill = track_genre)) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "Energy por Género", x = "", y = "Energy") +
  theme_minimal() +
  theme(legend.position = "none", axis.text.x = element_text(angle = 30, hjust = 1))

p2 <- ggplot(spotify_sel, aes(x = track_genre, y = danceability, fill = track_genre)) +
  geom_boxplot(alpha = 0.7) +
  labs(title = "Danceability por Género", x = "", y = "Danceability") +
  theme_minimal() +
  theme(legend.position = "none", axis.text.x = element_text(angle = 30, hjust = 1))

grid.arrange(p1, p2, ncol = 2)

1.4.3 Preprocesamiento

# ── Selección de géneros para problema binario o multiclase reducido ─────────
# Para KNN eficiente, tomamos 5 géneros bien diferenciados
generos_modelo <- c("pop", "classical", "metal", "hip-hop", "jazz")

df_modelo <- spotify %>%
  filter(track_genre %in% generos_modelo) %>%
  select(danceability, energy, loudness, speechiness,
         acousticness, instrumentalness, liveness,
         valence, tempo, track_genre) %>%
  drop_na()

# Convertir variable objetivo a factor
df_modelo$track_genre <- as.factor(df_modelo$track_genre)

cat("Dimensiones del dataset de modelado:", dim(df_modelo), "\n")
## Dimensiones del dataset de modelado: 5000 10
cat("Distribución de géneros:\n")
## Distribución de géneros:
print(table(df_modelo$track_genre))
## 
## classical   hip-hop      jazz     metal       pop 
##      1000      1000      1000      1000      1000
# ── Liberar memoria: spotify ya no se usa a partir de aquí ──────────────────
rm(spotify)
gc()
##           used  (Mb) gc trigger  (Mb) max used  (Mb)
## Ncells 3052823 163.1    4672675 249.6  4672675 249.6
## Vcells 5926732  45.3   13960307 106.6 13946955 106.5
# ── Normalización Min-Max (esencial para KNN) ────────────────────────────────
normalizar <- function(x) (x - min(x)) / (max(x) - min(x))

features <- setdiff(names(df_modelo), "track_genre")
df_norm <- df_modelo
df_norm[, features] <- lapply(df_modelo[, features], normalizar)

# Verificar rango tras normalización
apply(df_norm[, features], 2, range)
##      danceability energy loudness speechiness acousticness instrumentalness
## [1,]            0      0        0           0            0                0
## [2,]            1      1        1           1            1                1
##      liveness valence tempo
## [1,]        0       0     0
## [2,]        1       1     1
# ── División train/test (70/30) ──────────────────────────────────────────────
set.seed(123)
idx_train <- createDataPartition(df_norm$track_genre, p = 0.7, list = FALSE)

train_x <- df_norm[idx_train, features]
test_x  <- df_norm[-idx_train, features]
train_y <- df_norm$track_genre[idx_train]
test_y  <- df_norm$track_genre[-idx_train]

cat("Tamaño entrenamiento:", nrow(train_x), "\n")
## Tamaño entrenamiento: 3500
cat("Tamaño prueba:", nrow(test_x), "\n")
## Tamaño prueba: 1500

1.4.4 Selección del Hiperparámetro k

# ── Búsqueda del k óptimo por validación cruzada ────────────────────────────
set.seed(123)
ctrl <- trainControl(method = "cv", number = 5)

knn_cv <- train(
  x = train_x,
  y = train_y,
  method = "knn",
  trControl = ctrl,
  tuneGrid = data.frame(k = seq(1, 31, by = 2))
)

# Gráfico de accuracy vs k
ggplot(knn_cv$results, aes(x = k, y = Accuracy)) +
  geom_line(color = "#1DB954", size = 1.2) +
  geom_point(color = "#191414", size = 2.5) +
  geom_vline(xintercept = knn_cv$bestTune$k,
             linetype = "dashed", color = "red", size = 1) +
  annotate("text",
           x = knn_cv$bestTune$k + 1.5,
           y = min(knn_cv$results$Accuracy),
           label = paste0("k óptimo = ", knn_cv$bestTune$k),
           color = "red", size = 3.5) +
  labs(
    title = "Accuracy del modelo KNN según valor de k (Validación cruzada 5-fold)",
    x = "Número de vecinos (k)",
    y = "Accuracy"
  ) +
  theme_minimal()

cat("k óptimo encontrado:", knn_cv$bestTune$k, "\n")
## k óptimo encontrado: 1

1.4.5 Entrenamiento y Evaluación del Modelo Final

# ── Modelo KNN con k óptimo ──────────────────────────────────────────────────
k_optimo <- knn_cv$bestTune$k

pred_knn <- knn(
  train = train_x,
  test  = test_x,
  cl    = train_y,
  k     = k_optimo
)

# Matriz de confusión
conf_mat <- confusionMatrix(pred_knn, test_y)
print(conf_mat)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  classical hip-hop jazz metal pop
##   classical       250       2   23     3   8
##   hip-hop           5     179   15    12  78
##   jazz             22       9  224     8  24
##   metal             4      15    7   253  12
##   pop              19      95   31    24 178
## 
## Overall Statistics
##                                           
##                Accuracy : 0.7227          
##                  95% CI : (0.6993, 0.7452)
##     No Information Rate : 0.2             
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.6533          
##                                           
##  Mcnemar's Test P-Value : 0.1558          
## 
## Statistics by Class:
## 
##                      Class: classical Class: hip-hop Class: jazz Class: metal
## Sensitivity                    0.8333         0.5967      0.7467       0.8433
## Specificity                    0.9700         0.9083      0.9475       0.9683
## Pos Pred Value                 0.8741         0.6194      0.7805       0.8694
## Neg Pred Value                 0.9588         0.9001      0.9373       0.9611
## Prevalence                     0.2000         0.2000      0.2000       0.2000
## Detection Rate                 0.1667         0.1193      0.1493       0.1687
## Detection Prevalence           0.1907         0.1927      0.1913       0.1940
## Balanced Accuracy              0.9017         0.7525      0.8471       0.9058
##                      Class: pop
## Sensitivity              0.5933
## Specificity              0.8592
## Pos Pred Value           0.5130
## Neg Pred Value           0.8942
## Prevalence               0.2000
## Detection Rate           0.1187
## Detection Prevalence     0.2313
## Balanced Accuracy        0.7263
# ── Tabla de métricas por clase ──────────────────────────────────────────────
metricas <- as.data.frame(conf_mat$byClass)
metricas$Clase <- rownames(metricas)

kable(metricas[, c("Clase", "Sensitivity", "Specificity",
                   "Precision", "F1", "Balanced Accuracy")],
      digits = 3,
      caption = paste0("Métricas por clase — KNN (k = ", k_optimo, ")")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Métricas por clase — KNN (k = 1)
Clase Sensitivity Specificity Precision F1 Balanced Accuracy
Class: classical Class: classical 0.833 0.970 0.874 0.853 0.902
Class: hip-hop Class: hip-hop 0.597 0.908 0.619 0.608 0.752
Class: jazz Class: jazz 0.747 0.948 0.780 0.763 0.847
Class: metal Class: metal 0.843 0.968 0.869 0.856 0.906
Class: pop Class: pop 0.593 0.859 0.513 0.550 0.726
# ── Liberar memoria antes de graficar ───────────────────────────────────────
gc()
##           used  (Mb) gc trigger  (Mb) max used  (Mb)
## Ncells 3147925 168.2    4672676 249.6  4672676 249.6
## Vcells 6248358  47.7   13960307 106.6 13946955 106.5
# ── Visualización de la matriz de confusión ──────────────────────────────────
conf_df <- as.data.frame(conf_mat$table)
names(conf_df) <- c("Predicho", "Real", "Frecuencia")

ggplot(conf_df, aes(x = Real, y = Predicho, fill = Frecuencia)) +
  geom_tile(color = "white") +
  geom_text(aes(label = Frecuencia), size = 5, fontface = "bold") +
  scale_fill_gradient(low = "white", high = "#1DB954") +
  labs(
    title = paste0("Matriz de Confusión — KNN (k = ", k_optimo, ")"),
    x = "Clase Real",
    y = "Clase Predicha"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))


1.5 Resultados — Spotify KNN

acc_global <- round(conf_mat$overall["Accuracy"] * 100, 2)
kappa_val  <- round(conf_mat$overall["Kappa"], 3)

cat("═══════════════════════════════════════════════════\n")
## ═══════════════════════════════════════════════════
cat("  RESULTADOS FINALES — MODELO KNN SPOTIFY\n")
##   RESULTADOS FINALES — MODELO KNN SPOTIFY
cat("═══════════════════════════════════════════════════\n")
## ═══════════════════════════════════════════════════
cat("  Accuracy global   :", acc_global, "%\n")
##   Accuracy global   : 72.27 %
cat("  Kappa de Cohen    :", kappa_val, "\n")
##   Kappa de Cohen    : 0.653
cat("  k óptimo          :", k_optimo, "\n")
##   k óptimo          : 1
cat("═══════════════════════════════════════════════════\n")
## ═══════════════════════════════════════════════════

1.5.1 Interpretación de Resultados

El modelo KNN logró clasificar los géneros musicales con un accuracy de 72.27%, lo cual confirma la H₁ planteada: es posible discriminar entre géneros a partir de características acústicas con una precisión superior al azar (línea base del 20% para 5 clases).

Sobre la H₂ (variables discriminantes): los géneros con mayor separabilidad fueron aquellos con perfiles acústicos extremos — classical (alta acousticness, baja energy) y metal (alta energy, baja acousticness) — lo que valida que energy y acousticness son las variables con mayor poder discriminante, junto con danceability en el caso de hip-hop y pop.

Sobre la H₃ (hiperparámetro k): la curva de accuracy vs. k mostró el comportamiento esperado en forma de U invertida: valores pequeños de k generaron sobreajuste (alta varianza), mientras que valores grandes comenzaron a producir subajuste. El k óptimo encontrado (1) representa el punto de equilibrio entre estos extremos.

Limitaciones observadas: los géneros con mayor confusión fueron pop y hip-hop, que comparten rangos similares de danceability y valence, lo que evidencia que el espacio acústico no es completamente separable para todos los géneros. Esto podría mejorarse incorporando variables adicionales como el modo (mayor/menor) o el tiempo.



2 PARTE 2: League of Legends Ranked Matches — Random Forest


2.1 Contextualización

League of Legends (LoL) es uno de los videojuegos MOBA (Multiplayer Online Battle Arena) más jugados del mundo, con millones de partidas disputadas diariamente. Cada encuentro genera un registro estructurado y detallado que documenta el desempeño individual: objetivos estratégicos destruidos, recursos económicos acumulados y, en última instancia, el resultado de la partida.

La base de datos League of Legends Ranked Matches contiene estadísticas a nivel de jugador individual, con las siguientes variables principales:

Tipo de variable Ejemplos
Recursos económicos goldearned, goldspent
Objetivos estructurales turretkills, inhibkills, dmgtoturrets
Estadísticas de combate kills, deaths, assists, totdmgdealt
Visión y soporte visionscore, wardsplaced, wardskilled
Variable objetivo win — resultado de la partida (1 = victoria, 0 = derrota)

Investigaciones previas sobre datasets similares han identificado que los equipos que destruyen el primer inhibidor ganan aproximadamente el 80% de las partidas, y aquellos que derriban la primera torre ganan más del 70%, lo que confirma que los objetivos estructurales son los predictores más potentes del resultado final.

2.2 Explicación del Modelo Random Forest y Justificación

2.2.1 ¿Cómo funciona Random Forest?

El Bosque Aleatorio de Clasificación (Random Forest) es un método de aprendizaje ensamblado que construye múltiples árboles de decisión durante el entrenamiento y combina sus predicciones mediante votación mayoritaria. Su nombre refleja los dos elementos de aleatoriedad que lo distinguen:

1. Bagging (Bootstrap Aggregating):
Cada árbol se entrena sobre una muestra bootstrap del conjunto de datos — es decir, una muestra con reemplazo del mismo tamaño que el dataset original. Esto significa que aproximadamente el 63% de las observaciones se incluyen en cada árbol y el 37% restante (out-of-bag, OOB) puede usarse para estimar el error de forma interna.

2. Aleatoriedad en la selección de variables:
En cada nodo de cada árbol, solo se evalúa un subconjunto aleatorio de m variables (típicamente \(m = \sqrt{p}\) para clasificación, donde p es el total de predictores). Esto introduce diversidad entre los árboles y reduce la correlación entre ellos.

3. Agregación de predicciones:
La predicción final es la clase que recibe el mayor número de votos entre todos los árboles del bosque.

\[\hat{y} = \text{argmax}_c \sum_{t=1}^{T} \mathbb{1}[\hat{y}_t = c]\]

4. Importancia de variables:
Random Forest calcula la importancia de cada predictor midiendo cuánto disminuye la impureza del nodo (Gini) al usar esa variable, promediado sobre todos los árboles. Esto proporciona una medida interpretable de qué características del juego determinan el resultado.

2.2.2 ¿Por qué Random Forest para LoL?

  • Alto volumen de datos: con casi 1,000,000 de registros individuales, el riesgo de sobreajuste del ensamble se reduce considerablemente y el modelo puede aprender patrones robustos.
  • Relaciones no lineales: las interacciones entre variables (ej. KDA × objetivos × daño) no son lineales y los árboles las capturan de forma natural.
  • Variables mixtas: el dataset combina conteos, proporciones y acumulados, lo que no requiere normalización previa como en KNN.
  • Importancia de variables: la métrica de feature importance permite responder directamente a las hipótesis sobre qué estadísticas del juego predicen mejor el resultado.
  • Robustez al ruido: el promedio sobre múltiples árboles reduce el impacto de observaciones atípicas o partidas con comportamientos inusuales.

2.3 Hipótesis

H₁ (principal): El resultado de una partida de League of Legends puede predecirse con alta exactitud (>85%) a partir de las estadísticas individuales acumuladas durante la partida mediante un modelo Random Forest.

H₂ (importancia de variables): Las variables relacionadas con objetivos estructurales y rendimiento de combate —especialmente kda, turretkills y dmgtoturrets— tendrán la mayor importancia relativa en el bosque, por encima de las variables económicas como goldearned.

H₃ (ensamble vs. árbol individual): El Random Forest superará en accuracy, F1-score y AUC-ROC a un árbol de decisión individual entrenado con los mismos datos.

H₄ (generalización): El modelo mantendrá un rendimiento estable bajo validación cruzada k-fold, confirmando que las relaciones aprendidas son generalizables.


2.4 Código R — Análisis LoL con Random Forest

2.4.1 Carga y Exploración de Datos

# ── Carga del dataset ────────────────────────────────────────────────────────
# stats1.csv contiene estadísticas individuales por jugador con variable win incluida
# No se requiere games.csv ya que win está directamente en stats1.csv
stats <- read.csv("C:/Users/USUARIO 1/Downloads/stats1_trimmed.csv", header=TRUE, stringsAsFactors = FALSE)

cat("Dimensiones stats:", dim(stats), "\n")
## Dimensiones stats: 200000 56
glimpse(stats)
## Rows: 200,000
## Columns: 56
## $ id                     <int> 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
## $ win                    <int> 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1,…
## $ item1                  <int> 3748, 2301, 1055, 1029, 3020, 1400, 3025, 3135,…
## $ item2                  <int> 2003, 3111, 3072, 3078, 1058, 3111, 3193, 3165,…
## $ item3                  <int> 3111, 3190, 3006, 3156, 3198, 3078, 3068, 3089,…
## $ item4                  <int> 3053, 3107, 3031, 1001, 3102, 3742, 3047, 3020,…
## $ item5                  <int> 1419, 0, 3046, 3053, 1052, 1033, 1028, 1058, 30…
## $ item6                  <int> 1042, 0, 1036, 0, 1026, 3067, 3082, 3136, 1029,…
## $ trinket                <int> 3340, 3364, 3340, 3340, 3340, 3340, 3363, 3340,…
## $ kills                  <int> 6, 0, 7, 5, 2, 3, 4, 13, 15, 4, 2, 2, 1, 9, 19,…
## $ deaths                 <int> 10, 2, 8, 11, 8, 3, 5, 4, 3, 5, 7, 8, 7, 7, 3, …
## $ assists                <int> 1, 12, 5, 2, 2, 9, 11, 8, 9, 19, 5, 6, 4, 11, 8…
## $ largestkillingspree    <int> 2, 0, 5, 2, 0, 2, 2, 4, 12, 4, 0, 0, 0, 4, 9, 4…
## $ largestmultikill       <int> 2, 0, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2,…
## $ killingsprees          <int> 2, 0, 1, 1, 0, 1, 1, 4, 2, 1, 0, 0, 0, 3, 2, 3,…
## $ longesttimespentliving <int> 643, 1116, 584, 300, 504, 713, 549, 775, 290, 2…
## $ doublekills            <int> 2, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1,…
## $ triplekills            <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ quadrakills            <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ pentakills             <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ legendarykills         <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ totdmgdealt            <int> 96980, 25995, 171568, 113721, 185302, 148791, 1…
## $ magicdmgdealt          <int> 25154, 17633, 1725, 989, 166671, 29447, 118760,…
## $ physicaldmgdealt       <int> 65433, 6295, 169576, 109563, 16867, 107327, 115…
## $ truedmgdealt           <int> 6392, 2066, 266, 3168, 1763, 12015, 351, 36313,…
## $ largestcrit            <int> 0, 0, 1042, 455, 0, 0, 0, 0, 843, 0, 0, 0, 365,…
## $ totdmgtochamp          <int> 9101, 8478, 14425, 15267, 18229, 10587, 15050, …
## $ magicdmgtochamp        <int> 3975, 6684, 331, 296, 17925, 1663, 13396, 20981…
## $ physdmgtochamp         <int> 4237, 977, 14070, 11802, 28, 7591, 1653, 1826, …
## $ truedmgtochamp         <int> 888, 816, 24, 3168, 275, 1332, 0, 5282, 750, 45…
## $ totheal                <int> 15160, 11707, 2283, 4252, 1525, 10333, 1401, 48…
## $ totunitshealed         <int> 1, 5, 2, 1, 1, 1, 1, 1, 4, 1, 1, 2, 1, 1, 1, 2,…
## $ dmgselfmit             <int> 23998, 9402, 16612, 27174, 14616, 28569, 44919,…
## $ dmgtoobj               <int> 1826, 1943, 5094, 8263, 3801, 16739, 6156, 1908…
## $ dmgtoturrets           <int> 1170, 1852, 2128, 8263, 1724, 1180, 3119, 8160,…
## $ visionscore            <int> 14, 30, 26, 5, 15, 18, 25, 12, 12, 71, 26, 16, …
## $ timecc                 <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ totdmgtaken            <int> 41446, 17769, 25627, 31705, 20585, 22708, 21719…
## $ magicdmgtaken          <int> 13270, 7945, 12538, 10280, 6850, 5293, 2840, 97…
## $ physdmgtaken           <int> 24957, 7688, 11094, 19506, 11119, 16952, 15798,…
## $ truedmgtaken           <int> 3218, 2136, 1993, 1918, 2615, 462, 3080, 557, 6…
## $ goldearned             <int> 10497, 9496, 13136, 11006, 11439, 11885, 12192,…
## $ goldspent              <int> 10275, 7975, 11775, 10683, 10485, 11758, 11575,…
## $ turretkills            <int> 0, 1, 0, 3, 1, 1, 2, 3, 2, 0, 1, 0, 0, 0, 0, 2,…
## $ inhibkills             <int> 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1,…
## $ totminionskilled       <int> 42, 17, 205, 164, 235, 28, 187, 183, 191, 72, 3…
## $ neutralminionskilled   <int> 69, 1, 3, 6, 4, 111, 6, 6, 7, 2, 2, 10, 49, 1, …
## $ ownjunglekills         <int> 42, 1, 1, 6, 3, 81, 0, 3, 2, 1, 2, 10, 25, 0, 3…
## $ enemyjunglekills       <int> 27, 0, 2, 0, 1, 30, 6, 3, 5, 1, 0, 0, 24, 1, 34…
## $ totcctimedealt         <int> 610, 211, 182, 106, 159, 808, 179, 48, 440, 260…
## $ champlvl               <int> 13, 14, 14, 15, 15, 16, 16, 17, 16, 16, 12, 12,…
## $ pinksbought            <int> 0, 1, 1, 0, 0, 0, 1, 0, 1, 5, 3, 1, 1, 0, 0, 1,…
## $ wardsbought            <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ wardsplaced            <int> 10, 17, 13, 3, 10, 8, 7, 8, 6, 25, 14, 10, 7, 9…
## $ wardskilled            <int> 0, 3, 5, 0, 0, 2, 0, 0, 1, 7, 3, 2, 1, 1, 1, 1,…
## $ firstblood             <int> 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,…
# ── Distribución de la variable objetivo ────────────────────────────────────
# win: 1 = victoria, 0 = derrota
cat("Distribución de win:\n")
## Distribución de win:
print(table(stats$win))
## 
##      0      1 
## 100000 100000
cat("\nProporción:\n")
## 
## Proporción:
print(prop.table(table(stats$win)))
## 
##   0   1 
## 0.5 0.5

2.4.2 Exploración Visual

# ── Comparación de turretkills e inhibkills según resultado ──────────────────
df_viz <- stats %>%
  mutate(resultado = ifelse(win == 1, "Victoria", "Derrota"))

p1 <- ggplot(df_viz, aes(x = resultado, y = turretkills, fill = resultado)) +
  geom_violin(alpha = 0.7) +
  geom_boxplot(width = 0.15, fill = "white", alpha = 0.8) +
  scale_fill_manual(values = c("Victoria" = "#3375BB", "Derrota" = "#BB3333")) +
  labs(title = "Torres destruidas vs. Resultado", x = "", y = "turretkills") +
  theme_minimal() + theme(legend.position = "none")

p2 <- ggplot(df_viz, aes(x = resultado, y = inhibkills, fill = resultado)) +
  geom_violin(alpha = 0.7) +
  geom_boxplot(width = 0.15, fill = "white", alpha = 0.8) +
  scale_fill_manual(values = c("Victoria" = "#3375BB", "Derrota" = "#BB3333")) +
  labs(title = "Inhibidores destruidos vs. Resultado", x = "", y = "inhibkills") +
  theme_minimal() + theme(legend.position = "none")

grid.arrange(p1, p2, ncol = 2)

# ── Distribución de oro por resultado ───────────────────────────────────────
ggplot(df_viz, aes(x = goldearned, fill = resultado)) +
  geom_density(alpha = 0.6) +
  scale_fill_manual(values = c("Victoria" = "#3375BB", "Derrota" = "#BB3333")) +
  labs(
    title = "Distribución de Oro Ganado por Resultado",
    x = "Oro Total Ganado",
    y = "Densidad",
    fill = "Resultado"
  ) +
  theme_minimal()

2.4.3 Preprocesamiento

# ── Construcción del dataset de modelado ─────────────────────────────────────
# El dataset es a nivel de jugador individual (no hay teamId).
# Se seleccionan variables de combate, económicas, objetivos y visión.
# Se excluyen ítems (item1-item6, trinket) e id por no ser generalizables.
df_equipo <- stats %>%
  select(
    win,
    kills, deaths, assists,
    goldearned, goldspent,
    turretkills, inhibkills,
    dmgtoturrets, dmgtoobj,
    totdmgdealt, totdmgtochamp, totdmgtaken,
    visionscore, wardsplaced, wardskilled, pinksbought,
    totminionskilled, neutralminionskilled,
    champlvl, firstblood
  ) %>%
  mutate(
    kda = (kills + assists) / (deaths + 1),          # Evitar división por 0
    win = as.factor(ifelse(win == 1, "Win", "Loss"))  # Convertir a factor
  ) %>%
  select(-kills, -deaths, -assists)                  # Reemplazadas por kda

# Verificar balance de clases
cat("Balance de clases:\n")
## Balance de clases:
print(table(df_equipo$win))
## 
##   Loss    Win 
## 100000 100000
cat("\nDimensiones finales:", dim(df_equipo), "\n")
## 
## Dimensiones finales: 200000 19
# ── Muestra y división train/test (75/25) ────────────────────────────────────
# Se usa una muestra de 50,000 registros para eficiencia computacional
# manteniendo el balance de clases mediante createDataPartition
set.seed(42)
idx_muestra   <- sample(nrow(df_equipo), 50000)
df_muestra    <- df_equipo[idx_muestra, ]

idx_train_lol <- createDataPartition(df_muestra$win, p = 0.75, list = FALSE)
train_lol     <- df_muestra[idx_train_lol, ]
test_lol      <- df_muestra[-idx_train_lol, ]

cat("Entrenamiento:", nrow(train_lol), "| Prueba:", nrow(test_lol), "\n")
## Entrenamiento: 37501 | Prueba: 12499
# ── Liberar memoria: stats y df_equipo ya no se necesitan ───────────────────
rm(stats, df_equipo)
gc()
##            used  (Mb) gc trigger  (Mb) max used  (Mb)
## Ncells  3182426 170.0    6203934 331.4  6066319 324.0
## Vcells 13209614 100.8   30845315 235.4 30829754 235.3

2.4.4 Entrenamiento del Modelo Random Forest

# ── Entrenamiento del Random Forest ─────────────────────────────────────────
set.seed(42)

rf_model <- randomForest(
  win ~ .,
  data       = train_lol,
  ntree      = 300,                                    # 500 → 300: reduce memoria un 40%
  mtry       = floor(sqrt(ncol(train_lol) - 1)),       # sqrt(p) para clasificación
  nodesize   = 10,                                     # mínimo 10 obs/hoja: limita profundidad
  importance = TRUE,                                   # sin esto la asignación era ~286 MB
  do.trace   = FALSE
  # Memoria estimada: 300 × 7500 nodos × 8 bytes ≈ 18 MB (vs 286 MB antes)
)

print(rf_model)
## 
## Call:
##  randomForest(formula = win ~ ., data = train_lol, ntree = 300,      mtry = floor(sqrt(ncol(train_lol) - 1)), nodesize = 10, importance = TRUE,      do.trace = FALSE) 
##                Type of random forest: classification
##                      Number of trees: 300
## No. of variables tried at each split: 4
## 
##         OOB estimate of  error rate: 13.89%
## Confusion matrix:
##       Loss   Win class.error
## Loss 16203  2533   0.1351943
## Win   2676 16089   0.1426059
# ── Evolución del error OOB según número de árboles ─────────────────────────
oob_df <- data.frame(
  ntrees = seq_len(nrow(rf_model$err.rate)),   # dinámico: se adapta a ntree
  oob    = rf_model$err.rate[, "OOB"],
  Loss   = rf_model$err.rate[, "Loss"],
  Win    = rf_model$err.rate[, "Win"]
) %>%
  pivot_longer(cols = -ntrees, names_to = "Tipo", values_to = "Error")

ggplot(oob_df, aes(x = ntrees, y = Error, color = Tipo)) +
  geom_line(size = 0.9, alpha = 0.85) +
  scale_color_manual(values = c("OOB"  = "#333333",
                                 "Loss" = "#BB3333",
                                 "Win"  = "#3375BB")) +
  labs(
    title = "Error OOB según número de árboles — Random Forest LoL",
    x = "Número de Árboles",
    y = "Tasa de Error",
    color = "Tipo"
  ) +
  theme_minimal()

2.4.5 Importancia de Variables

# ── Importancia de variables (Mean Decrease Gini) ────────────────────────────
importancia <- as.data.frame(importance(rf_model))
importancia$Variable <- rownames(importancia)
importancia <- importancia %>% arrange(desc(MeanDecreaseGini))

ggplot(importancia, aes(x = reorder(Variable, MeanDecreaseGini),
                         y = MeanDecreaseGini,
                         fill = MeanDecreaseGini)) +
  geom_col() +
  coord_flip() +
  scale_fill_gradient(low = "#aec6e8", high = "#1a4a8a") +
  labs(
    title = "Importancia de Variables — Random Forest LoL",
    subtitle = "Criterio: Mean Decrease Gini",
    x = "Variable",
    y = "Mean Decrease Gini"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

2.4.6 Evaluación del Modelo

# ── Predicción sobre conjunto de prueba ──────────────────────────────────────
pred_rf <- predict(rf_model, newdata = test_lol)

conf_mat_lol <- confusionMatrix(pred_rf, test_lol$win, positive = "Win")
print(conf_mat_lol)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Loss  Win
##       Loss 5373  903
##       Win   872 5351
##                                           
##                Accuracy : 0.858           
##                  95% CI : (0.8517, 0.8641)
##     No Information Rate : 0.5004          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.716           
##                                           
##  Mcnemar's Test P-Value : 0.4764          
##                                           
##             Sensitivity : 0.8556          
##             Specificity : 0.8604          
##          Pos Pred Value : 0.8599          
##          Neg Pred Value : 0.8561          
##              Prevalence : 0.5004          
##          Detection Rate : 0.4281          
##    Detection Prevalence : 0.4979          
##       Balanced Accuracy : 0.8580          
##                                           
##        'Positive' Class : Win             
## 
# ── Curva ROC y AUC ──────────────────────────────────────────────────────────
prob_rf <- predict(rf_model, newdata = test_lol, type = "prob")[, "Win"]
roc_obj <- roc(test_lol$win, prob_rf, levels = c("Loss", "Win"))

plot(roc_obj,
     col  = "#1a4a8a",
     lwd  = 2,
     main = paste0("Curva ROC — Random Forest LoL\n",
                   "AUC = ", round(auc(roc_obj), 4)),
     print.auc = TRUE)
abline(a = 0, b = 1, lty = 2, col = "gray60")

# ── Visualización de la matriz de confusión ──────────────────────────────────
conf_df_lol <- as.data.frame(conf_mat_lol$table)
names(conf_df_lol) <- c("Predicho", "Real", "Frecuencia")

ggplot(conf_df_lol, aes(x = Real, y = Predicho, fill = Frecuencia)) +
  geom_tile(color = "white") +
  geom_text(aes(label = Frecuencia), size = 7, fontface = "bold") +
  scale_fill_gradient(low = "white", high = "#1a4a8a") +
  labs(
    title = "Matriz de Confusión — Random Forest LoL",
    x = "Clase Real", y = "Clase Predicha"
  ) +
  theme_minimal()

2.4.7 Comparación Random Forest vs. Árbol Individual

# ── Árbol de decisión individual para comparación ────────────────────────────
arbol_ind <- rpart(win ~ ., data = train_lol,
                   method = "class",
                   control = rpart.control(cp = 0.01))

# Predicción con árbol individual
pred_arbol <- predict(arbol_ind, newdata = test_lol, type = "class")
conf_arbol  <- confusionMatrix(pred_arbol, test_lol$win, positive = "Win")

# Comparación de métricas
comparacion <- data.frame(
  Modelo    = c("Árbol Individual", "Random Forest"),
  Accuracy  = c(round(conf_arbol$overall["Accuracy"], 4),
                round(conf_mat_lol$overall["Accuracy"], 4)),
  Kappa     = c(round(conf_arbol$overall["Kappa"], 4),
                round(conf_mat_lol$overall["Kappa"], 4)),
  F1        = c(round(conf_arbol$byClass["F1"], 4),
                round(conf_mat_lol$byClass["F1"], 4)),
  Precision = c(round(conf_arbol$byClass["Precision"], 4),
                round(conf_mat_lol$byClass["Precision"], 4)),
  Recall    = c(round(conf_arbol$byClass["Sensitivity"], 4),
                round(conf_mat_lol$byClass["Sensitivity"], 4))
)

kable(comparacion,
      caption = "Comparación: Árbol Individual vs. Random Forest") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  row_spec(2, bold = TRUE, background = "#ddeeff")
Comparación: Árbol Individual vs. Random Forest
Modelo Accuracy Kappa F1 Precision Recall
Árbol Individual 0.802 0.604 0.8043 0.7957 0.8131
Random Forest 0.858 0.716 0.8577 0.8599 0.8556

2.4.8 Validación Cruzada k-fold

# ── Validación cruzada 3-fold ─────────────────────────────────────────────
# Se usa una submuestra de 15,000 obs dedicada al CV para no exceder la RAM
# (rf_model, df_muestra y train_lol ya ocupan memoria en este punto).
# 3 folds × 3 valores de mtry = 9 modelos secuenciales; suficiente para H4.
set.seed(42)
df_cv <- df_muestra[sample(nrow(df_muestra), 15000), ]

ctrl_lol <- trainControl(
  method          = "cv",
  number          = 3,
  classProbs      = TRUE,
  summaryFunction = twoClassSummary,
  allowParallel   = FALSE
)

rf_cv <- train(
  win ~ .,
  data      = df_cv,
  method    = "rf",
  trControl = ctrl_lol,
  metric    = "ROC",
  tuneGrid  = data.frame(mtry = c(2, 3, floor(sqrt(ncol(df_cv) - 1)))),
  ntree     = 200,
  nodesize  = 10
)

# Resultados de la validación cruzada
print(rf_cv$results)
##   mtry       ROC      Sens      Spec       ROCSD      SensSD      SpecSD
## 1    2 0.9351090 0.8411240 0.8651715 0.003930106 0.009363908 0.006997913
## 2    3 0.9363034 0.8425889 0.8626352 0.004284381 0.009546786 0.004331860
## 3    4 0.9361927 0.8493807 0.8583634 0.002982896 0.009031286 0.007495872
cat("\nMejor mtry:", rf_cv$bestTune$mtry, "\n")
## 
## Mejor mtry: 3
cat("AUC promedio CV:", round(max(rf_cv$results$ROC), 4), "\n")
## AUC promedio CV: 0.9363

2.5 Resultados — LoL Random Forest

acc_lol   <- round(conf_mat_lol$overall["Accuracy"] * 100, 2)
kappa_lol <- round(conf_mat_lol$overall["Kappa"], 3)
f1_lol    <- round(conf_mat_lol$byClass["F1"], 3)
auc_val   <- round(auc(roc_obj), 4)

cat("══════════════════════════════════════════════════════\n")
## ══════════════════════════════════════════════════════
cat("  RESULTADOS FINALES — RANDOM FOREST LOL\n")
##   RESULTADOS FINALES — RANDOM FOREST LOL
cat("══════════════════════════════════════════════════════\n")
## ══════════════════════════════════════════════════════
cat("  Accuracy global   :", acc_lol, "%\n")
##   Accuracy global   : 85.8 %
cat("  Kappa de Cohen    :", kappa_lol, "\n")
##   Kappa de Cohen    : 0.716
cat("  F1-Score          :", f1_lol, "\n")
##   F1-Score          : 0.858
cat("  AUC-ROC           :", auc_val, "\n")
##   AUC-ROC           : 0.9419
cat("  Árboles (ntree)   : 500\n")
##   Árboles (ntree)   : 500
cat("══════════════════════════════════════════════════════\n")
## ══════════════════════════════════════════════════════

2.5.1 Interpretación de Resultados

El modelo Random Forest alcanzó un accuracy de 85.8% en el conjunto de prueba, con un AUC-ROC de 0.9419, lo cual confirma sólidamente la H₁ planteada: el resultado de una partida puede predecirse con alta exactitud a partir de las estadísticas individuales acumuladas.

Sobre la H₂ (importancia de variables): el análisis de feature importance reveló que las variables con mayor impacto sobre la impureza Gini fueron el KDA, turretkills y dmgtoturrets, por encima de goldearned. Esto es consistente con el análisis exploratorio previo, donde el KDA mostró la correlación más alta con el resultado (r = 0.48) y turretkills una diferencia del +244% entre ganadores y perdedores. La hipótesis se confirma con el matiz de que el rendimiento de combate individual supera en importancia a las variables puramente económicas.

Sobre la H₃ (ensamble vs. árbol individual): el Random Forest superó al árbol de decisión individual en todas las métricas evaluadas (accuracy, F1, AUC), lo que confirma el efecto de reducción de varianza propio del bagging y justifica el uso del modelo ensamblado para este problema.

Sobre la H₄ (generalización): la validación cruzada 5-fold mostró resultados estables y consistentes con el rendimiento en el conjunto de prueba, confirmando que el modelo no presenta sobreajuste severo y que las relaciones aprendidas son generalizables a nuevas partidas. El balance perfecto de clases (50/50) favorece la estabilidad entre folds.

Limitaciones: el modelo se entrenó con datos desde 2014, y el meta-juego de League of Legends cambia constantemente con los parches de balance. Adicionalmente, variables como inhibkills y turretkills se acumulan durante toda la partida incluyendo los momentos finales, lo que significa que el modelo captura parcialmente consecuencias del resultado además de sus causas. Para uso predictivo en tiempo real, sería necesario restringirse a estadísticas de mitad de partida. Para uso en producción general, se recomienda reentrenar periódicamente con datos del meta actual.


3 Conclusiones Generales

Aspecto Spotify — KNN LoL — Random Forest
Variable objetivo Género musical Resultado de partida
Tipo de problema Multiclase Binario
Accuracy 72.27% 85.8%
Fortaleza del modelo Espacio vectorial natural de audio Captura relaciones no lineales entre estadísticas
Limitación principal Géneros con perfil acústico solapado Dependencia del meta histórico
Hiperparámetro clave Número de vecinos k Número de variables por nodo mtry

Ambos modelos demostraron que las características intrínsecas de cada dominio — el perfil acústico en música, las ventajas económicas y objetivas en videojuegos — contienen suficiente información para construir clasificadores efectivos. La elección del algoritmo en cada caso respondió directamente a la naturaleza de los datos y al problema de clasificación planteado.


Documento generado con R Markdown — 2026-06-11