¿A qué llamamos Machine Learning?

Machine Learning, uno de los términos más utilizados dentro de la Ciencia de Datos, hace referencia a modelos que aprenden patrones de diversos features de los datos, lo cual les permite realizar predicciones/clasificaciones.

Tal como se ve en la imágen, Machine Learning (ML) es una rama de la Inteligencia Artificial; y dentro de ML también existe Deep Learning (DL), pero este último no lo veremos aquí ya que es un tema más complejo y avanzado.

Por ahora, concentrémonos en ML! Así como vimos regresión lineal para hablar de modelos estadísticos, a continuación veremos árboles de decisión o decision tree y bosques aleatorios random forest para hablar de modelos de aprendizaje automático.

Cabe destacar que, para evaluar los modelos de ML es necesario separar los datos en 2 conjuntos llamados:

Tal como sus nombres lo indican, lo ideal es entrenar el modelo con el Set de Entrenamiento y luego calcular todas las métricas a partir del Set de Prueba que nunca fue visto por el modelo.

ÁRBOLES DE DECISIÓN

Los árboles de decisión son uno de los algoritmos o modelos supervisados más populares dentro del Aprendizaje Automático o Machine Learning. Son llamados “árboles” porque inician con un nodo raíz (que representa a la variable/dato que más influye en el modelo) y luego a partir de decisiones, se van ramificando en diferentes resultados, que a su vez generan nuevos nodos que vuelven a ramificarse en otros resultados. Estos nodos se particionan de acuerdo al valor del atributo, y esto es conocido como recursive partitioning o “partición recursiva”.

Cabe destacar que, mientras más variables incluyamos en estos modelos, más preciso será el resultado, ya que el modelo entiende mejor el tema estudiado.

Según la variable a predecir (target) existen 2 tipos de árbol:

  1. Árbol de Clasificación: La variable target es una categoría.

  2. Árbol de Regresión: La variable target es numérica.

Pero como dijimos más arriba, estos árboles también pueden agruparse y generar Bosques Aleatorios o Random Forest que también veremos a continuación.

Como siempre, comencemos instalando y activando las librerías que vamos a utilizar. En este caso se trata de la ya conocida tidyverse y de 3 nuevas librerías que aprenderemos hoy para armar modelos basados en árboles de decisión y bosques aleatorios: rpart, rpart.plot y randomForest

#install.packages("tidyverse")
library(tidyverse)

#install.packages("rpart")
library(rpart)

#install.packages("rpart.plot")
library(rpart.plot)

#install.packages("randomforest")
library(randomForest)

Ahora si arranquemos con un ejemplo práctico utilizando datos provenientes de la Encuesta Permanente de Hogares (EPH) realizada el 3er Trimestre de 2020. Cabe destacar que esta Encuesta se raliza trimestralmente como parte de un programa nacional de indicadores sociales llevado a cabo por el Instituto Nacional de Estadística y Censos (INDEC) con el objetivo de conocer y analizar las condiciones sociodemográficas y socioeconómicas de la población de los diferentes aglomerados urbanos del país.

La fuente oficial de donde se obtuvo la información es https://www.indec.gob.ar/indec/web/Institucional-Indec-BasesDeDatos; y pueden descargar el dataset procesado y “pulido” desde https://data.world/angie-scetta/eph-individual-3t2020

Recomendación: Al descargarlo, moverlo de la carpeta “Descargas” a una nueva carpeta llamada “data” dentro de la carpeta del Proyecto donde estén trabajando.

Ya estamos listos para cargar nuestro set de datos asegurándonos que los string se carguen como factor ya que los modelos no trabajan con datos de tipo character:

eph <- read.csv("data/3T2020_eph_individual.csv",
                    stringsAsFactors = TRUE)

Y como siempre, el primer paso es explorar de que se trata su contenido:

dim(eph)
## [1] 9722   11

El dataset contiene 9722 registros/observaciones y 11 columnas. Veamos el contenido!

