Objetivo

Implementar el modelo de vecinos mas cercanos KNN con programación Python para resolver la tarea de clasificación de una condición de salud de las personas mediante predicción de anomalías de corazón evaluando la exactitud del modelo mediante la matriz de confusión.

Descripción

Se cargan librerías y se descargan los datos: https://raw.githubusercontent.com/rpizarrog/Analisis-Inteligente-de-datos/main/datos/heart_2020_cleaned.csv

Los datos están relacionados con aspectos médicos y son valores numéricos de varias variables que caracterizan el estado de salud de 319,795 personas.

Se construye un modelo supervisado basado en el algoritmo de vecinos mas cercanos KNN para resolver la tarea de clasificación binaria e identificar si una persona padece del corazón o no.

Se construye el modelo con sklearn.neighbors import KNeighborsClassifier

Se construyen datos de entrenamiento y validación al 80% y 20% cada uno.

Se desarrollan los modelos de:

Los modelo se aceptan si tienen un valor de exactitud por encima del 70%..

Fundamento teórico

El algoritmo vecinos mas cercanos KNN clasifica cada dato nuevo en el grupo que corresponda, según tenga k vecinos más cerca de un grupo o de otro. Es decir, calcula la distancia del elemento nuevo a cada uno de los existentes, y ordena dichas distancias de menor a mayor para ir seleccionando el grupo al que pertenecer.

Este grupo será, por tanto, el de mayor frecuencia con menores distancias.

El KNN es un algoritmo de aprendizaje supervisado, es decir, que a partir de un juego de datos inicial su objetivo será el de clasificar correctamente todas las instancias nuevas. El juego de datos típico de este tipo de algoritmos está formado por varios atributos descriptivos y un solo atributo objetivo (también llamado clase).

El método K-NN es un método importantes de clasificación supervisada. En el proceso de aprendizaje no se hace ninguna suposición acerca de la distribución de las variables predictoras, es por ello que es un método de clasificación no paramétrico, que estima el valor de la función de densidad de probabilidad o directamente la probabilidad posterior de que un elemento \(x\) pertenezca a la clase \(CjCj\) a partir de la información proporcionada por el conjunto de entrenamiento.

Es un método bastante sencillo y robusto que simplemente busca en las observaciones más cercanas a la que se está tratando de predecir y clasifica el punto de interés basado en la mayoría de datos que le rodean.

Es un algoritmo muy simple de implementar y de entrenar, pero tienen una carga computacional elevada y no es apropiado cuando se tienen muchos grados de libertad.

Desarrollo

Cargar librerías

Algunas librerías son nuevas, hay que instalarlas desde R, aquí se indican cuáles librerías y con comentario dado que ya se instalaron previamente.

# library(reticulate)
# py_install("statsmodels")
# Tratamiento de datos
import pandas as pd
import numpy as np
import statsmodels.api as sm

# Estadísticas
import scipy 
from scipy import stats

# Para partir datos entrenamiento y validación
from sklearn.model_selection import train_test_split

# Modelo de Clasificación 
from sklearn.metrics import classification_report

from sklearn.neighbors import KNeighborsClassifier

from sklearn.model_selection import GridSearchCV
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score

# Gráficos
import matplotlib.pyplot as plt
import seaborn as sb

Cargar los datos

Se cargan datos del enlace URL, se observan los primeros y últimos registros del conjunto de datos.

datos = pd.read_csv("https://raw.githubusercontent.com/rpizarrog/Analisis-Inteligente-de-datos/main/datos/danios%20al%20corazon%20numericos%20limpios.csv")
datos
##        HeartDisease    BMI  Smoking  ...  Asthma  KidneyDisease  SkinCancer
## 0                No  16.60        1  ...       1              2           1
## 1                No  20.34        2  ...       2              2           2
## 2                No  26.58        1  ...       1              2           2
## 3                No  24.21        2  ...       2              2           1
## 4                No  23.71        2  ...       2              2           2
## ...             ...    ...      ...  ...     ...            ...         ...
## 319790          Yes  27.41        1  ...       1              2           2
## 319791           No  29.84        1  ...       1              2           2
## 319792           No  24.24        2  ...       2              2           2
## 319793           No  32.81        2  ...       2              2           2
## 319794           No  46.56        2  ...       2              2           2
## 
## [319795 rows x 18 columns]

