Contexto: El dataset
injury_data.csvcontiene información sobre jugadores deportivos y factores asociados a la probabilidad de sufrir una lesión. Cada fila corresponde a un jugador
# cargamos los datos, nos aseguramos de cargarlo con dos opciones: una es cargando el archivo del data set en la carpeta en la que está guardado el script, o cargandolo directamente en environment > import dataset > from text(readr)
datos <- read.csv("injury_data.csv", header = TRUE, stringsAsFactors = FALSE)
# Convertimos la variable objetivo a factor (categórica binaria)
datos$Likelihood_of_Injury <- factor(datos$Likelihood_of_Injury,
levels = c(0, 1),
labels = c("No Lesión", "Lesión"))
# Vemos las primeras 6 filas del dataset
kable(head(datos, 6),
caption = "Primeras 6 observaciones del dataset",
digits = 2) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, font_size = 12)| Player_Age | Player_Weight | Player_Height | Previous_Injuries | Training_Intensity | Recovery_Time | Likelihood_of_Injury |
|---|---|---|---|---|---|---|
| 24 | 66.25 | 175.73 | 1 | 0.46 | 5 | No Lesión |
| 37 | 71.00 | 174.58 | 0 | 0.23 | 6 | Lesión |
| 32 | 80.09 | 186.33 | 0 | 0.61 | 2 | Lesión |
| 28 | 87.47 | 175.50 | 1 | 0.25 | 4 | Lesión |
| 25 | 84.66 | 190.18 | 0 | 0.58 | 1 | Lesión |
| 38 | 75.82 | 206.63 | 1 | 0.36 | 4 | No Lesión |
Variables que tenemos:
Player_Age: Edad del jugador (años).Player_Weight: Peso del jugador (kg).Player_Height: Altura del jugador (cm).Previous_Injuries: Lesiones previas (0 = No, 1 =
Sí).Training_Intensity: Intensidad de entrenamiento
[0–1].Recovery_Time: Tiempo de recuperación (días).Likelihood_of_Injury: Variable objetivo (0 = No lesión,
1 = Lesión).# estadistica descriptiva (univariada)
# media, mediana, desviación estándar,
# mínimo, máximo y cuartiles por variable
# Variables numéricas para el análisis
numericas <- c("Player_Age", "Player_Weight", "Player_Height",
"Previous_Injuries", "Training_Intensity", "Recovery_Time")
resumen <- data.frame(
Variable = numericas,
N = sapply(datos[, numericas], function(x) sum(!is.na(x))),
Media = sapply(datos[, numericas], mean, na.rm = TRUE),
Mediana = sapply(datos[, numericas], median, na.rm = TRUE),
DE = sapply(datos[, numericas], sd, na.rm = TRUE),
Min = sapply(datos[, numericas], min, na.rm = TRUE),
Max = sapply(datos[, numericas], max, na.rm = TRUE),
Q1 = sapply(datos[, numericas], quantile, 0.25, na.rm = TRUE),
Q3 = sapply(datos[, numericas], quantile, 0.75, na.rm = TRUE)
)
rownames(resumen) <- NULL
kable(resumen,
caption = "Estadistica descriptiva por Variable",
digits = 3) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, font_size = 12)| Variable | N | Media | Mediana | DE | Min | Max | Q1 | Q3 |
|---|---|---|---|---|---|---|---|---|
| Player_Age | 1000 | 28.231 | 28.000 | 6.538 | 18.000 | 39.000 | 22.000 | 34.000 |
| Player_Weight | 1000 | 74.794 | 75.021 | 9.893 | 40.192 | 104.650 | 67.944 | 81.303 |
| Player_Height | 1000 | 179.751 | 180.034 | 9.889 | 145.286 | 207.309 | 173.037 | 186.558 |
| Previous_Injuries | 1000 | 0.515 | 1.000 | 0.500 | 0.000 | 1.000 | 0.000 | 1.000 |
| Training_Intensity | 1000 | 0.491 | 0.484 | 0.286 | 0.000 | 0.998 | 0.241 | 0.730 |
| Recovery_Time | 1000 | 3.466 | 4.000 | 1.701 | 1.000 | 6.000 | 2.000 | 5.000 |
Interpretación: Se observan escalas muy distintas
entre variables ( Player_Height en cm vs
Training_Intensity en [0,1]). Esto justifica la
estandarización antes de hacer el analisis multi. ##
Distribución por Variable: Histogramas
# histogramas
# usamos la función multi.hist del paquete psych para hacer los distintos histogramas que queremos por variable
# superpone la curva de densidad y la normal teórica
multi.hist(datos[, numericas], global = FALSE,
main = "Histogramas con densidad por variable")#boxplot
# Los puntos fuera de los bigotes (±1.5*IQR)
# son candidatos a valores atípicos univariados
#vemos que las escalas son heterogéneas
# Player_Height domina visualmente por su escala (cm).
# Se requiere estandarización para análisis comparativos
boxplot(datos[, numericas],
main = "Boxplots - Escala Original",
xlab = "Variables",
ylab = "Valor",
col = "steelblue",
frame = FALSE,
las = 2,
cex.axis = 0.7)
grid()# Variable objetivo, Conteo y proporción de cada clase.
# Un desbalance severo (>80/20) puede afectar modelos
# supervisados y la interpretación de clusters
#
# Si las clases están balanceadas, los
# clusters obtenidos deberían reflejar ambas categorías
# de forma proporcional
tabla_target <- table(datos$Likelihood_of_Injury)
prop_target <- round(prop.table(tabla_target) * 100, 1)
df_target <- data.frame(
Clase = names(tabla_target),
Frecuencia = as.integer(tabla_target),
Porcentaje = as.numeric(prop_target)
)
kable(df_target, caption = "Distribución de la Variable Objetivo") %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| Clase | Frecuencia | Porcentaje |
|---|---|---|
| No Lesión | 500 | 50 |
| Lesión | 500 | 50 |
ggplot(df_target, aes(x = Clase, y = Frecuencia, fill = Clase)) +
geom_bar(stat = "identity", width = 0.5) +
geom_text(aes(label = paste0(Porcentaje, "%")), vjust = -0.5, size = 4) +
scale_fill_manual(values = c("steelblue", "tomato")) +
labs(title = "Distribución de Likelihood_of_Injury",
x = "Clase", y = "Frecuencia") +
theme_minimal() +
theme(legend.position = "none")# mapa de calor de correlaciones (si recuerdan es como el de la vez pasada, basicamente entre mas color mas correlación hay entre las variables comparadas)
# Valores cercanos a ±1 indican
# relaciones lineales fuertes
#Las correlaciones altas entre variables
# sugieren redundancia de información, lo cual es relevante
# para PCA (que busca maximizar varianza explicada reduciendo
# dimensionalidad)
cor_matrix <- cor(datos[, numericas], use = "complete.obs")
corrplot(cor_matrix,
method = "color",
type = "upper",
addCoef.col = "black",
tl.col = "black",
tl.srt = 45,
number.cex = 0.7,
title = "Correlaciones entre Variables",
mar = c(0, 0, 1, 0))# Estandarizacion (z-score)
# Técnicamente: Transforma cada variable a media=0 y DE=1.
# Fórmula: z = (x - media) / DE
#
#Necesario porque las variables tienen escalas
# muy distintas. Sin estandarizar, variables como Player_Height
# (~170-210 cm) dominarían sobre Training_Intensity (~0-1)
datos.std <- scale(datos[, numericas])
rownames(datos.std) <- rownames(datos)
boxplot(datos.std,
main = "Boxplots - Variables Estandarizadas",
xlab = "Variables",
ylab = "Escala Z",
col = "lightgreen",
frame = FALSE,
las = 2,
cex.axis = 0.7)
grid()# detección univariada de atípicos
# Técnicamente: Una observación es atípica si su valor
# supera Q3 + 1.5*IQR o es menor que Q1 - 1.5*IQR.
# Se itera sobre cada variable numérica estandarizada
#Los índices listados son observaciones que se desvían significativamente en al menos una variable
atipicos <- list()
atipicos.unicos <- c()
for (variable in colnames(datos.std)) {
vals <- boxplot(datos.std[, variable], plot = FALSE)$out
idx <- which(datos.std[, variable] %in% vals)
atipicos[[variable]] <- idx
atipicos.unicos <- union(atipicos.unicos, idx)
}
# Resumen por variable
resumen_atip <- data.frame(
Variable = names(atipicos),
N_atipicos = sapply(atipicos, length)
)
kable(resumen_atip,
caption = "Atípicos Univariados por Variable",
row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| Variable | N_atipicos |
|---|---|
| Player_Age | 0 |
| Player_Weight | 7 |
| Player_Height | 5 |
| Previous_Injuries | 0 |
| Training_Intensity | 0 |
| Recovery_Time | 0 |
## Total de observaciones atípicas únicas (univariado): 12
# Distancia de Mahalanobis
# La distancia de Mahalanobis considera la
# covarianza entre variables. Un punto es atípico multivariado
# si su distancia supera el cuantil 97.5% de una distribución
# Chi-cuadrado con p grados de libertad (donde p = núm. vars).
#
# Un punto con distancia de Mahalanobis alta
# es inusual considerando simultáneamente todas las variables
maha_dist <- mahalanobis(datos.std, colMeans(datos.std), cov(datos.std))
# Umbral Chi-cuadrado al 97.5%
umbral_maha <- qchisq(0.975, df = ncol(datos.std))
# Hist. de distancias
hist(maha_dist,
freq = FALSE,
col = "steelblue",
border = 0,
main = "Distancias de Mahalanobis",
xlab = "Distancia",
ylab = "Densidad")
curve(dchisq(x, df = ncol(datos.std)), add = TRUE, col = "red", lwd = 2)
abline(v = umbral_maha, col = "orange", lty = 2, lwd = 2)
legend("topright", legend = c("Chi-cuadrado teórico", "Umbral 97.5%"),
col = c("red", "orange"), lty = c(1, 2), bty = "n")
grid()# Identificamos atípicos multivariados
idx_maha <- which(maha_dist > umbral_maha)
atipicos[["Mahalanobis"]] <- idx_maha
cat("Atípicos multivariados detectados:", length(idx_maha), "\n")## Atípicos multivariados detectados: 6
## Índices: 226, 281, 365, 435, 658, 961
# Univariados n Multivariados (intersección)
# Un atípico que aparece tanto en el análisis
# univariado como en el multivariado tiene mayor evidencia
# de ser "verdaderamente" atípico
# Estos son los casos que con mayor seguridad
# requieren revisión o tratamiento especial en el análisis
implicados <- intersect(atipicos.unicos, idx_maha)
cat("Observaciones atípicas en ambos métodos:", length(implicados), "\n")## Observaciones atípicas en ambos métodos: 5
## Índices: 281, 435, 961, 365, 658
# Etiquetas para gráficos
etiquetas <- rep("Típico", nrow(datos.std))
etiquetas[idx_maha] <- "Atípico"
etiquetas <- factor(etiquetas)# CLUSTERIZACIÓN JERÁRQUICA DIVISIVA (DIANA)
# DIANA (DIvisive ANAlysis) parte de un único
# cluster que contiene todos los datos y los va dividiendo
# recursivamente. El coeficiente de divisibilidad mide la
# cohesión: valores cercanos a 1 = clusters homogéneos
#
# El dendrograma muestra cómo se forman los
# grupos. Las ramas largas indican separaciones claras entre
# clusters (alta heterogeneidad entre grupos)
# Matriz de disimilaridades Euclidiana sobre datos estandarizados
D <- daisy(datos.std, metric = "euclidean", stand = FALSE)
# Clusterizacion
datos.diana <- diana(x = D, diss = TRUE, stand = FALSE)
cat("Coeficiente de Divisibilidad DIANA:", round(datos.diana$dc, 4), "\n")## Coeficiente de Divisibilidad DIANA: 0.8817
## (Cercano a 1 = clusters bien definidos)
# Dendrograma
fviz_dend(datos.diana,
k = 3,
k_colors = c("steelblue", "tomato", "forestgreen"),
rect = TRUE,
rect_fill = TRUE,
main = "Dendrograma DIANA - Injury Data",
ylab = "Altura",
cex = 0.3)# num. óptimo de clusters
# se evalúan soluciones de K=2 a K=7 usando
# dos índices internos de validación:
#
# - indice DUNN: Rango [0, ∞]. MAYOR = mejor.
# - indice Davies-Bouldin (DB): Rango [0, ∞]. MENOR = mejor
#
#El K óptimo es aquel donde DUNN es máximo
# y DB es mínimo
dunn_vals <- numeric()
db_vals <- numeric()
for (k in 2:7) {
clusters_k <- cutree(datos.diana, k = k)
dunn_vals <- c(dunn_vals,
as.numeric(intCriteria(datos.std, clusters_k, "Dunn")))
db_vals <- c(db_vals,
as.numeric(intCriteria(datos.std, clusters_k, "Davies_Bouldin")))
}
names(dunn_vals) <- names(db_vals) <- 2:7
# resumen
df_indices <- data.frame(
K = 2:7,
Dunn = round(dunn_vals, 4),
Davies_Bouldin = round(db_vals, 4)
)
kable(df_indices,
caption = "Índices de Validación por Número de Clusters",
row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| K | Dunn | Davies_Bouldin |
|---|---|---|
| 2 | 0.0560 | 2.1906 |
| 3 | 0.0538 | 2.0867 |
| 4 | 0.0551 | 1.9684 |
| 5 | 0.0559 | 2.0878 |
| 6 | 0.0565 | 1.9946 |
| 7 | 0.0607 | 1.8834 |
# Graficas
par(mfrow = c(1, 2))
plot(2:7, dunn_vals,
type = "b",
pch = 16,
col = "steelblue",
bty = "n",
main = "Índice DUNN",
xlab = "Número de Clusters",
ylab = "Dunn")
abline(v = which.max(dunn_vals) + 1, col = "red", lty = 2)
grid()
plot(2:7, db_vals,
type = "b",
pch = 16,
col = "tomato",
bty = "n",
main = "Davies-Bouldin",
xlab = "Número de Clusters",
ylab = "DB")
abline(v = which.min(db_vals) + 1, col = "red", lty = 2)
grid()par(mfrow = c(1, 1))
k_optimo_dunn <- which.max(dunn_vals) + 1
k_optimo_db <- which.min(db_vals) + 1
cat("K óptimo según DUNN:", k_optimo_dunn, "\n")## K óptimo según DUNN: 7
## K óptimo según Davies-Bouldin: 7
# Metodo del silhouette (validación adicional)
# El coeficiente de silueta mide qué tan similar
# es cada observación a su propio cluster vs el vecino más
# cercano. Rango [-1, 1]: valores altos = buena asignación
#
#Un pico en el promedio de silueta indica el
# número ideal de grupos para este dataset
fviz_nbclust(datos.std,
FUN = hcut,
method = "silhouette",
k.max = 7,
hc_method = "ward.D2") +
labs(title = "Método Silhouette - Número Óptimo de Clusters",
subtitle = "Máximo silhouette = K óptimo") +
theme_minimal()# DENDROGRAMA FINAL CON K ÓPTIMO
# Se corta el árbol jerárquico de DIANA en el
# nivel que produce el número óptimo de clusters determinado
# por los índices de validación
# el color de cada rama identifica el cluster
# al que pertenece cada observación
k_final <- k_optimo_dunn # Usamos el mejor según DUNN
clusters_finales <- cutree(datos.diana, k = k_final)
fviz_dend(datos.diana,
k = k_final,
rect = TRUE,
rect_fill = TRUE,
main = paste("Dendrograma DIANA - K =", k_final),
cex = 0.3,
ylab = "Altura")## Distribución de observaciones por cluster:
## clusters_finales
## 1 2 3 4 5 6 7
## 306 237 127 57 110 114 49
#aitpicos por clusterizaicon
# En métodos jerárquicos, los clusters con
# muy pocas observaciones (< 2% del total) suelen contener
# outliers
#
# Los jugadores en clusters pequeños tienen
# perfiles únicos que los separan del resto
tabla_clusters <- table(clusters_finales)
n_total <- nrow(datos.std)
umbral_cluster <- max(3, floor(0.02 * n_total)) # 2% del total o mínimo 3
cat("Tamaño de cada cluster:\n")## Tamaño de cada cluster:
## clusters_finales
## 1 2 3 4 5 6 7
## 306 237 127 57 110 114 49
##
## Umbral para considerar cluster 'pequeño': 20 observaciones
# Identificar posibles atípicos
clusters_pequenos <- names(tabla_clusters[tabla_clusters <= umbral_cluster])
obs_atipicas_cluster <- which(clusters_finales %in% as.integer(clusters_pequenos))
cat("\nClusters pequeños identificados:", paste(clusters_pequenos, collapse = ", "), "\n")##
## Clusters pequeños identificados:
## Observaciones en clusters pequeños: 0
# Gráfica
barplot(tabla_clusters,
col = rainbow(k_final),
main = "Tamaño de Clusters",
xlab = "Cluster",
ylab = "Número de Observaciones",
border = NA)
abline(h = umbral_cluster, col = "red", lty = 2, lwd = 2)
legend("topright", legend = "Umbral atípico", col = "red", lty = 2, bty = "n")
grid()# escalamiento multidimensional metrico (cmsdscale)
# MDS busca una representación en baja
# dimensión (2D o 3D) que preserve las distancias originales
# entre observaciones
# Observaciones cercanas en el plano MDS
# tienen perfiles similares. Los clusters DIANA se colorean
# para ver si MDS los separa visualmente
datos.centrados <- scale(datos[, numericas], center = TRUE, scale = FALSE)
distancias <- dist(datos.centrados)
# MDS a 2 dimensiones
mds_coords <- cmdscale(distancias, k = 2, eig = TRUE)
# Proporción de varianza explicada por cada dimensión
varianza_mds <- mds_coords$eig / sum(abs(mds_coords$eig))
cat("Varianza explicada por Dim1:", round(varianza_mds[1] * 100, 1), "%\n")## Varianza explicada por Dim1: 41.7 %
## Varianza explicada por Dim2: 39.4 %
## Total explicado (2D): 81.1 %
# Gráfico MDS coloreado por cluster
df_mds <- data.frame(
Dim1 = mds_coords$points[, 1],
Dim2 = mds_coords$points[, 2],
Cluster = factor(clusters_finales),
Lesion = datos$Likelihood_of_Injury
)
ggplot(df_mds, aes(x = Dim1, y = Dim2, color = Cluster, shape = Lesion)) +
geom_point(alpha = 0.7, size = 2) +
scale_color_brewer(palette = "Set1") +
labs(title = "MDS Métrico - Injury Data",
subtitle = paste0("Dim1: ", round(varianza_mds[1]*100,1),
"% | Dim2: ", round(varianza_mds[2]*100,1), "%"),
x = "Dimensión 1", y = "Dimensión 2",
color = "Cluster", shape = "Lesión") +
theme_minimal() +
theme(legend.position = "right")# ============================================================
# Escalamiento multidimensioanl no metrico (isoMDS)
# A diferencia del MDS métrico, el no métrico
# solo preserva el ORDEN de las distancias (ranking).
# El STRESS mide el ajuste: stress < 0.10 = bueno,
# stress < 0.05 = excelente
#
# Útil cuando las distancias no son
# perfectamente euclidianas
# Se trabaja con una muestra para eficiencia computacional
set.seed(42)
n_muestra <- min(300, nrow(datos.std))
idx_muestra <- sample(1:nrow(datos.std), n_muestra)
datos_muestra <- datos.std[idx_muestra, ]
dist_muestra <- dist(datos_muestra)
fit_iso <- isoMDS(dist_muestra, k = 2)## initial value 35.464899
## iter 5 value 31.064631
## final value 30.174144
## converged
## Stress del MDS No Métrico: 30.17 %
## (< 10% = bueno, < 5% = excelente)
# Gráfico
df_iso <- data.frame(
x = fit_iso$points[, 1],
y = fit_iso$points[, 2],
Cluster = factor(clusters_finales[idx_muestra]),
Lesion = datos$Likelihood_of_Injury[idx_muestra]
)
ggplot(df_iso, aes(x = x, y = y, color = Cluster, shape = Lesion)) +
geom_point(alpha = 0.7, size = 2) +
scale_color_brewer(palette = "Set2") +
labs(title = paste0("MDS No Métrico (isoMDS) - n = ", n_muestra),
subtitle = paste0("Stress = ", round(fit_iso$stress, 2), "%"),
x = "Dimensión 1", y = "Dimensión 2",
color = "Cluster", shape = "Lesión") +
theme_minimal()#Ánalisis de componentes principales PCA
# PCA descompone la matriz de covarianzas en
# vectores propios (direcciones de máxima varianza) y valores
# propios (cantidad de varianza en esa dirección)
# Se usa princomp() con datos estandarizados y cor=FALSE
# (ya estan en misma escala)
#
# Se busca retener suficientes componentes
# para explicar al menos el 80% de la varianza total
datos.pca <- princomp(datos.std, cor = FALSE)
resumen_pca <- summary(datos.pca)
# Varianza acumulada
varianza_acum <- cumsum(datos.pca$sdev^2) / sum(datos.pca$sdev^2)
df_pca_var <- data.frame(
Componente = paste0("PC", 1:length(datos.pca$sdev)),
Varianza_Prop = round(datos.pca$sdev^2 / sum(datos.pca$sdev^2) * 100, 2),
Varianza_Acum = round(varianza_acum * 100, 2)
)
kable(df_pca_var,
caption = "Varianza Explicada por Componente Principal",
row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| Componente | Varianza_Prop | Varianza_Acum |
|---|---|---|
| PC1 | 18.62 | 18.62 |
| PC2 | 18.09 | 36.71 |
| PC3 | 17.14 | 53.85 |
| PC4 | 16.10 | 69.95 |
| PC5 | 15.44 | 85.39 |
| PC6 | 14.61 | 100.00 |
# Número de componentes para 80% de varianza
n_comp_80 <- which(varianza_acum >= 0.80)[1]
cat("\nComponentes necesarios para explicar >= 80% de varianza:", n_comp_80, "\n")##
## Componentes necesarios para explicar >= 80% de varianza: 5
# Scree plot
#Grafica el valor propio (eigenvalue) de cada
# componente en orden descendente. El codo de la
# curva señala el número óptimo de componentes a retener
#
# La línea roja discontinua marca el umbral del 80%
eigenvalues <- datos.pca$sdev^2
par(mfrow = c(1, 2))
# Scree plot
plot(eigenvalues,
type = "b",
pch = 16,
col = "steelblue",
bty = "n",
main = "Scree Plot",
xlab = "Componente",
ylab = "Valor Propio")
abline(h = 1, col = "red", lty = 2)
grid()
# Varianza acumulada
plot(varianza_acum * 100,
type = "b",
pch = 16,
col = "steelblue",
bty = "n",
main = "Varianza Acumulada",
xlab = "Número de Componentes",
ylab = "% Varianza Acumulada",
ylim = c(0, 105))
abline(h = 80, col = "red", lty = 2, lwd = 2)
abline(v = n_comp_80, col = "orange", lty = 2, lwd = 2)
text(n_comp_80 + 0.2, 75,
labels = paste("PC", n_comp_80, "\n>=80%"),
col = "orange", cex = 0.8, adj = 0)
grid()# BIPLOT PCA
#El biplot representa simultáneamente las
# observaciones (puntos) y las variables originales (flechas)
# en el espacio de las primeras dos componentes
# Flechas en la misma dirección = variables correlacionadas positivamente.
# Flechas opuestas = correlación negativa
# Flechas perpendiculares = variables independientes
biplot(datos.pca,
main = "Biplot - PCA Injury Data",
col = c("gray50", "steelblue"),
cex = 0.5,
scale = 0,
xlab = paste0("PC1 (", round(df_pca_var$Varianza_Prop[1], 1), "%)"),
ylab = paste0("PC2 (", round(df_pca_var$Varianza_Prop[2], 1), "%)"))
grid()# cargas de las componentes principales
# Las cargas muestran la correlación entre
# cada variable original y cada componente principal.
# Valores absolutos altos (> 0.3) indican contribución
# significativa a la componente
#
# Permite "nombrar" cada componente principal
# en términos de las variables que la definen
loadings_df <- as.data.frame(unclass(datos.pca$loadings)[, 1:n_comp_80])
colnames(loadings_df) <- paste0("PC", 1:n_comp_80)
rownames(loadings_df) <- numericas
kable(round(loadings_df, 3),
caption = paste0("Cargas (Loadings) de las Primeras ", n_comp_80, " Componentes")) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| PC1 | PC2 | PC3 | PC4 | PC5 | |
|---|---|---|---|---|---|
| Player_Age | 0.405 | 0.528 | 0.117 | 0.510 | 0.110 |
| Player_Weight | 0.374 | -0.595 | -0.053 | -0.274 | 0.416 |
| Player_Height | 0.387 | 0.115 | -0.715 | 0.118 | 0.339 |
| Previous_Injuries | -0.108 | -0.535 | -0.283 | 0.679 | -0.387 |
| Training_Intensity | 0.600 | 0.025 | -0.070 | -0.328 | -0.726 |
| Recovery_Time | -0.419 | 0.258 | -0.622 | -0.288 | -0.148 |
# Scatter plot PC1 vs PC2 coloreado por cluster
# Se proyectan las observaciones al espacio
# de los dos primeros componentes y se colorean según el
# cluster asignado por DIANA
#
# Si los clusters DIANA se separan bien en
# el plano PCA, esto sugiere que PCA y la clusterización
# capturan la misma estructura en los datos
scores_pca <- as.data.frame(datos.pca$scores[, 1:2])
colnames(scores_pca) <- c("PC1", "PC2")
scores_pca$Cluster <- factor(clusters_finales)
scores_pca$Lesion <- datos$Likelihood_of_Injury
scores_pca$Atipico <- ifelse(1:nrow(datos.std) %in% idx_maha, "Atípico", "Típico")
ggplot(scores_pca, aes(x = PC1, y = PC2, color = Cluster,
shape = Atipico, size = Atipico)) +
geom_point(alpha = 0.7) +
scale_color_brewer(palette = "Set1") +
scale_shape_manual(values = c("Atípico" = 17, "Típico" = 16)) +
scale_size_manual(values = c("Atípico" = 3, "Típico" = 1.5)) +
labs(title = "PCA: PC1 vs PC2 (Clusters + Atípicos)",
subtitle = paste0("PC1: ", round(df_pca_var$Varianza_Prop[1], 1),
"% | PC2: ", round(df_pca_var$Varianza_Prop[2], 1), "%"),
x = paste0("PC1 (", round(df_pca_var$Varianza_Prop[1], 1), "%)"),
y = paste0("PC2 (", round(df_pca_var$Varianza_Prop[2], 1), "%)"),
color = "Cluster", shape = "Tipo obs.", size = "Tipo obs.") +
theme_minimal()# Comparación: MDS MÉTRICO vs PCA (Primeras 2 Dimensiones)
# Tanto MDS como PCA producen representaciones
# en baja dimensión. La equivalencia teórica es:
# - PCA en datos centrados = MDS con distancias Euclidianas
#
# Si ambas representaciones son similares,
# la estructura encontrada es robusta
# equivalencia con MDS
pca_centrado <- princomp(datos.centrados, cor = FALSE)
# Comparación visual
par(mfrow = c(1, 2))
plot(mds_coords$points[, 1], mds_coords$points[, 2],
col = rainbow(k_final)[clusters_finales],
pch = 16,
cex = 0.5,
bty = "n",
main = "MDS Métrico",
xlab = paste0("Dim1 (", round(varianza_mds[1]*100,1), "%)"),
ylab = paste0("Dim2 (", round(varianza_mds[2]*100,1), "%)"))
grid()
plot(pca_centrado$scores[, 1], pca_centrado$scores[, 2],
col = rainbow(k_final)[clusters_finales],
pch = 16,
cex = 0.5,
bty = "n",
main = "PCA (datos centrados)",
xlab = paste0("PC1 (", round(summary(pca_centrado)$sdev[1]^2/
sum(summary(pca_centrado)$sdev^2)*100, 1), "%)"),
ylab = paste0("PC2 (", round(summary(pca_centrado)$sdev[2]^2/
sum(summary(pca_centrado)$sdev^2)*100, 1), "%)"))
grid()# Tabla comparativa mds vs pca
#La tabla resume las características
# fundamentales de cada enfoque
df_comp <- data.frame(
Criterio = c("Objetivo", "Input", "Output",
"Equivalencia", "Escala requerida",
"Varianza explicada (2D)",
"Ventaja principal"),
MDS_Metrico = c("Preservar distancias",
"Matriz de distancias",
"Coordenadas en baja dim.",
"Con PCA (dist. Euclidiana)",
"No requerida",
paste0(round(sum(varianza_mds[1:2]) * 100, 1), "%"),
"Funciona con cualquier distancia"),
PCA = c("Maximizar varianza",
"Matriz de datos",
"Componentes principales",
"Con MDS (datos centrados)",
"Estandarización recomendada",
paste0(round(sum(df_pca_var$Varianza_Prop[1:2]), 1), "%"),
"Interpretación de loadings")
)
kable(df_comp,
caption = "Comparación Técnica: MDS vs PCA",
col.names = c("Criterio", "MDS Métrico", "PCA")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE, font_size = 12)| Criterio | MDS Métrico | PCA |
|---|---|---|
| Objetivo | Preservar distancias | Maximizar varianza |
| Input | Matriz de distancias | Matriz de datos |
| Output | Coordenadas en baja dim. | Componentes principales |
| Equivalencia | Con PCA (dist. Euclidiana) | Con MDS (datos centrados) |
| Escala requerida | No requerida | Estandarización recomendada |
| Varianza explicada (2D) | 81.1% | 36.7% |
| Ventaja principal | Funciona con cualquier distancia | Interpretación de loadings |
# FEATURE IMPORTANCE CON RANDOM FOREST
#Random Forest es un ensamble de árboles de
# decisión. La importancia de cada variable se mide por:
# . Mean Decrease in Gini (MDG): reducción promedio del
# índice de impureza Gini al dividir por esa variable
# . Mean Decrease in Accuracy (MDA): reducción del accuracy
# cuando se permuta aleatoriamente esa variable
# Mayor MDG/MDA = variable más importante para predecir
#
# Variables con alta importancia en RF son
# las que más contribuyen a distinguir jugadores con/sin lesión
set.seed(42)
# Variable objetivo
y_rf <- datos$Likelihood_of_Injury
# Entrenamiento del Random Forest
rf_model <- randomForest(
x = datos[, numericas],
y = y_rf,
ntree = 500,
importance = TRUE,
random.seed = 42
)
cat("Random Forest - OOB Error Rate:",
round(rf_model$err.rate[500, "OOB"] * 100, 2), "%\n")## Random Forest - OOB Error Rate: 50.3 %
## (Out-of-Bag: estimación sin necesidad de conjunto de prueba)
importance_df <- as.data.frame(importance(rf_model))
importance_df$Variable <- rownames(importance_df)
# Renombrar columnas según lo que devuelva importance()
colnames(importance_df)[1:2] <- c("MDA", "MDG")
importance_df <- importance_df[order(-importance_df$MDG), ]
tabla_imp <- importance_df[, c("Variable", "MDA", "MDG")]
tabla_imp$MDA <- round(tabla_imp$MDA, 4)
tabla_imp$MDG <- round(tabla_imp$MDG, 4)
kable(tabla_imp,
caption = "Feature Importance - Random Forest",
row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| Variable | MDA | MDG |
|---|---|---|
| Previous_Injuries | -1.7241 | 4.2598 |
| Recovery_Time | 3.1825 | -0.2438 |
| Player_Age | -0.4875 | -0.3096 |
| Player_Weight | 1.3844 | -0.5173 |
| Training_Intensity | -0.1112 | -0.7322 |
| Player_Height | -1.5623 | -4.1006 |
#feature importance visualiacion
# se grafican ambas métricas de importancia
# (MDA y MDG) en graficas de barras ordenadas
#
#
# - MDA (Accuracy): más relevante para predicción
# - MDG (Gini): más relevante para separación de clases
importance_df_plot <- importance_df[order(-importance_df$MDA), ]
p1 <- ggplot(importance_df_plot,
aes(x = reorder(Variable, MDA), y = MDA)) +
geom_bar(stat = "identity", fill = "steelblue") +
coord_flip() +
labs(title = "Feature Importance\n(Mean Decrease Accuracy)",
x = NULL, y = "MDA") +
theme_minimal()
p2 <- ggplot(importance_df_plot,
aes(x = reorder(Variable, MDG), y = MDG)) +
geom_bar(stat = "identity", fill = "tomato") +
coord_flip() +
labs(title = "Feature Importance\n(Mean Decrease Gini)",
x = NULL, y = "MDG") +
theme_minimal()
grid.arrange(p1, p2, ncol = 2)#
# COMPARACIÓN: PCA (varianza) vs FEATURE IMPORTANCE (predicción)
#
# - PCA Loadings: contribución de cada variable a la primera
# componente (estructura de varianza)
# - RF Importance: contribución de cada variable a predecir
# la probabilidad de lesion (utilidad predictiva)
#
#
# - Alta carga PCA Y alta importancia RF = variable informativa y predictiva
# - Alta carga PCA pero baja importancia RF = varianza sin predictibilidad
# - Baja carga PCA pero alta importancia RF = variable discriminante pese a poca varianza
# Carga absoluta en PC1 (primera componente de mayor varianza)
loadings_pc1 <- abs(datos.pca$loadings[, 1])
# Normalizar ambas métricas al rango [0,1]
norm01 <- function(x) (x - min(x)) / (max(x) - min(x))
comp_df <- data.frame(
Variable = numericas,
PCA_Loading_PC1 = norm01(loadings_pc1[numericas]),
RF_Importance = norm01(importance_df[match(numericas, importance_df$Variable), "MDG"])
)
comp_df <- comp_df[order(-comp_df$RF_Importance), ]
# comparativa
comp_long <- reshape(comp_df,
varying = c("PCA_Loading_PC1", "RF_Importance"),
v.names = "Valor",
timevar = "Metodo",
times = c("PCA Loading |PC1|", "RF Importance (MDG)"),
direction = "long")
ggplot(comp_long, aes(x = reorder(Variable, Valor),
y = Valor, fill = Metodo)) +
geom_bar(stat = "identity", position = "dodge") +
coord_flip() +
scale_fill_manual(values = c("steelblue", "tomato")) +
labs(title = "Comparación: PCA Loadings vs Feature Importance (RF)",
subtitle = "Valores normalizados al rango [0,1] para comparabilidad",
x = NULL, y = "Importancia Normalizada",
fill = "Método") +
theme_minimal() +
theme(legend.position = "bottom")# pca vs feautre importance
# La tabla consolida el ranking de cada
# variable según ambos enfoques
comp_df$Rank_PCA <- rank(-comp_df$PCA_Loading_PC1)
comp_df$Rank_RF <- rank(-comp_df$RF_Importance)
comp_df$Diferencia_Ranking <- abs(comp_df$Rank_PCA - comp_df$Rank_RF)
kable(comp_df[order(comp_df$Rank_RF), ],
caption = "Comparación de Rankings: PCA vs Random Forest",
row.names = FALSE, digits = 3) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE, font_size = 12)| Variable | PCA_Loading_PC1 | RF_Importance | Rank_PCA | Rank_RF | Diferencia_Ranking |
|---|---|---|---|---|---|
| Previous_Injuries | 0.000 | 1.000 | 6 | 1 | 5 |
| Recovery_Time | 0.632 | 0.461 | 2 | 2 | 0 |
| Player_Age | 0.603 | 0.453 | 3 | 3 | 0 |
| Player_Weight | 0.541 | 0.429 | 5 | 4 | 1 |
| Training_Intensity | 1.000 | 0.403 | 1 | 5 | 4 |
| Player_Height | 0.567 | 0.000 | 4 | 6 | 2 |
## === RESUMEN DEL ANÁLISIS ===
## 1. DATOS:
## - 1000 jugadores, 6 variables predictoras
## - Variable objetivo: Likelihood_of_Injury
cat(" - Distribución:", table(datos$Likelihood_of_Injury)[1], "sin lesión /",
table(datos$Likelihood_of_Injury)[2], "con lesión\n\n")## - Distribución: 500 sin lesión / 500 con lesión
## 2. ATÍPICOS:
## - Univariados (IQR): 12 observaciones
## - Multivariados (Mahalanobis): 6 observaciones
## - Ambos métodos (intersección): 5 observaciones
## 3. CLUSTERIZACIÓN (DIANA):
## - K óptimo (DUNN): 7 clusters
## - K óptimo (Davies-Bouldin): 7 clusters
## - Coef. divisibilidad: 0.882
## 4. MDS:
## - Varianza explicada (MDS métrico 2D): 81.1 %
## - Stress MDS no métrico: 30.17 %
## 5. PCA:
## - Componentes para >=80% varianza: 5
cat(" - Varianza acumulada con", n_comp_80, "CPs:",
round(varianza_acum[n_comp_80] * 100, 1), "%\n\n")## - Varianza acumulada con 5 CPs: 85.4 %
## 6. FEATURE IMPORTANCE (RF):
## - Variable más importante (MDG): Previous_Injuries
## - OOB Error Rate: 50.3 %
Estadísticas Descriptivas: Las variables presentan escalas heterogéneas, lo que requiere estandarización obligatoria. No se detectaron porcentajes elevados de atípicos extremos, pero la distancia de Mahalanobis reveló casos multivariados que conviene monitorear.
Clusterización (DIANA): El método jerárquico divisivo permitió identificar grupos de jugadores con perfiles de riesgo diferenciados. Los índices Dunn y Davies-Bouldin confirmaron el número óptimo de clusters, y los clusters pequeños concentran los perfiles más atípicos.
MDS: La representación bidimensional captura una porción razonable de la varianza original. La concordancia entre MDS métrico y PCA (datos centrados) confirma la equivalencia teórica entre ambos métodos.
PCA: Con los primeros componentes se alcanza el umbral del 80% de varianza. Los loadings permiten identificar qué variables dominan cada dimensión latente.
Feature Importance vs PCA: La comparación revela que algunas variables con alta varianza (alta carga en PCA) no necesariamente son las más predictivas de lesión (RF importance), y viceversa. Esto ilustra la diferencia fundamental entre análisis exploratorio (no supervisado) y predictivo (supervisado).