summary(eph)
##                REGION                              AGLOMERADO   MAS_500HAB
##  Cuyo             :1349   Partidos del GBA              : 643   NO:6400   
##  Gran Buenos Aires: 886   Gran Salta                    : 528   SI:3322   
##  NEA              :1325   Gran Mendoza                  : 516             
##  NOA              :2445   Jujuy-Palpalá                 : 452             
##  Pampeana         :2560   Santiago del Estero - La Banda: 448             
##  Patagonia        :1157   La Rioja                      : 441             
##                           (Other)                       :6694             
##      SEXO           EDAD            NIVEL_EDUCATIVO  TIPO_TRABAJO  OBRA_SOCIAL
##  HOMBRE:5248   Min.   :15.00   PRIMARIO     :1371   ESTATAL:3610   NO:2601    
##  MUJER :4474   1st Qu.:31.00   SECUNDARIO   :4366   OTRO   : 112   SI:7121    
##                Median :40.00   UNIVERSITARIO:3985   PRIVADO:6000              
##                Mean   :40.41                                                  
##                3rd Qu.:49.00                                                  
##                Max.   :82.00                                                  
##                                                                               
##  SUBSIDIO     INGRESOS      INGRESOS_28K
##  NO:8684   Min.   :   150   MAYOR:4998  
##  SI:1038   1st Qu.: 16000   MENOR:4724  
##            Median : 28000               
##            Mean   : 31401               
##            3rd Qu.: 40000               
##            Max.   :390000               
## 

A partir del resumen estadístico podemos ver que el dataset contiene las encuestas realizadas en todos los aglomerados urbanos y que las 11 columnas refieren a: REGIÓN, AGLOMERADO, HABITANTES DEL AGLOMERADO, SEXO DEL ENTREVISTADO, EDAD, NIVEL EDUCATIVO MÁXIMO, TIPO_TRABAJO, OBRA SOCIAL, SUBSIDIOS e INGRESOS.

Pero antes de continuar tenemos que definir cuál el problema a resolver a partir de un modelo de ML: Con nuestra base de datos podríamos hacer varios modelos, pero hoy vamos a trabajar con la variable INGRESOS_28K que indica quienes de la base de datos cobran más de 28000 pesos y quienes menos. Por lo tanto, el objetivo será entrenar un modelo que aprenda que características (máximo nivel educativo alcanzado, edad, lugar de residencia, etc) tienen en común (o mejor dicho, “qué requisitos cumplen”) las personas que cobran menos de 28000 a diferencia de las que cobran un monto mayor.

Ya estamos en condiciones de armar nuestros set de train y test utilizando un 80% y un 20% de la muestra respectivamente. Pero antes que nada, al tratarse de una selección aleatoria, debemos asegurarnos de definir una “semilla” para obtener resultados reproducibles. Utilicemos set.seed():

set.seed(123)
# seleccionemos aleatoriamente las posiciones de un 80% de la muestra
set_sample <- sample(1:nrow(eph), size = nrow(eph) * 0.8)

# seleccionemos el set de train a partir de la muestra aleatoria del 80%
set_train <- eph %>% 
    filter(row_number() %in% set_sample)

# seleccionemos el set de test a partir del opuesto (!) de la muestra del 80%
set_test <- eph %>% 
    filter(!(row_number() %in% set_sample))

Revisemos la dimensión de ambos set:

dim(set_train)
## [1] 7777   11
dim(set_test)
## [1] 1945   11

Perfecto, tal como esperabamos tenemos 7777 (80%) registros en el train y 1945 (20%) en el test.

ÁRBOL DE DECISIÓN: CLASIFICACIÓN

Hagamos nuestro primer árbol de decisión en R con la función rpart() para entender qué variables de la base de EPH que estamos analizando tienen peso en la variable INGRESOS_28K. Como la variable mencionada es de tipo categórica y tiene 2 valores niveles (“MAYOR” o “MENOR”), el árbol a entrenar con el set de train será de clasificación:

arbol_clasificacion <- rpart(formula = INGRESOS_28K ~ ., #Variable a predecir
                             data = set_train, #Utilizamos set de entrenamiento
                             method = "class") #En este caso el método es de clasificación

Veamos el resultado:

arbol_clasificacion
## n= 7777 
## 
## node), split, n, loss, yval, (yprob)
##       * denotes terminal node
## 
## 1) root 7777 3790 MAYOR (0.5126656 0.4873344)  
##   2) INGRESOS>=27950 3987    0 MAYOR (1.0000000 0.0000000) *
##   3) INGRESOS< 27950 3790    0 MENOR (0.0000000 1.0000000) *

El modelo solo usó una variable explicativa (INGRESOS). Grafiquemos esto para entenderlo mejor:

rpart.plot(arbol_clasificacion, digits = -1)

Como vemos en el gráfico, lógicamente la variable INGRESOS explica el 100% de la variable INGRESOS_28K, por lo tanto se solapan y no nos da información extra para nuestro modelo así que probemos reentrenar pero eliminándola del set de entrenamiento:

arbol_clasificacion <- rpart(formula= INGRESOS_28K ~.,
                       data = set_train %>% select(-INGRESOS),
                       method = "class")

Y ahora veamos el nuevo resultado:

arbol_clasificacion
## n= 7777 
## 
## node), split, n, loss, yval, (yprob)
##       * denotes terminal node
## 
##  1) root 7777 3790 MAYOR (0.51266555 0.48733445)  
##    2) OBRA_SOCIAL=SI 5698 1896 MAYOR (0.66725167 0.33274833)  
##      4) AGLOMERADO=Bahía Blanca - Cerri,Ciudad Autónoma de Buenos Aires,Comodoro Rivadavia - Rada Tilly,Gran Rosario,Gran Santa Fé,Neuquén – Plottier,Rawson – Trelew,Río Gallegos,San Nicolás – Villa Constitución,Santa Rosa – Toay,Viedma – Carmen de Patagones 1683  302 MAYOR (0.82055853 0.17944147) *
##      5) AGLOMERADO=Concordia,Corrientes,Formosa,Gran Catamarca,Gran Córdoba,Gran La Plata,Gran Mendoza,Gran Paraná,Gran Resistencia,Gran Salta,Gran San Juan,Gran San Luis,Gran Tucumán - Tafí Viejo,Jujuy-Palpalá,La Rioja,Mar del Plata,Partidos del GBA,Posadas,Río Cuarto,Santiago del Estero - La Banda 4015 1594 MAYOR (0.60298879 0.39701121)  
##       10) SUBSIDIO=NO 3875 1469 MAYOR (0.62090323 0.37909677)  
##         20) AGLOMERADO=Concordia,Corrientes,Formosa,Gran Catamarca,Gran Córdoba,Gran La Plata,Gran Mendoza,Gran Paraná,Gran Salta,Gran San Juan,Gran San Luis,Gran Tucumán - Tafí Viejo,Jujuy-Palpalá,La Rioja,Mar del Plata,Partidos del GBA,Posadas,Río Cuarto 3494 1246 MAYOR (0.64338867 0.35661133) *
##         21) AGLOMERADO=Gran Resistencia,Santiago del Estero - La Banda 381  158 MENOR (0.41469816 0.58530184) *
##       11) SUBSIDIO=SI 140   15 MENOR (0.10714286 0.89285714) *
##    3) OBRA_SOCIAL=NO 2079  185 MENOR (0.08898509 0.91101491) *

Ahora si aparecen más variables explicativas en el resultado de nuestro árbol de clasificación, donde cada fila hace referencia a un nodo junto a la regla que le corresponde. Se marca con un "*" a los nodos terminales u hojas del árbol.

Volvamos al recurso visual que siempre ayuda a la hora de interpretar resultados. Hagamos un gráfico con rpart.plot():

rpart.plot(arbol_clasificacion)

Como se puede ver en el gráfico, los árboles de decisión están compuestos por nodos con “preguntas” o “reglas” que según la respuesta, seguirán un camino u el otro. Por ejemplo en el primer nodo de nuestro modelo, que dice OBRA_SOCIAL, si la respuesta es NO, el modelo predice con una probabilidad del 91% que los ingresos son menores a 28000. Sin embargo, si es SI, el modelo pasa a otra pregunta que es AGLOMERADO, y así sucesivamente.

¿Cómo podemos interpretar los números dentro de cada nodo y hoja del árbol de clasificación?

  • El primer dato hace referencia a la categoría producto de la predicción (MAYOR o MENOR).

  • El segundo dato contiene la probabilidad de que la predicción sea MENOR. Es decir que, indica que proporción de los casos de ese nodo pertenecen a la categoría MENOR.

  • Por último, el tercer dato muestra la proporción de los casos totales del set de train que han sido agrupados en el nodo u hoja.

Cabe destacar que, de las 9 variables del dataset de EPH, el árbol solo consideró importante a 3 (OBRA_SOCIAL, AGLOMERADO, SUBSIDIO), dejando de lado al resto de variables que no resultaron relevantes en la predicción.

Otra forma de ver las reglas que generó nuestro árbol, es utilizando rpart.rules():