Exploración de datos

Son 319795 observaciones y 18 variables

print("Observaciones y variables: ", datos.shape)
## Observaciones y variables:  (319795, 18)
print("Columnas y tipo de dato")
# datos.columns
## Columnas y tipo de dato
datos.dtypes
## HeartDisease         object
## BMI                 float64
## Smoking               int64
## AlcoholDrinking       int64
## Stroke                int64
## PhysicalHealth        int64
## MentalHealth          int64
## DiffWalking           int64
## Sex                   int64
## AgeCategory           int64
## Race                  int64
## Diabetic              int64
## PhysicalActivity      int64
## GenHealth             int64
## SleepTime             int64
## Asthma                int64
## KidneyDisease         int64
## SkinCancer            int64
## dtype: object

Visualización de datos

¿Cuántos casos hay de cada clase?

Hay 292422 casos sin daño al corazón y el resto que si tienen daño 27373.

frecuencia = (datos.groupby("HeartDisease").agg(frecuencia=("HeartDisease","count")).reset_index())
  
frecuencia
##   HeartDisease  frecuencia
## 0           No      292422
## 1          Yes       27373


fig, ax = plt.subplots()
# Colores
bar_labels = ['No', 'Yes']
bar_colors = ['tab:blue', 'tab:red']

#frecuencia['frecuencia'].plot(kind="bar")
ax.bar(frecuencia['HeartDisease'], frecuencia['frecuencia'], label=bar_labels, color=bar_colors)
## <BarContainer object of 2 artists>
ax.set_ylabel('Frecuencia')
ax.set_title('Daños al Corazón')
ax.legend(title='Daño')

plt.show()
# plt.gcf().clear()

Transformar datos

Crear variable llamada HeartDisease01 que se utilizará en el modelo de Regresión Logística tendrá valores 0 de para ‘No’ daño y 1 para si hay daño (‘Yes’).

datos['HeartDisease01'] = np.where(datos ['HeartDisease']== "Yes", 1, 0)
 

Quitar la variable HeartDisease que ya tiene variable transformada a HeartDisease01

datos = datos.drop("HeartDisease", axis='columns')

Quedaron las columnas:

datos.columns.values
## array(['BMI', 'Smoking', 'AlcoholDrinking', 'Stroke', 'PhysicalHealth',
##        'MentalHealth', 'DiffWalking', 'Sex', 'AgeCategory', 'Race',
##        'Diabetic', 'PhysicalActivity', 'GenHealth', 'SleepTime', 'Asthma',
##        'KidneyDisease', 'SkinCancer', 'HeartDisease01'], dtype=object)

Las variables de interés

