logo

Introducción

La clasificación supervisada es una de las técnicas más importantes dentro del aprendizaje automático, con aplicaciones prácticas en diversas áreas como la banca, medicina, mercadeo, entre otras. Uno de los métodos más intuitivos, sencillos y efectivos para la clasificación supervisada es el algoritmo K-Vecinos más Cercanos (K-NN).

El método K-NN clasifica observaciones nuevas según la clase predominante entre sus vecinos más cercanos, basándose en la proximidad definida mediante una distancia, generalmente euclidiana. La popularidad del método radica en su simplicidad conceptual y su facilidad de implementación.

En esta clase aprenderemos en detalle cómo funciona el método K-NN, cómo implementarlo en R paso a paso y cómo interpretar sus resultados en contextos prácticos.

¿Cómo funciona el K-NN?

Paso 1. Preparación inicial y limpieza de los datos:

El primer paso consiste en preparar cuidadosamente el conjunto de datos:

  • Cargar los datos: importa el conjunto de datos y verificar su estructura (variables predictoras y variable respuesta).

  • Explorar los datos: revisa la distribución de las clases y posibles problemas como valores atípicos o faltantes.

  • Normalizar los datos: dado que K-NN utiliza distancias, es importante estandarizar las variables predictoras para evitar sesgos debido a diferencias en escalas.

Paso 2: Dividir los datos en conjunto de entrenamiento y prueba:

Divide el conjunto total en dos subconjuntos de forma aleatoria:

  • Un conjunto de entrenamiento (aproximadamente 70-80%), que se usa para ajustar el modelo.

  • Un conjunto de prueba (20-30%) que no se usará en la construcción inicial del modelo y servirá para evaluar el desempeño del mismo.

La división del conjunto de datos en entrenamiento y prueba puede realizarse fácilmente en R utilizando la función createFolds() del paquete caret, que permite crear particiones aleatorias y balanceadas de los datos.

Paso 3: Aplicar el algoritmo K-NN:

La lógica del algoritmo K-NN se estructura en tres etapas claras y sucesivas:

  • 1: Calcular la distancia entre observaciones:

El método comienza calculando la distancia entre la observación nueva (por clasificar) y cada una de las observaciones del conjunto de entrenamiento. Generalmente se utiliza la distancia euclidiana, definida como: \[ d(x,y) = \sqrt{\sum_{i=1}^p (x_i-y_i)^2} \] donde \(x\) y \(y\) son observaciones, y \(p\) es el número de variables predictoras.

La distancia puede cambiarse dependiendo del problema (por ejemplo, distancia de Manhattan o Minkowski), pero la euclidiana es la más frecuente.

  • 2: Selección de los K vecinos más cercanos:

Luego de calcular las distancias, el algoritmo selecciona las \(K\) observaciones del conjunto de entrenamiento que tienen las menores distancias con respecto a la nueva observación.

La elección del valor \(k\) es crucial:

  • Un valor pequeño puede generar ruido y sobreajuste.
  • Un valor demasiado alto puede hacer que se pierda precisión en la clasificación.

Para elegir un valor adecuado de \(k\), puedes utilizar la función train.kknn() de la librería kknn, la cual automáticamente evalúa distintos valores de \(K\) mediante validación cruzada, facilitando así seleccionar aquel que ofrezca el mejor rendimiento del modelo.

  • 3: Clasificación según la mayoría:

Finalmente, asignamos la clase que más se repite entre los \(K\) vecinos seleccionados.

La clase predominante se asigna a la observación nueva. Si existiera empate, generalmente se rompe de manera aleatoria o se prueba con otro valor de \(k\). Para evitar esta situación, se recomienda utilizar un valor impar de \(k\).

Paso 4: Validar la estabilidad del modelo

Para validar el modelo se usa Validación Cruzada (K-fold Cross-validation) Ahora, realiza una validación cruzada K-fold para comprobar la estabilidad del modelo. La forma más usual que se hace es:

  • Divide tu conjunto de entrenamiento en \(k\) grupos.
  • Entrena el modelo en \(K−1\) grupos y prueba en el restante.
  • Repite el proceso hasta que cada grupo haya sido usado para prueba exactamente una vez.

Calcula el rendimiento promedio en términos de las métricas anteriores.

¿Por qué es importante validar el modelo con validación cruzada?

Este procedimiento asegura que el desempeño del modelo no dependa exclusivamente de una sola división específica y proporciona una medida más confiable y robusta del desempeño general del algoritmo.

