1 Introducción

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.

1.1 Sobre el Dataset Wine

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
cat("Variables:", paste(names(wine), collapse = ", "), "\n")
## Variables: Type, Alcohol, Malic, Ash, Alcalinity, Magnesium, Phenols, Flavanoids, Nonflavanoids, Proanthocyanins, Color, Hue, Dilution, Proline
cat("Distribución de clases:\n")
## Distribución de clases:
print(table(wine$Type))
## 
##  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")
Tabla 1. Descripción de variables del dataset Wine
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)

2 Análisis Exploratorio de Datos (EDA)

Antes de modelar, exploramos la distribución y relaciones entre variables para entender la estructura del dataset.

2.1 Estadísticas Descriptivas por Cultivar

# ── 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)
Tabla 2. Estadísticas descriptivas clave por cultivar
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.

2.2 Distribución de 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.

2.3 Mapa de Correlaciones

# ── 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.

2.4 Boxplots: Variables Clave por Cultivar

# ── 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.


3 Árbol de Decisión

3.1 Fundamento Teórico

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

3.2 Partición Entrenamiento / Prueba

# ── 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
cat("Proporción real entrenamiento:\n")
## Proporción real entrenamiento:
print(round(prop.table(table(train_data$Type)) * 100, 1))
## 
##    1    2    3 
## 33.3 39.7 27.0

3.3 Entrenamiento del Árbol

# ── 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)
Tabla 3. Importancia de Variables — Árbol de Decisión
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

3.4 Visualización del Árbol

# ── 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.

3.5 Evaluación del Modelo

# ── 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)
Tabla 4. Métricas de Evaluación — Árbol de Decisión
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)
Tabla 5. Matriz de Confusión — Árbol de Decisión
Real  Predicho
Cultivar 1
Cultivar 2
Cultivar 3
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.


4 K-Vecinos Más Cercanos (KNN)

4.1 Fundamento Teórico

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.

4.2 Normalización de Variables

# ── 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
cat("Rango Alcohol normalizado:", round(range(wine_norm$Alcohol), 3), "\n")
## Rango Alcohol normalizado: 0 1
# Dividir usando los mismos índices que el árbol
train_norm <- wine_norm[ idx_train, ]
test_norm  <- wine_norm[-idx_train, ]

4.3 Selección del Hiperparámetro k

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

cat("k óptimo:", k_optimo,
    "| Exactitud:", round(max(resultados_k$Accuracy) * 100, 2), "%\n")
## 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).

4.4 Entrenamiento y Evaluación

# ── 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)
Tabla 6. Métricas de Evaluación — KNN (k = 5)
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)
Tabla 7. Matriz de Confusión — KNN
Real  Predicho
Cultivar 1
Cultivar 2
Cultivar 3
Real 1 2 3
1 17 1 0
2 0 19 0
3 0 1 14

4.5 Visualización en Espacio PCA

# ── 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.


5 K-Means (Agrupamiento No Supervisado)

5.1 Fundamento Teórico

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:

  1. Inicializar k centroides aleatoriamente.
  2. Asignar cada punto al centroide más cercano.
  3. Recalcular centroides como la media del cluster.
  4. Repetir hasta convergencia.

En este caso comparamos los grupos encontrados con las etiquetas reales para evaluar qué tan bien el algoritmo recupera la estructura conocida del dataset.

5.2 Determinación del Número Óptimo de Clusters

# ── 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.

5.3 Entrenamiento del Modelo K-Means

# ── 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)
Tabla 8. Métricas del Modelo K-Means
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)
Tabla 9. Contingencia — Cluster K-Means vs Cultivar Real
Cultivar Real
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)
Tabla 10. Centroides de los Clusters (variables seleccionadas, escala Z)
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

5.4 Visualización de los Clusters

# ── 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.


6 Comparación de Modelos

# ── 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")
Tabla 11. Comparación de los Tres Métodos Aplicados
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

7 Conclusiones

  1. Á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.

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

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

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


8 Referencias

  • Aha, D. & Kibler, D. (1991). Instance-based learning algorithms. Machine Learning, 6(1), 37–66.
  • Breiman, L., Friedman, J., Stone, C. J. & Olshen, R. A. (1984). Classification and Regression Trees. Wadsworth.
  • MacQueen, J. (1967). Some methods for classification and analysis of multivariate observations. Proc. 5th Berkeley Symposium on Mathematical Statistics and Probability.
  • R Core Team (2024). R: A language and environment for statistical computing. R Foundation for Statistical Computing, Vienna, Austria.
  • Williams, G. (2011). Data Mining with Rattle and R. Springer.

Informe generado con R Markdown · Compilar con Knit en RStudio