La técnica de aprendizaje supervisado KNN (vecinos más próximos) permite realizar tanto regresión como clasificación. Para el estudio de esta técnica, utilicemos el conjunto \(\texttt{clima}\) del paquete \(\texttt{datos}\).

library(datos)
dat <- clima |> 
  glimpse()
## Rows: 26,115
## Columns: 15
## $ origen           <chr> "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EWR", "EWR…
## $ anio             <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013,…
## $ mes              <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ dia              <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ hora             <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17…
## $ temperatura      <dbl> 39.02, 39.02, 39.02, 39.92, 39.02, 37.94, 39.02, 39.9…
## $ punto_rocio      <dbl> 26.06, 26.96, 28.04, 28.04, 28.04, 28.04, 28.04, 28.0…
## $ humedad          <dbl> 59.37, 61.63, 64.43, 62.21, 64.43, 67.21, 64.43, 62.2…
## $ direccion_viento <dbl> 270, 250, 240, 250, 260, 240, 240, 250, 260, 260, 260…
## $ velocidad_viento <dbl> 10.35702, 8.05546, 11.50780, 12.65858, 12.65858, 11.5…
## $ velocidad_rafaga <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ presion          <dbl> 1012.0, 1012.3, 1012.5, 1012.2, 1011.9, 1012.4, 1012.…
## $ visibilidad      <dbl> 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 1…
## $ fecha_hora       <dttm> 2013-01-01 01:00:00, 2013-01-01 02:00:00, 2013-01-01…

KNN para Regresión

Paso 1. Depuración del conjunto de datos

Para poder aplicar esta técnica como regresión es necesario disponer de variables cuantitativas, por ello vamos a depurar los datos, centrándonos en un único aeropuerto y eliminando las variables que no tienen relación con el clima, además de eliminar los valores ausentes. En concreto, vamos a considerar las variables que hemos comprobado que sí influyen sobre el punto de rocío.

dat_interes <- dat |> 
  filter(
    origen == "JFK"
  ) |> 
  select(punto_rocio,temperatura,humedad,precipitacion,velocidad_rafaga) |> 
  drop_na() |> 
  glimpse()
## Rows: 1,507
## Columns: 5
## $ punto_rocio      <dbl> 17.96, 17.06, 14.00, 10.94, 8.06, 8.06, 8.06, 8.06, 8…
## $ temperatura      <dbl> 37.94, 37.04, 33.08, 30.02, 26.96, 26.06, 26.06, 24.9…
## $ humedad          <dbl> 44.00, 43.85, 44.92, 44.41, 44.25, 45.93, 45.93, 48.0…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ velocidad_rafaga <dbl> 24.16638, 25.31716, 24.16638, 29.92028, 35.67418, 29.…

Paso 2. Búsqueda del valor de \(k\)

El paquete \(\texttt{caret}\) dispone de la función \(\texttt{knnreg()}\) que permite aplicar el método de vecinos más próximos para realizar regresión. Esta técnica predice el valor de la variable respuesta \(Y\) a partir la media de los \(k\) valores más cercanos a las observaciones de las variables predictoras \(X_1,...,X_p\). Por lo que es necesario previamente conocer el valor óptimo de \(k\), además de definir una medida que nos permite controlar el concepto de cercanía.

Por tanto, consideraremos la distancia euclídea entre los puntos como la medida de cercanía, esto es, para dos observaciones \(x = (x_1,...,x_p), \, z = (z_1,...,z_p)\), se define la distancia entre ellos como \[d(x,z) = \sqrt{(x_1 - z_1)^2 + \cdots + (x_p - z_p)^2}.\]

En cuanto al valor óptimo de \(k\) que nos permite conocer la cantidad de vecinos a emplear se utilizará una técnica de validación cruzada para identificarlo. Para ello, se emplea la función \(\texttt{train(method,preProc,metric,trControl,tuneGrid)}\) que nos permite entrenar los distintos métodos (a través del argumento \(\texttt{method}\)), además de indicar el error a optimizar (\(\texttt{metric}\)), y el tipo de preprocesado de los datos (\(\texttt{preProc}\)), pues esta técnica obliga que las variables estén estandarizados.

Respecto a la técnica de validación a emplear se controla a través del argumento \(\texttt{trControl}\) siendo necesario utilizar la función \(\texttt{trainControl()}\) y la rejilla en la que debe moverse el parámetro a optimizar a través del argumento \(\texttt{tuneGrid}\).