Paso 5: Interpretación de los resultados finales:

Finalmente, debes analizar la estabilidad y calidad de las predicciones del modelo, considerando lo siguiente:

  • Matriz de confusión promedio obtenida por la validación cruzada.
  • Indicadores promedio de desempeño: exactitud, sensibilidad, especificidad.
  • Analiza claramente si el modelo es adecuado para el objetivo planteado, identificando fortalezas y limitaciones.
  • Propón posibles mejoras, tales como:
    • Ajuste adicional de variables predictoras.
    • Cambio en el tipo de distancia.
    • Revisión más profunda sobre valores atípicos.

Ejemplo de motivación

Considera el siguiente conjunto sintético de datos, donde existen dos grupos claramente diferenciados (representados por puntos rojos y azules). Supón que queremos clasificar la nueva observación indicada por el triángulo verde:

¿Cómo funciona el método K-NN aquí?

  1. Calcular la distancia: primero, calculamos la distancia desde el nuevo punto verde hacia cada uno de los puntos rojos y azules (datos ya clasificados previamente).

  2. Seleccionamos los vecinos más cercanos: imagina que elegimos \(K=4\). El círculo alrededor del triángulo verde indica que dentro de esa región están los 4 puntos más cercanos a la observación que queremos clasificar.

  3. Clasificar según la mayoría: dentro del círculo observamos claramente que hay más puntos rojos (3 puntos rojos frente a 1 azules). Por lo tanto, la observación verde sería clasificada en la clase representada por los puntos rojos.

Ejemplos en R

Ejemplo 1:

Iniciamos con un ejemplo sencillo de clasificación con datos inventados: Supongamos que tenemos las calificaciones de una materia cualquiera, donde la calificación depende tres actividades:

  • Trabajo en clase.
  • Examen.
  • Participación en clase.
trabajo       <- c(100,40,60,70,70,60,80,90,20,50,60,50,30,20,20,10,80,90,20,70)
examen        <- c(90,50,60,70,80,70,60,95,10,55,70,60,20,10,50,55,90,100,40,60)
participacion <- c(1,2,1,1,1,2,2,1,3,3,3,2,3,3,2,2,1,1,3,3)
tabla         <- data.frame(trabajo,examen,participacion)

Solución paso a paso:

Paso 1

Preparación inicial y limpieza de los datos:

Supongamos que deseamos ver que tan buena es la apreciación del maestro colocando la calificación de participación en clase. Para ello, tomaremos esta variable como la variable categórica. Observemos en la siguiente gráfica los datos.

plot(tabla[,1:2],main="Relación entre trabajo en clase y Examen",xlab="Trabajo en clase", ylab="Examen ",col=tabla$participacion,pch=19)
 legend("topright",legend=c("1","2","3"),pch=19,col=c(1,2,3))

Para estos datos la exploración, limpieza y normalización de los datos no es necesaria.

Paso 2

Dividir los datos en conjunto de entrenamiento y prueba

Para este ejemplo, todos nuestros datos serán el conjunto de entrenamiento y supondremos nuevos datos para el conjunto de prueba. Por ejemplo, supongamos las siguientes dos nuevas observaciones.

nuevos <- data.frame(trabajo=c(20,90),examen=c(30,80))

Estos datos los podemos apreciar de forma gráfica en color azul en la siguiente figura.

plot(tabla[,1:2],xlab="Trabajo en clase", ylab="Examen 1",col=tabla$participacion,pch=19)
legend("topright",legend=c("1","2","3"),pch=19,col=c(1,2,3))
points(nuevos,col="blue",pch=19,lwd=2)

Paso 3

Aplicar el método K-NN

Construimos el modelo ajustado con los datos de entrenamiento. Para esto trabajaremos con la librería class , donde se utiliza la función knn() .

library(class)
modelo <- knn(train = tabla[,-3], test=nuevos, cl = tabla$participacion, k=3)
modelo
## [1] 3 1
## Levels: 1 2 3

Paso 4

Validar la estabilidad del modelo

Al ser un ejemplo sencillo no se realizó este paso.

Paso 5

Interpretación de los resultados finales

De esta manera se dice que el primer nuevo alumno deberá ser clasificado con participación 3, mientras que el segundo alumno deberá ser clasificado como participación 1. En este paquete se debe jugar a conseguir el numero de vecinos óptimo de forma manual, o se necesitaría primero calcular el valor óptimo y luego aplicar el modelo. Como ejemplo lo podemos ahora calcular con \(k=4\).

