Una implementación sencilla de un clasificador Naive Bayes

Erick Almaraz
Ciencia y Datos
Published in
6 min readDec 10, 2018

Para los que somos novatos en este asunto de la ciencia de datos siempre es emocionante buscar problemas que nos ayuden a desarrollar nuestras habilidades analíticas. Esto es particularmente excitante sobre todo cuando el problema en cuestión no tiene nada que ver con nuestro campo del conocimiento y por lo tanto no estamos familiarizados con lo que podemos obtener. Yo por ejemplo, soy físico y mis conocimientos en un campo tan dispar como la salud humana dejan mucho que desear (no me pregunten la función detallada de cada uno de los órganos del cuerpo humano porque se llevarían una sorpresa desagradable).

Pero bueno, en este artículo discutiré una implementación sencilla de un clasificador Naive Bayes para predecir si un paciente tiene o no diabetes. Para eso, he tomado el catálogo ‘Pima Indians Diabetes Database’ disponible en:

El notebook con el análisis (en inglés) se puede encontrar en mi cuenta de github. Aquí discutiré las diferentes partes del código. ¡Manos a la obra!

Empezamos, como siempre, cargando todas las bibliotecas que vamos a utilizar:

Aquí realmente no hay nada novedoso, salvo que definimos la constante pi (para no tener que andar escribiendo math.pi a cada rato) y cambiamos el estilo de las gráficas de matplotlib a ‘classic’. Ahora bien, no sólo en ciencia de datos, sino en general en cualquier problema en la vida, siempre es bueno tomarse un tiempo para revisar la información de la que disponemos. Esto nos puede ayudar a desarrollar nuestra intuición del problema, conocer las limitaciones de nuestro estudio, ver si hay necesidad de darle un nuevo formato a los datos , ver cómo éstos se hallan distribuidos, etc. La sección ‘Exploratory analysis’ justamente está pensada en eso. Empezamos creando un dataframe de pandas para todo el catálogo:

Y luego implementamos un par de máscaras booleanas para separar los pacientes enfermos y sanos:

El catálogo reúne la información de 768 pacientes, de los cuales 500 están sanos y 268 tienen diabetes. Podemos ver cómo se distribuye la información haciendo, por ejemplo:

El resultado se puede ver en la gráfica triangular:

A grandes rasgos, vemos que los campos ‘Pregnancies’, ‘Insulin’, ‘Diabetes Pedigree Function’ & ‘Age’ tienen una distribución exponencial, mientras que ‘Glucose’, ‘Blood Pressure’, ‘Skin Thickness’ & ‘BMI’ pueden ser modeladas con una distribución gaussiana. Esto nos servirá más adelante cuando calculemos las probabilidades condicionales (likelihood) en el teorema de Bayes Eq. (2).

Lo que sigue es propiamente la implementación de nuestro clasificador. En este análisis hemos construido cuatro funciones:

  1. create_training_test. Esta función se encarga de construir el training y el test set.

La función toma como argumentos el catálogo de los pacientes (dataset), la fracción correspondiente al training set (fraction_training) y una banderilla lógica usada para desplegar mensajes sobre lo que está realizando el programa (msg). El procedimiento consiste básicamente en tomar todo el cátalogo y hacer dos copias. Luego seleccionamos aleatoriamente una cantidad de índices cuyo tamaño es el del test set y de ahí aplicamos apropiadamente el método drop para quedarnos con cada uno de los conjuntos. Nótese que tanto el training set, como el test set son dataframes de pandas.

2. get_parameters. Esta función calcula la media y la desviación estándar de cada uno de los campos con las mediciones que se realizan en los pacientes.

Aquí features son los campos, es decir, las cantidades que se miden en los pacientes (‘Age’, ‘Pregnancies’, etc) y que están enlistadas en las columnas de los dataframes. Notemos que el primer loop se hace hasta la penúltima columna, pues la última columna Outcome corresponde al diagnóstico sano (0) — enfermo (1) con el cual compararemos nuestras predicciones.