rpart.rules(arbol_clasificacion)
##  INGRESOS_28K                                                                                                                                                                                                                                                                                                                                                                                                           
##          0.18 when OBRA_SOCIAL is SI & AGLOMERADO is                                                                      Bahía Blanca - Cerri or Ciudad Autónoma de Buenos Aires or Comodoro Rivadavia - Rada Tilly or Gran Rosario or Gran Santa Fé or Neuquén – Plottier or Rawson – Trelew or Río Gallegos or San Nicolás – Villa Constitución or Santa Rosa – Toay or Viedma – Carmen de Patagones                 
##          0.36 when OBRA_SOCIAL is SI & AGLOMERADO is                                                       Concordia or Corrientes or Formosa or Gran Catamarca or Gran Córdoba or Gran La Plata or Gran Mendoza or Gran Paraná or Gran Salta or Gran San Juan or Gran San Luis or Gran Tucumán - Tafí Viejo or Jujuy-Palpalá or La Rioja or Mar del Plata or Partidos del GBA or Posadas or Río Cuarto & SUBSIDIO is NO
##          0.59 when OBRA_SOCIAL is SI & AGLOMERADO is                                                                                                                                                                                                                                                                                                 Gran Resistencia or Santiago del Estero - La Banda & SUBSIDIO is NO
##          0.89 when OBRA_SOCIAL is SI & AGLOMERADO is Concordia or Corrientes or Formosa or Gran Catamarca or Gran Córdoba or Gran La Plata or Gran Mendoza or Gran Paraná or Gran Resistencia or Gran Salta or Gran San Juan or Gran San Luis or Gran Tucumán - Tafí Viejo or Jujuy-Palpalá or La Rioja or Mar del Plata or Partidos del GBA or Posadas or Río Cuarto or Santiago del Estero - La Banda & SUBSIDIO is SI
##          0.91 when OBRA_SOCIAL is NO

Pero esto no termina acá, todavía nos queda la parte más importante que es la de testear nuestro modelo con el set de test para poder entender qué tan bien hace sus predicciones.

¿Qué métricas utilizamos para medir la performance del modelo realizado?

Para medir el rendimiento de los modelos de ML de tipo clasificación se suele utilizar la Matriz de Confusión, que divide en 4 celdas los resultados obtenidos y luce así:

Los valores más importantes son los de la diagonal que desciende de izquierda a derecha ya que son los datos que nuestro modelo acertó en la predicción. Estos valores son llamados “Verdaderos Negativos (VN)” y “Verdaderos Positivos (VP)”.

Pero para poder ver nuestra matriz de confusión, antes debemos hacer las predicciones con el set de test usando la función predict(). En este caso la vamos a calcular dentro de una columna del set así es más sencillo realizar luego la matriz:

set_test <- set_test %>%
  mutate(PREDICCION_CLASS = predict(arbol_clasificacion, #nombre del modelo entrenado
                                    newdata = set_test, #set de test
                                    type="class")) #tipo de modelo

Utilicemos head() para revisar que se haya generado la nueva columna llamada PREDICCION_CLASS:

head(set_test)
##      REGION     AGLOMERADO MAS_500HAB   SEXO EDAD NIVEL_EDUCATIVO TIPO_TRABAJO
## 1       NOA  Jujuy-Palpalá         NO  MUJER   44   UNIVERSITARIO      ESTATAL
## 2 Patagonia   Río Gallegos         NO HOMBRE   63   UNIVERSITARIO      PRIVADO
## 3       NOA Gran Catamarca         NO HOMBRE   23   UNIVERSITARIO      ESTATAL
## 4       NOA Gran Catamarca         NO  MUJER   49      SECUNDARIO      ESTATAL
## 5       NOA Gran Catamarca         NO HOMBRE   52      SECUNDARIO      ESTATAL
## 6       NOA Gran Catamarca         NO HOMBRE   31      SECUNDARIO      ESTATAL
##   OBRA_SOCIAL SUBSIDIO INGRESOS INGRESOS_28K PREDICCION_CLASS
## 1          SI       NO    25000        MENOR            MAYOR
## 2          SI       NO    70000        MAYOR            MAYOR
## 3          SI       NO    35000        MAYOR            MAYOR
## 4          SI       NO    25000        MENOR            MAYOR
## 5          SI       NO    30000        MAYOR            MAYOR
## 6          SI       NO    50000        MAYOR            MAYOR

Y ahora si, ya estamos en condiciones de hacer nuestra matriz de confusión con las columnas INGRESO_35K y PREDICCION_CLASS para ver que tan bien funciona el modelo:

table(set_test$PREDICCION_CLASS, set_test$INGRESOS_28K)
##        
##         MAYOR MENOR
##   MAYOR   920   389
##   MENOR    91   545

A simple vista los resultados parecen buenos, ¿No? El modelo tiene un error del 25% (Falsos Positivos + Falsos Negativos) y una exactitud o accuracy del 75%, es decir que del total de 1945 registros, nuestro árbol de decisión encontró un patrón en la predicción de 1465 (1465/1945).

Pero, ¿Cómo podríamos saber si un 75% de exactitud es bueno o malo? Bueno para esto es necesario que entendamos la distribución inicial de los datos, es decir, que porcentaje tenía un sueldo mayor a 28000 y que porcentaje tenía un sueldo menor.