modelo1 <-knn(train = tabla[,-3], test=nuevos, cl = tabla$participacion, k=4)
modelo1
## [1] 3 1
## Levels: 1 2 3

Notemos que los resultados son estables al número de vecinos más cercano que se tome.

Ejemplo 2:

Para este segundo ejemplo, trabajaremos con un conjunto de datos iris.

Solución paso a paso:

Paso 1

Preparación inicial y limpieza de los datos:

Para estos datos la exploración, limpieza y normalización de los datos no es necesaria, pero cuando se tengan datos reales es muy importante realizarla.

data <-iris
head(data)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa

Paso 2

Dividir los datos en conjunto de entrenamiento y prueba

Mediante un muestreo aleatorio, separamos el conjunto de entrenamiento y en conjunto de prueba. Supongamos en 5 grupos (folds=5), 4 para entrenamiento y 1 para prueba.

set.seed(2020)
library(caret)
folds         <- createFolds(iris$Species, k = 5)
entrenamiento <- iris[c(folds$Fold1,folds$Fold2,folds$Fold3,folds$Fold4),]
prueba        <- iris[folds$Fold5,]
dim(entrenamiento)[1]
## [1] 120
dim(prueba)[1]
## [1] 30

Paso 3

Aplicar el método K-NN

Construimos el modelo ajustado con los datos de entrenamiento. Para este ejemplo utilizaremos la libreria kknn . En esta paquetería se usa la función rain.kknn(), a la cual se le debe indicar el valor máximo de \(k\) y ella determina el valor óptimo.

library(kknn)
modelo <- train.kknn(Species ~ ., data = entrenamiento, kmax = 8)
modelo
## 
## Call:
## train.kknn(formula = Species ~ ., data = entrenamiento, kmax = 8)
## 
## Type of response variable: nominal
## Minimal misclassification: 0.05833333
## Best kernel: optimal
## Best k: 5

Notemos que encuentra como valor óptimo \(k=5\) vecinos. Podemos correr el modelo con los datos de entrenamiento para revisar el margen de error.

Pres     <- predict(modelo, entrenamiento[,-5])
tt       <- table(entrenamiento[,5],Pres)
tt
/Pres setosa versicolor virginica
setosa 40 0 0
versicolor 0 39 1
virginica 0 1 39

Paso 4

Validar la estabilidad del modelo

No se aplico en este ejemplo, pero se podría hacer la validación haciendo 5 replicas del proceso, tomando en cada iteración como conjunto de entrenamiento cada una de las particiones creadas.

Paso 5

Interpretación de los resultados finales

Podemos observar en la tabla, en las filas la clasificación real y en las columnas la predicción. La tasa de aciertos del modelo respecto a las observaciones de entrenamiento sería:

TA <- (sum(diag(tt)))/sum(tt)
round(TA,2)
## [1] 0.98

Podemos hablar de una precisión del \(98\%\) o de un error del \(2\%\). Para analizar la calidad del modelo, se podrían construir las curvas ROC aprendidas en la sección anterior. Ahora, veamos que tan buena es la predicción para el conjunto de prueba.

Pred    <- predict(modelo, prueba[,-5])
table   <- table(prueba[,5],Pred)
table
/Pred setosa versicolor virginica
setosa 10 0 0
versicolor 0 8 2
virginica 0 0 10
clas    <- (sum(diag(table)))/sum(table)
clas
## [1] 0.9333333

Encontramos que el modelo logra predecir un \(93\%\) los datos de prueba.

Ejemplo 3:

Para este último ejemplo, trabajaremos con una base de datos de biopsias que se encuentra en el sitio web de la editorial O’Reilly . Esta base de datos contiene 569 observaciones y 32 variables relativas a las propiedades relacionadas con biopsias de tumores, calificados como “M” (maligno) o “B” (benigno).

# Librerías necesarias
library(tidyverse)
library(caret)
library(kknn)
library(scales)

# Cargar datos desde la web (O'Reilly)
url <- "https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/wisc_bc_data.csv"
data <- read.csv(url)

Solución paso a paso:

Paso 1

Preparación inicial y limpieza de los datos:

Eliminamos la variable id, ya que puede dar lugar a un sobreajuste ya que identifica cada observación de forma única.

# Eliminar columna id
data <- data %>% select(-id)

# Modificamos los nombres de los valores de la variable diagnosis para que sean más claros
data <- mutate(data,diagnosis = fct_recode(data$diagnosis,
                                           "Bening" = "B","Malignant" = "M"))