Básicamente lo que hicimos fue definir diez intervalos de clase (bins) para cada campo y contar la frecuencia absoluta, es decir, el número de pacientes por bin. A partir de las marcas de clase (the mid point of each bin) y de la frecuencia absoluta (the number of counts) calculamos la media (μ) y la desviación estándar (σ). Estos resultados los almacenamos en un diccionario cuyas llaves son el nombre del campo y los valores son las parejas ordenadas (μ,σ). Por ejemplo:

'Pregnancies':(5.081610,3.705020)

3. likelihood. Esta función se encarga de calcular el likelihood o probabilidad condicional:

Ecuación 1. Likelihood en el teorema de Bayes

La función toma como argumento un renglón de un dataframe (instance) y el diccionario con la media y la desviación estándar (dictionary) que calculamos en get_parameters. Para cada campo calculamos la probabilidad condicional P(feature|outcome). Aquí es donde aplicamos las funciones de densidad de probabilidad exponencial y gaussiana que vimos en nuestro análisis exploratorio.

Ahora bien, en estricto sentido esta función NO devuelve la probabilidad condicional P(feature|outcome), sino la función de densidad de probabilidad (f). Recordemos que la probabilidad P y la densidad de probabilidad f vienen relacionadas por: P(x)=f(x)*dx, en donde dx es un pequeño intervalo de la variable aleatoria x. El punto sutil es que f(x) puede ser mayor que uno, pero el producto P(x)=f(x)*dx siempre tiene que ser menor que uno y positivo. En este caso, la diferencia entre P y f no es importante porque para hacer nuestra clasificacion no necesitaremos conocer exactamente P(outcome=1|feature) & P(outcome=0|feature), sino ver cómo es uno relativamente con el otro. Por lo tanto, el factor dx y la evidencia bayesiana P(feature) que aparece en el denominador del teorema de Bayes (ver ecuación 2) sólo agregan las mismas constantes multiplicativas para los dos casos (outcome=1 & outcome=0) y por lo tanto podemos prescindir de ellas.

Al final, esta función devuelve un diccionario cuyas llaves son los nombres de los campos y los valores son el ‘likelihood’ P(feature|outcome).

4. bayes. En esta función usamos el teorema de Bayes:

Ecuación 2. Teorema de Bayes

para hacer nuestras predicciones.

Aquí P(features|outcome) ha sido calculado previamente en likelihood, mientras que P(outcome) viene dado por la cantidad de casos positivos (negativos) entre el total. Como la palabra naive lo indica, hemos supuesto que el resultado de la medición de un campo es independiente del resultado de la medición de otro, lo cual en términos operativos significa que las probabilidades condicionales se multiplican. Por conveniencia, en vez de manejar P hemos usado logP, de modo que tenemos una adición en lugar de un producto. Como lo acabamos de mencionar, para hacer nuestra predicción no es necesario conocer exactamente el valor de P(features|outcome) y de P(features), sino sólo determinar quién es mayor, si P(outcome=1|features) o P(outcome=0|features).

Al final juntamos todo en una función que llama oportunamente cada una de las cuatro funciones que implementamos:

Basicamente, hemos entrenado a nuestro clasificador al momento de determinar los parámetros (la media y la desviación estándar) de las distribuciones a partir del training set:

param_positive = get_parameters(training_positive,msg)
param_negative = get_parameters(training_negative,msg)

En el for loop simplemente vamos recorriendo el test set renglón por renglón, calculando el likelihood y haciendo las predicciones a través del teorema de Bayes. Al final hacemos un recuento de las veces que el clasificador falló.

Como se puede ver en el notebook, para una sola corrida y usando un training set del 75% del total de los datos, obtenemos una tasa de éxito del 78%. Esta cifra es variable y es necesario repetir el ejercicio un número mayor de veces. Al final hemos incluido el resultado cuando se corre el clasificador 500 veces. En este caso, la tasa de éxito es del 77%.

Y pues este es un ejemplito sencillo de cómo implementar un clasificador de Bayes. Personalmente, la parte más emocionante consiste en ir esbozando cada una de las partes del programa e irlas probando para ver que el código devuelva lo que uno espera. Desde luego el código puede ser enriquecido y corregido (errare humanum est). Cualquier sugerencia, duda y observación es bienvenida.

--

--