library(caret)
## Loading required package: lattice
## 
## Attaching package: 'caret'
## The following object is masked from 'package:purrr':
## 
##     lift
KNN_10FCV <- dat_interes |> 
  train(punto_rocio ~ .,
        data = _,
        method = "knn",
        preProc = c("center","scale"),
        metric = "RMSE",
        trControl = trainControl(method = "cv", number = 10),
        tuneGrid = data.frame(k = 1:40)
  )

KNN_10FCV |> 
  ggplot() +
  labs(x = "Número de vecinos (k)",
       y = "RMSE",
       title = "10-Folds CV")

Al representar el número de vecinos k junto a la raíz del error cuadrático medio RMSE se observa que a partir de \(k\) = 3, el RMSE comienza a aumentar, por tanto el valor óptimo que permite minimizar el error cuadrático medio es \(k\) = 3.

tibble(k = KNN_10FCV$bestTune$k,
       RMSE = mean(KNN_10FCV$results$RMSE))
## # A tibble: 1 × 2
##       k  RMSE
##   <int> <dbl>
## 1     3  2.82

Paso 3. Aplicación de la Técnica de Regresión: KNN

Conjuntos de Entrenamiento y Validación

Para aplicar el modelo de regresión KNN es necesario obtener los distintos conjuntos de entrenamiento y validación.

set.seed(1234)
library(rsample)
dat_split <- dat_interes |> 
  initial_split(prop = 0.7)

dat_train <- dat_split |> 
  training() |> 
  glimpse()
## Rows: 1,054
## Columns: 5
## $ punto_rocio      <dbl> 12.02, 51.08, 46.04, 62.96, 8.06, 59.00, 5.00, 48.92,…
## $ temperatura      <dbl> 33.98, 82.94, 69.98, 84.02, 44.06, 60.80, 41.00, 75.9…
## $ humedad          <dbl> 39.72, 33.20, 42.29, 49.22, 22.45, 96.22, 21.97, 38.5…
## $ precipitacion    <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.23, 0.00, 0.00, 0.00,…
## $ velocidad_rafaga <dbl> 24.16638, 17.26170, 23.01560, 23.01560, 29.92028, 23.…
dat_test <- dat_split |> 
  testing() |> 
  glimpse()
## Rows: 453
## Columns: 5
## $ punto_rocio      <dbl> 17.96, 17.06, 14.00, 8.06, 8.06, 8.06, 10.04, 10.04, …
## $ temperatura      <dbl> 37.94, 37.04, 33.08, 26.96, 26.06, 24.08, 30.92, 33.9…
## $ humedad          <dbl> 44.00, 43.85, 44.92, 44.25, 45.93, 49.87, 41.13, 36.3…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ velocidad_rafaga <dbl> 24.16638, 25.31716, 24.16638, 35.67418, 27.61872, 24.…

Aplicación del modelo KNN

A continuación, aplicamos el método KNN sobre el conjunto de entrenamiento para posteriormente aplicar sobre el conjunto de validación las predicciones para determinar las medidas de precisión \(MSE\) y \(RMSE\). Para aplicar el modelo emplearemos la función \(\texttt{knnreg(k)}\).

model_KNN <- dat_train |> 
  knnreg(punto_rocio ~ .,
         data = _,
         k = KNN_10FCV$bestTune$k)

Paso 4. Predicción y Medidas de Precisión

Para construir las predicciones empleamos la función \(\texttt{predict()}\).

dat_predict <- dat_test |> 
  mutate(
    predictions_KNN = model_KNN |> 
      predict(newdata = dat_test)
  ) |> 
  glimpse()
## Rows: 453
## Columns: 6
## $ punto_rocio      <dbl> 17.96, 17.06, 14.00, 8.06, 8.06, 8.06, 10.04, 10.04, …
## $ temperatura      <dbl> 37.94, 37.04, 33.08, 26.96, 26.06, 24.08, 30.92, 33.9…
## $ humedad          <dbl> 44.00, 43.85, 44.92, 44.25, 45.93, 49.87, 41.13, 36.3…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ velocidad_rafaga <dbl> 24.16638, 25.31716, 24.16638, 35.67418, 27.61872, 24.…
## $ predictions_KNN  <dbl> 18.62, 15.38, 14.00, 8.66, 8.00, 8.06, 9.68, 10.34, 1…
dat_predict |> 
  summarise(
    MSE = mean((punto_rocio - predictions_KNN)^2),
    RSME = sqrt(MSE)
  ) |> 
  glimpse()
## Rows: 1
## Columns: 2
## $ MSE  <dbl> 1.854167
## $ RSME <dbl> 1.361678

De manera que las predicciones obtenidas tienen un error medio de \(\pm\) 1.3617 ºF respecto a los valores reales.