Exploramos la cantidad de tumores benignos y malignos de la base de datos.

table(data$diagnosis)
Bening Malignant
357 212
round(prop.table(table(data$diagnosis)),2)
Bening Malignant
0.63 0.37

Note que las variables manejan magnitudes muy diversas, por lo cual algunas variables cuya magnitud de medida sea más grande que otras, tendrán mayor peso a la hora de calcular la distancia entre vecinos. Al ser ello un problema para el clasificador, reescalamos todas las variables a un rango estándar de valores. Es decir, podemos pensar en volverlas todas proporciones entre 0 y 1. Esto se puede hacer usando la función rescale() que usa la función min-max.

# Normalización Min-Max con rescale()
data_norm <- data %>% select(-diagnosis) %>%
  mutate(across(everything(), rescale))

Paso 2

Dividir los datos en conjunto de entrenamiento y prueba

Dividimos la muestra en dos conjuntos, uno para entrenamiento y otro para prueba. Para ello, Utilizaremos createFolds() de caret para garantizar balance de clases en ambos conjuntos.

set.seed(2025)
folds         <- createFolds(data$diagnosis, k = 6)
entrenamiento <- data_norm[-folds[[6]],]
prueba        <- data_norm[folds[[6]],]

Por otra parte, guardamos las etiquetas del diagnóstico de todas las observaciones en dos vectores por separado.

# Etiquetas
entrenamiento_labels <- data$diagnosis[-folds[[6]]]
prueba_labels        <- data$diagnosis[folds[[6]]]

Paso 3

Aplicar el método K-NN

train.kknn(entrenamiento_labels ~ ., data = entrenamiento, kmax = 50)
## 
## Call:
## train.kknn(formula = entrenamiento_labels ~ ., data = entrenamiento,     kmax = 50)
## 
## Type of response variable: nominal
## Minimal misclassification: 0.03164557
## Best kernel: optimal
## Best k: 19

Encontramos que el valor optimo de \(k=19\). Entonces la predicción, sería:

pred <- knn(entrenamiento,prueba, cl = entrenamiento_labels, k = 19)

confusionMatrix(data = pred, reference = prueba_labels)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  Bening Malignant
##   Bening        60         2
##   Malignant      0        33
##                                          
##                Accuracy : 0.9789         
##                  95% CI : (0.926, 0.9974)
##     No Information Rate : 0.6316         
##     P-Value [Acc > NIR] : <2e-16         
##                                          
##                   Kappa : 0.9542         
##                                          
##  Mcnemar's Test P-Value : 0.4795         
##                                          
##             Sensitivity : 1.0000         
##             Specificity : 0.9429         
##          Pos Pred Value : 0.9677         
##          Neg Pred Value : 1.0000         
##              Prevalence : 0.6316         
##          Detection Rate : 0.6316         
##    Detection Prevalence : 0.6526         
##       Balanced Accuracy : 0.9714         
##                                          
##        'Positive' Class : Bening         
## 

Con este modelo hemos obtenido una exactitud del \(97.89\%\) a la hora de acertar en una predicción. Probemos ahora con reescalando los valores con la función scale() , la cual convierte las variables en normales estándar, y quizás nos pueda ayudar a mejorar la clasificación.

data_z           <- as.data.frame(scale(data[,-1]))
entrenamiento_z <- data_z[-folds[[6]],]
prueba_z        <- data_z[folds[[6]],]

pred_z           <- knn(entrenamiento_z, prueba_z, cl = entrenamiento_labels, k = 19)
confusionMatrix(data = pred_z, reference = prueba_labels)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  Bening Malignant
##   Bening        60         3
##   Malignant      0        32
##                                           
##                Accuracy : 0.9684          
##                  95% CI : (0.9105, 0.9934)
##     No Information Rate : 0.6316          
##     P-Value [Acc > NIR] : 3.19e-15        
##                                           
##                   Kappa : 0.9309          
##                                           
##  Mcnemar's Test P-Value : 0.2482          
##                                           
##             Sensitivity : 1.0000          
##             Specificity : 0.9143          
##          Pos Pred Value : 0.9524          
##          Neg Pred Value : 1.0000          
##              Prevalence : 0.6316          
##          Detection Rate : 0.6316          
##    Detection Prevalence : 0.6632          
##       Balanced Accuracy : 0.9571          
##                                           
##        'Positive' Class : Bening          
## 

