Inteligencia Analítica de Datos con R

Clasificador Naive Bayes

Msc. Roberto Trespalacios

Universidad Tecnológica de Bolivar

6/8/23

Clasificador Naive Bayes

¿Qué son los modelos Naive Bayes?

  • En un sentido amplio, los modelos de Naive Bayes son una clase especial de algoritmos de clasificación de Aprendizaje Automatico, o Machine Learning, tal y como nos referiremos de ahora en adelante.

  • Se basan en una técnica de clasificación estadística llamada “teorema de Bayes”.

  • Estos modelos son llamados algoritmos “Naive”, o “Inocentes” en español. En ellos se asume que las variables predictoras son independientes entre sí.

  • En otras palabras, que la presencia de una cierta característica en un conjunto de datos no está en absoluto relacionada con la presencia de cualquier otra característica.

  • Proporcionan una manera fácil de construir modelos con un comportamiento muy bueno debido a su simplicidad.

Teorema de Bayes

El teorema de Bayes expresa la probabilidad de que ocurra el evento A, dado que ha ocurrido B, en función de la probabilidad de que ocurra B dado que ha ocurrido A, de la probabilidad de A y de la probabilidad de B. La fórmula del teorema de Bayes es la siguiente:

\[P(B|A)=\frac{P(B)P(A|B)}{P(A)}\]

Donde:

  • \(A\) y \(B\) son eventos, y además: \(P(B) \neq 0\).
  • \(P(A|B)\): es la probabilidad de que ocurra A, dado que ha ocurrido B.
  • \(P(B|A)\): es la probabilidad de que ocurra B, dado que ha ocurrido A.
  • \(P(A)\): es la probabilidad de que ocurra A.
  • \(P(B)\): es la probabilidad de que ocurra B.

Observaciones

  • Como se puede apreciar, el teorema de Bayes permite calcular la probabilidad de que ocurra un evento, a partir de valores conocidos de otras probabilidades relacionadas al evento.

  • El teorema o regla de Bayes fue planteado por el matemático y religioso inglés Thomas Bayes. Este teorema fue publicado en el año 1763, dos años después de la muerte de Bayes.

Ejemplo 1:

En la academia de Matemóvil, la probabilidad de que a un alumno seleccionado al azar le guste el helado es del 60%, mientras que la probabilidad de que a un alumno le guste la torta es del 36%. Además, se sabe que la probabilidad de que a un alumno le guste la torta dado que le gusta el helado es del 40%. Calcular la probabilidad de que a un alumno le guste el helado, dado que le gusta la torta.

Solución:

Primero definimos los 2 eventos con los que vamos a trabajar:

  • H: que a un alumno le guste el helado.
  • T: que a un alumno le guste la torta.

Tenemos los siguientes datos:

  • \(P(H) = 0.6\)
  • \(P(T) = 0.36\)
  • \(P(T|H) = 0.4\)

Nos piden calcular \(P(H|T)\). Aplicamos el teorema de Bayes:

\[\begin{align*} P(H|T)= & \frac{P(H)P(T|H)}{P(T)} \\ = & \frac{0.6 \times 0.4 }{0.36} \\ = & 0.6667 \end{align*}\]

Entonces, la probabilidad de que un alumno le guste el helado dado que le gusta la torta es de 0.6667 o 66.67%.

Conceptos básicos del clasificador Naive Bayes

El clasificador Naive Bayes aplica el bien conocido teorema de Bayes para la probabilidad condicional. En su forma más simple para el evento A y B, el teorema de Bayes relaciona dos probabilidades condicionales de la siguiente manera:

\[P(A\cap B)=P(A, B)=P(A)P(B|A)=P(B)P(A|B) \implies P(B|A)=\frac{P(B)P(A|B)}{P(A)}\]

Ahora veamos cómo se puede usar esta fórmula simple para hacer un clasificador.

  • En un problema de clasificación, tenemos algunos predictores (características/covaritas/variables independientes) y un resultado (objetivo/clase/variable dependiente).
  • Cada observación tiene unos valores para los predictores y una clase.
  • A partir de estos predictores y clases asociadas, queremos aprender de modo que, si se dan los valores de las características, podamos predecir la clase (¡eso es todo acerca del aprendizaje supervisado!).
  • En Naive Bayes, el algoritmo evalúa una probabilidad para cada clase, cuando se dan los valores predictores. Intuitivamente, podemos elegir la clase que tenga la probabilidad más alta.

