Hito 3

Grupo 3
Integrantes: Joaquín Figueroa, Maximiliano Prieto, Daniel Escobar

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.

Introducción, motivación, objetivos e hipótesis

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.

Experimentos: Creación del Predictor.

Modificación del Dataset

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.

In [2]:
import pandas as pd

data = pd.read_csv('dataset_proyecto_mineria.csv')  # abrimos el archivo csv y lo cargamos en data.
data.head()
Out[2]:
PatientId AppointmentID Gender ScheduledDay AppointmentDay Age Neighbourhood Scholarship Hipertension Diabetes Alcoholism Handcap SMS_received NoShow
0 2.987250e+13 5642903 F 2016-04-29T18:38:08Z 2016-04-29T00:00:00Z 62 JARDIM DA PENHA 0 1 0 0 0 0 No
1 5.589978e+14 5642503 M 2016-04-29T16:08:27Z 2016-04-29T00:00:00Z 56 JARDIM DA PENHA 0 0 0 0 0 0 No
2 4.262962e+12 5642549 F 2016-04-29T16:19:04Z 2016-04-29T00:00:00Z 62 MATA DA PRAIA 0 0 0 0 0 0 No
3 8.679512e+11 5642828 F 2016-04-29T17:29:31Z 2016-04-29T00:00:00Z 8 PONTAL DE CAMBURI 0 0 0 0 0 0 No
4 8.841186e+12 5642494 F 2016-04-29T16:07:23Z 2016-04-29T00:00:00Z 56 JARDIM DA PENHA 0 1 1 0 0 0 No

A continuación se muestra cómo se modifica el dataset original a partir de los resultados del hito 1 y 2:

In [3]:
#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:

In [4]:
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:

In [5]:
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:

In [6]:
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:

In [7]:
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:

In [8]:
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:

In [9]:
data=data.drop('Gender', 1)
data=data.drop('ScheduledDay', 1)
data=data.drop('AppointmentDay', 1)
data=data.drop('NoShow',1)
data.head()
Out[9]:
Age Scholarship Hipertension Diabetes Alcoholism Handcap SMS_received numero_genero_mujer numero_genero_hombre dia_semana ... Mes_Mayo Mes_Junio Mes_Julio Mes_Agosto Mes_Septiembre Mes_Octubre Mes_Noviembre Mes_Diciembre distancia_fechas numero_No_Show
0 62 0 1 0 0 0 0 1 0 5 ... 0 0 0 0 0 0 0 0 0 0
1 56 0 0 0 0 0 0 0 1 5 ... 0 0 0 0 0 0 0 0 0 0
2 62 0 0 0 0 0 0 1 0 5 ... 0 0 0 0 0 0 0 0 0 0
3 8 0 0 0 0 0 0 1 0 5 ... 0 0 0 0 0 0 0 0 0 0
4 56 0 1 1 0 0 0 1 0 5 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 24 columns

In [10]:
# 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:

In [26]:
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')

Predictor mediante clasificadores

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.

In [11]:
#Vemos el balance de clases
print("Distribucion de clases original")
data['numero_No_Show'].value_counts()
Distribucion de clases original
Out[11]:
0    88208
1    22319
Name: numero_No_Show, dtype: int64
In [12]:
#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())
Distribución de clases usando (over/sub)sampling

Data oversampled on class 'Yes'
1    88208
0    88208
Name: numero_No_Show, dtype: int64

Data subsampled on class 'No'
1    22319
0    22319
Name: numero_No_Show, dtype: int64
In [13]:
#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.

In [14]:
#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))
Original
             precision    recall  f1-score   support

          0       0.82      0.87      0.84     17648
          1       0.33      0.26      0.29      4458

avg / total       0.72      0.74      0.73     22106

Subsampling
             precision    recall  f1-score   support

          0       0.61      0.66      0.63      4497
          1       0.62      0.57      0.60      4431

avg / total       0.62      0.62      0.62      8928

Oversampling
             precision    recall  f1-score   support

          0       0.85      0.76      0.80     17544
          1       0.79      0.87      0.82     17740

avg / total       0.82      0.81      0.81     35284

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.

In [15]:
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))
Original
             precision    recall  f1-score   support

          0       0.81      0.95      0.87     17636
          1       0.37      0.11      0.17      4470

avg / total       0.72      0.78      0.73     22106