En este caso podemos ver que nuestra exactitud bajo a aproximadamente \(96.84\%\), por lo que no hemos conseguido mejorarla con respecto a la normalización min-max.

Paso 4

Validar la estabilidad del modelo

Apliquemos validación cruzada para validar la estabilidad del modelo. La idea es analizar que la tasa de aciertos conseguida no dependa de la partición utilizada.

Podemos implementar manualmente la validación cruzada, usando en cada iteración un fold diferente como prueba y los demás como entrenamiento.

# Guardar la exactitud de cada fold
exactitud <- numeric(length = 6)

for(i in 1:6){
# Definir conjuntos entrenamiento y prueba según el fold actual
prueba        <- data_norm[folds[[i]],]
entrenamiento <- data_norm[-folds[[i]],]

# Etiquetas
entrenamiento_labels <- data$diagnosis[-folds[[i]]]
prueba_labels        <- data$diagnosis[folds[[i]]]

pred_knn <- knn(entrenamiento,prueba, cl= entrenamiento_labels, k = 19)

# Evaluar exactitud del modelo en cada fold
cm <- confusionMatrix(pred_knn, prueba_labels)
exactitud[i] <- cm$overall["Accuracy"]

# Mostrar resultado del fold
  cat("Fold", i, "- Exactitud:", exactitud[i], "\n")
}
## Fold 1 - Exactitud: 0.9361702 
## Fold 2 - Exactitud: 0.9787234 
## Fold 3 - Exactitud: 0.9684211 
## Fold 4 - Exactitud: 0.9578947 
## Fold 5 - Exactitud: 0.9791667 
## Fold 6 - Exactitud: 0.9789474
# Exactitud promedio de validación cruzada
Exactitud_promedio <- round(mean(exactitud),4)*100
 paste("Exactitud_promedio: ",Exactitud_promedio,"%",sep="")
## [1] "Exactitud_promedio: 96.66%"

Paso 5

Interpretación de los resultados finales

También podemos hacer la validación cruzada de forma más automática y tomando posiblemente más folds.

set.seed(2025)
train_control <- trainControl(method="cv",number=10,savePredictions = TRUE)

knn_cv <- train(diagnosis ~ ., data=cbind(prueba, diagnosis=prueba_labels), 
                method = "knn", trControl = train_control, tuneGrid = data.frame(k=19))

# Resultados de validación cruzada
knn_cv
## k-Nearest Neighbors 
## 
## 95 samples
## 30 predictors
##  2 classes: 'Bening', 'Malignant' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 85, 85, 85, 86, 86, 86, ... 
## Resampling results:
## 
##   Accuracy   Kappa    
##   0.9577778  0.9019763
## 
## Tuning parameter 'k' was held constant at a value of 19
# Matriz de confusión usando predicciones guardadas por caret
confusionMatrix(knn_cv$pred$pred, knn_cv$pred$obs)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  Bening Malignant
##   Bening        60         4
##   Malignant      0        31
##                                           
##                Accuracy : 0.9579          
##                  95% CI : (0.8957, 0.9884)
##     No Information Rate : 0.6316          
##     P-Value [Acc > NIR] : 4.367e-14       
##                                           
##                   Kappa : 0.9073          
##                                           
##  Mcnemar's Test P-Value : 0.1336          
##                                           
##             Sensitivity : 1.0000          
##             Specificity : 0.8857          
##          Pos Pred Value : 0.9375          
##          Neg Pred Value : 1.0000          
##              Prevalence : 0.6316          
##          Detection Rate : 0.6316          
##    Detection Prevalence : 0.6737          
##       Balanced Accuracy : 0.9429          
##                                           
##        'Positive' Class : Bening          
## 

Podemos concluir que la tasa de aciertos es aproximadamente \(95.79%\) y es bastante robusta. Es decir, aunque se muevan un poco los parámetros la tasa se mantienes constante.

Ejercicio de clase

A continuación se presentan dos conjuntos datos reales.

Problema 1: Historiales clínicos de insuficiencia cardíaca.

Este conjunto de datos contiene información clínica de pacientes, con el objetivo de predecir la mortalidad por insuficiencia cardíaca.

Este conjunto de datos está disponible en el Repositorio de Machine Learning de la UCI.

Problema 2: Calidad del Vino.

Este conjunto de datos incluye variables físico-químicas de muestras de vino, con el objetivo de predecir la calidad realizada por expertos.

Este conjunto de datos está disponible en el Repositorio de Machine Learning de la UCI.

Aplique la metodología aprendida en clase a estos conjuntos de datos.