Clasificador Naive Bayes y sus elementos

  1. Supongamos que hay \(n\) predictores, denotados por \(X_1,X_2,X_3, \dots, X_n\)
  2. Además, tenemos la variable de resultado \(y\), procedente de uno de las \(k\) clases, indicadas por \(C_1,C_2,C_3,\dots,C_k\)
  3. Supongamos que los valores predictores se dan de la siguiente manera:

\[X_1=x_1,\ X_2=x_2,\ X_3=x_3,\dots , X_n=x_n\]

Ahora, lo que queremos hacer es evaluar la probabilidad de que la observación provenga de cualquiera de las \(K\) clases, es decir, de algún \(C_k\). Podemos escribir esto en términos de notación de probabilidad condicional:

\[P(y=C_k|X_1=x_1, X_2=x_2,\dots , X_n=x_n), \\ \text{de forma breve,}\ \ \ \ \ \ P(C_k|x_1, x_2,x_3,\dots , x_n)\]

En la fórmula de Bayes anterior, si reemplazamos

\[B = C_k \quad \text{ y } \quad A=x_1 \cap x_2 \cap, \dots, \cap x_n=\{x_1,x_2,\dots,x_n\}\]

podemos escribir la probabilidad condicional anterior de la siguiente manera:

\[P(C_k|x_1, x_2,\dots , x_n)=\frac{P(C_k)P(x_1, x_2,\dots,x_n|C_k)}{P(x_1, x_2,\dots,x_n)}\]

Ahora el numerador de lo anterior es solo la probabilidad conjunta de \(C_k\) y \(\{x_1,x_2,\dots,x_n\}\), es decir \[P(C_k\cap \{ x_1, x_2,\dots,x_n\})=P(C_k, x_1, x_2,\dots,x_n)\]

Ahora , podemos aplicar la fórmula de Bayes repetidamente para obtener el siguientes resultado:

\[P(C_k)P(x_n|C_k)P(x_{n-1}|x_n,C_k)\dots P(x_1|x_2,x_3, \dots,x_n, C_k)\]

El supuesto de independencia condicional

La expresión anterior es bastante larga, también implica un número de probabilidades condicionales. Pero con un supuesto de independencia condicional, esta larga expresión se puede reducir a una forma muy simple.

  • El supuesto de independencia condicional es que dada una clase, digamos \(C_k\)
  • los valores de predictor/característica son independientes entre sí.
  • No hay correlación entre las características de una determinada clase.
  • Matemáticamente, si el evento \(A\) y \(B\) son independientes condicionados al evento \(C\), entonces se cumple lo siguiente:

\[P(A|B,C)=P(A|C)\]

Ahora bien, esta fórmula se puede aplicar para nuestro caso. Asumimos que todos los predictores \(x_1,x_2, \dots,x_n\) son independientes condicionados a la clase \(C_k\). Por lo tanto,

\[\begin{align*} P(x_1|x_2,x_3,\dots, x_n,C_k)= & P(x_1|C_k)\\ P(x_2|x_3,x_4,\dots, x_n,C_k)= & P(x_2|C_k) \end{align*}\] etcétera. Entonces, la expresión larga anterior se puede escribir como:

\[P(x_1, x_2,\dots,x_n, C_k)=P(C_k)P(x_n|C_k)P(x_{n-1}|C_k)P(x_{n-2}|C_k)\dots P(x_1|C_k)\]

\[\implies P(x_1, x_2,\dots,x_n, C_k)=P(C_k)\prod_{j=1}^{n}P(x_j|C_k)\]

Luego, tenemos que:

\[P(C_k|x_1, x_2,x_3,\dots , x_n)=\frac{P(C_k)\prod_{j=1}^{n}P(x_j|C_k)}{P(x_1, x_2,\dots,x_n)}\]

Observaciones

Sobre la independencia de los eventos en Naive Bayes

  • Estamos tratando de obtener la probabilidad de \(C_k\) para valores de característica dados.
  • El denominador de arriba es un término constante para características dadas.
  • Por lo tanto, será suficiente considerar solo la parte del numerador para comparar las probabilidades de diferentes clases condicionadas en valores de característica fijos.
  • Es decir, evaluamos la parte del numerador para todos los valores posibles de \(k \subset \{1,2,\dots,K\}\) y vota por el de mayor valor.

