En este hito se mejorará la exploración de datos realizada en el Hito 1, además, se eliminarán columnas poco relevantes para los objetivos del proyecto y se agregarán nuevas columnas que puedan ser de utilidad, por otro lado, se modificarán algunas columnas para que sean más cómodas al momento de trabajar. Una vez terminada la modificación del dataset, se probarán distintos clasificadores vistos en clase y en laboratorio, para verificar si estos trabajan adecuadamente en el dataset del proyecto.
Se realizó la exploración inicial nuevamente para encontrar más información de interés que permitiera predecir el comportamiento del predictor. Ésta se incluye en el siguiente link: Exploración
Partimos por cargar el set de datos, y visualizamos los primeros datos para poder compararlo con el dataset "final" que se obtendrá.
import pandas as pd
data = pd.read_csv('dataset_proyecto_mineria.csv') # abrimos el archivo csv y lo cargamos en data.
data.head()
Del dataset recién ingresado estudiamos el balance de clases, en este caso la clase que nos importa es la de la columna "NoShow" pues eso es lo que se busca predecir mediante los clasificadores.
#Estudiamos el balance de clases
print("Distribucion de clases original")
data['NoShow'].value_counts()
La primera modificación del dataset será eliminar las 2 primeras columnas, correspondientes a la identificación del paciente y la identificación de la consulta. La identificación del paciente podría ser útil si esta se repitiera en los datos, sin embargo, las veces en que la identificación de un paciente se repite son muy pocas y al momento de entrenar un clasificador serán contraproducentes pues tendrá como parámetro para clasificar un atributo que se repetirá muy pocas veces, afectando así el buen funcionamiento del clasificador. Por otro lado está la identificación de la consulta, que será distinta en cada dato por lo que para los objetivos del proyecto serán datos irrelevantes, pues no entregan información alguna del paciente.
#Eliminamos las 2 primeras columnas, que no son de interés
data=data.drop('PatientId',1) #Se elimina la identificación del paciente
data=data.drop('AppointmentID',1) #Se elimina la identificación de la consulta
data #Observamos el nuevo dataset
Para poder trabajar con clasificadores se recomendó trabajar con un dataset que contuviera sólo números, por ello se modificarán todas las columnas correspondientes a strings para que representen la misma información mediante int's.
Lo primero que se modificará será la localidad de cada consulta, para ello se creará un diccionario que asocie a un número a cada localidad, de ese modo todos los posibles valores del atributo "Neighbourhood" Serán int's.
#Creamos un diccionario para las comunas
len(set(data['Neighbourhood']))
d_comuna = {}
i = 1
l_comuna = list(set(data['Neighbourhood']))
l_comuna.sort()
l_tuples = []
for comuna in l_comuna:
d_comuna[i] = comuna
l_tuples.append((comuna,i))
i+=1
#Vemos el número asociado a cada comuna
pd.DataFrame(l_tuples)
Una vez creado el diccionario, se asigna a cada dato el número asociado a su localidad, creando la nueva columna llamada "numero comuna". Para ello en cada dato se itera para buscar el número asociado a su comuna en el diccionario, y una vez que encuentra el número para su localidad, este se agrega agrega al vector "numero_comuna".
#Como el clasificador funciona solo con números, modificamos el dataset de modo que podamos usarlo.
#Ahora que cada comuna tiene asignado un número, actualizamos el dataset para trabajar las comunas como int's.
numero_comuna =[0]*110527
for j in range(0,110527):
for i in range(1,81):
if (data.Neighbourhood[j],i) in l_tuples:
numero_comuna[j]=(l_tuples[i-1][1])
else:
numero_comuna=numero_comuna
data['numero_comuna']=numero_comuna
Una vez transformada la columna "Neighbourhood" se separa la columna correspondiente al género del paciente "Gender", que originalmente toma valores "F" o "M", esta columna se dividirá en 2: 1 que indica si el paciente es hombre (1 si es que lo es, 0 sino), o es mujer (1 si es que lo es, 0 sino).
# Transformamos el género en un valor booleano para poder utilizar los clasificadores:
numero_genero_mujer =[0]*110527
numero_genero_hombre=[0]*110527
for j in range(0,110527):
if data.Gender[j]=='F':
numero_genero_mujer[j]=1
else:
numero_genero_mujer[j]=0
for j in range(0,110527):
if data.Gender[j]=='M':
numero_genero_hombre[j]=1
else:
numero_genero_hombre[j]=0
data['numero_genero_mujer']=numero_genero_mujer
data['numero_genero_hombre']=numero_genero_hombre
Para dejar de trabajar con las fechas como tal, se utilizarán las distancias entre fechas, y el día de la semana de la reserva médica. Un punto en contra de esto es que se volverá imposible analizar los días que sean especiales, por ejemplo días feriados o de temporada de carnaval (dado que se trabaja con datos de Brasil).
Para agregar el día de la semana se utiliza el módulo "datetime" de python, y se analiza mediante el string de la columna "AppointmentDay": Se sacan 3 parámetros de dicho String: Año, Mes, y Día. Con estos tres datos se calcula el día de la semana correspondiente mediante funciones de "datetime". Los números asociados a cada día son: Lunes=1, Martes=2 , Miércoles=3,..., Domingo=7
from datetime import datetime, date, timedelta #Se importa el módulo datetime para poder operar con fechas
dia_semana=[0]*110527
for j in range(0,110527):
Año_reserva=int(data.AppointmentDay[j][0:4])
Mes_reserva=int(data.AppointmentDay[j][5:7])
Dia_reserva=int(data.AppointmentDay[j][8:10])
fecha_reserva=date(Año_reserva,Mes_reserva, Dia_reserva)
dia_semana[j]= datetime.isoweekday(fecha_reserva)
data['dia_semana']=dia_semana
Análogo al caso anterior, se utiliza el módulo datetime, y de cada dato se sacan esta vez seis parámetros de cada dato, que corresponden a los años, meses y reservas de la reserva médica y del día en que se hizo la reserva. Mediante funciones de "datetime" se calcula la distancia entre fechas, y se agrega la nueva columna que contiene dicho información para cada dato.
#Creamos una columna con la distancia entre la fecha que se hizo la reserva, y la fecha en la que se hará la visita
from datetime import datetime, date, timedelta
distancia=[0]*110527
for j in range(0,110527):
Año_llamada=int(data.ScheduledDay[j][0:4])
Mes_llamada=int(data.ScheduledDay[j][5:7])
Dia_llamada=int(data.ScheduledDay[j][8:10])
Año_reserva=int(data.AppointmentDay[j][0:4])
Mes_reserva=int(data.AppointmentDay[j][5:7])
Dia_reserva=int(data.AppointmentDay[j][8:10])
fecha_llamada=date(Año_llamada,Mes_llamada,Dia_llamada)
fecha_reserva=date(Año_reserva,Mes_reserva, Dia_reserva)
distancia[j]=abs((fecha_llamada-fecha_reserva).days)
data['distancia_fechas']=distancia
Finalmente se modifica la columna de la clase que se busca estudiar. Cuando el atributo "NoShow" es "No" (es decir, que sí fue a la consulta) se le asigna el número 0, en caso contrario se le asigna un "1" para indiciar que no asistió a la reserva médica.
#Transformamos la columna no Show en 0's y 1's
#Yes=1 , No=0
numero_No_Show =[0]*110527
for j in range(0,110527):
if data.NoShow[j]=='No':
numero_No_Show[j]=0
else:
numero_No_Show[j]=1
data['numero_No_Show']=numero_No_Show
Ahora se tienen todos los datos representados como int's. Sin embargo, sólo se agregaron columnas, por lo que las columnas que contienen strings continúan en el dataset. Dichas columnas que ahora sobran se eliminan mediante el siguiente código expuesto a continuación, y se observa el set de datos ya modificado y listo para trabajar con clasificadores.
#Se eliminan las columnas que no se utilizarán gracias a la modificación del dataset:
data=data.drop('Gender', 1)
data=data.drop('ScheduledDay', 1)
data=data.drop('AppointmentDay', 1)
data=data.drop('Neighbourhood', 1)
data=data.drop('NoShow',1)
#Observamos el nuevo dataset, con los datos representados solo con ints:
data
El dataset obtenido finalmente contiene la información que se utilizará para cumplir el principal objetivo del proyecto que es lograr predecir la asistencia a reservas médicas por parte de los pacientes.
Es importante notar que hay información importante que no se pudo agregar al dataset por falta de información, siendo los principales el clima del día de la reserva, el tráfico ,y la hora en la que debía ser la reunión con el médico. Estos datos podrían ser útiles pues, por ejemplo se esperaría que un paciente esté más propenso a faltar un día lluvioso, o con un mucho tráfico. Además, a priori también se esperaría que se falte más durante la mañana (por quedarse dormidos por ejemplo) a que falten en la tarde. Para agregar dicha información se pensó en agregar 2 atributos, uno correspondiente al clima del día de la reserva, y otro con la hora en que se realizó la reserva, así se tendría la información sobre las precipitaciones y la hora. En cuanto al tráfico, la obtención de esta información sería muy difícil, por ello, para estimarla se hizo la aproximación de que el tráfico tendría una alta correlación con el día de la semana, y la hora de la reserva , y dada la falta de información respecto a la hora de la reserva, la estimación del tráfico asociado a cada consulta no se pudo lograr.
Por otro lado está el llamado "factor sorpresa" que es una de las causas principales de inasistencia a reservas médicas. Dado que este "factor sorpresa" puede ser de diversas naturalezas, fue imposible incluirlo en el dataset, y es un problema sin solución para el cumplimiento del objetivo del proyecto. Sin embargo, por se puede deducir que la posibilidad de la ocurrencia del "factor sorpresa" disminuye al disminuir la distancia entre la fecha en que se hizo la reserva y la fecha de la reserva. Sin embargo, este atributo no es suficiente para suplir el factor sorpresa.
Analizamos el balance de clases de la columna "numero_No_Show". Es análogo al balance hecho inicialmente, sólo que ahora se cuentan los 0's y los 1's.
#Estudiamos el balance de clases
print("Distribucion de clases original")
data['numero_No_Show'].value_counts()
Las clases están desbalanceadas, por lo que se utilizará oversampling y subsampling.
#Clases desbalanceadas -> usamos subsampling y oversampling
import numpy as np
print("Distribución de clases usando (over/sub)sampling")
print()
# oversampling sobre la clase 1
idx = np.random.choice(data.loc[data.numero_No_Show == 1].index, size=(88208-22319))
data_oversampled = pd.concat([data, data.iloc[idx]])
print("Data oversampled on class 'Yes'")
print(data_oversampled['numero_No_Show'].value_counts())
print()
# subsampling sobre la clase 0
idx = np.random.choice(data.loc[data.numero_No_Show == 0].index,size=(88208-22319), replace=False)
data_subsampled = data.drop(data.iloc[idx].index)
print("Data subsampled on class 'No'")
print(data_subsampled['numero_No_Show'].value_counts())
Se separan los atributos de clase "numero_No_Show", para la clasificación.
from sklearn.metrics import classification_report
# datos originales
X_orig = data[data.columns[:-1]]
y_orig = data[data.columns[-1]]
# datos "oversampleados"
X_over = data_oversampled[data.columns[:-1]]
y_over = data_oversampled[data.columns[-1]]
# datos "subsampleados"
X_subs = data_subsampled[data.columns[:-1]]
y_subs = data_subsampled[data.columns[-1]]
Utilizamos el clasificador: Árbol de decisión.
El objetivo de este clasificador es crear un modelo a partir de un training set, que predice el valor de una variable en función de diversas variables de entrada. La estructura de un árbol de decisión es la siguiente: Nodos: Es el instante en el que se toma la decisión entre varias posibles. Flechas: Unión entre un nodo y otro. Representa una acción. Etiqueta: Es lo que da nombre a la acción en cada flecha.
Entrenamos un árbol de decisión para los 3 casos: Oversampling, subsampling, y dataset original.
#Utilizamos el clasificador:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
# Aca esta el codigo usando el dataset: original
print("ORIGINAL::::::::::")
clf_orig = DecisionTreeClassifier()
X_orig_train, X_orig_test, y_orig_train, y_orig_test = train_test_split(X_orig, y_orig, test_size=0.2)
clf_orig.fit(X_orig_train,y_orig_train)
pred_orig = clf_orig.predict(X_orig_test)
print(classification_report(y_orig_test, pred_orig))
print("Subsampling::::::::::")
clf_subs = DecisionTreeClassifier()
X_subs_train, X_subs_test, y_subs_train, y_subs_test = train_test_split(X_subs, y_subs, test_size=0.2)
clf_subs.fit(X_subs_train,y_subs_train)
pred_subs = clf_subs.predict(X_subs_test)
print(classification_report(y_subs_test, pred_subs))
print("Oversampling::::::::::")
clf_over = DecisionTreeClassifier()
X_over_train, X_over_test, y_over_train, y_over_test = train_test_split(X_over, y_over, test_size=0.2)
clf_over.fit(X_over_train,y_over_train)
pred_over = clf_over.predict(X_over_test)
print(classification_report(y_over_test, pred_over))
Utilizamos el clasificador: KNN.
KNN, por sus siglas en ingles k-Nearest Neighbour, es un clasificador que utiliza los k puntos mas cercanos al punto bajo estudio, para realizar la clasificación, por lo que es un lazy learner. Necesita 3 elementos para clasificar: Un set de atributos almacenado, una métrica de distancia para calcular la distancia entre atributos y el valor de k, es decir, el número de vecinos cercanos a obtener. El valor de k debe ser óptimo. Si es muy pequeño, es susceptible a ruido. Si es muy grande, puede incluir puntos de otra clase no deseada. Por esta razón, mas adelante se busca este k óptimo.
Se clasifica según los vecinos más cercanos en 3 casos: Oversampling, subsampling, y dataset original.
#Ahora se utiliza otro clasificador: KNN
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
knn = KNeighborsClassifier(n_neighbors=2) # 2 vecinos
print("ORIGINAL::::::::::")
clf_orig = knn
X_orig_train, X_orig_test, y_orig_train, y_orig_test = train_test_split(X_orig, y_orig, test_size=0.2)
clf_orig.fit(X_orig_train,y_orig_train)
pred_orig = clf_orig.predict(X_orig_test)
print(classification_report(y_orig_test, pred_orig))
print("Subsampling::::::::::")
clf_subs = knn
X_subs_train, X_subs_test, y_subs_train, y_subs_test = train_test_split(X_subs, y_subs, test_size=0.2)
clf_subs.fit(X_subs_train,y_subs_train)
pred_subs = clf_subs.predict(X_subs_test)
print(classification_report(y_subs_test, pred_subs))
print("Oversampling::::::::::")
clf_over = knn
X_over_train, X_over_test, y_over_train, y_over_test = train_test_split(X_over, y_over, test_size=0.2)
clf_over.fit(X_over_train,y_over_train)
pred_over = clf_over.predict(X_over_test)
print(classification_report(y_over_test, pred_over))
A continuación se buscará el K óptimo para la técnica KNN.
Lo que se realiza a continuación es que para un determinado número de k (vecinos), se obtiene la precisión al aplicar KNN para cada uno de los k y se guarda en un vector, con el fin de graficar la precisión en función de los vecinos para así encontrar con qué numero de vecinos se obtiene la mayor precisión. Cabe destacar que el método utilizado para evaluar el desempeño del clasificador es Cross Validation. Lo que realiza este método es particionar el conjunto de entrenamiento en un cierto numero de folds (en nuestro caso, 10), entrenenado sobre los datos en folds - 1 y luego evaluando el conjunto restante. Otro aspecto a destacar es que las particiones evaluadas pueden ir variando cada vez que se aplica el método, por lo que los resultados pueden variar.
#Encontrar el k que obtenga la mayor precisión para oversampling
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
cv_scores = list()
k_range = range(1, 10)
CV=10
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
predictions = cross_validation.cross_val_predict(knn, X_over, y_over, cv=CV)
cv_scores.insert(k-1,metrics.precision_score(y_over, predictions))
pass
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(k_range, cv_scores)
plt.ylabel("precision")
plt.xlabel("K-nearest neighbors")
#Encontrar el k que obtenga la mayor precisión para subsampling
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
cv_scores = list()
k_range = range(1, 25)
CV=10
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
predictions = cross_validation.cross_val_predict(knn, X_subs, y_subs, cv=CV)
cv_scores.insert(k-1,metrics.precision_score(y_subs, predictions))
pass
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(k_range, cv_scores)
plt.ylabel("precision")
plt.xlabel("K-nearest neighbors")
Podemos observar en el gráfico anterior que la precisión se mantiene entre 0.63 y 0.65 al aumentar el valor de K, por lo que, para ahorrar recursos, se mantendrá k en 6 para el caso de subsampling.
#Encontrar el k que obtenga la mayor precisión para caso original
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
cv_scores = list()
k_range = range(1, 15)
CV=10
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
predictions = cross_validation.cross_val_predict(knn, X_orig, y_orig, cv=CV)
cv_scores.insert(k-1,metrics.precision_score(y_orig, predictions))
pass
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(k_range, cv_scores)
plt.ylabel("precision")
plt.xlabel("K-nearest neighbors")
Al igual que para el caso anterior, la variación de la precisión frente a variaciones de K son ínfimas, por lo que para ahorrar recursos, se utiliza k=6.
A continuación se emplea la función run_classifier() utilizada en el laboratorio. Esta función evalúa un clasificador muchas veces sobre un dataset, almacenando los valores de Precision, Recall y F1-score en cada iteración.
Se utiliza un 70% del dataset en "Training set", y el resto como test-set. Cabe destacar que los resultados del árbol de decisión y KNN serán distintos a los que se obtuvieron anteriormente, puesto que los datos de train y de test se están definiendo nuevamente, por lo que son diferentes a los que se utilizaron antes.
# utilizamos la función run_classifier() que evalúa un clasificador muchas veces sobre un dataset, para analizar distintos
#clasificadores para cada caso:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score
def run_classifier(clf, X, y, num_tests=100):
metrics = {'f1-score': [], 'precision': [], 'recall': []}
for _ in range(num_tests):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.30, random_state=37, stratify=y)
clf.fit(X_train,y_train)
predictions=clf.predict(X_test)
metrics['f1-score'].append(f1_score( y_test,predictions))
metrics['recall'].append(recall_score(y_test,predictions))
metrics['precision'].append(precision_score(y_test,predictions))
return metrics
A continuación se utilizará la función run classifier para distintos clasificadores en los casos oversampling, subsampling, y dataset original.
El clasificador Base Dummy realiza predicciones utilizando una regla muy simple, por lo que no es recomendable usarlo en problemas reales. Se puede utilizar como una línea base simple para comparar con otros clasificadores. Ilustremos su funcionamiento con un ejemplo: Supongamos que desea determinar si un objeto dado posee o no posee una determinada propiedad. Si se ha analizado una gran cantidad de esos objetos y ha encontrado que el 90% contiene la propiedad del objetivo, entonces adivinar que cada instancia futura del objeto posee la propiedad del objetivo le da un 90% de probabilidad de adivinar correctamente.
El clasificador Naive Bayes busca modelar la relación probabilística entre atributos y la clase deseada, cuando la relación entre atributos y clases no es determinística. Asume una distribución conjunta entre x e Y y además, asume que cada atributo es una variable independiente.
#Analizamos el caso oversampling
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import GaussianNB # naive bayes
from sklearn.neighbors import KNeighborsClassifier
X = X_over
y = y_over
c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
c1 = ("Decision Tree", DecisionTreeClassifier())
c2 = ("Gaussian Naive Bayes", GaussianNB())
c3 = ("KNN", KNeighborsClassifier(n_neighbors=2))
classifiers = [c0, c1, c2, c3]
for name, clf in classifiers:
metrics = run_classifier(clf, X, y)
print("----------------")
print("Resultados para clasificador: ",name)
print("Precision promedio:",np.array(metrics['precision']).mean())
print("Recall promedio:",np.array(metrics['recall']).mean())
print("F1-score promedio:",np.array(metrics['f1-score']).mean())
print("----------------\n\n")
#Analizamos el caso Subsampling
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import GaussianNB # naive bayes
from sklearn.neighbors import KNeighborsClassifier
X = X_subs
y = y_subs
c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
c1 = ("Decision Tree", DecisionTreeClassifier())
c2 = ("Gaussian Naive Bayes", GaussianNB())
c3 = ("KNN", KNeighborsClassifier(n_neighbors=6))
classifiers = [c0, c1, c2, c3]
for name, clf in classifiers:
metrics = run_classifier(clf, X, y)
print("----------------")
print("Resultados para clasificador: ",name)
print("Precision promedio:",np.array(metrics['precision']).mean())
print("Recall promedio:",np.array(metrics['recall']).mean())
print("F1-score promedio:",np.array(metrics['f1-score']).mean())
print("----------------\n\n")
#Analizamos el caso original
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import GaussianNB # naive bayes
from sklearn.neighbors import KNeighborsClassifier
X = X_orig
y = y_orig
c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
c1 = ("Decision Tree", DecisionTreeClassifier())
c2 = ("Gaussian Naive Bayes", GaussianNB())
c3 = ("KNN", KNeighborsClassifier(n_neighbors=6))
classifiers = [c0, c1, c2, c3]
for name, clf in classifiers:
metrics = run_classifier(clf, X, y)
print("----------------")
print("Resultados para clasificador: ",name)
print("Precision promedio:",np.array(metrics['precision']).mean())
print("Recall promedio:",np.array(metrics['recall']).mean())
print("F1-score promedio:",np.array(metrics['f1-score']).mean())
print("----------------\n\n")
Como se puede observar en las métricas de los 3 casos anteriores, es claro que el caso que obtiene mejores resultados es el de Oversampling. Esto debido a que, como las clases están desbalanceadas, entrenar algún clasificador con el dataset original disminuye el rendimiento del clasificador, entregando métricas poco confiables. Por otro lado, en el caso de subsampling, debido a la gran diferencia de valores entre clases, se pierde demasiada información al momento de realizar el subsampling, lo que empobrece el rendimiento del clasificador y afectando la validez de las métricas. En cuanto al oversampling, es evidente que trabaja de mejor forma que en los casos anteriores, sin embargo su buen funcionamiento genera sospechas de overfitting. Sin embargo esta alternativa es altamente improbable dado que como se comentó en clases las métricas en presencia de overfitting tienden a estar cercanas a 1 (0.99 o 0.98 por ejemplo), en este caso no es así pues las métricas no son tan "perfectas" como ocurre en el overfitting.
En el caso de oversampling, se observa que los mejores clasificadores son Decision Tree y KNN. Ambos clasificadores entregan métricas confiables, notándose cierta superioridad por parte del Decision Tree.
Es importante destacar que dada la naturaleza del problema que se busca resolver mediante el proyecto, tanto el recall como la precision son igual de importantes pues se busca predecir la asistencia de un paciente a su reserva médica para tomar medidas que optimicen el tiempo dispuesto por parte de los médicos.
Dado que dichas medidas serán en teoría tomadas por la gente que organice el sistema de salud, nos pondremos en el caso de la solución más radical para el problema en cuestión: Enviar un recordatorio a los pacientes con alta probabilidad de inasistencia, y en caso de no obtener una respuesta, cancelar la reserva, para darle lugar a otro paciente.
Bajo esa premisa es que el recall se vuelve importante, pues su fórmula es: True Positive/(True Positive + False Negative) Un recall bajo significa muchos falsos negativos, es decir, muchos pacientes que sí faltaran a su reserva se clasifican como si no faltarían. Si esto ocurre no se le enviarán recordatorio (y/o o no se anularán las reservas) a pacientes que tienen una alta probabilidad de inasistencia, por lo que no se estaría solucionando el problema.
Por otro lado está la métrica precision, en este caso la fórmula está dada por: True Positive/(True Positive + False Positive) Una precisión baja nos habla de una gran cantidad de falsos positivos, es decir, se está estimando que pacientes cuya asistencia es altamente probable no irán a su reserva médica, esto puede resultar en que se cancelen horas que sí serían aprovechadas por parte de los pacientes, siendo una consecuencia negativa del predictor.
Si la medida que se toma para resolver la posible inasistencia de pacientes consiste sólo en enviar recordatorios a los pacientes, entonces la métrica precisión no tendrá tanta importancia como el recall, pues una baja precisión significará sólo que se enviarán recordatorios innecesarios, lo que no afectará en las reservas de los pacientes que sí irán, y se siguen tratando de resolver los casos de inasistencia altamente probable de forma correcta.
Ahora, se utilizará Cross Validation para evaluar el desempeño de los clasificadores KNN y Decision Tree, nuevamente con el dataset original, subsampling y oversampling.
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
from sklearn.neighbors import KNeighborsClassifier
# Caso Original
# Cargar el modelo KNN
knn = KNeighborsClassifier(n_neighbors=6) # 2 vecinos
CV = 5 # numero de folds.
# ACCURACY
### Acá considera todos los datos y clases (X e Y) que fueron definidas más arriba en el archivo .ipynb del laboratorio,
### ... luego hace cross-validation (CV) y estima un "accuracy" por cada fold
accuracy_folds = cross_val_score(knn, X=X_orig, y=y_orig, cv=CV, scoring='accuracy')
print(accuracy_folds)
print("Promedio:", accuracy_folds.mean())
# PREDICCION (para ver precision, recall, f1-score)
predictions = cross_validation.cross_val_predict(knn, X_orig, y_orig, cv=CV)
print(metrics.classification_report(y_orig, predictions))
# Accuracy de lo anterior:
print("Accuracy:", metrics.accuracy_score(y_orig, predictions))
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
from sklearn.neighbors import KNeighborsClassifier
#Subsampling
# Cargar el modelo KNN
knn = KNeighborsClassifier(n_neighbors=6) # 6 vecinos
CV = 5 # numero de folds.
# ACCURACY
### Acá considera todos los datos y clases (X e Y) que fueron definidas más arriba en el archivo .ipynb del laboratorio,
### ... luego hace cross-validation (CV) y estima un "accuracy" por cada fold
accuracy_folds = cross_val_score(knn, X=X_subs, y=y_subs, cv=CV, scoring='accuracy')
print(accuracy_folds)
print("Promedio:", accuracy_folds.mean())
# PREDICCION (para ver precision, recall, f1-score)
predictions = cross_validation.cross_val_predict(knn, X_subs, y_subs, cv=CV)
print(metrics.classification_report(y_subs, predictions))
# Accuracy de lo anterior:
print("Accuracy:", metrics.accuracy_score(y_subs, predictions))
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
from sklearn.neighbors import KNeighborsClassifier
#OverSampling
# Cargar el modelo KNN
knn = KNeighborsClassifier(n_neighbors=2) # 2 vecinos
CV = 5 # numero de folds.
# ACCURACY
### Acá considera todos los datos y clases (X e Y) que fueron definidas más arriba en el archivo .ipynb del laboratorio,
### ... luego hace cross-validation (CV) y estima un "accuracy" por cada fold
accuracy_folds = cross_val_score(knn, X=X_over, y=y_over, cv=CV, scoring='accuracy')
print(accuracy_folds)
print("Promedio:", accuracy_folds.mean())
# PREDICCION (para ver precision, recall, f1-score)
predictions = cross_validation.cross_val_predict(knn, X_over, y_over, cv=CV)
print(metrics.classification_report(y_over, predictions))
# Accuracy de lo anterior:
print("Accuracy:", metrics.accuracy_score(y_over, predictions))
Como se puede observar en el caso original y de subsampling, el promedio de las métricas entre ambas clases no es bajo. No obstante, al observar cada métrica de cada clase por separado, se tiene una gran diferencia entre ambas clases, por lo que el promedio no es un buen indicador para evaluar el desempeño del clasificador. Aún así, el caso de oversampling es superior, tanto en el promedio de las métricas como en la distribución de las métricas en cada clase. Cabe destacar que para cada caso se utilizó el valor de k óptimo encontrado anteriormente.
La superioridad del oversampling sobre los otros dos casos puede atribuirse principalmente a: Subsampling: Al truncar la cantidad de datos con los que se trabajará, los K vecinos más cercanos tienen una mayor probabilidad de no ser similares al dato que se busca clasificar, es decir, hay una alta probabilidad de que aumente la distancia entre cada dato y sus vecinos más cercanos.
Dataset Original: La principal falencia del dataset original es el desbalance de clases, este desbalance hará que el clasificador esté mucho más acostumbrado a la clase asociada a la "No inasistencia", pues la mayoría de los datos pertenecen a ella, de ese modo, al entregarle un dato correspondiente a la clase opuesta tendrá mucho más difícil el trabajo.
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
from sklearn.tree import DecisionTreeClassifier
#OverSampling
# Cargar el modelo Decision Tree
dtc = DecisionTreeClassifier()
CV = 5 # numero de folds.
# ACCURACY
### Acá considera todos los datos y clases (X e Y) que fueron definidas más arriba en el archivo .ipynb del laboratorio,
### ... luego hace cross-validation (CV) y estima un "accuracy" por cada fold
accuracy_folds = cross_val_score(dtc, X=X_over, y=y_over, cv=CV, scoring='accuracy')
print(accuracy_folds)
print("Promedio:", accuracy_folds.mean())
# PREDICCION (para ver precision, recall, f1-score)
predictions = cross_validation.cross_val_predict(dtc, X_over, y_over, cv=CV)
print(metrics.classification_report(y_over, predictions))
# Accuracy de lo anterior:
print("Accuracy:", metrics.accuracy_score(y_over, predictions))
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
from sklearn.tree import DecisionTreeClassifier
#SubSampling
# Cargar el modelo KNN
dtc = DecisionTreeClassifier()
CV = 5 # numero de folds.
# ACCURACY
### Acá considera todos los datos y clases (X e Y) que fueron definidas más arriba en el archivo .ipynb del laboratorio,
### ... luego hace cross-validation (CV) y estima un "accuracy" por cada fold
accuracy_folds = cross_val_score(dtc, X=X_subs, y=y_subs, cv=CV, scoring='accuracy')
print(accuracy_folds)
print("Promedio:", accuracy_folds.mean())
# PREDICCION (para ver precision, recall, f1-score)
predictions = cross_validation.cross_val_predict(dtc, X_subs, y_subs, cv=CV)
print(metrics.classification_report(y_subs, predictions))
# Accuracy de lo anterior:
print("Accuracy:", metrics.accuracy_score(y_subs, predictions))
from sklearn.model_selection import cross_val_score
from sklearn import metrics, cross_validation
from sklearn.tree import DecisionTreeClassifier
#Original
# Cargar el modelo Decision Tree
dtc = DecisionTreeClassifier()
CV = 5 # numero de folds.
# ACCURACY
### Acá considera todos los datos y clases (X e Y) que fueron definidas más arriba en el archivo .ipynb del laboratorio,
### ... luego hace cross-validation (CV) y estima un "accuracy" por cada fold
accuracy_folds = cross_val_score(dtc, X=X_orig, y=y_orig, cv=CV, scoring='accuracy')
print(accuracy_folds)
print("Promedio:", accuracy_folds.mean())
# PREDICCION (para ver precision, recall, f1-score)
predictions = cross_validation.cross_val_predict(dtc, X_orig, y_orig, cv=CV)
print(metrics.classification_report(y_orig, predictions))
# Accuracy de lo anterior:
print("Accuracy:", metrics.accuracy_score(y_orig, predictions))
Al igual que para el caso anterior, las métricas de los datos originales, si bien tienen un buen promedio entre clases, entre clases tienen métricas muy diferentes, por lo que el promedio no es un buen indicador del rendimiento del arbol de decisión. Además, se observa que el caso de oversampling tiene mejores métricas que el caso de subsampling. No obstante, la distribución de estas métricas en cada clase de los datos "oversampleados" es de menor calidad que la de los datos "subsampleados", aunque no al nivel de los datos originales.
Una vez revisado el feedback entregado por el cuerpo docente y compañeros/as, las mejoras y/o modificaciones en el proyecto fueron:
Una vez finalizado la modificación del dataset y probados los clasificadores estudiados en el curso, en un futuro se buscará resolver los problemas del hito 1 que no se pudieron resolver en el hito 2, y además resolver los nuevos problemas que se presentaron en el hito 2. Sumado a lo anterior, también se buscará implementar en el desarrollo del proyecto la materia que aún no se ha visto en los laboratorios que pueda ayudar en la realización del predictor. A partir de lo anterior, los objetivos que se buscarán cumplir en lo que resta de proyecto son:
En el presente hito se buscaba trabajar con clasificadores, por lo que el set de datos debía ser modificado de forma que cada dato pudiera ser interpretado mediante números. Para lograr esto se codificó cada atributo de forma que cada valor que pudieran tomar fuera interpretado por un int. Por ejemplo, los valores como género se codificaron mediante 0 y 1, y para la localidad del hospital se creó un diccionario. Una vez que se expresó todo el dataset de forma numérica fue posible utilizar los clasificadores de forma adecuada. En cuanto a los clasificadores, se observó un importante desbalance en las clases, por ello es que los clasificadores se trabajaron se probaron con 3 categorías distintas: oversampling,subsampling y dataset original.
Como se puede observar en los resultados obtenidos para cada clasificador, la técnica de oversampling es la que destaca en cuanto a resultados generales, con respecto al caso original y al de subsampling. Otro aspecto a destacar es que, al evaluar los clasificadores utilizados con la técnica Holdout, se puede notar que los que tienen las mejores métricas son Decision Tree y KNN, destancándose más el primero que el segundo. Al utilizar la técnica de Cross Validation, sólo se evalúan los clasificadores anteriores, debido a que se observó que las otras opciones mostraban resultados deficientes, por lo que se decidió no evaluarlas con esta técnica. En este caso, ambos clasificadores obtienen métricas similares, en las que el caso de oversampling es superior a los demás.
En cuanto al futuro del proyecto, aún se presentan problemas sin solución y variables importantes no consideradas en el proyecto, por lo mismo, se espera lograr obtener las variables necesarias para lograr un buen predictor.