5  K vecinos más próximos

En esta práctica veremos cómo utilizar la técnica de los K vecinos más próximos (KNN) tanto para tareas de clasificación como regresión. Esta técnica es uno de los métodos de aprendizaje supervisado más simples que consiste en clasificar un caso en función de las clases de sus vecinos más cercanos en el espacio de características.

5.1 Ejercicios Resueltos

Para la realización de esta práctica se requieren los siguientes paquetes:

library(tidyverse) 
# Incluye los siguientes paquetes:
# - readr: para la lectura de ficheros csv. 
# - dplyr: para el preprocesamiento y manipulación de datos.
# - ggplot2: para la visualización de datos.
library(tidymodels)
# Incluye los siguientes paquetes:
# - recipes: para la preparación de los datos. 
# - parsnip: para la creación de modelos.
# - workflows: para la creación de flujos de trabajo.
# - rsample: para la creación de particiones de los datos.
# - yardstick: para la evaluación de modelos.
# - tune: para la optimización de hiperparámetros.
library(kknn) # para la implementación del algoritmo KNN.
library(skimr) # para el análisis exploratorio de datos.
library(plotly) # para la visualización interactiva de gráficos.
library(knitr) # para el formateo de tablas.

Ejercicio 5.1 El conjunto de datos pingüinos.csv contiene un conjunto de datos sobre tres especies de pingüinos con las siguientes variables:

  • Especie: Especie de pingüino (Adelie, Chinstrap o Gentoo).
  • Isla: Isla del archipiélago Palmer donde se realizó la observación.
  • Longitud_pico: Longitud del pico (mm).
  • Profundidad_pico: Profundidad del pico (mm)
  • Longitud_ala: Longitud de la aleta en (mm).
  • Peso: Masa corporal (g).
  • Sexo: Sexo (macho, hembra)
  1. Cargar los datos del archivo pingüinos.csv en un data frame.

    library(tidyverse)
    library(knitr)
    # Cargamos el conjunto de datos.
    df <- read.csv("https://aprendeconalf.es/aprendizaje-automatico-practicas-r/datos/pingüinos.csv", stringsAsFactors = TRUE)
    # Mostramos un resumen del data frame.
    glimpse(df)
    Rows: 344
    Columns: 7
    $ Especie          <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adeli…
    $ Isla             <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgersen…
    $ Longitud_pico    <dbl> 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, 4…
    $ Profundidad_pico <dbl> 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, 2…
    $ Longitud_ala     <int> 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186,…
    $ Peso             <int> 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, 4…
    $ Sexo             <fct> macho, hembra, hembra, NA, hembra, macho, hembra, mac…
  2. Realizar un análisis exploratorio de los datos.

    library(skimr)
    # Realizamos un análisis exploratorio de los datos.
    skim(df) 
    Data summary
    Name df
    Number of rows 344
    Number of columns 7
    _______________________
    Column type frequency:
    factor 3
    numeric 4
    ________________________
    Group variables None

    Variable type: factor

    skim_variable n_missing complete_rate ordered n_unique top_counts
    Especie 0 1.00 FALSE 3 Ade: 152, Gen: 124, Chi: 68
    Isla 0 1.00 FALSE 3 Bis: 168, Dre: 124, Tor: 52
    Sexo 11 0.97 FALSE 2 mac: 168, hem: 165

    Variable type: numeric

    skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
    Longitud_pico 2 0.99 43.92 5.46 32.1 39.23 44.45 48.5 59.6 ▃▇▇▆▁
    Profundidad_pico 2 0.99 17.15 1.97 13.1 15.60 17.30 18.7 21.5 ▅▅▇▇▂
    Longitud_ala 2 0.99 200.92 14.06 172.0 190.00 197.00 213.0 231.0 ▂▇▃▅▂
    Peso 2 0.99 4201.75 801.95 2700.0 3550.00 4050.00 4750.0 6300.0 ▃▇▆▃▂
  3. Eliminar del data frame las columnas Isla, Sexo y Peso y eliminar las filas con valores perdidos.

    # Eliminamos las columnas Isla, Sexo y Peso.
    library(dplyr)
    df <- df |> select(-Isla, -Sexo, -Peso) |>
        # Eliminamos las filas con valores perdidos.
        drop_na()
  4. Realizar un diagrama de dispersión tridimensional de las variables Longitud_pico, Profundidad_pico y Longitud_ala coloreando los puntos según la especie de pingüino.

    Utilizar la función plot_ly del paquete plotly para dibujar un diagrama de dispersión tridimensional.

    Parámetros:
    • x = Longitud_pico, y = Profundidad_pico, z = Longitud_ala para indicar las variables a utilizar.
    • color = Especie para colorear los puntos según la especie de pingüino.
    library(plotly)
    # Dibujamos el diagrama de dispersión tridimensional especificando la longitud del pico en el eje x, la profundidad del pico en el eje y y la longitud de la aleta en el eje z. Coloreamos los puntos según la especie de pingüino.
    df |> plot_ly(x = ~Longitud_pico, y = ~Profundidad_pico, z = ~Longitud_ala, 
            color = ~Especie,
            type = "scatter3d", mode = "markers") |>
        layout(title = "Diagrama de dispersión tridimensional de pingüinos",
            scene = list(xaxis = list(title = "Longitud del Pico (mm)"),
                            yaxis = list(title = "Profundidad del Pico (mm)"),
                            zaxis = list(title = "Longitud de la Aleta (mm)")))
  5. Dividir el conjunto de datos en un conjunto de entrenamiento (80%) y un conjunto de prueba (20%).

    Utilizar la función initial_split del paquete rsample para dividir el conjunto de datos en entrenamiento y test.

    Parámetros:
    • data: el data frame con los datos.
    • prop: la proporción del conjunto de datos que se utilizará para el conjunto de entrenamiento (en este caso, 0.8 para el 80%).
    • strata: la variable de estratificación (en este caso, Especie) para asegurar que la distribución de clases se mantenga en ambos conjuntos.
    library(tidymodels)
    # Establecemos una semilla aleatoria para la reproducibilidad.
    set.seed(123)
    # Dividimos el conjunto de datos en entrenamiento (80%) y test (20%).
    df_particion <- initial_split(df, prop = 0.8, strata = "Especie")  
     # Extraemos el conjunto de datos de entrenamiento.
    df_entrenamiento <- training(df_particion)
    # Extraemos el conjunto de datos de test.
    df_test <- testing(df_particion)
  6. Normalizar las variables Longitud_pico, Profundidad_pico y Longitud_ala en el conjunto de entrenamiento.

    Utilizar la función recipe del paquete recipes incluido en la colección de paquetes tidymodels para crear una receta de preprocesamiento.

    Parmámetros:
    • Especie ~. para indicar que la variable Especie es la variable respuesta y se deben utilizar todas las demás variables como predictivas.

    Después, utilizar la función step_normalize para normalizar las variables numéricas.

    Parámetros:
    • all_numeric_predictors() para indicar que se deben utilizar todas las variables numéricas.
    # Definimos la receta de preprocesamiento.
    receta <- recipe(Especie ~ ., data = df_entrenamiento) |>
        # Normalizamos las variables numéricas.
        step_normalize(all_numeric_predictors()) 
  7. Construir un modelo de clasificación de K=11 vecinos más próximos (KNN) para predecir la especie de pingüino a partir de las variables Longitud_pico, Profundidad_pico y Longitud_ala.

    Utilizar la función nearest_neighbor del paquete parsnip para crear un modelo de KNN.

    Parámetros:
    • neighbors: el número de vecinos a considerar (en este caso, 11).

    Después, utilizar la función set_engine para especificar el motor a utilizar (en este caso, kknn).

    Finalmente, utilizar la función set_mode para especificar que se trata de un modelo de clasificación.

    # Definimos el modelo de K vecinos más próximos con K=11.
    modelo <- nearest_neighbor(neighbors = 11) |>
        # Especificamos el motor de entrenamiento.
        set_engine("kknn") |>
        # Especificamos que es un modelo de clasificación.
        set_mode("classification")
    
    # Definimos el flujo de trabajo.
    modelo_entrenado <- workflow() |> 
        # Añadimos la receta de preprocesamiento.
        add_recipe(receta) |>
        # Añadimos el modelo KNN. 
        add_model(modelo) |>
        # Entrenamos el modelo con el conjunto de datos de entrenamiento.
        fit(data = df_entrenamiento) 
  8. Evaluar el modelo de KNN en el conjunto de test y calcular la matriz de confusión y la precisión del modelo.

    Usar la función augment del paquete parsnip para añadir al conjunto de test las probabilidades cada especie de pingüino.

    Parámetros:
    • new_data: el conjunto de datos de test.

    Usar la función conf_mat del paquete yardstick para calcular la matriz de confusión.

    Parámetros:
    • truth: la variable respuesta (en este caso, Especie).
    • estimate: la variable con las clases predichas por el modelo (en este caso, .pred_class).

    Usar la función metrics del paquete yardstick para calcular las métricas de evaluación del modelo.

    Parámetros:
    • truth: la variable respuesta (en este caso, Especie).
    • estimate: la variable con las clases predichas por el modelo (en este caso, .pred_class).
    # Añadimos las predicciones al conjunto de test.
    df_test_aumentado <- modelo_entrenado |> augment(new_data = df_test)
    
    # Calculamos la matriz de confusión.
    matriz_confusion <- df_test_aumentado |> 
        conf_mat(truth = Especie, estimate = .pred_class)
    kable(matriz_confusion$table)
    Adelie Chinstrap Gentoo
    Adelie 31 1 0
    Chinstrap 0 13 0
    Gentoo 0 0 25
    # Calculamos las métricas de evaluación del modelo.
    df_test_aumentado |> metrics(truth = Especie, estimate = .pred_class) |> 
        kable()
    .metric .estimator .estimate
    accuracy multiclass 0.9857143
    kap multiclass 0.9774266
  9. Explorar para qué número de vecinos (K) el modelo de KNN tiene mejor precisión. Para ello, entrenar el modelo de KNN con diferentes valores de K (por ejemplo, 1, 3, 5, 7, 9, 11, 13, 15) y calcular la precisión para cada valor de K mediante validación cruzada de 5 pliegues.

    Utilizar la función tune() del paquete hardhat para definir el parámetro neighbors como un parámetro a afinar en la especificación del modelo KNN.

    Después, utilizar la función tune_grid del paquete tune para optimizar el número de vecinos (K) del modelo KNN.

    Parámetros:
    • resamples: el conjunto de datos de entrenamiento particionado en pliegues para validación cruzada (en este caso, vfold_cv(df_entrenamiento, v = 5)).
    • grid: un data frame con los valores de K a probar.
    # Definimos el modelo KNN con el parámetro neighbors a afinar.
    modelo <- nearest_neighbor(neighbors = tune()) |> 
        # Especificamos el motor de entrenamiento.
        set_engine("kknn") |> 
        # Especificamos que es un modelo de clasificación.
        set_mode("classification") 
    
    # Definimos el flujo de trabajo.
    flujo <- workflow() |> 
        # Añadimos la receta de preprocesamiento.
        add_recipe(receta) |> 
        # Añadimos el modelo KNN.
        add_model(modelo) 
    
    # Entrenamos el modelo con los diferentes valores de K y calculamos las métricas de evaluación.
    modelos_entrenados <- flujo |> tune_grid(
        resamples = vfold_cv(df_entrenamiento, v = 5), # Validación cruzada con 5 pliegues.
        grid = tibble(neighbors = seq(1, 15, by = 2)), # Valores de K a probar.
        metrics = metric_set(accuracy, roc_auc) # Métricas de evaluación a calcular.
    )
    
    # Extraemos las métricas de evaluación de los modelos entrenados.
    collect_metrics(modelos_entrenados) |> kable()
    neighbors .metric .estimator mean n std_err .config
    1 accuracy multiclass 0.9668687 5 0.0069483 Preprocessor1_Model1
    1 roc_auc hand_till 0.9699566 5 0.0050823 Preprocessor1_Model1
    3 accuracy multiclass 0.9668687 5 0.0069483 Preprocessor1_Model2
    3 roc_auc hand_till 0.9990440 5 0.0006096 Preprocessor1_Model2
    5 accuracy multiclass 0.9816162 5 0.0082072 Preprocessor1_Model3
    5 roc_auc hand_till 0.9997531 5 0.0002469 Preprocessor1_Model3
    7 accuracy multiclass 0.9816162 5 0.0082072 Preprocessor1_Model4
    7 roc_auc hand_till 0.9997531 5 0.0002469 Preprocessor1_Model4
    9 accuracy multiclass 0.9816162 5 0.0082072 Preprocessor1_Model5
    9 roc_auc hand_till 0.9997531 5 0.0002469 Preprocessor1_Model5
    11 accuracy multiclass 0.9816162 5 0.0082072 Preprocessor1_Model6
    11 roc_auc hand_till 0.9997531 5 0.0002469 Preprocessor1_Model6
    13 accuracy multiclass 0.9779125 5 0.0068580 Preprocessor1_Model7
    13 roc_auc hand_till 0.9997531 5 0.0002469 Preprocessor1_Model7
    15 accuracy multiclass 0.9779125 5 0.0068580 Preprocessor1_Model8
    15 roc_auc hand_till 0.9997531 5 0.0002469 Preprocessor1_Model8
    # Seleccionamos el mejor valor de K según la exactitud.
    k_final <- modelos_entrenados |> select_best(metric = "accuracy") 
    # Finalizamos el flujo de trabajo construyendo el modelo con el mejor valor de K.
    modelo_final <- flujo |> finalize_workflow(k_final) |>
        # Entrenamos el modelo con lo el número de vecinos óptimo.
        last_fit(modelo_final, split = df_particion)
    
    # Extraemos las métricas de evaluación del modelo entrenado.
    collect_metrics(modelo_final) |> kable()
    .metric .estimator .estimate .config
    accuracy multiclass 0.9857143 Preprocessor1_Model1
    roc_auc hand_till 1.0000000 Preprocessor1_Model1
    brier_class multiclass 0.0120922 Preprocessor1_Model1
  10. Predecir la especie de un pingüino con las siguientes características: longitud del pico 40 mm, profundidad del pico 20 mm y longitud del ala 200 mm.

    Utilizar la función extract_workflow del paquete workflows para extraer el modelo entrenado del flujo de trabajo.

    Utilizar la función predict del paquete parsnip para predecir la especie de pingüino.

    Parámetros:
    • new_data: un data frame con las características del pingüino a predecir.
    # Creamos un data frame con las características del pingüino a predecir.
    nuevo_pingüino <- tibble(Longitud_pico = 40, Profundidad_pico = 20, Longitud_ala = 200) 
    # Extraemos el modelo entrenado del flujo de trabajo.
    extract_workflow(modelo_final) |> 
        # Predecimos la especie del pingüino.
        predict(new_data = nuevo_pingüino) |>  
        kable()
    .pred_class
    Adelie