Todas las variables de entrada o variables independientes:

  • BMI”: Indice de masa corporal con valores entre 12.02 y 94.85.

  • Smoking”: Si la persona es fumadora o no con valores categóritos de ‘Yes’ o ‘No’. [1 | 2]

  • AlcoholDrinking” : Si consume alcohol o no, con valores categóricos de ‘Yes’ o ‘No’.[1 | 2]

  • Stroke”: Si padece alguna anomalía cerebrovascular, apoplejia o algo similar, con valores categóricos de ‘Yes’ o ‘No’. [1 | 2]

  • PhysicalHealth” Estado físico en lo general con valores entre 0 y 30.

  • MentalHealth”. Estado mental en lo general con valores entre 0 y 30.

  • DiffWalking” . Que si se le dificulta caminar o tiene algún padecimiento al caminar, con valores categóritoc de ‘Yes’ o ‘No’.[1 | 2]

  • Sex”: Género de la persona, con valores de ‘Female’ y ‘Male’ para distinguir al género femenino y masculino respectivamente. [1 | 2]

  • AgeCategory”: Una clasificación de la edad de la persona de entre 18 y 80 años. La primera categoría con un rango de edad entre 18-24, a partir de 25 con rangos de 5 en 5 hasta la clase de 75-80 y una última categoría mayores de 80 años. [1 - 13]

  • Race”. Raza u origen de la persona con valores categóricos de ‘American Indian/Alaskan Native’, ’Asian’,’Black’, ’Hispanic’, ’Other’ y’White’. [1 - 6]

  • Diabetic”. Si padece o ha padecido de diabetes en cuatro condiciones siendo Yes y No para si o no: ‘No’, ‘borderline diabetes’ condición antes de detectarse diabetes tipo 2, ‘Yes’, y ‘Yes (during pregnancy)’ durante embarazo. [1 - 4]

  • PhysicalActivity” que si realiza actividad física, con valores categóricos de ‘Yes’ o ‘No’. [1 | 2]

  • GenHealth”: EStado general de salud de la persona con valores categóricos de ‘Excellent’, ‘Very good’, ‘Good’, ‘Fair’ y ‘Poor’ con significado en español de excelente, muy buena, buena, regular y pobre o deficiente. [1 - 5]

  • SleepTime”: valor numérico de las horas de sueño u horas que duerme la persona con valores en un rango entre 1 y 24.

  • Asthma”: si padece de asma o no, con valores categóricos de ‘Yes’ o ‘No’. [1 | 2].

  • KidneyDisease”: si tiene algún padecimiento en los riñones, con valores categóricos de ‘Yes’ o ‘No’. [1 | 2].

  • SkinCancer”: si padece algún tipo de cáncer de piel, con valores categóricos de ‘Yes’ o ‘No’. [1 | 2].

La variable de interés como dependiente o variable de salida es la de daño al corazón (HeartDisease), con valores categóricos de ‘Yes’ o ‘No’ , ahora la variable HeartDisease01 con valores 1 o 0.

Nuevamente la descripción de variables y ahora son 319795 observaciones y 18 variables

print("Observaciones y variables: ", datos.shape)
## Observaciones y variables:  (319795, 18)
print("Columnas y tipo de dato")
# datos.columns
## Columnas y tipo de dato
datos.dtypes
## BMI                 float64
## Smoking               int64
## AlcoholDrinking       int64
## Stroke                int64
## PhysicalHealth        int64
## MentalHealth          int64
## DiffWalking           int64
## Sex                   int64
## AgeCategory           int64
## Race                  int64
## Diabetic              int64
## PhysicalActivity      int64
## GenHealth             int64
## SleepTime             int64
## Asthma                int64
## KidneyDisease         int64
## SkinCancer            int64
## HeartDisease01        int64
## dtype: object

Para construir el modelo, se requiere variables de tipo numérica.

Datos de entrenamiento y validación

Datos de entrenamiento al 80% de los datos y 20% los datos de validación. Semilla 1280

X_entrena, X_valida, Y_entrena, Y_valida = train_test_split(datos.drop(columns = "HeartDisease01"), datos['HeartDisease01'],train_size = 0.80,  random_state = 1280)

Datos de entrenamiento

Se crea un conjunto de datos de validación con 255836 registros y 37 variables.

X_entrena
##           BMI  Smoking  AlcoholDrinking  ...  Asthma  KidneyDisease  SkinCancer
## 151258  31.28        2                2  ...       2              2           2
## 239066  30.85        1                1  ...       2              2           2
## 170512  32.78        1                2  ...       2              1           2
## 138101  27.12        1                2  ...       2              2           2
## 1625    21.95        1                2  ...       2              2           2
## ...       ...      ...              ...  ...     ...            ...         ...
## 89827   34.33        1                2  ...       2              2           2
## 37579   25.70        1                2  ...       2              2           2
## 152432  25.37        1                1  ...       2              2           2
## 17713   34.06        2                2  ...       2              2           2
## 64259   27.44        2                2  ...       1              2           2
## 
## [255836 rows x 17 columns]

Datos de validación

Se crea un conjunto de datos de validación con 63959 registros y 37 variables.