KNN para Clasificación

Paso 1. Depuración del conjunto de datos

En esta ocasión, vamos a aplicar la técnica KNN para clasificar las observaciones para la variable \(\texttt{visibilidad}\) a partir de la información proporcionada por las variables \(\texttt{humedad}\) y \(\texttt{precipitacion}\). En primer lugar, vamos a observar qué valores toma la variable respuesta.

dat_interes <- dat |> 
  select(visibilidad,humedad,precipitacion) |> 
  drop_na() |> 
  glimpse()
## Rows: 26,114
## Columns: 3
## $ visibilidad   <dbl> 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, …
## $ humedad       <dbl> 59.37, 61.63, 64.43, 62.21, 64.43, 67.21, 64.43, 62.21, …
## $ precipitacion <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

Vemos que es una variable cuantitativa y para poder aplicar las técnicas de clasificación es necesario disponer de una variable cualitativa. Por ello, vamos a transformar dicha variable a partir de una condición. En concreto, diremos que hay visibilidad si presenta valores inferiores a su mediana, y diremos que No hay visibilidad si son superiores a la mediana.

dat_interes <- dat_interes |> 
  mutate(
    visibilidad_fact = factor(ifelse(visibilidad < median(visibilidad),
                              "Yes",
                              "No"))
  ) |> 
  glimpse()
## Rows: 26,114
## Columns: 4
## $ visibilidad      <dbl> 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 1…
## $ humedad          <dbl> 59.37, 61.63, 64.43, 62.21, 64.43, 67.21, 64.43, 62.2…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ visibilidad_fact <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, N…

Además, debemos comprobar cómo está codificada la variable respuesta.

dat_interes |> 
  pull(visibilidad_fact) |> 
  contrasts()
##     Yes
## No    0
## Yes   1

Observando que la categoría Yes está codificada como 1 y la categoría No como 0. Una vez comprobado, quedémonos con las variables de interés para nuestro modelo.

dat_interes <- dat_interes |> 
  select(visibilidad_fact,humedad,precipitacion) |> 
  glimpse()
## Rows: 26,114
## Columns: 3
## $ visibilidad_fact <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, N…
## $ humedad          <dbl> 59.37, 61.63, 64.43, 62.21, 64.43, 67.21, 64.43, 62.2…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

Paso 2. Búsqueda del valor de \(k\)

Ahora ya podemos clasificar la variable \(\texttt{visibilidad}\) a partir de las variables predictoras utilizando KNN. Esta técnica asigna una observación a la categoría mayoritaria de la variable respuesta a la que pertenecen los \(k\) vecinos más próximos. En esta ocasión es necesario indicar el valor de \(k\) previamente para decidir cuántos vecinos son necesarios para poder clasificar una observación de modo preciso. En concreto, se busca el valor de \(k\) que maximice la exactitud del modelo.

Para ello utilizamos la función \(\texttt{knn()}\) del paquete \(\texttt{caret}\) que permite aplicar el método KNN utilizando las técnicas de validación cruzada.

library(caret)

CKNN_10FCV <- dat_interes |> 
  train(visibilidad_fact ~ .,
        data = _,
        method = "knn",
        preProc = c("center","scale"),
        metric = "Accuracy",
        trControl = trainControl(method = "cv", number = 10),
        tuneGrid = data.frame(k = 1:40)
  )

CKNN_10FCV |> 
  ggplot() +
  labs(x = "Número de vecinos (k)",
       y = "Accuracy",
       title = "10-Folds CV") +
  theme_minimal()

Como se puede observar el valor que proporciona una mayor exactitud del modelo es \(k\) = 24.

tibble(k = CKNN_10FCV$bestTune$k,
       Accuracy = mean(CKNN_10FCV$results$Accuracy))
## # A tibble: 1 × 2
##       k Accuracy
##   <int>    <dbl>
## 1    24    0.904

Paso 3. Aplicación de la Técnica de Clasificación: KNN

Conjuntos de Entrenamiento y Validación

Para aplicar el modelo de clasificación KNN es necesario obtener los distintos conjuntos de entrenamiento y validación.

set.seed(1234)
library(rsample)
dat_split <- dat_interes |> 
  initial_split(prop = 0.7)

dat_train <- dat_split |> 
  training() |> 
  glimpse()
## Rows: 18,279
## Columns: 3
## $ visibilidad_fact <fct> No, Yes, No, Yes, No, No, No, No, No, Yes, No, No, No…
## $ humedad          <dbl> 35.80, 61.63, 53.63, 87.28, 33.61, 43.00, 61.72, 83.4…
## $ precipitacion    <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,…
dat_test <- dat_split |> 
  testing() |> 
  glimpse()
