Capítulo 10 Aprendizaje Supervisado
Para entender el aprendizaje supervisado de forma intuitiva usaremos un ejemplo cotidiano. Todos hemos ido al doctor y alguna vez le hemos dicho que nos duele la garganta, que hemos tenido dolor de cabeza y fiebre. Éste nos hará unas cuantas preguntas más y luego nos dirá qué enfermedad podríamos tener y qué tratamiento seguir.
Intuitivamente sabemos que el doctor tuvo que entrenar inicialmente a partir de clases y libros donde muestran casos pasados, y estudiar qué síntomas son señal de cada enfermedad. Luego, empezó a testear lo aprendido en un grupo de pacientes durante su internado. Finalmente, cuando ya estaba entrenado tuvo licencia para poder aplicar este aprendizaje a pacientes en su consultorio u hospital.
Este es un ejemplo de aprendizaje supervisado porque el entrenamiento se realizó a partir de datos conocidos o inputs los cuales están etiquetados (duele la garganta, dolor de cabeza, fiebre) con la finalidad de obtener un resultado o output que también era conocido y etiquetado (¿tiene gripe o no?). Cuando un doctor testea lo aprendido se sabe los inputs de pacientes y también el output que es dado por un doctor con más experiencia que puede decir qué tan efectivo es su entrenamiento. Cuando el doctor sale a atender pacientes solo tendrá inputs etiquetados con la finalidad de predecir un output etiquetado.
Ésta es la lógica que se ha llevado a algoritmos computacionales. Lo podemos ver en facebook, quienes recopilan una serie de inputs, como nuestros likes, compartidos, etc, para predecir qué podríamos querer consumir y nos lo muestra a modo de recomendación. Y también lo veremos en nuestro ambiente de trabajo cuando tengamos inputs de nuestros clientes, como consumo, nivel adquisitivo, lugar donde vive, etc, para predecir cuál de nuestros productos predecimos que es más propenso a comprar y así llamarlo para ofrecerle ese producto.
10.1 Clasificacion y Regresión
Existen múltiples algoritmos de aprendizaje supervisado, pero los diferenciaremos en dos de acuerdo al tipo de variable que manejemos.
Cuando la variable sea discreta los llamaremos clasificación. Los ejemplos líneas arriba son muestra de ello. Hemos clasificado en dos clases (gripe o no) o varias clases (producto “x” a recomendar)
Cuando la variable sea continua los llamaremos regresión. La predicción de los precios de una casa dadas las características de la casa como tamaño, precio, etc. es uno de los ejemplos comunes de regresión.
En las siguientes secciones aprenderemos algunos algoritmos señalando si son de clasificación de regresión.
10.2 kNN: k vecinos más próximos
Comencemos por un simple, pero muy útil algoritmo de clasificación, el algoritmo de los k vecinos más próximos (kNN por sus siglas en inglés).
10.2.1 Dos variables como input
Comencemos por entenderlo visualmente. Imaginemos que tenemos dos variable como input y como output nos da si es Clase Roja o Clase Azul. Esta data es nuestra data de entrenamiento.
Ahora que ya tenemos nuestra data de entrenamiento empezaremos a usar la data de test. Como queremos predecir la clase, el output, veremos cómo uno de estos datos se vería visualmente y lo pintaremos de color amarillo. A continuación calculamos la distancia entre este punto y los demás datos.
Hemos trazado solo algunas distancias, pero podríamos hacerlo con todos. Para este ejemplo tomaremos los k = 3 vecinos más cercanos.
Notamos que si nos enfocamos solo en los 3 vecinos más cercanos hay más rojos que azules, entonces nuestra predicción será que este punto ha de ser clase R (rojo).
Calcular la distancia en un plano cartesiano es relativamente sencillo, solo tenemos variables como input: en el eje x y eje y. Sin embargo, la misma lógica se puede llevar a más variables.
10.2.2 Múltiples variables como input
Veamos cómo sería con 4 variables como input. Vamos a trabajar nuevamente con el data frame iris
, el cual, como recordaremos, tiene 4 atributos de una planta y la última columna es la especie a la que pertenece.
data(iris)
iris %>%
head(10)
#> Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> 1 5.1 3.5 1.4 0.2 setosa
#> 2 4.9 3.0 1.4 0.2 setosa
#> 3 4.7 3.2 1.3 0.2 setosa
#> 4 4.6 3.1 1.5 0.2 setosa
#> 5 5.0 3.6 1.4 0.2 setosa
#> 6 5.4 3.9 1.7 0.4 setosa
#> 7 4.6 3.4 1.4 0.3 setosa
#> 8 5.0 3.4 1.5 0.2 setosa
#> 9 4.4 2.9 1.4 0.2 setosa
#> 10 4.9 3.1 1.5 0.1 setosa
La idea es la siguiente, tomaremos una data de entrenamiento, 50 datos. De esta data tenemos los 4 atributos de input y la última columna es el output, la especie. Utilizaremos el algoritmo kNN tomando como input esta data de entrenamiento para crear nuestro modelo. Luego, con una data de testing, otros 50 datos, probaremos nuestro modelo.
Comencemos tomemos una muestra aleatoria de 100 registros y separaremos la mitad para entrenamiento y la mitad para testear. Dado que tenemos 150 datos en nuestro data frame, tomemos una muestra de los índices. En este caso vamos a utilizar la función set.seed(n)
para forzar a que los valores de la muestra aleatoria sean los mismos siempre. Así, poder todos obtener los mismos resultados y la explicación del libro en estos capítulos sea coherente con los resultados que cada lector obtenga. Para un ejercicio real no deberíamos de incluir esa línea. Se recomienda leer la documentación ?set.seed()
.
# 28 es el día de cumpleaños del autor
set.seed(28)
index_muestra <- sample(150, 100)
index_entrena <- sample(index_muestra, 50)
index_test <- index_muestra[!index_muestra %in% index_entrena]
Ahora que tenemos los índices podemos construir nuestra data de entrenamiento y nuestro test.
iris_entrena <- iris[index_entrena, ]
iris_test <- iris[index_test, ]
iris_entrena_input <- iris_entrena[, -5]
iris_entrena_output <- iris_entrena[, 5]
iris_test_input <- iris_test[, -5]
iris_test_output <- iris_test[, 5]
Si bien podríamos construir los algoritmos para calcular las distancias mínimas para cada punto, R nos provee de librerías que nos facilitan la creación de estos modelos. Para ello, cargaremos la librería class
, la cual nos permitirá ejecutar kNN rápidamente.
Esta libreriá nos provee la función knn()
, la cual tomará los datos de entrenamiento para crear el modelo y una vez creado el modelo tomará los datos de test para predecir el output para nuestra data de test.
iris_test_output_kNN <- knn(train = iris_entrena_input,
cl = iris_entrena_output,
test = iris_test_input,
k = 3)
iris_test_output_kNN
#> [1] versicolor versicolor versicolor versicolor setosa versicolor
#> [7] virginica virginica virginica virginica versicolor versicolor
#> [13] virginica versicolor versicolor versicolor setosa versicolor
#> [19] versicolor virginica virginica setosa versicolor versicolor
#> [25] versicolor virginica setosa setosa versicolor versicolor
#> [31] virginica setosa setosa virginica virginica setosa
#> [37] setosa virginica setosa versicolor setosa virginica
#> [43] setosa setosa setosa virginica virginica versicolor
#> [49] virginica versicolor
#> Levels: setosa versicolor virginica
Asi, la función knn nos arroja la predicción solo con ingresarle como atributos la data de entrenamiento, los inputs del test y cuántos vecinos cercanos buscará (k). Y no solo eso, podemos comparar nuestra predicción con los output del test para ver qué tan exacto (accuracy en inglés) es nuestro modelo. Para ello calculamos el porcentaje de aciertos de la predicción respecto al output del test.
Además, podemos colocar un resumen en una tabla, también conocida como matriz de confusión, para ver cuántos valores predichos fueron iguales a los reales utilizando la función table()
.
table(iris_test_output_kNN, iris_test_output)
#> iris_test_output
#> iris_test_output_kNN setosa versicolor virginica
#> setosa 14 0 0
#> versicolor 0 18 2
#> virginica 0 1 15
Intepretemos celda a celda este resultado:
- Nuestro modelo kNN predijo 14 valores como especie “setosa” y resulta que en nuestro test el valor real, output era también setosa.
- Nuestro modelo predijo 20 como especie versicolor. Sin embargo, en la data real-test, de esos 20, solo 18 son versicolor y 2 son virginica.
- Nuestro modelo predijo 16 como especie virginica. Sin embargo, en la data real-test, de esos 16, solo 15 son virginica.
10.2.3 Diversos valores de k
Hasta el momento solo hemos utilizado un solo valor para k, 3 vecinos más cercanos. Sin embargo, podríamos ver la exactitud o accuracy para diferentes valores de k. Como tenemos 50 valores en nuestra data de entrenamiento veremos los aciertos tomando máximo 50 vecinos más cercanos.
k <- 1:50
resultado <- data.frame(k, precision = 0)
for(n in k){
iris_test_output_kNN <- knn(train = iris_entrena_input,
cl = iris_entrena_output,
test = iris_test_input,
k = n)
resultado$precision[n] <- mean(iris_test_output_kNN == iris_test_output)
}
resultado %>%
ggplot() +
aes(k, precision) +
geom_line()
Como vemos, para este caso, a partir de cierto número de vecinos más cercanos el nivel de aciertos de nuestro algoritmo empieza a reducirse. Dependerá de cada caso el escoger el mejor “k” para nuestro modelo.
Ya hemos construido así nuestro primer modelo de machine learning.
10.3 Paquete caret
Ahora que ya hemos creado nuestro primer modelo de machine learning nos hemos visto con muchas líneas de código. Por ejemplo, para partir la muestra en entrenamiento y test, para calcular el “k” óptimo, etc. Para hacer el trabajo más sencillo usaremos la librería caret
. Caret14 es un potente paquete que proporciona soporte para más de 230 algoritmos de machine learning. También proporciona excelentes funciones para muestrear los datos (para entrenamiento y test), preprocesamiento, creación del modelo, evaluación del modelo, etc.
Vamos a hacer otro ejemplo con k vecinos más cercanos, pero ésta vez utilizando las funciones de la librería Caret. La data de este ejemplo la obtendremos de la librería ISLR
, la cual contiene los porcentajes diarios de rendimiento para el índice bursátil S&P 500 entre el 2001 y 2005. Este data frame tiene 8 columnas que usaremos de input y la última columna que tiene dos clases (si sube o baja el índice) que usaremos como output (Ver ?Smarket
).
# Data frame que usaremos
Smarket %>%
head(10)
#> Year Lag1 Lag2 Lag3 Lag4 Lag5 Volume Today Direction
#> 1 2001 0.381 -0.192 -2.624 -1.055 5.010 1.1913 0.959 Up
#> 2 2001 0.959 0.381 -0.192 -2.624 -1.055 1.2965 1.032 Up
#> 3 2001 1.032 0.959 0.381 -0.192 -2.624 1.4112 -0.623 Down
#> 4 2001 -0.623 1.032 0.959 0.381 -0.192 1.2760 0.614 Up
#> 5 2001 0.614 -0.623 1.032 0.959 0.381 1.2057 0.213 Up
#> 6 2001 0.213 0.614 -0.623 1.032 0.959 1.3491 1.392 Up
#> 7 2001 1.392 0.213 0.614 -0.623 1.032 1.4450 -0.403 Down
#> 8 2001 -0.403 1.392 0.213 0.614 -0.623 1.4078 0.027 Up
#> 9 2001 0.027 -0.403 1.392 0.213 0.614 1.1640 1.303 Up
#> 10 2001 1.303 0.027 -0.403 1.392 0.213 1.2326 0.287 Up
# Hacemos unas traducciones para facilitad del análisis
Smarket <- Smarket %>%
rename(Direccion = Direction) %>%
mutate(Direccion = ifelse(Direccion == "Up", "Sube", "Baja")) %>%
mutate_at(c("Direccion"), ~as.factor(.))
10.3.1 Creación de data de entrenamiento y test
Del total de nuestro data frame partiremos una parte de la data para entrenamiento y la otra para hacer los test. Para ello, caret nos ofrece la función createDataPartition(y = outcomes, p = proporcion, list = FALSE)
la cual nos permitirá partir una proporción para entrenamiento y dejar el resto para testing. Vamos a destinar el 75% de los datos para entrenamiento y el atributo list
lo dejamos en falso porque no queremos un objeto lista. El resultado de esta función es una serie de índices aleatorios, los cuales usaremos para construir nuestra data de entrenamiento y test.
set.seed(28)
indxEntrena <- createDataPartition(y = Smarket$Direccion, p = 0.75, list = FALSE)
SP_entrena <- Smarket[indxEntrena,]
SP_test <- Smarket[-indxEntrena,]
Esta función nos hace mucho más sencillo el muestro de datos y ya podríamos empezar a crear nuestro modelo.
10.3.2 Entrenando nuestro algoritmo de predicción
Para crear nuestro modelo de predicción y entrenarlo usaremos la función train()
y especificaremos el atributo tuneLength = 20
para indicar que queremos que nos evalúe 20 diferentes valores para “k”. A diferencia de la función utilizada cuando construimos nuestro anterior modelo kNN, con caret no tenemos que separar los inputs de los outputs, sino lo ingresamos como un parámetro más de la función train()
.
set.seed(28)
SP_knnEntrenado <- train(Direccion ~ .,
data = SP_entrena,
method = "knn",
tuneLength = 20
)
class(SP_knnEntrenado)
#> [1] "train" "train.formula"
SP_knnEntrenado
#> k-Nearest Neighbors
#>
#> 938 samples
#> 8 predictor
#> 2 classes: 'Baja', 'Sube'
#>
#> No pre-processing
#> Resampling: Bootstrapped (25 reps)
#> Summary of sample sizes: 938, 938, 938, 938, 938, 938, ...
#> Resampling results across tuning parameters:
#>
#> k Accuracy Kappa
#> 5 0.8317910 0.6625382
#> 7 0.8396091 0.6781770
#> 9 0.8429101 0.6847705
#> 11 0.8498168 0.6984716
#> 13 0.8484127 0.6955193
#> 15 0.8561378 0.7110835
#> 17 0.8584600 0.7157315
#> 19 0.8610032 0.7206949
#> 21 0.8598119 0.7183314
#> 23 0.8620446 0.7227893
#> 25 0.8666489 0.7321326
#> 27 0.8683517 0.7355501
#> 29 0.8668571 0.7324604
#> 31 0.8684517 0.7356510
#> 33 0.8695014 0.7377606
#> 35 0.8691338 0.7370225
#> 37 0.8702310 0.7392529
#> 39 0.8706310 0.7400393
#> 41 0.8734199 0.7456790
#> 43 0.8756463 0.7501696
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was k = 43.
Vemos la exactitud (accuracy) para cada valor de “k” y el indicador Kappa. En el caso de Kappa no lo usaremos por el momento ya que es usado para problemas que tienen un desequilibrio en las clases (por ejemplo, división de 70-30 para los outcomes 0 y 1 y puede lograr una exactitud del 70% al predecir que todas las instancias son para el outcome 0).
Así mismo, en la última línea tenemos el valor de “k” óptimo para este modelo. Además, nos encontramos con un nuevo tipo de objeto, el objeto train
, el cual está listo para ingresarlo directamente a un gráfico usando la función plot()
.
10.3.3 Ajuste de parámetros
Si bien ya podríamos probar nuestro modelo en la data de test nos vamos a detener a ajustar parámetros del modelo. Una de las partes más importantes del entrenamiento de modelos de Machine learning es ajustar los parámetros. Para ello usaremos la función trainControl
para especificar una serie de parámetros en el modelo. El objeto que sale de trainControl se proporcionará como argumento para la función train
que usaremos para entrenar.
Usaremos el método cv
(Validación Cruzada) para indicar que vamos a partir nuestra data de entrenamiento en 5 partes iguales de forma aleatoria (5 fold en inglés). Luego, cada una de estas partes las vamos a utilizar como test para el modelo que creemos por las otras 4 partes. Es decir, usaremos parte de nuestra data de entrenamiento para testear nuestro modelo. Esto nos permitirá tomar el error promedio de los 5 test y obtener un dato más certero. Anteriormente no habíamos configurado este parámetro y dejado al valor por default. Por default el método es bootstrap
el cual utiliza un método de muestreo con reemplazo a diferencia del cv
que el muestro lo hace sin reemplazo.
Ahora, entrenemos nuestro modelo con los parámetros de control:
10.3.4 Pre-procesando datos
Así mismo, un paso importante es pre-procesar la data, dado que, por ejemplo, los datos a diferentes escalas pueden alterar sustancialmente un algoritmo de aprendizaje. El método de escala
(división por la desviación estándar) y el centrado
(sustracción de la media) sons dos métodos muy frecuentes para poder pre-procesar los datos y obtener una mejor exactitud (ver ?preProcess
).
set.seed(28)
SP_knnEntrenado <- train(Direccion ~ .,
data = SP_entrena,
method = "knn",
tuneLength = 20,
trControl = SP_ctrl,
preProcess = c("center","scale")
)
SP_knnEntrenado
#> k-Nearest Neighbors
#>
#> 938 samples
#> 8 predictor
#> 2 classes: 'Baja', 'Sube'
#>
#> Pre-processing: centered (8), scaled (8)
#> Resampling: Cross-Validated (5 fold)
#> Summary of sample sizes: 751, 751, 750, 750, 750
#> Resampling results across tuning parameters:
#>
#> k Accuracy Kappa
#> 5 0.8667596 0.7324704
#> 7 0.8742235 0.7471798
#> 9 0.8753044 0.7495016
#> 11 0.8742007 0.7470650
#> 13 0.8806292 0.7600666
#> 15 0.8849073 0.7686722
#> 17 0.8795654 0.7579666
#> 19 0.8880817 0.7751289
#> 21 0.8891626 0.7773436
#> 23 0.8881044 0.7752995
#> 25 0.8902549 0.7794969
#> 27 0.8966378 0.7922259
#> 29 0.9008989 0.8007257
#> 31 0.8977017 0.7944032
#> 33 0.8966151 0.7921009
#> 35 0.9008761 0.8007176
#> 37 0.8955456 0.7899177
#> 39 0.8955399 0.7899431
#> 41 0.8965923 0.7921229
#> 43 0.8987200 0.7962878
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was k = 29.
plot(SP_knnEntrenado)
Vemos la mejora sustancial ahora que hemos ajustado algunos parámetros y hemos hecho que primero se reprocese. Tener en cuenta que cada vez que ajustamos parámetros el valor de “k” puede cambiar hasta encontrar al más óptimo. En este caso cambió a k = 29. Esto no quiere decir que a menor “k” el algoritmo es mejor, solo que es el más óptimo para este caso en particular con estos ajustes realizados.
10.3.5 Testeo del modelo de predicción
Ya tenemos nuestro modelo entrenado y listo para testearlo. Caret nos facilita realizar la predicción utilizando la función predict(modelo, newdata = data_de_test)
SP_knnPrediccion <- predict(SP_knnEntrenado, newdata = SP_test )
SP_knnPrediccion %>%
head(50)
#> [1] Sube Sube Baja Baja Sube Sube Baja Sube Baja Baja Baja Baja Sube Sube Sube
#> [16] Baja Sube Baja Baja Baja Sube Baja Sube Sube Baja Baja Sube Baja Sube Sube
#> [31] Baja Sube Baja Baja Baja Sube Sube Baja Baja Baja Baja Sube Sube Sube Baja
#> [46] Sube Sube Baja Baja Sube
#> Levels: Baja Sube
Si queremos ir aun más allá, para comprender mejor cómo operan estos algoritmos de machine learning, podemos obtener el resultado de la predicción antes de indicar qué clase le asigna el modelo. Para ello utilizamos la misma función predict()
pero esta vez modificamos el atributo type
. Por default type = "raw"
mostrándonos la clase predicha, pero el resultado real es un conjunto de probabilidades. El modelo calcula internamente qué tan probable es que sea de una u otra clase (Baja o Sube).
Mostremos las probabilidades en vez de la clase.
prob_knnPrediccion <- predict(SP_knnEntrenado, newdata = SP_test, type = "prob")
prob_knnPrediccion %>%
head(10)
#> Baja Sube
#> 1 0.24137931 0.75862069
#> 2 0.37931034 0.62068966
#> 3 0.62068966 0.37931034
#> 4 0.96551724 0.03448276
#> 5 0.31034483 0.68965517
#> 6 0.44827586 0.55172414
#> 7 0.93103448 0.06896552
#> 8 0.06896552 0.93103448
#> 9 0.55172414 0.44827586
#> 10 1.00000000 0.00000000
Como vemos, para cada valor del test el modelo nos calcula la probabilidad que estima de que Baje y la probabilidad de Suba el índice S&P. El algoritmo por default nos da la clase que tiene más de 50% de probabilidad.
Ya tenemos el objeto SP_knnPrediccion
con los valores predichos. Si queremos calcular la exactitud del modelo (accuracy) utilizaremos la función confusionMatrix(data_prediccion, output_real)
la cual no solo nos reporta la matriz de confusión, sino también una serie de mediciones de la presición de nuestro modelo.
confusionMatrix(SP_knnPrediccion, SP_test$Direccion)
#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction Baja Sube
#> Baja 129 4
#> Sube 21 158
#>
#> Accuracy : 0.9199
#> 95% CI : (0.884, 0.9475)
#> No Information Rate : 0.5192
#> P-Value [Acc > NIR] : < 2.2e-16
#>
#> Kappa : 0.8388
#>
#> Mcnemar's Test P-Value : 0.001374
#>
#> Sensitivity : 0.8600
#> Specificity : 0.9753
#> Pos Pred Value : 0.9699
#> Neg Pred Value : 0.8827
#> Prevalence : 0.4808
#> Detection Rate : 0.4135
#> Detection Prevalence : 0.4263
#> Balanced Accuracy : 0.9177
#>
#> 'Positive' Class : Baja
#>
Obtenemos casi 91.99% de exactitud (accuracy), así como un intervalo de confianza del 95% de esta exactitud.
10.4 Matriz de confusión
Ya hemos utilizado matrices de confusión en nuestros dos ejemplos anteriores. Ahora nos toca entender propiamente su definición así como algunas de las métricas de evaluación de esta matriz.
Una matriz de confusión, también conocida como matriz de error, nos permite la visualización del rendimiento de un algoritmo, generalmente uno de aprendizaje supervisado (en el aprendizaje no supervisado generalmente se denomina matriz coincidente). Cada fila de la matriz representa las instancias en una clase predicha, mientras que cada columna representa las instancias en una clase real (o viceversa). El nombre se deriva del hecho de que hace que sea fácil ver si el sistema confunde dos clases (es decir, comúnmente etiquetar incorrectamente una como otra).
Las clasificaciones binarias, cuando el outcome puede tomar solo dos clases, nos arrojan esta siguiente matriz de confusión.
10.4.1 exactitud
Ya hemos venido utilizando este término en nuestros ejemplos. La exactitud del modelo (accuracy en inglés), la podemos calcular a partir de la matriz de confusión:
\(Accuracy=\frac{VP+VN}{VP+VN+FP+FN}\)
El accuracy del modelo es la proporción de veces que el algoritmo predijo correctamente, respecto al total de datos evaluados.
10.4.2 Sensitividad
La sensibilidad (también llamada tasa positiva verdadera, el recuerdo o la probabilidad de detección en algunos campos) mide la proporción de positivos reales que se identifican correctamente como tales (por ejemplo, el porcentaje de personas enfermas que se identifican correctamente como que tienen condición).
\(Sensitivity=\frac{VP}{VP+FN}\)
10.4.3 Especificidad
La especificidad (también llamada tasa negativa verdadera) mide la proporción de negativos reales que se identifican correctamente como tales (por ejemplo, el porcentaje de personas sanas que se identifican correctamente como que no tienen la afección).
\(Specificity=\frac{VN}{VN+FP}\)
10.5 Ejercicios
- Utilizando la librería caret, particiona el data frame
iris
de tal forma de tener 70% de datos de entrenamiento y 30% de datos de prueba.
Solución
- Utilizando la librería caret y los datos de entrenamiento obtenidos en el ejercicio anterior, crea un modelo de k-vecino más cercano. Plotea el resultado del objeto de entrenamiento.
Solución
- Utiliza el modelo creado en el ejercicio anterior para predecir los outputs del objeto
test
. Reporta la matriz de confusión.
10.6 Regresión lineal simple
Ahora nos toca predecir sobre variables continuas, los algoritmos de supervisión para estos casos son llamados regresión.
Para entender regresión lineal vamos a iniciar con un ejemplo con una sola variable como input, a ello se le conoce como Regresión lineal simple. Para ello vamos a utilizar data de la librería HistData
donde encontraremos un conjunto de datos que enumeran las observaciones individuales de 934 niños en 205 familias almacenadas en el objeto GaltonFamilies
.
data(GaltonFamilies)
# Hacemos algunos filtros para tener un papá y un hijo por familia
alturas <- GaltonFamilies %>%
filter(gender == "male") %>%
group_by(family) %>%
sample_n(1) %>% # muestra aleatoria de 1 hijo por familia
ungroup() %>%
select(father, childHeight) %>%
rename(hijo = childHeight, papa = father) %>%
mutate(papa = papa/39.37) %>% # De pulgadas a metros
mutate(hijo = hijo/39.37) # De pulgadas a metros
Visualmente podríamos ver si hay una relación entre las alturas de papá e hijo:
Como vemos, hay una correlación positiva, de tal forma que mientras más alto el padre el hijo llega a tener una mayor altura de adulto. Esta línea, sin embargo, no es más que una línea por default. El reto está en encontrar cuál es le línea que minimiza la distancia de los puntos a esta línea, conocido como minimización del error.
Podríamos intentar predecir la estatura que tendrá el hijo a partir de la estatura del padre usando la ecuación de esta recta:
\(Y = \beta_0+\beta_1X\)
Donde \(X\) es una variable independiente, explicativa, en este caso la estatura del papá. \(\beta_1\) es un parámetro que mide la influencia que la variable explicativa tiene sobre la variable dependiente \(Y\) y \(\beta_0\) es la intersección o término constante. En nuestro caso, la estatura del hijo.
En estadística, la regresión lineal o ajuste lineal es un modelo matemático usado para aproximar la relación de dependencia entre una variable dependiente \(Y\) y las variables independientes \(X_i\).
Así, nuestro problema se reduce a entrenar a nuestro modelo para que encuentre los valores de a intersección, \(\beta_0\), y el valor del parámetro que acompaña a \(X_1\), \(\beta_1\), para luego utilizar estos datos como predicción en nuestra data de test.
index_entrena <- createDataPartition(alturas$hijo, p = 0.5, list = FALSE)
alturas_entrena <- alturas[index_entrena, ]
alturas_test <- alturas[-index_entrena, ]
Ahora que ya tenemos nuestra data podemos entrenar nuestro modelo.
alturas_ctrl <- trainControl(method="cv", number = 10)
alturas_lmEntrenado <- train(hijo ~ .,
data = alturas_entrena,
method = "lm",
trControl = alturas_ctrl
)
alturas_lmEntrenado
#> Linear Regression
#>
#> 91 samples
#> 1 predictor
#>
#> No pre-processing
#> Resampling: Cross-Validated (10 fold)
#> Summary of sample sizes: 81, 82, 81, 81, 83, 82, ...
#> Resampling results:
#>
#> RMSE Rsquared MAE
#> 0.06076013 0.2477919 0.04941711
#>
#> Tuning parameter 'intercept' was held constant at a value of TRUE
Vemos como resultados principales el RMSE, que son las siglas en inglés de error cuadrático medio, y es el valor que la regresión lineal buscar minimizar. Además, tenemos el Rsquared, R cuadrado o \(R^2\), que es el coeficiente de determinación el cual determina la calidad del modelo para replicar los resultados. Mientras más alto y cercano a 1 es mejor la calidad del modelo.
Finalmente, dado que ya tenemos el modelo entrenado podemos predecir los valores, que no sería más que evaluar la ecuación con los valores de la data de test. Además, como mencionamos, en regresión buscamos reducir el error cuadrático medio, RMSE. Entonces, calcularemos el RMSE para la data predicha vs la data de test.
# Predicción:
alturas_lmPrediccion <- predict(alturas_lmEntrenado, newdata = alturas_test )
# Cálculo del error
error <- alturas_lmPrediccion - alturas_test$hijo
# Cálculo del error cuadrático medio RMSE:
sqrt(mean(error^2))
#> [1] 0.06190904
Si deseamos también podemos reportar los coeficientes de la ecuación y visualizarlos:
\(Y = \beta_0+\beta_1X\)
alturas_lmEntrenado$finalModel$coefficients
#> (Intercept) papa
#> 1.0387526 0.4119603
intercepcion <- alturas_lmEntrenado$finalModel$coefficients[1]
pendiente <- alturas_lmEntrenado$finalModel$coefficients[2]
#Visualización
alturas %>%
ggplot() +
aes(papa, hijo) +
geom_point() +
geom_abline(lty = 2, intercept = intercepcion, slope = pendiente, color = "red")
10.7 Regresión lineal múltiple
Ahora que ya conocemos la regresión lineal podemos ejecutar un modelo de regresión lineal múltiple, el cual involucra a más de 1 variable como input. Para ello, utilizaremos el dataset diamonds
que contiene los precios y otros atributos de casi 54,000 diamantes.
diamonds <- diamonds %>%
rename(precio = price)
diamonds %>%
head(10)
#> # A tibble: 10 x 10
#> carat cut color clarity depth table precio x y z
#> <dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#> 1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
#> 2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
#> 3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
#> 4 0.290 Premium I VS2 62.4 58 334 4.2 4.23 2.63
#> 5 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
#> 6 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
#> 7 0.24 Very Good I VVS1 62.3 57 336 3.95 3.98 2.47
#> 8 0.26 Very Good H SI1 61.9 55 337 4.07 4.11 2.53
#> 9 0.22 Fair E VS2 65.1 61 337 3.87 3.78 2.49
#> 10 0.23 Very Good H VS1 59.4 61 338 4 4.05 2.39
Partimos la data en dos tomando 70% de data para entrenamiento:
index_entrena <- createDataPartition(diamonds$precio, p = 0.7, list = FALSE)
diamantes_entrena <- diamonds[index_entrena, ]
diamantes_test <- diamonds[-index_entrena, ]
Creamos ahora nuestro modelo de regresión lineal múltiple y reportamos tanto los resultados del error como los coeficientes de la ecuación lineal.
diamantes_ctrl <- trainControl(method="cv", number = 10)
diamantes_lmEntrenado <- train(precio ~ .,
diamantes_entrena,
method = "lm",
trControl = diamantes_ctrl
)
diamantes_lmEntrenado
#> Linear Regression
#>
#> 37759 samples
#> 9 predictor
#>
#> No pre-processing
#> Resampling: Cross-Validated (10 fold)
#> Summary of sample sizes: 33983, 33984, 33983, 33983, 33983, 33983, ...
#> Resampling results:
#>
#> RMSE Rsquared MAE
#> 1127.149 0.9200345 737.5332
#>
#> Tuning parameter 'intercept' was held constant at a value of TRUE
diamantes_lmEntrenado$finalModel$coefficients
#> (Intercept) carat cut.L cut.Q cut.C `cut^4`
#> 6033.33693 11432.25964 578.85759 -304.78440 149.25375 -17.73461
#> color.L color.Q color.C `color^4` `color^5` `color^6`
#> -1944.34903 -671.51207 -153.34540 49.34632 -88.31014 -44.29660
#> clarity.L clarity.Q clarity.C `clarity^4` `clarity^5` `clarity^6`
#> 4012.86187 -1875.53117 930.61616 -341.20511 208.50985 20.15169
#> `clarity^7` depth table x y z
#> 85.80580 -62.67254 -27.71926 -1059.07717 12.73836 -86.94498
Vemos que nos da el RMSE y un R cuadrado bastante cercano a 1, el cual nos denota una alta calidad del modelo para replicar los resultados.
Utilicemos nuestro modelo para predecir los precios de la data de test.
# Predicción:
diamantes_lmPrediccion <- predict(diamantes_lmEntrenado, newdata = diamantes_test )
# Cálculo del error
error <- diamantes_lmPrediccion - diamantes_test$precio
# Cálculo del error cuadrático medio RMSE:
sqrt(mean(error^2))
#> [1] 1141.211
Así, hemos aprendido a realizar un modelo más de machine learning: el de regresión lineal, tanto simple como múltiple.
10.8 Método estándar para evaluar exactitud
Ahora que ya conocemos cómo construir modelos aplicaremos métricas que nos permiten una mejor exactitud en los modelos de clasificación para dos clases.
Para esto recordemos los resultados del modelo que creamos usando el algoritmo de k-vecinos más próximos para predecir si sube o baja el índice S&P.
SP_knnEntrenado
#> k-Nearest Neighbors
#>
#> 938 samples
#> 8 predictor
#> 2 classes: 'Baja', 'Sube'
#>
#> Pre-processing: centered (8), scaled (8)
#> Resampling: Cross-Validated (5 fold)
#> Summary of sample sizes: 751, 751, 750, 750, 750
#> Resampling results across tuning parameters:
#>
#> k Accuracy Kappa
#> 5 0.8667596 0.7324704
#> 7 0.8742235 0.7471798
#> 9 0.8753044 0.7495016
#> 11 0.8742007 0.7470650
#> 13 0.8806292 0.7600666
#> 15 0.8849073 0.7686722
#> 17 0.8795654 0.7579666
#> 19 0.8880817 0.7751289
#> 21 0.8891626 0.7773436
#> 23 0.8881044 0.7752995
#> 25 0.8902549 0.7794969
#> 27 0.8966378 0.7922259
#> 29 0.9008989 0.8007257
#> 31 0.8977017 0.7944032
#> 33 0.8966151 0.7921009
#> 35 0.9008761 0.8007176
#> 37 0.8955456 0.7899177
#> 39 0.8955399 0.7899431
#> 41 0.8965923 0.7921229
#> 43 0.8987200 0.7962878
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was k = 29.
En la penúltima línea se puede leer que la exactitud (accuracy
) fue usada para seleccionar el modelo más óptimo usando el valor más grande. Sin embargo, esta no es la única forma de determinar cuál es el modelo más óptimo.
Recordemos cómo se calcula por default la exactitud (accuracy), hemos utilizado la regla simple de que si la probabilidad de que sea de una determinada clase es más de 50% entonces se le asigne esa clase y luego calculamos la proporción de aciertos entre el total de casos.
Sin embargo, no tiene que ser 50%, podríamos ser más exigentes e indicar que si la probabilidad es mayor del 60% u 80% entonces se le asigna una determinada clase. Vemos que hay diferentes probabilidades y ello nos daría diferentes accuracy
.
Es así como surge el indicador del área bajo la curva de Característica Operativa del Receptor, ROC por sus siglas en inglés (Fawcett 2005). Este indicador nos mide qué tan bien puede distinguir un modelo entre dos clases y es considerado el método estándar para evaluar la exactitud de los modelos de distribución predictiva (Jorge M. Lobo 2007) y calcula las exactitudes no solo para cuando discriminamos a partir del 50%, sino para más valores de probabilidad.
Para utilizar esta métrica modificaremos nuestros parámetros de control agregando tres atributos que permitirán calcular el ROC.
SP2_ctrl <- trainControl(
method="cv",
number = 5,
savePredictions = TRUE, # Guardamos las predicciones en cada iteración
classProb = TRUE, # Calculamos las probabilidades de ocurrencia
summaryFunction = twoClassSummary # Hacemos explícito que son dos clases
)
Con estos parámetros modificados procederemos a volver a entrenar a nuestro modelo.
set.seed(28)
SP2_knnEntrenado <- train(Direccion ~ .,
data = SP_entrena, # Misma data de entrenamiento para modelo SP2
method = "knn",
tuneLength = 20,
trControl = SP2_ctrl,
preProcess = c("center","scale"),
metric = "ROC"
)
SP2_knnEntrenado
#> k-Nearest Neighbors
#>
#> 938 samples
#> 8 predictor
#> 2 classes: 'Baja', 'Sube'
#>
#> Pre-processing: centered (8), scaled (8)
#> Resampling: Cross-Validated (5 fold)
#> Summary of sample sizes: 751, 751, 750, 750, 750
#> Resampling results across tuning parameters:
#>
#> k ROC Sens Spec
#> 5 0.9356332 0.8295482 0.9012203
#> 7 0.9491333 0.8229060 0.9217757
#> 9 0.9581705 0.8295971 0.9176941
#> 11 0.9581360 0.8162882 0.9279402
#> 13 0.9637686 0.8274481 0.9300021
#> 15 0.9628994 0.8340904 0.9320640
#> 17 0.9621501 0.8296947 0.9258784
#> 19 0.9642725 0.8429304 0.9299811
#> 21 0.9663917 0.8473504 0.9278982
#> 23 0.9670048 0.8473993 0.9258153
#> 25 0.9694807 0.8452259 0.9320219
#> 27 0.9714313 0.8452015 0.9443720
#> 29 0.9731740 0.8451282 0.9526404
#> 31 0.9724945 0.8473260 0.9443930
#> 33 0.9710535 0.8406838 0.9484957
#> 35 0.9716720 0.8495482 0.9484957
#> 37 0.9710530 0.8362393 0.9505365
#> 39 0.9738171 0.8384615 0.9484957
#> 41 0.9739988 0.8407082 0.9484746
#> 43 0.9739789 0.8362637 0.9567221
#>
#> ROC was used to select the optimal model using the largest value.
#> The final value used for the model was k = 41.
Vemos que ahora ROC fue utilizado para seleccionar el modelo más óptimo. Mientras más cercano el valor de ROC sea a 1 mejor será nuestro modelo. Con este modelo podemos predecir los valores a partir de la data de test.
SP2_knnPrediccion <- predict(SP2_knnEntrenado, newdata = SP_test )
confusionMatrix(SP2_knnPrediccion, SP_test$Direccion)
#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction Baja Sube
#> Baja 132 3
#> Sube 18 159
#>
#> Accuracy : 0.9327
#> 95% CI : (0.899, 0.9579)
#> No Information Rate : 0.5192
#> P-Value [Acc > NIR] : < 2e-16
#>
#> Kappa : 0.8647
#>
#> Mcnemar's Test P-Value : 0.00225
#>
#> Sensitivity : 0.8800
#> Specificity : 0.9815
#> Pos Pred Value : 0.9778
#> Neg Pred Value : 0.8983
#> Prevalence : 0.4808
#> Detection Rate : 0.4231
#> Detection Prevalence : 0.4327
#> Balanced Accuracy : 0.9307
#>
#> 'Positive' Class : Baja
#>
Vemos cómo nuestra exactitud (accuracy) ha incrementado de 91.99% a 93.27%. Ésta métrica es muy recomendada para mejorar la exactitud de nuestro modelo, además que nos permite más fácilmente utilizarlo como comparador entre diferentes modelos que podamos crear.
10.9 Selección de modelo más óptimo
Hemos aprendido cómo crear algunos modelos de machine learning. Como nos debemos de haber percatado, con caret seguimos el mismo patrón para la partición, entrenamiento y predicción. La variación está en el cómo pre-procesar los datos y el ajuste de parámetros. Podríamos así crear múltiples modelos, pero finalmente tenemos que elegir uno el cual nos servirá para hacer nuestras predicciones.
En esta sección, vamos a comparar diferentes modelos predictivos aceptando sus valores por default y elegiremos el mejor usando las herramientas presentadas en las secciones anteriores.
Para ello, vamos a utilizar un nuevo caso. Ésta vez estamos evaluando el comportamiento de nuestros 5,000 clientes, algunos de los cuales se han dado de baja de nuestros servicios. Tenemos 19 predictores, la mayoría de ellos numéricos, en el dataset mlc_churn
. Para acceder a la data tenemos que cargar la librería modeldata
.
data(mlc_churn)
str(mlc_churn)
#> tibble [5,000 × 20] (S3: tbl_df/tbl/data.frame)
#> $ state : Factor w/ 51 levels "AK","AL","AR",..: 17 36 32 36 37 2 20 25 19 50 ...
#> $ account_length : int [1:5000] 128 107 137 84 75 118 121 147 117 141 ...
#> $ area_code : Factor w/ 3 levels "area_code_408",..: 2 2 2 1 2 3 3 2 1 2 ...
#> $ international_plan : Factor w/ 2 levels "no","yes": 1 1 1 2 2 2 1 2 1 2 ...
#> $ voice_mail_plan : Factor w/ 2 levels "no","yes": 2 2 1 1 1 1 2 1 1 2 ...
#> $ number_vmail_messages : int [1:5000] 25 26 0 0 0 0 24 0 0 37 ...
#> $ total_day_minutes : num [1:5000] 265 162 243 299 167 ...
#> $ total_day_calls : int [1:5000] 110 123 114 71 113 98 88 79 97 84 ...
#> $ total_day_charge : num [1:5000] 45.1 27.5 41.4 50.9 28.3 ...
#> $ total_eve_minutes : num [1:5000] 197.4 195.5 121.2 61.9 148.3 ...
#> $ total_eve_calls : int [1:5000] 99 103 110 88 122 101 108 94 80 111 ...
#> $ total_eve_charge : num [1:5000] 16.78 16.62 10.3 5.26 12.61 ...
#> $ total_night_minutes : num [1:5000] 245 254 163 197 187 ...
#> $ total_night_calls : int [1:5000] 91 103 104 89 121 118 118 96 90 97 ...
#> $ total_night_charge : num [1:5000] 11.01 11.45 7.32 8.86 8.41 ...
#> $ total_intl_minutes : num [1:5000] 10 13.7 12.2 6.6 10.1 6.3 7.5 7.1 8.7 11.2 ...
#> $ total_intl_calls : int [1:5000] 3 3 5 7 3 6 7 6 4 5 ...
#> $ total_intl_charge : num [1:5000] 2.7 3.7 3.29 1.78 2.73 1.7 2.03 1.92 2.35 3.02 ...
#> $ number_customer_service_calls: int [1:5000] 1 1 0 2 3 0 3 0 1 0 ...
#> $ churn : Factor w/ 2 levels "yes","no": 2 2 2 2 2 2 2 2 2 2 ...
# Traducimos outputs
mlc_churn <- mlc_churn %>%
rename(de_baja = churn) %>%
mutate(de_baja = ifelse(de_baja == "yes", "Sí", "No")) %>%
mutate(de_baja = as.factor(de_baja))
# Proporción de "Sí" y "No"s:
prop.table(table(mlc_churn$de_baja))
#>
#> No Sí
#> 0.8586 0.1414
Creamos ahora nuestra de entrenamiento y de test, 70% entrenamiento.
set.seed(28)
index_entrena <- createDataPartition(mlc_churn$de_baja, p = 0.7, list = FALSE)
churn_entrena <- mlc_churn[index_entrena, ]
churn_test <- mlc_churn[-index_entrena, ]
Hasta aquí hemos hecho exactamente el mismo paso que en los modelos anteriores. Sin embargo, anteriormente hemos especificado el método cross-validation dentro de nuestros parámetros de control y como recordaremos este método parte nuestra data en n partes seleccionadas aleatoriamente (folds). Ahora no necesitamos que sean aleatoriamente para cada modelo, sino los mismos folds.
Así, crearemos manualmente una lista de 5 folds utilizando la función createFolds(outputs, numero_folds)
.
set.seed(28)
folds_manual <- createFolds(churn_entrena$de_baja, k = 5)
str(folds_manual)
#> List of 5
#> $ Fold1: int [1:701] 1 3 8 19 29 49 61 63 65 72 ...
#> $ Fold2: int [1:700] 2 5 17 27 30 31 41 48 55 56 ...
#> $ Fold3: int [1:700] 11 13 20 23 28 34 36 38 39 46 ...
#> $ Fold4: int [1:700] 6 7 9 10 12 14 16 18 22 32 ...
#> $ Fold5: int [1:700] 4 15 21 24 25 26 33 35 37 40 ...
Utilizaremos la métrica ROC para todos los modelos. Además, agregraremos el atributo index
donde ingresaremos como atributo los folds creados.
churn_ctrl <- trainControl(
savePredictions = TRUE,
classProb = TRUE,
summaryFunction = twoClassSummary,
index = folds_manual # Lista con elementos para cada iteración
)
El siguiente sería elegir los algoritmos de machine learning que queremos utilizar para crear nuestros modelos. Caret nos provee más de 230 algoritmos que se pueden listar utilizando la función getModelInfo()
, la cual nos hace un llamado a la lista total de algoritmos. Usaremos names()
para obtener solo los nombres.
names(getModelInfo())
#> [1] "ada" "AdaBag" "AdaBoost.M1"
#> [4] "adaboost" "amdai" "ANFIS"
#> [7] "avNNet" "awnb" "awtan"
#> [10] "bag" "bagEarth" "bagEarthGCV"
#> [13] "bagFDA" "bagFDAGCV" "bam"
#> [16] "bartMachine" "bayesglm" "binda"
#> [19] "blackboost" "blasso" "blassoAveraged"
#> [22] "bridge" "brnn" "BstLm"
#> [25] "bstSm" "bstTree" "C5.0"
#> [28] "C5.0Cost" "C5.0Rules" "C5.0Tree"
#> [31] "cforest" "chaid" "CSimca"
#> [34] "ctree" "ctree2" "cubist"
#> [37] "dda" "deepboost" "DENFIS"
#> [40] "dnn" "dwdLinear" "dwdPoly"
#> [43] "dwdRadial" "earth" "elm"
#> [46] "enet" "evtree" "extraTrees"
#> [49] "fda" "FH.GBML" "FIR.DM"
#> [52] "foba" "FRBCS.CHI" "FRBCS.W"
#> [55] "FS.HGD" "gam" "gamboost"
#> [58] "gamLoess" "gamSpline" "gaussprLinear"
#> [61] "gaussprPoly" "gaussprRadial" "gbm_h2o"
#> [64] "gbm" "gcvEarth" "GFS.FR.MOGUL"
#> [67] "GFS.LT.RS" "GFS.THRIFT" "glm.nb"
#> [70] "glm" "glmboost" "glmnet_h2o"
#> [73] "glmnet" "glmStepAIC" "gpls"
#> [76] "hda" "hdda" "hdrda"
#> [79] "HYFIS" "icr" "J48"
#> [82] "JRip" "kernelpls" "kknn"
#> [85] "knn" "krlsPoly" "krlsRadial"
#> [88] "lars" "lars2" "lasso"
#> [91] "lda" "lda2" "leapBackward"
#> [94] "leapForward" "leapSeq" "Linda"
#> [97] "lm" "lmStepAIC" "LMT"
#> [100] "loclda" "logicBag" "LogitBoost"
#> [103] "logreg" "lssvmLinear" "lssvmPoly"
#> [106] "lssvmRadial" "lvq" "M5"
#> [109] "M5Rules" "manb" "mda"
#> [112] "Mlda" "mlp" "mlpKerasDecay"
#> [115] "mlpKerasDecayCost" "mlpKerasDropout" "mlpKerasDropoutCost"
#> [118] "mlpML" "mlpSGD" "mlpWeightDecay"
#> [121] "mlpWeightDecayML" "monmlp" "msaenet"
#> [124] "multinom" "mxnet" "mxnetAdam"
#> [127] "naive_bayes" "nb" "nbDiscrete"
#> [130] "nbSearch" "neuralnet" "nnet"
#> [133] "nnls" "nodeHarvest" "null"
#> [136] "OneR" "ordinalNet" "ordinalRF"
#> [139] "ORFlog" "ORFpls" "ORFridge"
#> [142] "ORFsvm" "ownn" "pam"
#> [145] "parRF" "PART" "partDSA"
#> [148] "pcaNNet" "pcr" "pda"
#> [151] "pda2" "penalized" "PenalizedLDA"
#> [154] "plr" "pls" "plsRglm"
#> [157] "polr" "ppr" "PRIM"
#> [160] "protoclass" "qda" "QdaCov"
#> [163] "qrf" "qrnn" "randomGLM"
#> [166] "ranger" "rbf" "rbfDDA"
#> [169] "Rborist" "rda" "regLogistic"
#> [172] "relaxo" "rf" "rFerns"
#> [175] "RFlda" "rfRules" "ridge"
#> [178] "rlda" "rlm" "rmda"
#> [181] "rocc" "rotationForest" "rotationForestCp"
#> [184] "rpart" "rpart1SE" "rpart2"
#> [187] "rpartCost" "rpartScore" "rqlasso"
#> [190] "rqnc" "RRF" "RRFglobal"
#> [193] "rrlda" "RSimca" "rvmLinear"
#> [196] "rvmPoly" "rvmRadial" "SBC"
#> [199] "sda" "sdwd" "simpls"
#> [202] "SLAVE" "slda" "smda"
#> [205] "snn" "sparseLDA" "spikeslab"
#> [208] "spls" "stepLDA" "stepQDA"
#> [211] "superpc" "svmBoundrangeString" "svmExpoString"
#> [214] "svmLinear" "svmLinear2" "svmLinear3"
#> [217] "svmLinearWeights" "svmLinearWeights2" "svmPoly"
#> [220] "svmRadial" "svmRadialCost" "svmRadialSigma"
#> [223] "svmRadialWeights" "svmSpectrumString" "tan"
#> [226] "tanSearch" "treebag" "vbmpRadial"
#> [229] "vglmAdjCat" "vglmContRatio" "vglmCumulative"
#> [232] "widekernelpls" "WM" "wsrf"
#> [235] "xgbDART" "xgbLinear" "xgbTree"
#> [238] "xyf"
A continuación, crearemos una serie de modelos con los datos de entrenamiento y nuestros parámetros de control utilizando los algoritmos más utilizados en machine learning. Así mismo, utilizaremos la métrica de rendimiento ROC
en vez de accuracy
para comparar los modelos.
10.9.1 Modelo de los k-vecinos más cercanos
Si bien es muy modelo simple también es muy útil. Empecemos con este modelo que ya aprendimos a crear durante este capítulo.
churn_modelo_knn <- train(de_baja ~ .,
churn_entrena,
metric = "ROC",
method = "knn",
tuneLength = 20,
trControl = churn_ctrl)
churn_modelo_knn
#> k-Nearest Neighbors
#>
#> 3501 samples
#> 19 predictor
#> 2 classes: 'No', 'Sí'
#>
#> No pre-processing
#> Resampling: Bootstrapped (5 reps)
#> Summary of sample sizes: 701, 700, 700, 700, 700
#> Resampling results across tuning parameters:
#>
#> k ROC Sens Spec
#> 5 0.6607479 0.9805389 0.21767677
#> 7 0.6713682 0.9877748 0.19747475
#> 9 0.6816558 0.9924321 0.17222222
#> 11 0.6864001 0.9938458 0.16161616
#> 13 0.6874802 0.9947607 0.14040404
#> 15 0.6889439 0.9959249 0.13080808
#> 17 0.6943288 0.9959249 0.11666667
#> 19 0.6974810 0.9962576 0.10757576
#> 21 0.6966771 0.9965902 0.10000000
#> 23 0.6982154 0.9972556 0.09090909
#> 25 0.7005844 0.9977545 0.08686869
#> 27 0.7009869 0.9974219 0.08030303
#> 29 0.6999713 0.9975882 0.07070707
#> 31 0.7008243 0.9979209 0.06767677
#> 33 0.7015388 0.9981704 0.05656566
#> 35 0.7018798 0.9980041 0.05151515
#> 37 0.7026704 0.9984199 0.05050505
#> 39 0.7024471 0.9985030 0.04393939
#> 41 0.7016133 0.9986693 0.03838384
#> 43 0.7008964 0.9989189 0.03434343
#>
#> ROC was used to select the optimal model using the largest value.
#> The final value used for the model was k = 37.
10.9.2 Modelo lineal generalizado - GLM
El modelo lineal generalizado (GLM) es una generalización flexible de la regresión lineal ordinaria que permite variables de respuesta que tienen modelos de distribución de errores distintos de una distribución normal. El GLM generaliza la regresión lineal al permitir que el modelo lineal esté relacionado con la variable de respuesta a través de una función de enlace y al permitir que la magnitud de la varianza de cada medición sea una función de su valor predicho.
Para ello necesitamos instalar la librería glmnet
antes de crear nuestro modelo.
churn_modelo_glm <- train(de_baja ~ .,
churn_entrena,
metric = "ROC",
method = "glmnet",
trControl = churn_ctrl)
churn_modelo_glm
#> glmnet
#>
#> 3501 samples
#> 19 predictor
#> 2 classes: 'No', 'Sí'
#>
#> No pre-processing
#> Resampling: Bootstrapped (5 reps)
#> Summary of sample sizes: 701, 700, 700, 700, 700
#> Resampling results across tuning parameters:
#>
#> alpha lambda ROC Sens Spec
#> 0.10 0.0001762239 0.7427113 0.9469393 0.28838384
#> 0.10 0.0017622392 0.7598271 0.9504323 0.27323232
#> 0.10 0.0176223919 0.7847811 0.9696435 0.19040404
#> 0.55 0.0001762239 0.7445318 0.9475214 0.28787879
#> 0.55 0.0017622392 0.7675345 0.9534261 0.26565657
#> 0.55 0.0176223919 0.8078084 0.9824511 0.13282828
#> 1.00 0.0001762239 0.7462263 0.9480205 0.28787879
#> 1.00 0.0017622392 0.7739245 0.9557546 0.25707071
#> 1.00 0.0176223919 0.8100897 0.9891877 0.08888889
#>
#> ROC was used to select the optimal model using the largest value.
#> The final values used for the model were alpha = 1 and lambda = 0.01762239.
10.9.3 Modelo de bosque aleatorio o Random Forest
Random Forest es un técnica de aprendizaje automático supervisada basada en árboles de decisión. Usaremos el modelo de bosque aleatorio (RF) y no un árbol de decisión porque un RF es un conjunto de árboles de decisión combinados. Al combinarlos, lo que en realidad está pasando, es que distintos árboles ven distintas porciones de los datos. Ningún árbol ve todos los datos de entrenamiento. Esto hace que cada árbol se entrene con distintas muestras de datos para un mismo problema. De esta forma, al combinar sus resultados, unos errores se compensan con otros y tenemos una predicción que generaliza mejor.
Para ello primero instalaremos la librería ranger
y luego crearemos el modelo.
churn_modelo_rf <- train(de_baja ~ .,
churn_entrena,
metric = "ROC",
method = "ranger",
trControl = churn_ctrl)
churn_modelo_rf
#> Random Forest
#>
#> 3501 samples
#> 19 predictor
#> 2 classes: 'No', 'Sí'
#>
#> No pre-processing
#> Resampling: Bootstrapped (5 reps)
#> Summary of sample sizes: 701, 700, 700, 700, 700
#> Resampling results across tuning parameters:
#>
#> mtry splitrule ROC Sens Spec
#> 2 gini 0.8799397 0.9998336 0.02222222
#> 2 extratrees 0.8407305 1.0000000 0.00000000
#> 35 gini 0.9088837 0.9828678 0.65050505
#> 35 extratrees 0.9081950 0.9885221 0.53787879
#> 69 gini 0.9061447 0.9796243 0.65757576
#> 69 extratrees 0.9079349 0.9841977 0.60858586
#>
#> Tuning parameter 'min.node.size' was held constant at a value of 1
#> ROC was used to select the optimal model using the largest value.
#> The final values used for the model were mtry = 35, splitrule = gini
#> and min.node.size = 1.
10.9.4 Modelo de máquina de soporte vectorial - SVM
Las máquinas de soporte vectorial o máquinas de vectores de soporte son un conjunto de algoritmos de aprendizaje supervisado desarrollados por Vladimir Vapnik y su equipo en los laboratorios AT&T. Dado un conjunto de puntos, subconjunto de un conjunto mayor (espacio), en el que cada uno de ellos pertenece a una de dos posibles categorías, un algoritmo basado en SVM construye un modelo capaz de predecir si un punto nuevo (cuya categoría desconocemos) pertenece a una categoría o a la otra.
Para crear este modelo usaremos:
churn_modelo_svm <- train(de_baja ~ .,
churn_entrena,
metric = "ROC",
method = "svmRadial",
tuneLength = 10,
trControl = churn_ctrl)
churn_modelo_svm
#> Support Vector Machines with Radial Basis Function Kernel
#>
#> 3501 samples
#> 19 predictor
#> 2 classes: 'No', 'Sí'
#>
#> No pre-processing
#> Resampling: Bootstrapped (5 reps)
#> Summary of sample sizes: 701, 700, 700, 700, 700
#> Resampling results across tuning parameters:
#>
#> C ROC Sens Spec
#> 0.25 0.8133013 0.9860274 0.1035354
#> 0.50 0.8133131 0.9881058 0.1020202
#> 1.00 0.8133082 0.9797067 0.1459596
#> 2.00 0.8138342 0.9854448 0.1156566
#> 4.00 0.8186346 0.9844472 0.1560606
#> 8.00 0.8187712 0.9822850 0.1858586
#> 16.00 0.8103373 0.9824516 0.1818182
#> 32.00 0.8014889 0.9810373 0.1691919
#> 64.00 0.7974460 0.9747168 0.1984848
#> 128.00 0.7967994 0.9814534 0.1494949
#>
#> Tuning parameter 'sigma' was held constant at a value of 0.007422193
#> ROC was used to select the optimal model using the largest value.
#> The final values used for the model were sigma = 0.007422193 and C = 8.
10.9.5 Modelo Bayesiano ingenuo - Naive Bayes
Naïve Bayes (NB), Bayesiano ingenuo o el Ingenuo Bayes es uno de los algoritmos más simples, pero potentes, para la clasificación basado en el Teorema de Bayes con una suposición de independencia entre los predictores. Naive Bayes es fácil de construir y particularmente útil para conjuntos de datos muy grandes. El clasificador Naive Bayes asume que el efecto de una característica particular en una clase es independiente de otras características.
Para utilizar este modelo utilizaremos la librería naivebayes
.
churn_modelo_nb <- train(de_baja ~ .,
churn_entrena,
metric = "ROC",
method = "naive_bayes",
trControl = churn_ctrl)
churn_modelo_nb
#> Naive Bayes
#>
#> 3501 samples
#> 19 predictor
#> 2 classes: 'No', 'Sí'
#>
#> No pre-processing
#> Resampling: Bootstrapped (5 reps)
#> Summary of sample sizes: 701, 700, 700, 700, 700
#> Resampling results across tuning parameters:
#>
#> usekernel ROC Sens Spec
#> FALSE 0.5909390 0.06827533 0.9540404
#> TRUE 0.6431009 1.00000000 0.0000000
#>
#> Tuning parameter 'laplace' was held constant at a value of 0
#> Tuning
#> parameter 'adjust' was held constant at a value of 1
#> ROC was used to select the optimal model using the largest value.
#> The final values used for the model were laplace = 0, usekernel = TRUE
#> and adjust = 1.
10.9.6 Comparación de modelos
Para comparar los modelos, primero crearemos una lista con todos los modelos que hemos creado.
lista_de_modelos <- list(glmmet = churn_modelo_glm,
rf = churn_modelo_rf,
knn = churn_modelo_knn,
svm = churn_modelo_svm,
nb = churn_modelo_nb)
Luego, utilizaremos la función resamples
la cual nos preparará la data para analizar y visualizar el conjunto de resultados de remuestreo del conjunto de datos común.
comparacion_modelos <- resamples(lista_de_modelos)
comparacion_modelos
#>
#> Call:
#> resamples.default(x = lista_de_modelos)
#>
#> Models: glmmet, rf, knn, svm, nb
#> Number of resamples: 5
#> Performance metrics: ROC, Sens, Spec
#> Time estimates for: everything, final model fit
Finalmente, usaremos la función bwplot()
para trazar una serie de diagramas de caja donde los diagramas de caja individuales representan los datos subdivididos por cada modelo de acuerdo a la métrica que señalemos.
Para este caso el modelo de bosques aleatorios (RF) parece ser el mejor. Esto no es de extrañar dado que este algoritmo estar relacionado con su capacidad para hacer frente a diferentes tipos de entrada y requerir poco preprocesamiento. Podemos hacer nuestros modelos mejor pre procesando datos y cambiando los parámetros ad-hoc de cada modelo.
10.9.7 Prediciendo utilizando el mejor modelo
Ahora que ya tenemos nuestro mejor modelo procedemos a realizar la predicción.
# Este fue nuestro modelo óptimo
modelo_optimo <- churn_modelo_rf
churn_prediccion <- predict(modelo_optimo, churn_test)
confusionMatrix(churn_prediccion, churn_test$de_baja)
#> Confusion Matrix and Statistics
#>
#> Reference
#> Prediction No Sí
#> No 1275 49
#> Sí 12 163
#>
#> Accuracy : 0.9593
#> 95% CI : (0.948, 0.9687)
#> No Information Rate : 0.8586
#> P-Value [Acc > NIR] : < 2.2e-16
#>
#> Kappa : 0.8193
#>
#> Mcnemar's Test P-Value : 4.04e-06
#>
#> Sensitivity : 0.9907
#> Specificity : 0.7689
#> Pos Pred Value : 0.9630
#> Neg Pred Value : 0.9314
#> Prevalence : 0.8586
#> Detection Rate : 0.8506
#> Detection Prevalence : 0.8833
#> Balanced Accuracy : 0.8798
#>
#> 'Positive' Class : No
#>
Así, hemos encontrado cómo crear un modelo de predicción de fuga de clientes dado 19 variables de predicción con una exactitud del 96%.
10.10 Ejercicios
- El data frame
attrition
de la libreríamodeldata
muestra los datos de una relación de casi 1,500 trabajadores de una empresa. Crear una copia de este data frame y almacenalo en el objetotrabajadores
. Luego, constuir un modelo RF con estos datos para predecir el campoAttrition
(deserción laboral). Donde la clase “Yes” quiere decir que renunció y “No” quiere decir que aun trabaja.
Solución
data(attrition)
str(attrition)
trabajadores <- attrition
trabajadores <- trabajadores %>%
rename(renuncia = Attrition)
# 70% para la data de entrenamiento
index_entrena <- createDataPartition(y = trabajadores$renuncia, p = 0.7, list = FALSE)
trabajadores_entrena <- trabajadores[index_entrena, ]
trabajadores_test <- trabajadores[-index_entrena, ]
# Creamos folds para potenciales comparaciones futuras
folds_manual <- createFolds(trabajadores_entrena$renuncia, k = 5)
trabajadores_ctrl <- trainControl(
savePredictions = TRUE,
classProb = TRUE,
summaryFunction = twoClassSummary,
index = folds_manual
)
# Creamos el modelo
trabajadores_modelo_rf <- train(renuncia ~ .,
trabajadores_entrena,
metric = "ROC",
method = "ranger",
trControl = trabajadores_ctrl)
trabajadores_modelo_rf
- Utilizando la data de entrenamiento del ejercicio anterior, construye el modelo GLM.
Solución
- Utilizando la data de entrenamiento, construye el modelo SVM.
Solución
- A partir de los modelos creados, ¿cuál es el más óptimo?
Solución
# Generamos lista de modelos
lista_de_modelos <- list(rf = trabajadores_modelo_rf,
glmmet = trabajadores_modelo_glm,
svm = trabajadores_modelo_svm)
# Comparamos los modelos
comparacion_modelos <- resamples(lista_de_modelos)
# Visualizamos la comparación
bwplot(comparacion_modelos, metric = "ROC")
# Obtenemos el resumen de la comparación
summary(comparacion_modelos, metric = "ROC")
Vemos cómo los resultados se superponen, con lo que podríamos optar por los dos que tengan la media más alta de ROC y entre ellos elegir el que nos un menor rango de valores.
- Crea las matrices de confusión para los tres modelos creados.
Solución
prediccion_rf <- predict(trabajadores_modelo_rf, trabajadores_test)
confusionMatrix(prediccion_rf, trabajadores_test$renuncia)
prediccion_glm <- predict(trabajadores_modelo_glm, trabajadores_test)
confusionMatrix(prediccion_glm, trabajadores_test$renuncia)
prediccion_svm <- predict(trabajadores_modelo_svm, trabajadores_test)
confusionMatrix(prediccion_svm, trabajadores_test$renuncia)
Tener en cuenta que el modelo con el más alto valor de ROC
no necesariamente va a tener el más alto accuracy
. Por ello la elección del modelo se realizó en un paso previo. El ROC balancea mejor sensitividad con el ratio de falsos positivos.
Referencias
Fawcett, Tom. 2005. An Introduction to Roc Analysis. Elsevier. https://people.inf.elte.hu/kiss/11dwhdm/roc.pdf.
Jorge M. Lobo, Raimundo Real, Alberto Jiménez-Valverde. 2007. AUC: A Misleading Measure of the Performance of Predictive Distribution Models. Ecological Sounding. https://www2.unil.ch/biomapper/Download/Lobo-GloEcoBioGeo-2007.pdf.