Probabilidad a priori y la verosimilitud

\[P(C_k) \quad \text { y } \quad \prod^n_{j=1}P(x_j|C_k)\] en la expresión anterior se conocen como a priori y verosimilitud respectivamente. La probabilidad a priori se puede estimar como

\[P(C_k)=\frac{n_{class \ k}}{n_{total}}\]

  • Para la probabilidad, necesitamos las distribuciones de probabilidad condicional, \(f(X_j|C_k)\) para \(j=1,2,\dots,n\).
  • Si \(X_j\) es continua, la normalidad es una suposición común.
  • Si \(X_k\) es discreta, una suposición común es la distribución multinomial.
  • También podemos aplicar distribuciones no paramétricas.

Análisis de clasificación en R: Naive Bayes

Usaremos los paquetes DescTools, caret y e1071; además, el archivo DirectMarketing.csv, que contiene información de 1000 clientes de una empresa que vende productos por catálogo de pedidos por correo. El problema es el siguiente:

  • El director de marketing de la empresa desea utilizar datos históricos sobre sus clientes (información demográfica y de marketing anterior) para predecir si el cliente gastará (Cantidad) igual o por debajo del promedio (Por debajo del promedio) o por encima del promedio (Above_Avg).
  • La empresa prioriza predecir correctamente si un cliente comprará más que el cliente promedio, porque tienen un presupuesto de publicidad limitado y necesitan tomar decisiones estratégicas de marketing.

Las variables incluyen:

  • Edad: edad del cliente (joven, mediana, vieja)
  • Género: género del cliente (masculino, femenino)
  • Vivienda Propia: si el cliente es propietario de una vivienda (Propiedad) o alquila (Rent)
  • Casado: si el cliente es casado (Married) o soltero (Single)
  • Ubicación: si el cliente vive cerca de la empresa (Cerca) o no (Lejos)
  • Salario: el salario anual de los clientes.
  • Hijos: el número de hijos que tiene el cliente (0, 1, 2, 3)
  • Catálogos: el número total de catálogos que ha recibido el cliente (6, 12, 18, 24)
  • Cantidad: el nivel de dinero que el cliente ha gastado, ya sea en o por debajo del promedio (Below_Avg) o por encima (Above_Avg)

Librerias, exploración de datos y preparación de variables

Librerías nocesarias

library(caret)
library(DescTools)
library(e1071)
library(readr)
DM <- read_csv("/home/rober/Documents/utb_diplomado/sem_5.2/datos/DirectMarketing.csv")

Primero, podemos obtener información de alto nivel sobre el marco de datos de DM para observar los tipos de variables y verificar los valores faltantes (NA).

Abstract(DM)

Preparación de la variable objetivo (Y)

A continuación, podemos convertir nuestra variable de clase objetivo que queremos predecir, Importe, en una variable de factor nominal.

Nota:

Aunque es ordinal, la usaremos como variable nominal por compatibilidad. Identificaremos el nivel 1 como Below_Avg y el nivel 2 como Above_Avg para reflejar el orden conocido.

DM$Amount <- ifelse(DM$AmountSpent < mean(DM$AmountSpent), "Below_Avg" , "Above_Avg")

DM <- DM[, -9]

Podemos trazar la distribución utilizando un gráfico de barras, que es el gráfico predeterminado para la función plot() cuando se aplica a variables de factores.

dt <- data.frame(Count = c(399,  601), Amount = c("Below_Avg", "Above_Avg"))

barplot(Count ~ Amount, data = dt, main = "Amount Spent")

Preparación de variables predictoras (X)

Variables nominales

  • Primero, podemos convertir nuestras variables predictoras nominales en variables factoriales.
  • Estableceremos un vector de conveniencia de los nombres de las variables nominales (noms) para que podamos referirnos a ellas más fácilmente.
noms <- c("Age", "Gender", "OwnHome", "Married", "Location", "Amount")
DM[ ,noms] <- lapply(X = DM[ ,noms], FUN = factor)

Variables Ordinales

  • A continuación, podemos convertir nuestras variables ordinales.
  • La variable Edad toma tres niveles, que necesitaremos especificar con el orden correcto.
  • Dado que las variables Children y Catalogs toman números, podemos usar el orden predeterminado y convertirlos al mismo tiempo.
  • Estableceremos un vector de conveniencia de los nombres de las variables ordinales (ords) para que podamos referirnos a ellas más fácilmente.