X_valida
##           BMI  Smoking  AlcoholDrinking  ...  Asthma  KidneyDisease  SkinCancer
## 111619  35.44        2                2  ...       2              2           2
## 53644   32.92        1                2  ...       2              2           2
## 143225  27.12        1                2  ...       2              2           2
## 191086  22.86        2                2  ...       1              2           2
## 210521  21.63        2                2  ...       2              2           2
## ...       ...      ...              ...  ...     ...            ...         ...
## 175963  27.44        1                2  ...       2              2           2
## 280784  26.58        1                2  ...       2              2           2
## 266584  20.34        2                2  ...       2              2           2
## 175596  25.10        2                2  ...       1              2           2
## 232930  31.32        2                2  ...       2              2           2
## 
## [63959 rows x 17 columns]

Modelos Supervisados de vecinos mas cercanos KNN

Creación del modelo

Se crea el modelo de árbol de clasificación con datos de entrenamiento con un valor inicial de 12 vecinos \(k=12\).

knn = KNeighborsClassifier(n_neighbors=12)

Entrenando al modelo

Se entrena el modelo precisamente con los datos de entrenamiento contenida en las variables independientes X_entrena y la variable dependiente Y_entrena que contiene la etiqueta HeartDisease01 de 0 No daño y 1 que si tiene daño en el corazón.

knn.fit(X_entrena, Y_entrena)
KNeighborsClassifier(n_neighbors=12)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Prediccions

Se construyen predicciones con los datos de validación. Se tarda mucho en hacer predicciones dado que son 255833 observaciones en datos de entrenamiento y 63959 observaciones en datos de validación.

predicciones = knn.predict(X_valida)
print(predicciones)
## [0 0 0 ... 0 0 0]

Tabla comparativa

comparaciones = pd.DataFrame(X_valida)
comparaciones = comparaciones.assign(HeartDisease_Real = Y_valida)
comparaciones = comparaciones.assign(HeartDisease_Pred = predicciones.flatten().tolist())
print(comparaciones)
##           BMI  Smoking  ...  HeartDisease_Real  HeartDisease_Pred
## 111619  35.44        2  ...                  0                  0
## 53644   32.92        1  ...                  0                  0
## 143225  27.12        1  ...                  0                  0
## 191086  22.86        2  ...                  0                  0
## 210521  21.63        2  ...                  0                  0
## ...       ...      ...  ...                ...                ...
## 175963  27.44        1  ...                  1                  0
## 280784  26.58        1  ...                  0                  0
## 266584  20.34        2  ...                  0                  0
## 175596  25.10        2  ...                  0                  0
## 232930  31.32        2  ...                  0                  0
## 
## [63959 rows x 19 columns]

Evaluación del modelo

Se evalúa el modelo con la matriz de confusión

Matriz de confusión

print(confusion_matrix(comparaciones['HeartDisease_Real'], comparaciones['HeartDisease_Pred']))
## [[58348   128]
##  [ 5396    87]]
matriz = confusion_matrix(comparaciones['HeartDisease_Real'], comparaciones['HeartDisease_Pred'])

¿A cuantos le atina el modelo?

print(classification_report(comparaciones['HeartDisease_Real'], comparaciones['HeartDisease_Pred']))
##               precision    recall  f1-score   support
## 
##            0       0.92      1.00      0.95     58476
##            1       0.40      0.02      0.03      5483
## 
##     accuracy                           0.91     63959
##    macro avg       0.66      0.51      0.49     63959
## weighted avg       0.87      0.91      0.88     63959
accuracy = accuracy_score(
    y_true = comparaciones['HeartDisease_Real'],
    y_pred = comparaciones['HeartDisease_Pred'],
    normalize = True
    )
print(f" El valor de exactitud = accuracy es de: {100 * accuracy} %")
##  El valor de exactitud = accuracy es de: 91.36321706092966 %

Prediccions con un registro nuevo

Se crea un registro de una persona con ciertas condiciones de salud a partir de un diccionario.

# Se crea un diccionario