table(eph$INGRESOS_28K)/nrow(eph)
## 
##     MAYOR     MENOR 
## 0.5140918 0.4859082

Podemos ver que el 51% de los encuestados tiene un sueldo mayor a 28000, por lo tanto, si categorizamos al 100% de nuestros datos como “mayores de 28000”, vamos a tener un error del 49%. Sin embargo el árbol que creamos lo redujo a 25%. Nada mal, ¿No?

Veamos si en el set de train estos resultados eran los mismos:

table(predict(arbol_clasificacion, type="class"), set_train$INGRESOS_28K)
##        
##         MAYOR MENOR
##   MAYOR  3629  1548
##   MENOR   358  2242

Efectivamente, hubo una exactitud del 75% ya que el árbol encontró un patrón en 5871 de 7777 registros totales.

Pero como dijimos antes, existe una versión “evolucionada” de los árboles de decisión, y son los random forest que combinan varios árboles. Así que probemos este mismo caso de estudio pero con un RF.

RANDOM FOREST: CLASIFICACIÓN

El concepto de Random Forest o Bosque Aleatorio hace referencia a una técnica de aprendizaje automático supervisado muy popular ya que presenta una gran capacidad de generalización a la hora de resolver varios problemas. Un Random Forest es una combinación o ensamble de árboles de decisión. La ventaja del bosque frente a los árboles, es que ningúno de los árboles que lo componen ven la totalidad de datos de train. Es decir, cada uno ve distintas porciones/partes de los datos. Por lo tanto, cada árbol se entrena con distintas muestras para el mismo problema, al combinar resultados, se “compensan” los errores y obtenemos predicciones más exactas ya que el modelo generaliza mejor.

Es importante destacar que, los árboles de decisión tienen tendencia al overfitting ya que para entrenar ven la totalidad de los datos, entonces los aprenden muy bien. Así que justamente, el plus que pueden llegar a tener los RF son que mejora la capacidad de generalización del modelo.

Sigamos trabajando con los mismos set de train y test pero ahora utilicemos la función randomForest():

modelo_RF <- randomForest(INGRESOS_28K ~ .,
                          data = set_train %>% select(-INGRESOS),
                          ntree = 500) #Por default este parámetro es siempre 500

Y veamos los resultados del set de train:

modelo_RF
## 
## Call:
##  randomForest(formula = INGRESOS_28K ~ ., data = set_train %>%      select(-INGRESOS), ntree = 500) 
##                Type of random forest: classification
##                      Number of trees: 500
## No. of variables tried at each split: 3
## 
##         OOB estimate of  error rate: 22.35%
## Confusion matrix:
##       MAYOR MENOR class.error
## MAYOR  3406   581   0.1457236
## MENOR  1157  2633   0.3052770

Volvimos a encontrarnos con la matriz de confusión, donde podemos ver que en el entrenamiento logró separar correctamente 6049 registros de los 7777, es decir que la exactitud aumentó respecto al árbol. Pasó de ser 75% a 77%.

Probemos con el set de test a ver que pasa:

set_test <- set_test %>%
  mutate(PREDICCION_RF = predict(modelo_RF,
                                 newdata = set_test))

Y veamos la matriz de confusión:

table(set_test$PREDICCION_RF, set_test$INGRESOS_28K)
##        
##         MAYOR MENOR
##   MAYOR   848   275
##   MENOR   163   659

Se mantiene el mismo accuracy que en el entrenamiento (77%) ya que el modelo predice correctamente 1505 registros de un total de 1945.

Por lo tanto, como ambas performance son similares podríamos asegurar que nuestro modelo no sufre de overfitting, es decir que puede generaliza bien porque no aprendió demasiado las especificidades de la muestra con la que se lo entrenó.

Por último, hagamos un scatter plot que nos permita visualizar los resultados:

ggplot(set_test) +
    geom_jitter(aes(x = PREDICCION_RF, y = INGRESOS_28K, color=INGRESOS_28K)) 

Pero como dijimos al principio, los árboles y bosques también pueden ser utilizados para problemas del tipo “regresión”, es decir que nuestra variable a predecir sea numérica.

Ahora les toca a uds hacer otro ejemplo para practicar!

Elegir una variable categórica a predecir y realizar un modelo predictivo: 1. Modelo Árbol de Clasificación. Interpretar y describir resultados de ambos set (Accuracy o Exactitud). 2. Modelo Random Forest. Interpretar y describir resultados de ambos set (Accuracy o Exactitud).

Aclaración: En cualquiera de los 2 casos deberán separar un 80% de la muestra para entrenar y un 20% para testear.