ords <- c("Age", "Children", "Catalogs")

DM$Age <- factor(x = DM$Age,
                 levels = c("Young", "Middle", "Old"),
                 ordered = TRUE)

DM[,c("Children", "Catalogs")] <- lapply(X = DM[, c("Children", "Catalogs")] ,
                                         FUN = factor,
                                         ordered = TRUE)

Variables numéricas

Solo tenemos una variable numérica, Salario. Podemos configurar un valor con nombre, nums (aunque no es necesario).

nums <- c("Salary")

Finalmente, podemos crear nuestro vector de conveniencia de variables predictoras (vars) que contiene todos nuestros predictores. Podemos ver los predictores por nombre ejecutando una línea de código del objeto vars.

vars <- c(noms, ords, nums)
vars

Preprocesamiento y transformación de datos

Veamos la información de summary() para nuestros datos preparados.

summary(DM)

Observación

  • Para la clasificación de Naive Bayes, sabemos que podemos mantener las características irrelevantes
  • Los valores faltantes pueden eliminarse, imputarse o ignorarse durante el análisis,
  • Las variables categóricas pueden incorporarse como variables de factor.
  • Necesitamos eliminar las características numéricas redundantes cor().
  • También nos gustaría que nuestras variables numéricas tuvieran una distribución aproximadamente normal.

Preprocesamiento y transformación de datos

  1. Valores perdidos: si hay valores faltantes, podemos eliminarlos por filas (na.omit()) o realizar una imputación (o podemos manejarlos durante el análisis).
PlotMiss(DM)
  1. Variables redundantes: necesitamos identificar variables predictoras numéricas (X) altamente correlacionadas y excluirlas de nuestro modelo predictivo. Dado que solo tenemos una variable numérica, no debemos preocuparnos por la redundancia.

  2. Variables numéricas normalmente distribuidas: por suposición, Naive Bayes espera que las variables numéricas tengan una distribución normal. Podemos inspeccionar visualmente la distribución de nuestra variable numérica, Salario, utilizando un histograma.

hist(DM$Salary, main = "Salary")

Transformación Box-Cox (para normalizar)

Podemos transformar la variable Salario a aproximadamente normal.

  • Primero, podemos observar la distribución de la variable Salario para determinar qué transformación utilizar.
    • Si tenemos valores estrictamente positivos, podemos usar una transformación de Box-Cox.
    • Si no, podemos usar una transformación de Yeo-Johnson.
cen_bcs <- preProcess(x = as.data.frame(DM[ ,vars]), method = c("BoxCox", "center", "scale"))
DM_bcs <- predict(object = cen_bcs, newdata = as.data.frame(DM))
hist(DM_bcs$Salary, main = "Salary")

Conjuntos de entrenamiento y prueba (training y test)

Usamos la función createDataPartition() del paquete caret para el conjunto de entrenamiento. Dado que la función toma una muestra aleatoria, primero inicializamos una semilla (set.seed(831)) aleatoria para la reproducibilidad de la partición de los datos.

set.seed(831) # semilla aleatoria

# generamos el train
sub <- createDataPartition(y = DM_bcs$Amount, 
                           p = 0.80, 
                           list = FALSE)

Subconjunto de las filas del marco de datos DM_bcs para incluir los números de fila en el subobjeto para crear el marco de datos del tren. Usamos todas las observaciones que no están en el subobjeto para crear el marco de datos de prueba.

train <- DM_bcs[sub, ] 
test <- DM_bcs[-sub, ]

Análisis

Usamos la función naiveBayes() del paquete e1071 para realizar la clasificación Naive Bayes.

  • Si tenemos valores de NA que no manejamos durante el preprocesamiento, podemos usar el argumento na.action, que por defecto es na.pass, lo que significa que las NA no se incluirán al calcular las probabilidades. Alternativamente, se puede especificar na.action = na.omit y los NA no se incluirán en el modelado.

  • El valor predeterminado de Laplace es 0, lo que significa que no se aplica el suavizado de Laplace.

  • Para determinar si necesitamos usar el suavizado de Laplace, debemos buscar categorías de probabilidad cero.

aggregate(train[ , c(noms, ords)],
          by = list(train$Amount),
          FUN = table)

Como tenemos categorías con frecuencia 0, estableceremos laplace = 1.

