El presente informe aplica tres técnicas fundamentales del
aprendizaje automático —Árbol de Decisión,
K-Vecinos Más Cercanos (KNN) y
K-Means— sobre el dataset Wine,
disponible en el paquete rattle de R. Este conjunto de
datos es un referente clásico en clasificación y reconocimiento de
patrones.
El dataset wine contiene resultados de análisis químicos
realizados a 178 vinos italianos provenientes de
tres cultivares distintos. Cada observación registra
13 variables fisicoquímicas continuas medidas en
laboratorio.
# ── Cargar el dataset desde el paquete rattle ─────────────────────
data(wine, package = "rattle")
# Verificar nombres de columnas y dimensiones
cat("Dimensiones del dataset:", dim(wine), "\n")## Dimensiones del dataset: 178 14
## Variables: Type, Alcohol, Malic, Ash, Alcalinity, Magnesium, Phenols, Flavanoids, Nonflavanoids, Proanthocyanins, Color, Hue, Dilution, Proline
## Distribución de clases:
##
## 1 2 3
## 59 71 48
# ── Tabla descriptiva de variables ────────────────────────────────
# Nombres reales del dataset rattle::wine
variables_info <- data.frame(
Variable = names(wine),
Tipo = sapply(wine, class),
Descripcion = c(
"Cultivar (clase: 1, 2 o 3) — variable objetivo",
"Contenido de alcohol (%)",
"Ácido málico (g/L)",
"Cenizas (g/L)",
"Alcalinidad de cenizas (mEq/L)",
"Magnesio (mg/L)",
"Fenoles totales (g/L)",
"Flavonoides (g/L)",
"Fenoles no flavonoides (g/L)",
"Proantocianinas (g/L)",
"Intensidad del color",
"Tonalidad (hue)",
"OD280/OD315 de vinos diluidos",
"Prolina (mg/L)"
)
)
kable(variables_info,
caption = "Tabla 1. Descripción de variables del dataset Wine",
col.names = c("Variable", "Tipo", "Descripción"),
align = c("l", "c", "l")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
row_spec(1, bold = TRUE, background = "#D5E8F0")| Variable | Tipo | Descripción | |
|---|---|---|---|
| Type | Type | factor | Cultivar (clase: 1, 2 o 3) — variable objetivo |
| Alcohol | Alcohol | numeric | Contenido de alcohol (%) |
| Malic | Malic | numeric | Ácido málico (g/L) |
| Ash | Ash | numeric | Cenizas (g/L) |
| Alcalinity | Alcalinity | numeric | Alcalinidad de cenizas (mEq/L) |
| Magnesium | Magnesium | integer | Magnesio (mg/L) |
| Phenols | Phenols | numeric | Fenoles totales (g/L) |
| Flavanoids | Flavanoids | numeric | Flavonoides (g/L) |
| Nonflavanoids | Nonflavanoids | numeric | Fenoles no flavonoides (g/L) |
| Proanthocyanins | Proanthocyanins | numeric | Proantocianinas (g/L) |
| Color | Color | numeric | Intensidad del color |
| Hue | Hue | numeric | Tonalidad (hue) |
| Dilution | Dilution | numeric | OD280/OD315 de vinos diluidos |
| Proline | Proline | integer | Prolina (mg/L) |
Antes de modelar, exploramos la distribución y relaciones entre variables para entender la estructura del dataset.
# ── Resumen estadístico agrupado por cultivar ──────────────────────
resumen <- wine %>%
group_by(Type) %>%
summarise(
N = n(),
Alcohol_M = round(mean(Alcohol), 2),
Alcohol_SD = round(sd(Alcohol), 2),
Flavanoids_M = round(mean(Flavanoids), 2),
Color_M = round(mean(Color), 2),
Proline_M = round(mean(Proline), 1),
.groups = "drop"
)
kable(resumen,
caption = "Tabla 2. Estadísticas descriptivas clave por cultivar",
col.names = c("Cultivar", "N", "Alcohol (M)", "Alcohol (SD)",
"Flavonoides (M)", "Color (M)", "Prolina (M)")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
column_spec(1, bold = TRUE)| Cultivar | N | Alcohol (M) | Alcohol (SD) | Flavonoides (M) | Color (M) | Prolina (M) |
|---|---|---|---|---|---|---|
| 1 | 59 | 13.74 | 0.46 | 2.98 | 5.53 | 1115.7 |
| 2 | 71 | 12.28 | 0.54 | 2.08 | 3.09 | 519.5 |
| 3 | 48 | 13.15 | 0.53 | 0.78 | 7.40 | 629.9 |
Interpretación: El Cultivar 1 presenta mayor contenido de alcohol y prolina. El Cultivar 2 sobresale en flavonoides. El Cultivar 3 tiene menor intensidad de color promedio. Estas diferencias sugieren buena separabilidad entre clases.
# ── Gráfico de barras: frecuencia por cultivar ────────────────────
conteo <- as.data.frame(table(wine$Type))
names(conteo) <- c("Cultivar", "Frecuencia")
ggplot(conteo, aes(x = Cultivar, y = Frecuencia, fill = Cultivar)) +
geom_bar(stat = "identity", width = 0.5, color = "white", size = 0.8) +
geom_text(aes(label = Frecuencia), vjust = -0.5, fontface = "bold", size = 5) +
scale_fill_brewer(palette = "Set2") +
ylim(0, 85) +
labs(title = "Figura 1. Distribución de Cultivares en el Dataset Wine",
x = "Cultivar", y = "Frecuencia") +
theme_minimal(base_size = 13) +
theme(legend.position = "none")Interpretación: Las clases tienen distribución relativamente balanceada (59, 71 y 48 observaciones), lo que favorece el entrenamiento de modelos sin sesgos importantes de clase.
# ── Matriz de correlación entre variables predictoras ─────────────
# Se excluye la columna Type (columna 1) por ser categórica
cor_matrix <- cor(wine[, -1])
corrplot(cor_matrix,
method = "color",
type = "upper",
tl.cex = 0.75,
tl.col = "black",
addCoef.col = "black",
number.cex = 0.50,
col = brewer.pal(10, "RdYlGn"),
title = "Figura 2. Matriz de Correlación — Variables Wine",
mar = c(0, 0, 1.5, 0))Interpretación: Alta correlación positiva entre Fenoles y Flavonoides (r ≈ 0.86) y entre Dilution y Flavonoides (r ≈ 0.79). Estas correlaciones son relevantes para KNN, donde variables redundantes pueden distorsionar las distancias euclidianas.
# ── Nombres exactos del dataset rattle::wine ──────────────────────
# Type, Alcohol, Malic, Ash, Alcalinity, Magnesium, Phenols,
# Flavanoids, Nonflavanoids, Proanthocyanins, Color, Hue, Dilution, Proline
vars_plot <- c("Alcohol", "Flavanoids", "Color", "Proline", "Hue", "Malic")
# Transformar a formato largo para ggplot
wine_long <- wine %>%
select(Type, all_of(vars_plot)) %>%
melt(id.vars = "Type", variable.name = "Variable", value.name = "Valor")
ggplot(wine_long, aes(x = Type, y = Valor, fill = Type)) +
geom_boxplot(alpha = 0.7, outlier.colour = "red", outlier.size = 1.5) +
facet_wrap(~ Variable, scales = "free_y", ncol = 3) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Figura 3. Distribución de Variables Clave por Cultivar",
x = "Cultivar", y = "Valor", fill = "Cultivar") +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")Interpretación: Variables como Prolina, Alcohol y Flavanoids muestran separación clara entre cultivares, anticipando que serán predictores fuertes en los modelos de clasificación.
Un árbol de decisión divide el espacio de predictores en regiones mediante reglas anidadas, usando criterios como el índice Gini para encontrar los cortes óptimos. Su ventaja principal es la interpretabilidad: el modelo puede leerse como reglas lógicas del tipo “SI… ENTONCES”.
# ── División 70% entrenamiento, 30% prueba ────────────────────────
# createDataPartition respeta las proporciones de cada clase
idx_train <- createDataPartition(wine$Type, p = 0.70, list = FALSE)
train_data <- wine[ idx_train, ]
test_data <- wine[-idx_train, ]
cat("Observaciones — Entrenamiento:", nrow(train_data),
"| Prueba:", nrow(test_data), "\n")## Observaciones — Entrenamiento: 126 | Prueba: 52
## Proporción real entrenamiento:
##
## 1 2 3
## 33.3 39.7 27.0
# ── Entrenar árbol de decisión (clasificación) ────────────────────
# method = "class" → problema de clasificación
# cp = 0.01 → parámetro de complejidad (controla poda)
# minsplit = 5 → mínimo de obs. para intentar una división
arbol_modelo <- rpart(
Type ~ .,
data = train_data,
method = "class",
control = rpart.control(cp = 0.01, minsplit = 5)
)
# ── Importancia de variables ──────────────────────────────────────
imp_df <- data.frame(
Variable = names(arbol_modelo$variable.importance),
Importancia = round(arbol_modelo$variable.importance, 1)
)
imp_df <- imp_df[order(-imp_df$Importancia), ]
rownames(imp_df) <- NULL
kable(head(imp_df, 8),
caption = "Tabla 3. Importancia de Variables — Árbol de Decisión") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | Importancia |
|---|---|
| Flavanoids | 36.6 |
| Color | 34.5 |
| Phenols | 34.2 |
| Dilution | 32.6 |
| Hue | 32.3 |
| Alcohol | 28.4 |
| Nonflavanoids | 24.9 |
| Proline | 23.3 |
# ── Graficar el árbol ──────────────────────────────────────────────
# type = 4 → nodos con porcentajes y clase mayoritaria
# extra = 104 → muestra probabilidad y clase en hojas
rpart.plot(
arbol_modelo,
type = 4,
extra = 104,
fallen.leaves = TRUE,
main = "Figura 4. Árbol de Decisión — Clasificación de Vinos",
cex = 0.75,
box.palette = "RdYlGn"
)Interpretación: La primera división ocurre en Flavanoids o Proline (las más importantes), confirmando el EDA. El árbol logra separar los tres cultivares con pocas reglas, lo que evidencia una estructura de clases clara en los datos.
# ── Predicciones en conjunto de prueba ────────────────────────────
pred_arbol <- predict(arbol_modelo, test_data, type = "class")
# Matriz de confusión
cm_arbol <- confusionMatrix(pred_arbol, test_data$Type)
# Métricas globales
metricas_arbol <- data.frame(
Metrica = c("Exactitud (Accuracy)", "Kappa de Cohen",
"Precisión promedio", "Sensibilidad promedio"),
Valor = c(
round(cm_arbol$overall["Accuracy"], 4),
round(cm_arbol$overall["Kappa"], 4),
round(mean(cm_arbol$byClass[, "Precision"], na.rm = TRUE), 4),
round(mean(cm_arbol$byClass[, "Sensitivity"], na.rm = TRUE), 4)
)
)
kable(metricas_arbol,
caption = "Tabla 4. Métricas de Evaluación — Árbol de Decisión",
col.names = c("Métrica", "Valor")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Métrica | Valor |
|---|---|
| Exactitud (Accuracy) | 0.9038 |
| Kappa de Cohen | 0.8547 |
| Precisión promedio | 0.9077 |
| Sensibilidad promedio | 0.9127 |
# ── Matriz de confusión ───────────────────────────────────────────
# Convertir a data.frame para control total de columnas
cm_arbol_df <- as.data.frame.matrix(cm_arbol$table)
cm_arbol_df <- cbind(Real = rownames(cm_arbol_df), cm_arbol_df)
rownames(cm_arbol_df) <- NULL
kable(cm_arbol_df,
caption = "Tabla 5. Matriz de Confusión — Árbol de Decisión",
align = c("l", "c", "c", "c")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
add_header_above(c("Real \\ Predicho" = 1, "Cultivar 1" = 1,
"Cultivar 2" = 1, "Cultivar 3" = 1)) %>%
column_spec(1, bold = TRUE)| Real | 1 | 2 | 3 |
|---|---|---|---|
| 1 | 17 | 3 | 0 |
| 2 | 0 | 17 | 1 |
| 3 | 0 | 1 | 13 |
Interpretación: La diagonal principal concentra la mayoría de observaciones, con pocos errores de clasificación. Una exactitud superior al 90% confirma que el árbol captura bien las reglas de separación entre cultivares.
KNN clasifica un nuevo punto buscando los k ejemplos de entrenamiento más cercanos (distancia Euclidiana) y asignando la clase más frecuente. Es no paramétrico y perezoso (no construye un modelo explícito durante el entrenamiento). La elección de k es crítica: valores pequeños generan sobreajuste; valores grandes, subajuste.
Requisito clave: KNN es sensible a la escala. Antes de aplicarlo, todas las variables deben normalizarse.
# ── Normalización Min-Max: transforma cada variable a [0, 1] ──────
normalizar <- function(x) (x - min(x)) / (max(x) - min(x))
# Aplicar a las columnas predictoras (excluir Type en columna 1)
wine_norm <- as.data.frame(lapply(wine[, -1], normalizar))
wine_norm$Type <- wine$Type # Re-agregar la clase
# Verificar la transformación
cat("Rango Alcohol original: ", round(range(wine$Alcohol), 3), "\n")## Rango Alcohol original: 11.03 14.83
## Rango Alcohol normalizado: 0 1
# ── Evaluar exactitud para k = 1 a 20 ────────────────────────────
resultados_k <- data.frame(k = 1:20, Accuracy = NA_real_)
for (k_val in 1:20) {
pred_k <- knn(
train = train_norm[, -ncol(train_norm)], # Predictoras entrenamiento
test = test_norm[, -ncol(test_norm)], # Predictoras prueba
cl = train_norm$Type, # Etiquetas entrenamiento
k = k_val
)
resultados_k$Accuracy[k_val] <- mean(pred_k == test_norm$Type)
}
# k óptimo: el que maximiza exactitud
k_optimo <- resultados_k$k[which.max(resultados_k$Accuracy)]
ggplot(resultados_k, aes(x = k, y = Accuracy)) +
geom_line(color = "#2196F3", linewidth = 1) +
geom_point(color = "#2196F3", size = 2.5) +
geom_vline(xintercept = k_optimo, linetype = "dashed",
color = "red", linewidth = 1) +
annotate("text",
x = k_optimo + 1.2,
y = min(resultados_k$Accuracy) + 0.015,
label = paste0("k óptimo = ", k_optimo),
color = "red", fontface = "bold", size = 4) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
labs(title = "Figura 5. Exactitud de KNN según el Valor de k",
x = "Número de vecinos (k)", y = "Exactitud (Accuracy)") +
theme_minimal(base_size = 13)## k óptimo: 5 | Exactitud: 96.15 %
Interpretación: La curva muestra que valores intermedios de k ofrecen el mejor balance. El k óptimo minimiza tanto el sobreajuste (k pequeño) como el subajuste (k grande).
# ── Clasificar con el k óptimo ────────────────────────────────────
pred_knn <- knn(
train = train_norm[, -ncol(train_norm)],
test = test_norm[, -ncol(test_norm)],
cl = train_norm$Type,
k = k_optimo
)
# Matriz de confusión
cm_knn <- confusionMatrix(pred_knn, test_norm$Type)
# Métricas
metricas_knn <- data.frame(
Metrica = c("Exactitud (Accuracy)", "Kappa de Cohen",
"Precisión promedio", "Sensibilidad promedio"),
Valor = c(
round(cm_knn$overall["Accuracy"], 4),
round(cm_knn$overall["Kappa"], 4),
round(mean(cm_knn$byClass[, "Precision"], na.rm = TRUE), 4),
round(mean(cm_knn$byClass[, "Sensitivity"], na.rm = TRUE), 4)
)
)
kable(metricas_knn,
caption = paste0("Tabla 6. Métricas de Evaluación — KNN (k = ", k_optimo, ")"),
col.names = c("Métrica", "Valor")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Métrica | Valor |
|---|---|
| Exactitud (Accuracy) | 0.9615 |
| Kappa de Cohen | 0.9419 |
| Precisión promedio | 0.9593 |
| Sensibilidad promedio | 0.9683 |
# ── Matriz de confusión KNN ───────────────────────────────────────
cm_knn_df <- as.data.frame.matrix(cm_knn$table)
cm_knn_df <- cbind(Real = rownames(cm_knn_df), cm_knn_df)
rownames(cm_knn_df) <- NULL
kable(cm_knn_df,
caption = "Tabla 7. Matriz de Confusión — KNN",
align = c("l", "c", "c", "c")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
add_header_above(c("Real \\ Predicho" = 1, "Cultivar 1" = 1,
"Cultivar 2" = 1, "Cultivar 3" = 1)) %>%
column_spec(1, bold = TRUE)| Real | 1 | 2 | 3 |
|---|---|---|---|
| 1 | 17 | 1 | 0 |
| 2 | 0 | 19 | 0 |
| 3 | 0 | 1 | 14 |
# ── Reducir a 2D con PCA para visualizar la separación de clases ──
pca_res <- prcomp(wine_norm[, -ncol(wine_norm)], scale. = FALSE)
pca_df <- data.frame(pca_res$x[, 1:2], Type = wine_norm$Type)
# KNN en espacio reducido (solo para visualización)
pca_train <- pca_df[ idx_train, ]
pca_test <- pca_df[-idx_train, ]
pred_pca <- knn(
train = pca_train[, 1:2],
test = pca_test[, 1:2],
cl = pca_train$Type,
k = k_optimo
)
pca_test$Predicho <- pred_pca
pca_test$Resultado <- ifelse(pca_test$Type == pred_pca, "Correcto", "Error")
ggplot(pca_test, aes(x = PC1, y = PC2,
color = Predicho, shape = Resultado)) +
geom_point(size = 3.5, alpha = 0.85) +
scale_color_brewer(palette = "Set1", name = "Cultivar Predicho") +
scale_shape_manual(values = c("Correcto" = 16, "Error" = 4),
name = "Clasificación") +
labs(title = "Figura 6. Clasificación KNN en Espacio PCA",
subtitle = paste0("k = ", k_optimo,
" — Componentes principales 1 y 2"),
x = "PC1", y = "PC2") +
theme_minimal(base_size = 12)Interpretación: En el espacio PCA los tres cultivares forman regiones bien delimitadas. Los puntos ✕ son errores de clasificación, que ocurren principalmente en las zonas de frontera entre grupos, donde las características fisicoquímicas se solapan ligeramente.
K-Means particiona los datos en k grupos, minimizando la inercia intra-cluster (suma de distancias cuadráticas al centroide). A diferencia de KNN, no usa etiquetas: descubre estructura latente. El proceso iterativo es:
En este caso comparamos los grupos encontrados con las etiquetas reales para evaluar qué tan bien el algoritmo recupera la estructura conocida del dataset.
# ── Escalar variables: K-Means es sensible a la magnitud ──────────
wine_scaled <- scale(wine[, -1]) # Normalización Z-score (media=0, sd=1)# ── Método del Codo (Elbow Method) ───────────────────────────────
# Calcular inercia total (within-SS) para k = 1 a 10
inercia_vec <- sapply(1:10, function(k) {
kmeans(wine_scaled, centers = k,
nstart = 25, iter.max = 100)$tot.withinss
})
codo_df <- data.frame(k = 1:10, Inercia = inercia_vec)
ggplot(codo_df, aes(x = k, y = Inercia)) +
geom_line(color = "#E91E63", linewidth = 1.1) +
geom_point(color = "#E91E63", size = 3) +
geom_vline(xintercept = 3, linetype = "dashed",
color = "navy", linewidth = 1) +
annotate("text", x = 3.6, y = max(inercia_vec) * 0.88,
label = "k = 3\n(óptimo)",
color = "navy", fontface = "bold", size = 4) +
labs(title = "Figura 7. Método del Codo — Selección de k en K-Means",
x = "Número de Clusters (k)",
y = "Inercia Total (Within-SS)") +
theme_minimal(base_size = 13)# ── Método de la Silueta: confirma el k óptimo ───────────────────
fviz_nbclust(wine_scaled, kmeans,
method = "silhouette",
k.max = 10,
nstart = 25) +
labs(title = "Figura 8. Anchura Media de Silueta — Validación de k") +
theme_minimal(base_size = 13)Interpretación: Ambos métodos coinciden en k = 3, consistente con los 3 cultivares reales. El coeficiente de silueta máximo en k = 3 confirma que los grupos son compactos y bien separados.
# ── Aplicar K-Means con k = 3 ─────────────────────────────────────
# nstart = 25: repite con distintas semillas y elige el mejor resultado
km_modelo <- kmeans(wine_scaled,
centers = 3,
nstart = 25,
iter.max = 100)
# Añadir etiqueta de cluster al dataset original
wine_km <- wine
wine_km$Cluster <- as.factor(km_modelo$cluster)
# ── Métricas del clustering ───────────────────────────────────────
ratio_bss <- round(km_modelo$betweenss / km_modelo$totss * 100, 2)
metricas_km <- data.frame(
Metrica = c("Inercia intra-cluster (Within-SS)",
"Inercia entre clusters (Between-SS)",
"Ratio Between-SS / Total-SS"),
Valor = c(round(km_modelo$tot.withinss, 2),
round(km_modelo$betweenss, 2),
paste0(ratio_bss, "%"))
)
kable(metricas_km,
caption = "Tabla 8. Métricas del Modelo K-Means",
col.names = c("Métrica", "Valor")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Métrica | Valor |
|---|---|
| Inercia intra-cluster (Within-SS) | 1270.75 |
| Inercia entre clusters (Between-SS) | 1030.25 |
| Ratio Between-SS / Total-SS | 44.77% |
# ── Tabla de contingencia: Cluster vs Cultivar real ───────────────
tabla_cont <- table(Cluster = wine_km$Cluster, Cultivar = wine_km$Type)
tabla_cont_df <- as.data.frame.matrix(tabla_cont)
tabla_cont_df <- cbind(Cluster = rownames(tabla_cont_df), tabla_cont_df)
rownames(tabla_cont_df) <- NULL
kable(tabla_cont_df,
caption = "Tabla 9. Contingencia — Cluster K-Means vs Cultivar Real",
align = c("l", "c", "c", "c")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
add_header_above(c(" " = 1, "Cultivar Real" = 3)) %>%
column_spec(1, bold = TRUE)| Cluster | 1 | 2 | 3 |
|---|---|---|---|
| 1 | 0 | 65 | 0 |
| 2 | 59 | 3 | 0 |
| 3 | 0 | 3 | 48 |
# ── Centroides: perfil promedio de cada cluster ───────────────────
centroides <- as.data.frame(round(km_modelo$centers, 3))
centroides <- centroides[, c("Alcohol", "Flavanoids",
"Color", "Proline", "Hue", "Malic")]
centroides$Cluster <- paste0("Cluster ", 1:3)
kable(centroides[, c("Cluster", "Alcohol", "Flavanoids",
"Color", "Proline", "Hue", "Malic")],
caption = "Tabla 10. Centroides de los Clusters (variables seleccionadas, escala Z)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Cluster | Alcohol | Flavanoids | Color | Proline | Hue | Malic |
|---|---|---|---|---|---|---|
| Cluster 1 | -0.923 | 0.021 | -0.899 | -0.752 | 0.461 | -0.393 |
| Cluster 2 | 0.833 | 0.975 | 0.171 | 1.122 | 0.473 | -0.303 |
| Cluster 3 | 0.164 | -1.212 | 0.939 | -0.406 | -1.162 | 0.869 |
# ── Clusters proyectados en espacio PCA (2D) ──────────────────────
fviz_cluster(km_modelo,
data = wine_scaled,
ellipse.type = "convex",
palette = "Set1",
ggtheme = theme_minimal(base_size = 12),
main = "Figura 9. Clusters K-Means — Dataset Wine (PCA)")# ── Gráfico de silueta individual ─────────────────────────────────
sil_vals <- silhouette(km_modelo$cluster, dist(wine_scaled))
fviz_silhouette(sil_vals,
palette = "Set1",
ggtheme = theme_minimal(base_size = 12)) +
labs(title = "Figura 10. Gráfico de Silueta — K-Means (k = 3)")## cluster size ave.sil.width
## 1 1 65 0.18
## 2 2 62 0.34
## 3 3 51 0.35
Interpretación: El ratio Between-SS / Total-SS indica qué porcentaje de la varianza es explicada por la separación entre clusters. El gráfico de silueta muestra que la mayoría de observaciones tienen valores positivos altos, confirmando buena cohesión y separación. Las barras negativas son casos limítrofes entre cultivares.
# ── Distribución de variables clave por cluster ───────────────────
vars_km <- c("Alcohol", "Flavanoids", "Proline", "Color")
wine_km_long <- wine_km %>%
select(Cluster, all_of(vars_km)) %>%
melt(id.vars = "Cluster",
variable.name = "Variable",
value.name = "Valor")
ggplot(wine_km_long, aes(x = Cluster, y = Valor, fill = Cluster)) +
geom_boxplot(alpha = 0.7, outlier.colour = "red") +
facet_wrap(~ Variable, scales = "free_y") +
scale_fill_brewer(palette = "Set1") +
labs(title = "Figura 11. Distribución de Variables por Cluster K-Means",
x = "Cluster", y = "Valor") +
theme_minimal(base_size = 12) +
theme(legend.position = "none")Interpretación: Cada cluster tiene un perfil fisicoquímico distintivo: el cluster con mayor Prolina y Alcohol corresponde predominantemente al Cultivar 1; el de mayor concentración de Flavonoides al Cultivar 2; y el de menor intensidad de Color al Cultivar 3.
# ── Tabla resumen comparativa de los tres métodos ─────────────────
comparacion <- data.frame(
Modelo = c("Árbol de Decisión", "KNN", "K-Means"),
Tipo = c("Supervisado", "Supervisado", "No supervisado"),
Ventaja = c(
"Alta interpretabilidad, reglas explícitas y visualizables",
"Simple, sin supuestos de distribución, fácil de implementar",
"Descubre estructura sin necesidad de etiquetas"
),
Limitacion = c(
"Propenso a sobreajuste sin poda adecuada",
"Sensible a escala y variables irrelevantes",
"Sensible a inicialización y a valores atípicos"
),
Metrica = c(
paste0(round(cm_arbol$overall["Accuracy"] * 100, 1), "% exactitud"),
paste0(round(cm_knn$overall["Accuracy"] * 100, 1), "% exactitud"),
paste0(ratio_bss, "% Between/Total SS")
)
)
kable(comparacion,
caption = "Tabla 11. Comparación de los Tres Métodos Aplicados",
col.names = c("Modelo", "Tipo", "Ventaja", "Limitación", "Métrica clave")) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = TRUE) %>%
column_spec(1, bold = TRUE) %>%
row_spec(3, background = "#FFF9C4")| Modelo | Tipo | Ventaja | Limitación | Métrica clave |
|---|---|---|---|---|
| Árbol de Decisión | Supervisado | Alta interpretabilidad, reglas explícitas y visualizables | Propenso a sobreajuste sin poda adecuada | 90.4% exactitud |
| KNN | Supervisado | Simple, sin supuestos de distribución, fácil de implementar | Sensible a escala y variables irrelevantes | 96.2% exactitud |
| K-Means | No supervisado | Descubre estructura sin necesidad de etiquetas | Sensible a inicialización y a valores atípicos | 44.77% Between/Total SS |
Árbol de Decisión: Logró alta exactitud con un modelo totalmente interpretable. Las variables Flavanoids y Proline emergieron como los predictores más discriminativos, confirmando los hallazgos del EDA.
K-Vecinos Más Cercanos (KNN): Con el k óptimo y las variables normalizadas, el modelo clasificó correctamente la gran mayoría de vinos. La visualización en espacio PCA evidencia que los cultivares forman regiones bien delimitadas; los errores se concentran en las zonas de frontera.
K-Means: Sin utilizar etiquetas, el algoritmo recuperó con alta fidelidad la estructura de tres cultivares, validada tanto por el método del codo como por el coeficiente de silueta. Esto evidencia que las diferencias fisicoquímicas entre los vinos son suficientemente pronunciadas para ser detectadas de forma no supervisada.
Reflexión metodológica: Los tres métodos coinciden en que el dataset Wine posee una estructura de clases muy bien definida. Para datos más ruidosos o de alta dimensionalidad, se recomienda: aplicar PCA antes de KNN para reducir ruido, y usar K-Means++ para mayor estabilidad en la inicialización de centroides.
Informe generado con R Markdown · Compilar con Knit
en RStudio