El proyecto del grupo número 3 consta de un predictor de reservas médicas, en el presente hito se dará cierre a dicho proyecto y se verá si se cumple el objetivo principal del predictor.
En este hito se corregirán los errores en la exploración, además de modificar nuevamente el set de datos pues el anterior contaba con errores, siendo el principal de ello la enumeración de las comunas, pues la forma de codificar cada comuna les daba un orden jerárquico, lo que es indeseado para el predictor. Además de eso se eliminarán y/o agregarán columnas convenientemente para la eficiencia del predictor.
Por otro lado, se realizará un PCA para ver qué atributos son los más importantes para el predictor.
Finalmente se estudiará la presencia de Clusters que puedan ayudar al predictor, dichos Cluster serán verificados externamente pues se cuenta con datos etiquetados.
Se pueden revisar también los hitos anteriores: Hito 1, Hito 2.
La salud es un tema que ha generado bastantes problemas en Chile por diversos motivos, dentro de los que destacan son las largas esperas para poder ser atendido, y la dificultad para agendar una reserva en un tiempo prudente. Esta fue la principal motivación del grupo, y a raíz de ello es que se eligió como proyecto un predictor de reservas médicas, cuyo principal objetivo es que en función de dicho predictor se puedan tomar medidas (por ejemplo, "Spamear" recordatorios y confirmaciones de la reserva) para combatir al menos una parte de este gran problema que hay en la salud, para lograrlo los objetivos fueron obtener un dataset adecuado, y en base a él crear un programa que al recibir un dato (paciente) entre como resultado un "1" si el paciente tiene una probabilidad importante de faltar a su reserva médica, y "0" si no, todo esto mediante un training set.
En la búsqueda de datos no fue posible encontrar datos que ayudaran a lograr el predictor, por ello, se buscaron datasets de otros lugares siendo un dataset de Brasil el elegido para la elaboración del proyecto. Es importante notar que en un principio una de las hipótesis importantes era que se espera un comportamiento análogo entre los pacientes de Brasil y los de Chile, pero en el transcurso del proyecto esta hipótesis se desechó y se optó por crear un predictor que funcione exclusivamente en Brasil. Sin embargo, con un dataset de pacientes Chilenos se podrían hacer los mismos experimentos y ver las "tendencias" en Chile.
Otra de las hipótesis del proyecto era la tendencia a faltar de la gente más jóven, y gente con condiciones como diabétes e hipertensión faltarían menos a sus reservas dada la delicadeza de su estado. Todas estas hipótesis fueron verificadas (y confirmadas) en los hitos 1 y 2, más precisamente en la exploración de datos.
Dado el error en la codificación de las comunas, se decidió crear una columna por cada comuna. Sin embargo, dada la cantidad de comunas en el dataset, iba a generar un predictor con muchas dimensiones lo que iba a resultar ser contraproducente, por lo mismo, se decidió eliminar la columna con la información de la ubicación de cada hospital, que ademá de la exploración de datos se observa que la variablidad que aporta al predictor es baja.
Por otro lado, como se menciona en el hito 2, se busca agregar información sobre el clima al dataset, sin embargo los registros históricos de Brasil encontrados conseguidos no tienen todas las fechas presentes en el dataset, lo que dejaría muchos datos incompletos. Para lograr incluir el factor del clima en el dataset se agregarán columnas correspondientes a los meses del año, si bien no habla directamente del clima, existe una correlación que puede ser útil para el predictor, además de introducir de introducir la información del mes en si mismo, que podría ser también un aporte al predictor. Es importante notar que agregar una columna por cada mes puede traer consigo problemas de dimensionalidad, los efectos de haber hecho esto se verán reflejados en el desempeño del clasificador.
import pandas as pd
data = pd.read_csv('dataset_proyecto_mineria.csv') # abrimos el archivo csv y lo cargamos en data.
data.head()
A continuación se muestra cómo se modifica el dataset original a partir de los resultados del hito 1 y 2:
#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=data.drop('Neighbourhood',1) # Se elimina la localidad
Codificación del género del paciente:
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
Codificación del día de la semana de la consulta:
from datetime import datetime, date, timedelta
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
Codificación de cada mes:
from datetime import datetime, date, timedelta
Mes_Enero=[0]*110527
Mes_Febrero=[0]*110527
Mes_Marzo=[0]*110527
Mes_Abril=[0]*110527
Mes_Mayo=[0]*110527
Mes_Junio=[0]*110527
Mes_Julio=[0]*110527
Mes_Agosto=[0]*110527
Mes_Septiembre=[0]*110527
Mes_Octubre=[0]*110527
Mes_Noviembre=[0]*110527
Mes_Diciembre=[0]*110527
for j in range(0,110527):
Mes_reserva=int(data.AppointmentDay[j][5:7])
if Mes_reserva==1:
Mes_Enero[j]= 1
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==2:
Mes_Enero[j]= 0
Mes_Febrero[j]=1
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==3:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=1
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==4:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=1
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==5:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=1
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==6:
Mes_Enero[j]=0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=1
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==7:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=1
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==8:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=1
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==9:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=1
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==10:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=1
Mes_Noviembre[j]=0
Mes_Diciembre[j]=0
else:
if Mes_reserva==11:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=1
Mes_Diciembre[j]=0
else:
if Mes_reserva==12:
Mes_Enero[j]= 0
Mes_Febrero[j]=0
Mes_Marzo[j]=0
Mes_Abril[j]=0
Mes_Mayo[j]=0
Mes_Junio[j]=0
Mes_Julio[j]=0
Mes_Agosto[j]=0
Mes_Septiembre[j]=0
Mes_Octubre[j]=0
Mes_Noviembre[j]=0
Mes_Diciembre[j]=1
data['Mes_Enero']=Mes_Enero
data['Mes_Febrero']=Mes_Febrero
data['Mes_Marzo']=Mes_Marzo
data['Mes_Abril']=Mes_Abril
data['Mes_Mayo']=Mes_Mayo
data['Mes_Junio']=Mes_Junio
data['Mes_Julio']=Mes_Julio
data['Mes_Agosto']=Mes_Agosto
data['Mes_Septiembre']=Mes_Septiembre
data['Mes_Octubre']=Mes_Octubre
data['Mes_Noviembre']=Mes_Noviembre
data['Mes_Diciembre']=Mes_Diciembre
Codificación de la distancia entre fechas:
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
Codificación de la clase que se busca estimar mediante el predictor:
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
Eliminamos las columnas originales de los datos codificados y se observa cómo queda el dataset final:
data=data.drop('Gender', 1)
data=data.drop('ScheduledDay', 1)
data=data.drop('AppointmentDay', 1)
data=data.drop('NoShow',1)
data.head()
# Pasamos el nuevo dataset a un CSV para poder utilizarlo en otros programas
data.to_csv('datasetfinalproyecto1.csv')
El dataset recién expuesto será el dataset final del proyecto, es importante mencionar que este el dataset no contiene toda la información que el grupo consideró relevante, pues hubo información importante que no se pudo obtener tales como la hora de la reserva, el tráfico, y el "factor sorpresa" que nunca se pudieron obtener. El tráfico se asumirá con una alta correlación al día de la semana y el mes, mientras que la hora de la reserva y el "factor sorpresa" se omiten pues fue imposible obtener dicha información o ver alguna correlación con algún atributo disponible.
Se creará un nuevo dataset que no contenga la información de los meses para facilitar el PCA y comparar:
data_sinmes=data
data_sinmes=data_sinmes.drop('Mes_Enero', 1)
data_sinmes=data_sinmes.drop('Mes_Febrero', 1)
data_sinmes=data_sinmes.drop('Mes_Marzo', 1)
data_sinmes=data_sinmes.drop('Mes_Abril', 1)
data_sinmes=data_sinmes.drop('Mes_Mayo', 1)
data_sinmes=data_sinmes.drop('Mes_Junio', 1)
data_sinmes=data_sinmes.drop('Mes_Julio', 1)
data_sinmes=data_sinmes.drop('Mes_Agosto', 1)
data_sinmes=data_sinmes.drop('Mes_Septiembre', 1)
data_sinmes=data_sinmes.drop('Mes_Octubre', 1)
data_sinmes=data_sinmes.drop('Mes_Noviembre', 1)
data_sinmes=data_sinmes.drop('Mes_Diciembre', 1)
data_sinmes.to_csv('datasetfinalproyecto_sinmeses.csv')
Como se menciona en un comienzo, dado el error cometido durante la aplicación anterior de clasificadores, estos se analizarán nuevamente, y se compararán con los obtenidos en el Hito 2, para ver si el corregir el error y agregar la información del mes del año resulta ser un verdadero aporte al predictor,y si la clasificación mediante oversampling resulta seguir siendo la mejor técnica.
#Vemos el balance de clases
print("Distribucion de clases original")
data['numero_No_Show'].value_counts()
#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())
#Retiramos la última columna del dataset que contiene la clase que se busca estimar y la dejamos en un nuevo vector.
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]]
Probamos el clasificador Decision Tree , cuyo funcionamiento consiste en predecir mediante un training set 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.
#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))
Comparación (se analizará sólo oversampling, pues entrega mejores resultados):
Clasificador Hito 3: Precision=0.82 ; Recall=0.81 ; F1-Score=0.81
Clasificador Hito 2: Precision=0.88 ; Recall=0.87 ; F1-Score=0.87
Al igual que en el hito 2, resulta funcionar mejor el Oversampling, además, se observa que sus parámetros de precision recall y f1 son peores a los obtenidos en el hito 2, sin embargo, este predictor se considera mejor pues a pesar de peores resultados, la información utilizada es más fiel a la realidad, a diferencia del utilizado anteriormente que contaba con errores al ordenar jerárquicamente las localidades, lo que da una noción errada de distancia en dicho atributo.
Utilizamos el clasificador: KNN. Utiliza los k puntos mas cercanos al punto bajo estudio para realizar la clasificación. 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.
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))
Comparación (se analizará sólo oversampling, pues entrega mejores resultados):
Clasificador Hito 3: Precision=0.76 ; Recall=0.76 ; F1-Score=0.76
Clasificador Hito 2: Precision=0.83 ; Recall=0.83 ; F1-Score=0.83
Nuevamente el Oversampling entrega mejores resultados, y el clasificador del hito 3 resulta con peores resultados, sin embargo se dejará este como el clasificador definitivo por lo anteriormente mencionado.
Dada la modificación que sufrió el dataset, el K óptimo para el clasificador pudo verse modificado, a continuación se estudiará el K óptimo para el nuevo dataset:
#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")
El K óptimo resulta en 2, que es lo mismo que se obtuvo en el hito 2. Para subsampling y oversampling no se analizará el K óptimo pues ya se demostró que KNN funciona mejor con oversampling.
Se analizará sólo el caso de Oversampling, principalmente para reducir la extensión del informe, además que la diferencia entre subsampling, oversampling y dataset original ya fue analizada previamente en el proyecto. Por otro lado, en los casos de subsampling y dataset original se observa que tienden a entregar peores resultados, y dado que la tendencia ha sido a obtener parámetros parecidos en el hito 2, se omitirán dichos casos pues no es sabido que no entregarán los mejores resultados.
Al igual que en el hito anterior se emplea la función run_classifier() utilizada en el laboratorio, cuyo fin es evaluar un clasificador.
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 compararán distintos clasificadores, y los que resulten con mejores resultados se compararán con los obtenidos previamente en el hito 2:
#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")
De los resultados se observa que el mejor clasificador es el Decision Tree, seguido por el KNN.
Mejores clasificadores de los resultados previos:
Resultados para clasificador: Decision Tree
Precision promedio: 0.8088693660385122
Recall promedio: 0.9438525488417789
F1-score promedio: 0.8711630556264679
Resultados para clasificador: KNN
Precision promedio: 0.8199133211678834
Recall promedio: 0.8149869629293728
F1-score promedio: 0.8174427198817438
Los mejores clasificadores resultaron ser lo mismos que en el hito 2, sin embargo sus parámetros empeoraron. El empobrecimiento de los parámetros se puede adjudicar principalmente a la sobredimensionalidad que las columnas de los meses significan.
Se recalca que tanto precision como recall son importantes. Pero dado que no se puede saber qué medida tomaría cada hospital con un paciente que se predijo que no asistiría, se asumirá que su forma de atacar el problema será mediante spam de confirmaciones a la asistencia. Bajo ese supuesto una gran cantidad de falsos positivos (poca precisión) no sería tan mala pues sólo significaría Spam a gente que sí irá a la consulta, lo que no generará pérdidas significativas para el hospital, y lo más importante, no significará pérdida de tiempo del médico con el que se atenderá.
En la vereda opuesta está el recall, que dada la naturaleza del problema resutla ser el parámetro más importante del clasificador. Pues un bajo recall implica una gran cantidad de falsos negativos, es decir, se predice que mucha gente sí irá a su consulta siendo que en la realidad la probabilidad de faltar es altísima, y dado que se asume que sí irán, se tomarán menos medidas para evitar o cubrir su inasistencia, lo que resulta en una potencial pérdida de tiempo del médico, que es justamente lo que el predictor busca evitar.
Se estudiarán los clasificadores KNN y Decision Tree dados sus buenos resultados previos. Nuevamente se analizará sólo el caso de oversampling por los motivos previamente mencionados.
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))
Resultados de Decision Tree con Oversampling en el hito anterior:
Promedio: 0.8563341106785325
precision recall f1-score support
0 0.96 0.74 0.84 88208
1 0.79 0.97 0.87 88208
avg / total 0.88 0.86 0.85 176416
Accuracy: 0.8562998820968619
Aquí se puede observar que el "aporte" de cada clase a los parámetros mantienen la misma proporción que en el hito anterior, por lo que las modificaciones no tuvieron repercusiones en dicho aspecto. Sin embargo, se observa que empeoraron los resultados obtenidos en el hito 2, cosa que se atribuye principalmente a la sobredimensionalidad.
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))
Resultados de KNN con oversampling en hito anterior:
Promedio: 0.8245060288406763
precision recall f1-score support
0 0.86 0.78 0.82 88208
1 0.80 0.87 0.83 88208
avg / total 0.83 0.82 0.82 176416
Accuracy: 0.8245057137674587
A diferencia de lo obtenido en el hito 2, en KNN se observa que el aporte de cada clase a los parámetros en el caso de Oversampling es levemente más equitativo, mientras que en el caso anterior se observaba cierto desbalance. A pesar de mejorar en dicho aspecto, la sobredimensionalidad nuevamente juega en contra empeorando los parámetros del clasificador.
Se corrobororará lo anterior mencionado de la sobredimensionalidad haciendo un nuevo análisis de los clasificadores sin las columnas de los meses:
data=data_sinmes
#Vemos el balance de clases
print("Distribucion de clases original")
data['numero_No_Show'].value_counts()
#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())
#Retiramos la última columna del dataset que contiene la clase que se busca estimar y la dejamos en un nuevo vector.
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:
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))
Comparación (se analizará sólo oversampling, pues entrega mejores resultados):
Clasificador Hito 3 con meses incluidos: Precision=0.82 ; Recall=0.81 ; F1-Score=0.81
Clasificador Hito 3 sin meses incluidos: Precision=0.80 ; Recall=0.80 ; F1-Score=0.80
Notamos que la diferencia en los resultados es despreciable,y resulta mejor utilizar el dataset sin meses pues obtiene resultados muy parecidos utilizando menos recuros.
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))
Comparación (se analizará sólo oversampling, pues entrega mejores resultados):
Clasificador Hito 3 con meses: Precision=0.76 ; Recall=0.76 ; F1-Score=0.76
Clasificador Hito 3 sin meses: Precision=0.74 ; Recall=0.74 ; F1-Score=0.74
Análogamente al caso anterior, al excluir los meses los resultados son muy similares y se gastan menos recursos computacionales.
De la comparación anterior en clasificadores se deduce que es mejor trabajar sin considerar los meses de la consulta pues si bien traen consigo una mejora en los parámetros, esta mejora se ve disminuida por la excesiva cantidad de dimensiones que se agregan para información que se demostró no ser tan relevante.
Es importante destacar esto último pues si las 12 columnas que se agregaran fueran de información que aportara real variabilidad al problema, el aumento en la dimensionalidad hubiera sido cubierto por la variabilidad de los nuevos atributos, pero en este caso, al agregar información con bajo aporte al predictor, el aumento en la complejidad del problema no entrega los beneficios mínimos que debiera, dado el aumento en el costo que genera.
Si bien las pruebas del clasificador ya fueron realizadas hasta este punto, las pruebas de PCA y Clustering sirven para caracterizar aún más el clustering y así ajustar aún más el dataset para obtener mejores métricas y por ende, un mejor clasificador.
from IPython.display import Image
El resultado obtenido para ver el aporte de variabilidad de cada una de las variables se puede ver en el siguiente gráfico. Cabe destacar que PCA fue realizado en Matlab.
El desarrollo de aplicar Clustering al Dataset se puede encontrar en este enlace.
Los resultados obtenidos de los experimentos definitivos del clasificador son positivos, sin embargo, crear un predictor estricto en base a los clasificadores utilizados puede ser riesgoso dependiendo de las medidas que se quieran utilizar, pues su eficiencia oscila entre 75% y 80% aproximadamente, lo que genera un margen de error no menor, por ello es que si bien se estima correctamente la mayoría de los casos, el predictor no es totalmente confiable. Es por esto que no se cumple el objetivo de lograr un predictor confiable mediante clasificadores, dado que no entrega la confianza mínims que la herramienta requiere para funcionar de buena forma.
Por otro lado, si las medidas para combatir la ausencia a las reservas no serán de considerables (una medida considerable sería cancelar una reserva, por ejemplo), puede ser una herramienta útil. Es importante notar que si se utilizara el predictor, y después el hospital verificará si acertó o no en su predicción, se podrían agregar más datos, lo que mejoraría el predictor.
Algo importante que ocurrió en el proyecto es que el predictor con errores en su estructura entregó mejores resultados que el predictor final, cuya estrucutra no tenía los errores en la distancia que sí se presentaban previamente. Esto se debe a que el dataset con errores contaba con una menor cantidad de atributos, lo que si bien hacía el predictor más sencillo, tenía una cantidad de dimensiones adecuada para predecir la asistencia a la reserva médica, siendo su principal falencia el error cometido al jerarquizar las localidades de los hospitales.
Ligado a lo anterior, dado que el nuevo set de datos contaba con más columnas por agregar información respectiva al mes del año, agregar esta información no fue útil pues si bien entrega más elementos para analizar, la sobredimensionalidad que esto significó terminó por ser más un problema que un aporte al clasificador, de lo que se desprende también que el mes del año en que se realizó la consulta no tiene un peso importante en el resultado final del predictor. De lo anterior mencionado se deduce que el agregar datos que no son un verdadero aporte es una falla tan grave como el cometer una error en la modificación del set de datos, siendo este un claro ejemplo, pues el empobrecimiento de los parámetros producto de la sobredimensionalidad resulta ser peor que el generado por los errores en el dataset.
Dado el bajo aporte de las columnas de cada mes, se decidió realizar el experimento de comparar el predictor con y sin las columnas recién mencionadas, donde se observa que sin los meses los resultados son muy parecidos (levemente peores), esto es pues la información que esas columnas agrega se anula con la sobredimensionalidad que implican, por ello se optó por no considerar dichas columnas, pues sin ellas los resultados eran muy similares, pero se conseguían a un menor costo.
Del PCA se observa que varias otras variables tienen un aporte escaso al predictor, donde a partir del PCA mismo y la exploración se deduce que dichos atributos que aportan baja variabilidad (además de los meses) son el género y la localidad del hospital (que se terminó eliminando para evitar problemas dimensionalidad).Contrariamente a los meses, el género, y la localidad; la edad, y la distancia entre fechas resultan ser atributos que aportan una alta variabilidad en la clase a la que pertenece cada dato. Cabe destacar que, si bien existe un diferencia alta entre las variables que tienen una mayor varianza relativa y las que tienen una menor, esta diferencia no es lo suficientemente elevada como para despreciar las de menor varianza y además estas últimas siguen contribuyendo de manera importante a la variabilidad total del set de datos, por lo que no puede ser eliminadas del dataset. Otro aspecto importante que se debe mencionar es que, dado el poco aporte total de las dos primeras componentes a la variabilidad total (40% aprox.), lo que entrega un indicio de los posibles resultados obtenido en la prueba de clustering, lo que apoya nuestra hipótesis de que aplicar técnicas de clustering es inviable en nuestro proyecto.
A modo de cierre, no se cumple el principal objetivo de crear un predictor de reserva médicas pues la confianza que se obtuvo de los clasificadores no fue suficiente, sin embargo, los resultados no son del todo malos pues la efectividad del predictor es cercana al 80%, por ello es que de todas formas puede ser un aporte en la resolución del problema que se busca enfrentar, a pesar de no resolverlo del todo.
Es importante destacar también que el desbalance de clases fue un problema importante en la elaboración del predictor, y no hubo forma de lograr un predictor eficiente sin utilizar oversampling, pues con subsampling se truncaban muchos y con el dataset original el desbalance era considerable y afectaba en el training set.
Otro problema importante fue la obtención de información, pues a partir del dataset original hubo mucha información importante que no se pudo conseguir y que su aporte en el predictor sería importante. Siendo la principal de ellas la hora de la consulta. Contrario a esto se contaba con columnas cuyo aporte era excesivamente bajo en el predictor, y se mantuvieron durante el desarrollo del predictor, lo que fue un costo de recursos computacionales evitable pues los resultados no cambiarían de forma importante sin dicha información.
En cuanto a Clustering, podemos concluir que nuestro problema no es abordable utilizando esta metodología, pues al ser solo dos clases, y una cantidad de variables no pequeña, es dificil separar los datos en grupos eficientes y representativos de la variable que buscamos: la inasistencia.
Finalmente, el proyecto fue una herramienta útil para introducir al grupo a la minería de datos, pues se aplicaron conceptos vistos en cátedra para lograr el predictor y se presentaron dificultades que fueron resueltas mediante conceptos aprendidos en cátedra (por ejemplo, balancear clases, o reducir dimensionalidad). Gracias a lo anterior mencionado, si bien no se cumplió el objetvio del proyecto en sí, este cumplió el objetivo pedagógico que implicaba.