El modelo con sus parámetros sería:

nb_mod <- naiveBayes(x = train[ ,vars],
                     y = train$Amount,
                     laplace = 0)

Rendimiento y ajuste del modelo

Rendimiento del entrenamiento

Para evaluar la bondad de ajuste del modelo, comparamos el rendimiento de entrenamiento y prueba. Por esta razón, necesitamos evaluar qué tan bien funciona el modelo en la muestra de entrenamiento.

  • Primero, usamos la función predict() para obtener las predicciones de clase (tipo = “clase”) para los datos de entrenamiento (tren) basados en nuestro modelo NB. Esto crea un vector de predicciones de clase.
nb.train <- predict(object = nb_mod,           # modelo NB
                    newdata = train[ ,vars],   # modelo NB
                    type = "class")

Matriz de confusión

  • Podemos usar la función confusionMatrix() del paquete caret para obtener una matriz de confusión y obtener medidas de rendimiento para nuestro modelo aplicado al conjunto de datos de entrenamiento (tren).
  • Podemos establecer mode = “everything” para obtener todas las medidas de rendimiento disponibles.
  • Identificamos la clase Above_Avg como positiva, ya que es la clase que más nos interesa poder predecir.
  • Lo guardaremos para poder hacer comparaciones.
train_conf <- confusionMatrix(data = nb.train, # predictions
                              reference = train$Amount, # actual
                              positive = "Above_Avg",
                              mode = "everything")
train_conf

Prueba de rendimiento del modelo

Para evaluar el rendimiento del modelo, nos enfocamos en el rendimiento del modelo aplicado al conjunto de prueba. A continuación, usamos la función predict() para obtener las predicciones de clase (type = “class”) para los datos de prueba basados en nuestro modelo NB.

nb.test <- predict(object = nb_mod, # NB model
                   newdata = test[ ,vars], # predictors
                   type = "class")

Nuevamente, usamos la función confusionMatrix() del paquete caret para obtener una matriz de confusión y obtener medidas de rendimiento para nuestro modelo, esta vez aplicadas al conjunto de datos de prueba (prueba).

test_conf <- confusionMatrix(data = nb.test, 
                             reference = test$Amount, 
                             positive = "Above_Avg",
                             mode = "everything")
test_conf

Podemos describir el rendimiento general en función de nuestra precisión y valores kappa.

test_conf$overall[c("Accuracy", "Kappa")]

Podemos describir el rendimiento a nivel de clase para los diferentes niveles de clase. Tenga en cuenta que arriba establecemos positivo = “Above_Avg”, ya que estamos más interesados en predecir clientes que gastarán una cantidad superior a la media

test_conf$byClass

Bondad de ajuste del modelo

  • Para evaluar la bondad de ajuste del modelo, queremos saber si el modelo está equilibrado, con ajuste insuficiente o con ajuste excesivo.
  • Comparamos el rendimiento en los conjuntos de entrenamiento y prueba.
  • Podemos usar la función cbind() para comparar nuestra salida confusionMatrix() una al lado de la otra, para comparar el rendimiento general en los conjuntos de entrenamiento y prueba.
cbind(Training = train_conf$overall,
      Testing = test_conf$overall)

Comparemos ahora el rendimiento de nivel de clase en los conjuntos de entrenamiento y prueba.

cbind(Training = train_conf$byClass,
      Testing = test_conf$byClass)
  • Como se muestra, tenemos un rendimiento similar en nuestros conjuntos de entrenamiento y prueba.
  • Por ello, concluiríamos que el modelo está equilibrado.
  • Además, es un modelo de alto rendimiento, con un rendimiento general y de nivel de clase bastante alto cuando se aplica al conjunto de datos de prueba.

Ejercicio

Con los datos salary_data_2

  • Graficar las variables Age y Salary diferenciando los niveles de la variable Gender.
  • Realizar una gráfica pareada de Gender vs Age, Salary y Years_of_Experience.
  • Verificar el supuesto de normalidad de las variables numericas del modelo y si es necesario, transformar con BOX-COX.
  • Crear los conjuntos de datos de entrenamiento y prueba (train y test).
  • Use Naive Bayes para ajustar un modelo de clasificación para la variable Gender.
  • Analizar el modelo.
  • Graficar las variables Age y Salary diferenciando los niveles de la variable Gender pero con las clases predichas por el modelo austado.