Subsampling
             precision    recall  f1-score   support

          0       0.57      0.79      0.66      4474
          1       0.65      0.39      0.49      4454

avg / total       0.61      0.59      0.58      8928

Oversampling
             precision    recall  f1-score   support

          0       0.73      0.82      0.77     17551
          1       0.80      0.70      0.75     17733

avg / total       0.76      0.76      0.76     35284

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.

K óptimo

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:

In [16]:
#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
C:\Users\Daniel\Anaconda3\lib\site-packages\sklearn\cross_validation.py:41: DeprecationWarning: This module was deprecated in version 0.18 in favor of the model_selection module into which all the refactored classes and functions are moved. Also note that the interface of the new CV iterators are different from that of this module. This module will be removed in 0.20.
  "This module will be removed in 0.20.", DeprecationWarning)
In [17]:
import matplotlib.pyplot as plt
%matplotlib inline

plt.plot(k_range, cv_scores)
plt.ylabel("precision")
plt.xlabel("K-nearest neighbors")
Out[17]:
Text(0.5,0,'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.

Comparación de Clasificadores

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.

In [19]:
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:

In [20]:
#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")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.499799817118921
Recall promedio: 0.5001681593167819
F1-score promedio: 0.4999819317441879
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7754592801933268
Recall promedio: 0.8553882779730191
F1-score promedio: 0.813465039122398
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.6181305777360262
Recall promedio: 0.5470279257831691
F1-score promedio: 0.5804097670502388
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.7866362619395921
Recall promedio: 0.6908891660053659
F1-score promedio: 0.7356603963383966
----------------


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.

Evaluación con Cross Validation

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.

Decision Tree
In [21]:
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)) 
[0.73849337 0.78225258 0.79939916 0.82705062 0.68352134]
Promedio: 0.7661434142610026
             precision    recall  f1-score   support

          0       0.84      0.65      0.74     88208
          1       0.72      0.88      0.79     88208

avg / total       0.78      0.77      0.76    176416

Accuracy: 0.7657978868129874

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.

KNN
In [25]:
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)) 
[0.71570684 0.73259834 0.75391112 0.76730344 0.6487444 ]
Promedio: 0.723652829019874
             precision    recall  f1-score   support

          0       0.72      0.73      0.72     88208
          1       0.73      0.72      0.72     88208

avg / total       0.72      0.72      0.72    176416

Accuracy: 0.7236531833847271

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:

In [27]:
data=data_sinmes
In [28]:
#Vemos el balance de clases
print("Distribucion de clases original")
data['numero_No_Show'].value_counts()
Distribucion de clases original
Out[28]:
0    88208
1    22319
Name: numero_No_Show, dtype: int64
In [29]:
#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())
Distribución de clases usando (over/sub)sampling

Data oversampled on class 'Yes'
1    88208
0    88208
Name: numero_No_Show, dtype: int64

Data subsampled on class 'No'
1    22319
0    22319
Name: numero_No_Show, dtype: int64
In [30]:
#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]]
In [31]:
#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))
Original
             precision    recall  f1-score   support

          0       0.82      0.88      0.85     17522
          1       0.35      0.24      0.28      4584

avg / total       0.72      0.75      0.73     22106

Subsampling
             precision    recall  f1-score   support

          0       0.60      0.66      0.63      4475
          1       0.62      0.56      0.59      4453

avg / total       0.61      0.61      0.61      8928

Oversampling
             precision    recall  f1-score   support

          0       0.82      0.75      0.79     17559
          1       0.77      0.84      0.81     17725

avg / total       0.80      0.80      0.80     35284

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.

In [32]:
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))
Original
             precision    recall  f1-score   support

          0       0.81      0.96      0.88     17718
          1       0.37      0.11      0.16      4388

avg / total       0.72      0.79      0.74     22106

Subsampling
             precision    recall  f1-score   support

          0       0.56      0.79      0.65      4435
          1       0.65      0.37      0.47      4493

avg / total       0.60      0.58      0.56      8928

Oversampling
             precision    recall  f1-score   support

          0       0.70      0.82      0.75     17538
          1       0.78      0.66      0.71     17746

avg / total       0.74      0.74      0.73     35284

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.

Pruebas para caracterizar el dataset: PCA y Clustering

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.

PCA

In [3]:
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.

Clustering

El desarrollo de aplicar Clustering al Dataset se puede encontrar en este enlace.

Análisis de Resultados y Hallazgos

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.

Conclusión del 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.