Ejercicio 5.2 El fichero vinos.csv contiene información sobre las características de vinos blancos y tintos portugueses de la denominación “Vinho Verde”. Las variables que contiene son las siguientes:

Variable Descripción Tipo (unidades)
tipo Tipo de vino Factor (blanco, tinto)
meses.barrica Meses de envejecimiento en barrica Numérica(meses)
acided.fija Cantidad de ácidotartárico Numérica(g/dm3)
acided.volatil Cantidad de ácido acético Numérica(g/dm3)
acido.citrico Cantidad de ácidocítrico Numérica(g/dm3)
azucar.residual Cantidad de azúcar remanente después de la fermentación Numérica(g/dm3)
cloruro.sodico Cantidad de clorurosódico Numérica(g/dm3)
dioxido.azufre.libre Cantidad de dióxido de azufre en forma libre Numérica(mg/dm3)
dioxido.azufre.total Cantidad de dióxido de azufre total en forma libre o ligada Numérica(mg/dm3)
densidad Densidad Numérica(g/cm3)
ph pH Numérica(0-14)
sulfatos Cantidad de sulfato de potasio Numérica(g/dm3)
alcohol Porcentaje de contenido de alcohol Numérica(0-100)
calidad Calificación otorgada por un panel de expertos Numérica(0-10)
  1. Crear un data frame con los datos del archivo vinos.csv.

    library(tidyverse)
    # Cargamos el conjunto de datos.
    df <- read.csv("https://aprendeconalf.es/aprendizaje-automatico-practicas-r/datos/vinos.csv", stringsAsFactors = TRUE)
    # Mostramos un resumen del data frame.
    glimpse(df)
    Rows: 5,320
    Columns: 14
    $ tipo                 <fct> blanco, blanco, blanco, blanco, blanco, blanco, b…
    $ meses_barrica        <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
    $ acided_fija          <dbl> 7.0, 6.3, 8.1, 7.2, 6.2, 8.1, 8.1, 8.6, 7.9, 6.6,…
    $ acided_volatil       <dbl> 0.27, 0.30, 0.28, 0.23, 0.32, 0.22, 0.27, 0.23, 0…
    $ acido_citrico        <dbl> 0.36, 0.34, 0.40, 0.32, 0.16, 0.43, 0.41, 0.40, 0…
    $ azucar_residual      <dbl> 20.70, 1.60, 6.90, 8.50, 7.00, 1.50, 1.45, 4.20, …
    $ cloruro_sodico       <dbl> 0.045, 0.049, 0.050, 0.058, 0.045, 0.044, 0.033, …
    $ dioxido_azufre_libre <dbl> 45, 14, 30, 47, 30, 28, 11, 17, 16, 48, 41, 28, 3…
    $ dioxido_azufre_total <dbl> 170, 132, 97, 186, 136, 129, 63, 109, 75, 143, 17…
    $ densidad             <dbl> 1.0010, 0.9940, 0.9951, 0.9956, 0.9949, 0.9938, 0…
    $ ph                   <dbl> 3.00, 3.30, 3.26, 3.19, 3.18, 3.22, 2.99, 3.14, 3…
    $ sulfatos             <dbl> 0.45, 0.49, 0.44, 0.40, 0.47, 0.45, 0.56, 0.53, 0…
    $ alcohol              <dbl> 8.8, 9.5, 10.1, 9.9, 9.6, 11.0, 12.0, 9.7, 10.8, …
    $ calidad              <int> 6, 6, 6, 6, 6, 6, 5, 5, 5, 7, 5, 7, 6, 8, 6, 5, 7…
  2. Realizar un análisis exploratorio de los datos.

    library(skimr)
    # Realizamos un análisis exploratorio de los datos.
    skim(df)
    Data summary
    Name df
    Number of rows 5320
    Number of columns 14
    _______________________
    Column type frequency:
    factor 1
    numeric 13
    ________________________
    Group variables None

    Variable type: factor

    skim_variable n_missing complete_rate ordered n_unique top_counts
    tipo 0 1 FALSE 2 bla: 3961, tin: 1359

    Variable type: numeric

    skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
    meses_barrica 0 1 1.23 3.14 0.00 0.00 0.00 0.00 24.00 ▇▁▁▁▁
    acided_fija 0 1 7.22 1.32 3.80 6.40 7.00 7.70 15.90 ▂▇▁▁▁
    acided_volatil 0 1 0.34 0.17 0.08 0.23 0.30 0.41 1.58 ▇▂▁▁▁
    acido_citrico 0 1 0.32 0.15 0.00 0.24 0.31 0.40 1.66 ▇▅▁▁▁
    azucar_residual 0 1 5.03 4.41 0.60 1.80 2.70 7.50 26.05 ▇▂▁▁▁
    cloruro_sodico 0 1 0.06 0.04 0.01 0.04 0.05 0.07 0.61 ▇▁▁▁▁
    dioxido_azufre_libre 0 1 30.04 17.81 1.00 16.00 28.00 41.00 289.00 ▇▁▁▁▁
    dioxido_azufre_total 0 1 114.11 56.77 6.00 74.00 116.00 153.25 440.00 ▅▇▂▁▁
    densidad 0 1 0.99 0.00 0.99 0.99 0.99 1.00 1.04 ▇▂▁▁▁
    ph 0 1 3.22 0.16 2.72 3.11 3.21 3.33 4.01 ▁▇▆▁▁
    sulfatos 0 1 0.53 0.15 0.22 0.43 0.51 0.60 2.00 ▇▃▁▁▁
    alcohol 0 1 10.55 1.19 8.00 9.50 10.40 11.40 14.90 ▃▇▅▂▁
    calidad 0 1 5.80 0.88 3.00 5.00 6.00 6.00 9.00 ▁▆▇▃▁
  3. Dividir el conjunto de datos en un conjunto de entrenamiento (80%) y un conjunto de prueba (20%).

    library(tidymodels)
    # Establecemos una semilla aleatoria para la reproducibilidad.
    set.seed(123) 
    # Dividimos el conjunto de datos en entrenamiento (80%) y test (20%) estratificando por la variable tipo.
    df_particion <- initial_split(df, prop = 0.8, strata = "tipo")
    # Extraemos el conjunto de datos de entrenamiento.
    df_entrenamiento <- training(df_particion) 
    # Extraemos el conjunto de datos de test.
    df_test <- testing(df_particion)
  4. Normalizar las variables físico-químicas del vino.

    # Definimos la receta de preprocesamiento.
    receta <- recipe(tipo ~ ., data = df_entrenamiento) |> 
        # Normalizamos las variables numéricas.
        step_normalize(all_numeric_predictors())
  5. Construir un modelo de K vecinos más próximos para clasificar el vino como blanco o tinto a partir de todas las variables físico-químicas del vino. Explorar para qué número de vecinos (K) el modelo de KNN tiene mejor precisión. Para ello, entrenar el modelo de KNN con diferentes valores de K y calcular la precisión para cada valor de K mediante validación cruzada de 10 pliegues.

    library(knitr)
    # Definimos el modelo KNN con el parámetro neighbors a afinar.
    modelo <- nearest_neighbor(neighbors = tune()) |> 
        # Especificamos el motor de entrenamiento.
        set_engine("kknn") |> 
        # Especificamos que es un modelo de clasificación.
        set_mode("classification")
    
    # Definimos el flujo de trabajo.
    flujo <- workflow() |>
        # Añadimos la receta de preprocesamiento.
        add_recipe(receta) |>
        # Añadimos el modelo KNN.
        add_model(modelo)
    
    # Entrenamos el modelo con los diferentes valores de K y calculamos las métricas de evaluación utilizando validación cruzada de 10 pliegues.
    modelos_entrenados <- flujo |> tune_grid(
        resamples = vfold_cv(df_entrenamiento, v = 10), # Validación cruzada con 10 pliegues.
        grid = tibble(neighbors = seq(1, 15, by = 2)), # Valores de K a probar.
        metrics = metric_set(accuracy, roc_auc) # Métricas de evaluación a calcular.
    ) 
    
    # Dibujamos la exactitud y el área bajo la curva ROC en función del número de vecinos.
    autoplot(modelos_entrenados)

     # Seleccionamos el mejor valor de K según la exactitud.
    k_final <- select_best(modelos_entrenados, metric = "accuracy")
    # Finalizamos el flujo de trabajo con el mejor valor de K.
    modelo_entrenado <- finalize_workflow(flujo, k_final) |> 
        # Entrenamos el modelo con el número de vecinos óptimo.
        last_fit(modelo_entrenado, split = df_particion)
    
    # Extraemos las métricas de evaluación del modelo entrenado.
    collect_metrics(modelo_entrenado) |> kable()
    .metric .estimator .estimate .config
    accuracy binary 0.9953052 Preprocessor1_Model1
    roc_auc binary 0.9936995 Preprocessor1_Model1
    brier_class binary 0.0050876 Preprocessor1_Model1
  6. Construir otro modelo de K vecinos más próximos para predecir la calidad del vino a partir de todas las variables físico-químicas del vino. Explorar para qué número de vecinos (K) el modelo de KNN tiene mejor precisión. Para ello, entrenar el modelo de KNN con diferentes valores de K (por ejemplo de 10 a 30) y calcular la precisión para cada valor de K mediante validación cruzada de 10 pliegues. Dibujar un gráfico con el RMSE en función del número de vecinos (K).

    # Definimos la receta de preprocesamiento.
    receta <- recipe(calidad ~ ., data = df_entrenamiento) |> 
        # Normalizamos las variables numéricas.
        step_normalize(all_numeric_predictors())
    
    # Definimos el modelo KNN con el parámetro neighbors a afinar.
    modelo <- nearest_neighbor(neighbors = tune()) |> 
        # Especificamos el motor de entrenamiento.
        set_engine("kknn") |>
        # Especificamos que es un modelo de regresión.
        set_mode("regression")
    
    # Definimos el flujo de trabajo.
    flujo <- workflow() |>
        # Añadimos la receta de preprocesamiento.
        add_recipe(receta) |>
        # Añadimos el modelo KNN.
        add_model(modelo)
    
    # Establecemos una semilla aleatoria para la reproducibilidad.
    set.seed(123) 
    # Entrenamos el modelo con los diferentes valores de K y calculamos las métricas de evaluación utilizando validación cruzada de 10 pliegues.
    modelos_entrenados <- flujo |>  tune_grid(
        resamples = vfold_cv(df_entrenamiento, v = 10), # Validación cruzada con 10 pliegues.
        grid = tibble(neighbors = 20:40), # Valores de K a probar.
        metrics = metric_set(rmse, rsq) # Métricas de evaluación a calcular.
    ) 
    
    # Dibujamos el RMSE en función del número de vecinos.
    autoplot(modelos_entrenados)

    A la vista del gráfico, el mejor modelo se obtiene para K=31 vecinos.

  7. Construir el modelo de K vecinos más próximos con el mejor valor de K y evaluarlo en el conjunto de test.

    k_final <- select_best(modelos_entrenados, metric = "rmse") # Seleccionamos el mejor valor de K según el RMSE.
    modelo_entrenado <- finalize_workflow(flujo, k_final) |> # Finalizamos el flujo de trabajo con el mejor valor de K.
        last_fit(modelo_entrenado, split = df_particion) # Entrenamos el modelo con el conjunto de entrenamiento.
    collect_metrics(modelo_entrenado) |>  # Extraemos las métricas de evaluación del modelo entrenado.
        kable()
    .metric .estimator .estimate .config
    rmse standard 0.6504289 Preprocessor1_Model1
    rsq standard 0.4267465 Preprocessor1_Model1

5.2 Ejercicios Propuestos

Ejercicio 5.3 El conjunto de datos glaucoma.csv contiene información sobre el grosor de los sectores de los anillos peripapilares de la capa de fibras nerviosas de la retina obtenidos mediante tomografía de coherencia óptica (OTC) en pacientes con y sin glaucoma. En la OTC se toman 4 anillos con distintos radios (BMO, 3.5 mm, 4.1 mm y 4.7 mm) y para cada anillo se miden 6 sectores (Nasal Superior, Nasal, Nasal Inferior, Temporal Inferior, Temporal y Temporal Superior) y también la media global. Los datos están ya normalizados.

Tomografía de coherencia óptica
  1. Cargar el conjunto de datos del archivo glaucoma.csv en un data frame.

  2. Construir otro modelo de K vecinos más próximos para predecir el glaucoma a partir del grosor de los anillos peripapilares usando el número de vecinos óptimo.