# Mismo que R
# BMI <- 38
# Smoking <- 1  # 'Yes'
# AlcoholDrinking = 1 # 'Yes'
# Stroke <- 1 # 'Yes'
# PhysicalHealth <- 2 
# MentalHealth <- 5
# DiffWalking <- 1 # 'Yes'
# Sex = 2 # 'Male
# AgeCategory = 11 # '70-74'
# Race = 2 # 'Black'
# Diabetic <- 1 # 'Yes'
# PhysicalActivity = 2 # 'No'
# GenHealth = 1 # "Fair"
# SleepTime = 12
# Asthma = 1 # 'Yes'
# KidneyDisease = 1 # 'Yes'
# SkinCancer = 2 # 'No'
registro = {'BMI': 38, 'Smoking' : 1, 'AlcoholDrinking' : 1, 'Stroke' : 1,
'PhysicalHealth': 2, 'MentalHealth': 5, 
'DiffWalking': 1, 'Sex': 2, 'AgeCategory': 11,
'Race' : 2, 'Diabetic' : 1,
'PhysicalActivity' : 2, 'GenHealth' : 1,
'SleepTime' : 12,
'Asthma' : 1, 'KidneyDisease':1, 'SkinCancer': 2}
persona = pd.DataFrame()
persona = persona.append(registro, ignore_index=True)
## <string>:1: FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.
persona
##    BMI  Smoking  AlcoholDrinking  ...  Asthma  KidneyDisease  SkinCancer
## 0   38        1                1  ...       1              1           2
## 
## [1 rows x 17 columns]

Se hace la predicción en términos de clasificación de la persona con estos valores para saber si tiene o no daño en el corazón:

prediccion = knn.predict(persona)
print(prediccion)
## [0]

La predicción en términos de clasificación de la persona con las características proporcionadas es 0 NO que está enfermo o que NO tiene daño del corazón.

¿que predicciones se generaron en los otros modelos?. Habiendo construido modelos de basados en los algoritmos de regresión logística binaria y árbol de regresión, en estos las predicciones fueron de que ‘YES’ si tiene daño al corazón.

Interpretación

De entrada me parece un modelo mucho más sencillo que los anteriores algoritmos de clasificación, sin embargo, la premisa de la que parte y la manera en la que lleva a cabo la construcción de la estructura de sus operaciones son bastante prácticas y funcionales. Su métodología que consiste en buscar la información que necesita en lo que ya tiene, es decir, en la búsqueda de las menores distancias del elemento nuevo con respecto al conjunto de datos que posee, es muy interesante y ciertamente tiene potencial para desenvolverse en entornos donde exista una precisión de los datos, o en otras palabras, donde el entorno sea generalmente conocido.

El contexto de los datos corresponden al estado de salud de cierto número de personas, a los cuales se les aplicó el modelo de Vecinos Cercanos (KNN), el cual es un algoritmo de clasificación binaria, en este caso representada por las siguientes dos opciones o grupos a los que se puede pertenecer, si padece o no problemas del corazón.

Una de las desventajas de este algoritmo es su aplicación en entornos donde haya una imprecisión de los datos, además resulta que tiene un elevado nivel de latencia en lo que respecta a la construcción del modelo markdown. A juzgar por su comportamiento, a mayor cantidad de datos, mayor será su tardanza. Para estos casos sería necesario hacer uso de muestras de la cantidad de los datos de entrenamiento y validación respectivamente.

Por otro lado, pasemos a lo que son los datos que ha arrojado el modelo con la información que se le ha proporcionado. La semilla que se le propinó fue 1280. Los datos de entrenamiento están a un 80% y los datos de validación a un 20%, tal como se han venido manejando.

En este caso y con los datos especificos que se le han suministrado al modelo, el valor de exactitud = accuracy es de: 91.36321706092966 %. Es decir, que su nivel de predicción le permite acertar en el 91.36% de cada 100 casos. Naturalmente es un valor que permite tomar como valido el modelo siguiendo con la medida (mayor al 70%) que se ha establecido por nosotros mismos. Comparando este valor con el que se obtenido haciendo uso del lenguaje R, que es de 0.9132 %, este posee una ligera diferencia que es mayor en decimales. Pero en términos generales es la misma.

Bibliografía