## Rows: 7,835
## Columns: 3
## $ visibilidad_fact <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, N…
## $ humedad          <dbl> 61.63, 62.21, 64.43, 62.21, 62.21, 57.06, 69.67, 54.6…
## $ precipitacion    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

Aplicación del modelo KNN

A continuación, aplicamos el método KNN para clasificar observaciones de la variable respuesta utilizando \(k\) = 24 sobre el conjunto de entrenamiento para posteriormente aplicar sobre el conjunto de validación las clasificaciones para determinar las medidas de precisión a partir de la matriz de confusión. Para aplicar el modelo emplearemos la función \(\texttt{knn3(k)}\) del paquete \(\texttt{caret}\).

library(caret)

model_CKNN <- dat_train |> 
  knn3(visibilidad_fact ~ .,
         data = _,
         k = CKNN_10FCV$bestTune$k)

model_CKNN
## 24-nearest neighbor model
## Training set outcome distribution:
## 
##    No   Yes 
## 15328  2951

Paso 4. Clasificación y Medidas de Precisión

Para clasificar las observaciones vamos a utilizar la función \(\texttt{predict(model)}\) pero añadiéndole el argumento \(\texttt{type} = \texttt{"class"}\).

library(caret)

dat_clasification <- dat_test |> 
  mutate(
    clasification_KNN = model_CKNN |> 
      predict(newdata = dat_test,
              type = "class")
  ) |> 
  glimpse()
## Rows: 7,835
## Columns: 4
## $ visibilidad_fact  <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, …
## $ humedad           <dbl> 61.63, 62.21, 64.43, 62.21, 62.21, 57.06, 69.67, 54.…
## $ precipitacion     <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ clasification_KNN <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, …

Una vez que se han clasificado nuevas observaciones podemos proceder a calcular las medidas de precisión a través de la matriz de confusión. Para calcular dicha matriz utilizamos la función \(\texttt{confusionMatrix(positive,mode="everything")}\) del paquete \(\texttt{caret}\) indicando con el argumento \(\texttt{positive}\) la categoría de observaciones positivas (en nuestro caso, observaciones que tienen visibilidad).

Es necesario tener en cuenta que debemos seleccionar las variables de interés, en esta ocasión, las clasificaciones (como factor) y la variable respuesta, además debemos construir la tabla de frecuencias con la función \(\texttt{table()}\).

library(caret)

dat_clasification |> 
  select(clasification_KNN, visibilidad_fact) |> #Se utiliza el conjunto de clasificaciones predichas en primer lugar
  table() |> 
  confusionMatrix(positive = "Yes",
                  mode = "everything") 
## Confusion Matrix and Statistics
## 
##                  visibilidad_fact
## clasification_KNN   No  Yes
##               No  6345  619
##               Yes  174  697
##                                           
##                Accuracy : 0.8988          
##                  95% CI : (0.8919, 0.9054)
##     No Information Rate : 0.832           
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.5814          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.52964         
##             Specificity : 0.97331         
##          Pos Pred Value : 0.80023         
##          Neg Pred Value : 0.91111         
##               Precision : 0.80023         
##                  Recall : 0.52964         
##                      F1 : 0.63740         
##              Prevalence : 0.16796         
##          Detection Rate : 0.08896         
##    Detection Prevalence : 0.11117         
##       Balanced Accuracy : 0.75147         
##                                           
##        'Positive' Class : Yes             
## 

Podemos observar que:

  • \(VP\) = 6345 observaciones que No tienen visibilidad y se han clasificado correctamente.
  • \(FN\) = 619 observaciones que tienen visibilidad y se han clasificado incorrectamente.
  • \(FP\) = 174 observaciones que No tienen visibilidad y se han clasificado incorrectamente.
  • \(VN\) = 697 observaciones que tienen visibilidad y se han clasificado correctamente.

En cuanto a las medidas de precisión, tenemos que:

  • \(Precision\) = 0.8002, esto es, el 80.02 % de las observaciones que se han clasificado con visibilidad realmente la tienen.
  • \(Recall\) = 0.5296, es decir, el 52.96 % de las observaciones con visibilidad se han clasificado correctamente.
  • \(Specificity\) = 0.9733, por tanto el 97.33 % de las observaciones sin visibilidad se han clasificado correctamente.
  • \(Accuracy\) = 0.8988, de modo que el modelo proporciona el 89.88 % de clasificaciones correctas.
  • \(F1-Score\) = 0.6374, el modelo proporciona un balance moderado entre su precisión y su sensibilidad.