Hito 2

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

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.

Exploración de datos

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

Preparación Previa

Partimos por cargar el set de datos, y visualizamos los primeros datos para poder compararlo con el dataset "final" que se obtendrá.

In [35]:
import pandas as pd

data = pd.read_csv('dataset_proyecto_mineria.csv')  # abrimos el archivo csv y lo cargamos en data.
data.head()
Out[35]:
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

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.

In [36]:
#Estudiamos el balance de clases
print("Distribucion de clases original")
data['NoShow'].value_counts()
Distribucion de clases original
Out[36]:
No     88208
Yes    22319
Name: NoShow, dtype: int64

Modificación del Dataset

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.

In [37]:
#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
Out[37]:
Gender ScheduledDay AppointmentDay Age Neighbourhood Scholarship Hipertension Diabetes Alcoholism Handcap SMS_received NoShow
0 F 2016-04-29T18:38:08Z 2016-04-29T00:00:00Z 62 JARDIM DA PENHA 0 1 0 0 0 0 No
1 M 2016-04-29T16:08:27Z 2016-04-29T00:00:00Z 56 JARDIM DA PENHA 0 0 0 0 0 0 No
2 F 2016-04-29T16:19:04Z 2016-04-29T00:00:00Z 62 MATA DA PRAIA 0 0 0 0 0 0 No
3 F 2016-04-29T17:29:31Z 2016-04-29T00:00:00Z 8 PONTAL DE CAMBURI 0 0 0 0 0 0 No
4 F 2016-04-29T16:07:23Z 2016-04-29T00:00:00Z 56 JARDIM DA PENHA 0 1 1 0 0 0 No
5 F 2016-04-27T08:36:51Z 2016-04-29T00:00:00Z 76 REPÚBLICA 0 1 0 0 0 0 No
6 F 2016-04-27T15:05:12Z 2016-04-29T00:00:00Z 23 GOIABEIRAS 0 0 0 0 0 0 Yes
7 F 2016-04-27T15:39:58Z 2016-04-29T00:00:00Z 39 GOIABEIRAS 0 0 0 0 0 0 Yes
8 F 2016-04-29T08:02:16Z 2016-04-29T00:00:00Z 21 ANDORINHAS 0 0 0 0 0 0 No
9 F 2016-04-27T12:48:25Z 2016-04-29T00:00:00Z 19 CONQUISTA 0 0 0 0 0 0 No
10 F 2016-04-27T14:58:11Z 2016-04-29T00:00:00Z 30 NOVA PALESTINA 0 0 0 0 0 0 No
11 M 2016-04-26T08:44:12Z 2016-04-29T00:00:00Z 29 NOVA PALESTINA 0 0 0 0 0 1 Yes
12 F 2016-04-28T11:33:51Z 2016-04-29T00:00:00Z 22 NOVA PALESTINA 1 0 0 0 0 0 No
13 M 2016-04-28T14:52:07Z 2016-04-29T00:00:00Z 28 NOVA PALESTINA 0 0 0 0 0 0 No
14 F 2016-04-28T10:06:24Z 2016-04-29T00:00:00Z 54 NOVA PALESTINA 0 0 0 0 0 0 No
15 F 2016-04-26T08:47:27Z 2016-04-29T00:00:00Z 15 NOVA PALESTINA 0 0 0 0 0 1 No
16 M 2016-04-28T08:51:47Z 2016-04-29T00:00:00Z 50 NOVA PALESTINA 0 0 0 0 0 0 No
17 F 2016-04-28T09:28:57Z 2016-04-29T00:00:00Z 40 CONQUISTA 1 0 0 0 0 0 Yes
18 F 2016-04-26T10:54:18Z 2016-04-29T00:00:00Z 30 NOVA PALESTINA 1 0 0 0 0 1 No
19 F 2016-04-29T10:43:14Z 2016-04-29T00:00:00Z 46 DA PENHA 0 0 0 0 0 0 No
20 F 2016-04-27T07:51:14Z 2016-04-29T00:00:00Z 30 NOVA PALESTINA 0 0 0 0 0 0 Yes
21 F 2016-04-27T10:50:45Z 2016-04-29T00:00:00Z 4 CONQUISTA 0 0 0 0 0 0 Yes
22 M 2016-04-25T13:29:16Z 2016-04-29T00:00:00Z 13 CONQUISTA 0 0 0 0 0 1 Yes
23 F 2016-04-28T10:27:05Z 2016-04-29T00:00:00Z 46 CONQUISTA 0 0 0 0 0 0 No
24 F 2016-04-29T14:19:19Z 2016-04-29T00:00:00Z 65 TABUAZEIRO 0 0 0 0 0 0 No
25 M 2016-04-26T15:04:17Z 2016-04-29T00:00:00Z 46 CONQUISTA 0 1 0 0 0 1 No
26 F 2016-04-29T14:19:42Z 2016-04-29T00:00:00Z 45 BENTO FERREIRA 0 1 0 0 0 0 No
27 F 2016-04-27T10:51:45Z 2016-04-29T00:00:00Z 4 CONQUISTA 0 0 0 0 0 0 No
28 M 2016-04-29T15:48:02Z 2016-04-29T00:00:00Z 51 SÃO PEDRO 0 0 0 0 0 0 No
29 F 2016-04-29T15:16:29Z 2016-04-29T00:00:00Z 32 SANTA MARTHA 0 0 0 0 0 0 No
... ... ... ... ... ... ... ... ... ... ... ... ...
110497 M 2016-06-01T09:46:33Z 2016-06-01T00:00:00Z 76 MARIA ORTIZ 0 0 0 0 0 0 No
110498 F 2016-06-08T10:21:14Z 2016-06-08T00:00:00Z 59 MARIA ORTIZ 0 0 0 0 0 0 No
110499 F 2016-06-01T09:42:56Z 2016-06-01T00:00:00Z 66 MARIA ORTIZ 0 1 1 0 0 0 No
110500 F 2016-06-08T09:35:13Z 2016-06-08T00:00:00Z 59 MARIA ORTIZ 0 0 0 0 0 0 No
110501 M 2016-06-01T10:19:12Z 2016-06-01T00:00:00Z 44 MARIA ORTIZ 0 0 0 0 0 0 No
110502 F 2016-06-08T10:50:42Z 2016-06-08T00:00:00Z 22 GOIABEIRAS 0 0 0 0 0 0 No
110503 F 2016-06-01T13:00:36Z 2016-06-01T00:00:00Z 64 SOLON BORGES 0 0 0 0 0 0 No
110504 F 2016-06-08T11:06:21Z 2016-06-08T00:00:00Z 4 MARIA ORTIZ 0 0 0 0 0 0 No
110505 F 2016-06-01T10:45:50Z 2016-06-01T00:00:00Z 55 MARIA ORTIZ 0 0 0 0 0 0 No
110506 M 2016-06-01T11:09:20Z 2016-06-01T00:00:00Z 5 MARIA ORTIZ 0 0 0 0 0 0 No
110507 F 2016-06-08T09:04:18Z 2016-06-08T00:00:00Z 0 MARIA ORTIZ 0 0 0 0 0 0 No
110508 F 2016-06-01T09:41:00Z 2016-06-01T00:00:00Z 59 MARIA ORTIZ 0 0 0 0 0 0 No
110509 M 2016-06-08T08:50:51Z 2016-06-08T00:00:00Z 33 MARIA ORTIZ 0 0 0 0 0 0 No
110510 F 2016-06-01T09:35:48Z 2016-06-01T00:00:00Z 64 SOLON BORGES 0 0 0 0 0 0 No
110511 F 2016-06-08T08:50:20Z 2016-06-08T00:00:00Z 14 MARIA ORTIZ 0 0 0 0 0 0 No
110512 F 2016-06-08T08:20:01Z 2016-06-08T00:00:00Z 41 MARIA ORTIZ 0 0 0 0 0 0 No
110513 M 2016-06-08T07:52:55Z 2016-06-08T00:00:00Z 2 ANTÔNIO HONÓRIO 0 0 0 0 0 0 No
110514 F 2016-06-08T08:35:31Z 2016-06-08T00:00:00Z 58 MARIA ORTIZ 0 0 0 0 0 0 No
110515 M 2016-06-06T15:58:05Z 2016-06-08T00:00:00Z 33 MARIA ORTIZ 0 1 0 0 0 0 Yes
110516 F 2016-06-07T07:45:16Z 2016-06-08T00:00:00Z 37 MARIA ORTIZ 0 0 0 0 0 0 Yes
110517 F 2016-06-07T07:38:34Z 2016-06-07T00:00:00Z 19 MARIA ORTIZ 0 0 0 0 0 0 No
110518 F 2016-04-27T15:15:06Z 2016-06-07T00:00:00Z 50 MARIA ORTIZ 0 0 0 0 0 1 No
110519 F 2016-04-27T15:23:14Z 2016-06-07T00:00:00Z 22 MARIA ORTIZ 0 0 0 0 0 1 No
110520 F 2016-05-03T07:51:47Z 2016-06-07T00:00:00Z 42 MARIA ORTIZ 0 0 0 0 0 1 No
110521 F 2016-05-03T08:23:40Z 2016-06-07T00:00:00Z 53 MARIA ORTIZ 0 0 0 0 0 1 No
110522 F 2016-05-03T09:15:35Z 2016-06-07T00:00:00Z 56 MARIA ORTIZ 0 0 0 0 0 1 No
110523 F 2016-05-03T07:27:33Z 2016-06-07T00:00:00Z 51 MARIA ORTIZ 0 0 0 0 0 1 No
110524 F 2016-04-27T16:03:52Z 2016-06-07T00:00:00Z 21 MARIA ORTIZ 0 0 0 0 0 1 No
110525 F 2016-04-27T15:09:23Z 2016-06-07T00:00:00Z 38 MARIA ORTIZ 0 0 0 0 0 1 No
110526 F 2016-04-27T13:30:56Z 2016-06-07T00:00:00Z 54 MARIA ORTIZ 0 0 0 0 0 1 No

110527 rows × 12 columns

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.

In [43]:
#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
In [39]:
#Vemos el número asociado a cada comuna
pd.DataFrame(l_tuples)
Out[39]:
0 1
0 AEROPORTO 1
1 ANDORINHAS 2
2 ANTÔNIO HONÓRIO 3
3 ARIOVALDO FAVALESSA 4
4 BARRO VERMELHO 5
5 BELA VISTA 6
6 BENTO FERREIRA 7
7 BOA VISTA 8
8 BONFIM 9
9 CARATOÍRA 10
10 CENTRO 11
11 COMDUSA 12
12 CONQUISTA 13
13 CONSOLAÇÃO 14
14 CRUZAMENTO 15
15 DA PENHA 16
16 DE LOURDES 17
17 DO CABRAL 18
18 DO MOSCOSO 19
19 DO QUADRO 20
20 ENSEADA DO SUÁ 21
21 ESTRELINHA 22
22 FONTE GRANDE 23
23 FORTE SÃO JOÃO 24
24 FRADINHOS 25
25 GOIABEIRAS 26
26 GRANDE VITÓRIA 27
27 GURIGICA 28
28 HORTO 29
29 ILHA DAS CAIEIRAS 30
... ... ...
51 PARQUE INDUSTRIAL 52
52 PARQUE MOSCOSO 53
53 PIEDADE 54
54 PONTAL DE CAMBURI 55
55 PRAIA DO CANTO 56
56 PRAIA DO SUÁ 57
57 REDENÇÃO 58
58 REPÚBLICA 59
59 RESISTÊNCIA 60
60 ROMÃO 61
61 SANTA CECÍLIA 62
62 SANTA CLARA 63
63 SANTA HELENA 64
64 SANTA LUÍZA 65
65 SANTA LÚCIA 66
66 SANTA MARTHA 67
67 SANTA TEREZA 68
68 SANTO ANDRÉ 69
69 SANTO ANTÔNIO 70
70 SANTOS DUMONT 71
71 SANTOS REIS 72
72 SEGURANÇA DO LAR 73
73 SOLON BORGES 74
74 SÃO BENEDITO 75
75 SÃO CRISTÓVÃO 76
76 SÃO JOSÉ 77
77 SÃO PEDRO 78
78 TABUAZEIRO 79
79 UNIVERSITÁRIO 80
80 VILA RUBIM 81

81 rows × 2 columns

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".

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

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

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

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

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

In [12]:
#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
Out[12]:
Age Scholarship Hipertension Diabetes Alcoholism Handcap SMS_received numero_comuna numero_genero_mujer numero_genero_hombre dia_semana distancia_fechas numero_No_Show
0 62 0 1 0 0 0 0 40 1 0 5 0 0
1 56 0 0 0 0 0 0 40 0 1 5 0 0
2 62 0 0 0 0 0 0 46 1 0 5 0 0
3 8 0 0 0 0 0 0 55 1 0 5 0 0
4 56 0 1 1 0 0 0 40 1 0 5 0 0
5 76 0 1 0 0 0 0 59 1 0 5 2 0
6 23 0 0 0 0 0 0 26 1 0 5 2 1
7 39 0 0 0 0 0 0 26 1 0 5 2 1
8 21 0 0 0 0 0 0 2 1 0 5 0 0
9 19 0 0 0 0 0 0 13 1 0 5 2 0
10 30 0 0 0 0 0 0 51 1 0 5 2 0
11 29 0 0 0 0 0 1 51 0 1 5 3 1
12 22 1 0 0 0 0 0 51 1 0 5 1 0
13 28 0 0 0 0 0 0 51 0 1 5 1 0
14 54 0 0 0 0 0 0 51 1 0 5 1 0
15 15 0 0 0 0 0 1 51 1 0 5 3 0
16 50 0 0 0 0 0 0 51 0 1 5 1 0
17 40 1 0 0 0 0 0 13 1 0 5 1 1
18 30 1 0 0 0 0 1 51 1 0 5 3 0
19 46 0 0 0 0 0 0 16 1 0 5 0 0
20 30 0 0 0 0 0 0 51 1 0 5 2 1
21 4 0 0 0 0 0 0 13 1 0 5 2 1
22 13 0 0 0 0 0 1 13 0 1 5 4 1
23 46 0 0 0 0 0 0 13 1 0 5 1 0
24 65 0 0 0 0 0 0 79 1 0 5 0 0
25 46 0 1 0 0 0 1 13 0 1 5 3 0
26 45 0 1 0 0 0 0 7 1 0 5 0 0
27 4 0 0 0 0 0 0 13 1 0 5 2 0
28 51 0 0 0 0 0 0 78 0 1 5 0 0
29 32 0 0 0 0 0 0 67 1 0 5 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ...
110497 76 0 0 0 0 0 0 44 0 1 3 0 0
110498 59 0 0 0 0 0 0 44 1 0 3 0 0
110499 66 0 1 1 0 0 0 44 1 0 3 0 0
110500 59 0 0 0 0 0 0 44 1 0 3 0 0
110501 44 0 0 0 0 0 0 44 0 1 3 0 0
110502 22 0 0 0 0 0 0 26 1 0 3 0 0
110503 64 0 0 0 0 0 0 74 1 0 3 0 0
110504 4 0 0 0 0 0 0 44 1 0 3 0 0
110505 55 0 0 0 0 0 0 44 1 0 3 0 0
110506 5 0 0 0 0 0 0 44 0 1 3 0 0
110507 0 0 0 0 0 0 0 44 1 0 3 0 0
110508 59 0 0 0 0 0 0 44 1 0 3 0 0
110509 33 0 0 0 0 0 0 44 0 1 3 0 0
110510 64 0 0 0 0 0 0 74 1 0 3 0 0
110511 14 0 0 0 0 0 0 44 1 0 3 0 0
110512 41 0 0 0 0 0 0 44 1 0 3 0 0
110513 2 0 0 0 0 0 0 3 0 1 3 0 0
110514 58 0 0 0 0 0 0 44 1 0 3 0 0
110515 33 0 1 0 0 0 0 44 0 1 3 2 1
110516 37 0 0 0 0 0 0 44 1 0 3 1 1
110517 19 0 0 0 0 0 0 44 1 0 2 0 0
110518 50 0 0 0 0 0 1 44 1 0 2 41 0
110519 22 0 0 0 0 0 1 44 1 0 2 41 0
110520 42 0 0 0 0 0 1 44 1 0 2 35 0
110521 53 0 0 0 0 0 1 44 1 0 2 35 0
110522 56 0 0 0 0 0 1 44 1 0 2 35 0
110523 51 0 0 0 0 0 1 44 1 0 2 35 0
110524 21 0 0 0 0 0 1 44 1 0 2 41 0
110525 38 0 0 0 0 0 1 44 1 0 2 41 0
110526 54 0 0 0 0 0 1 44 1 0 2 41 0

110527 rows × 13 columns

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.

Clasificadores

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.

In [13]:
#Estudiamos el balance de clases
print("Distribucion de clases original")
data['numero_No_Show'].value_counts()
Distribucion de clases original
Out[13]:
0    88208
1    22319
Name: numero_No_Show, dtype: int64

Las clases están desbalanceadas, por lo que se utilizará oversampling y subsampling.

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

Se separan los atributos de clase "numero_No_Show", para la clasificación.

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

In [16]:
#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.83      0.83      0.83     17660
          1       0.33      0.33      0.33      4446

avg / total       0.73      0.73      0.73     22106

Subsampling::::::::::
             precision    recall  f1-score   support

          0       0.61      0.62      0.61      4493
          1       0.61      0.59      0.60      4435

avg / total       0.61      0.61      0.61      8928

Oversampling::::::::::
             precision    recall  f1-score   support

          0       0.95      0.79      0.86     17649
          1       0.82      0.96      0.88     17635

avg / total       0.88      0.87      0.87     35284

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.

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

          0       0.81      0.95      0.88     17654
          1       0.39      0.12      0.18      4452

avg / total       0.73      0.78      0.74     22106

Subsampling::::::::::
             precision    recall  f1-score   support

          0       0.57      0.80      0.67      4458
          1       0.67      0.39      0.49      4470

avg / total       0.62      0.60      0.58      8928

Oversampling::::::::::
             precision    recall  f1-score   support

          0       0.84      0.82      0.83     17618
          1       0.83      0.84      0.83     17666

avg / total       0.83      0.83      0.83     35284

Buscando el K óptimo

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.

In [18]:
#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 [19]:
import matplotlib.pyplot as plt
%matplotlib inline

plt.plot(k_range, cv_scores)
plt.ylabel("precision")
plt.xlabel("K-nearest neighbors")
Out[19]:
Text(0.5,0,'K-nearest neighbors')
In [20]:
#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
In [21]:
import matplotlib.pyplot as plt
%matplotlib inline

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

In [22]:
#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
In [23]:
import matplotlib.pyplot as plt
%matplotlib inline

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

Comparación de clasificadores

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.

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

In [25]:
#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.49987632050276803
Recall promedio: 0.5000702868155538
F1-score promedio: 0.4999712907650972
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.8088693660385122
Recall promedio: 0.9438525488417789
F1-score promedio: 0.8711630556264679
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.6136056718433491
Recall promedio: 0.5494463968559875
F1-score promedio: 0.5797563747283636
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.8199133211678834
Recall promedio: 0.8149869629293728
F1-score promedio: 0.8174427198817438
----------------


In [26]:
#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")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.5001437427377384
Recall promedio: 0.5001792114695341
F1-score promedio: 0.5001523970795289
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.6150773570591767
Recall promedio: 0.5955451015531661
F1-score promedio: 0.605152414702155
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.6216126900198281
Recall promedio: 0.5618279569892471
F1-score promedio: 0.590210229055538
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6398557258791706
Recall promedio: 0.5298685782556749
F1-score promedio: 0.5796912016992076
----------------


In [27]:
#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")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.20237688019438985
Recall promedio: 0.20186081242532855
F1-score promedio: 0.20211212096820577
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.32179934066364113
Recall promedio: 0.33507915173237746
F1-score promedio: 0.3283037140513867
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.33014001473839344
Recall promedio: 0.13381123058542416
F1-score promedio: 0.1904357066950053
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.4072022160664819
Recall promedio: 0.10976702508960573
F1-score promedio: 0.17292083284319493
----------------


Análisis

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.

Evaluación de clasificadores con Cross Validation

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.

KNN

In [28]:
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)) 
[0.78137157 0.77766217 0.77205284 0.78484506 0.78243757]
Promedio: 0.779673841673694
             precision    recall  f1-score   support

          0       0.81      0.96      0.87     88208
          1       0.33      0.09      0.14     22319

avg / total       0.71      0.78      0.72    110527

Accuracy: 0.7796737448768174
In [29]:
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))  
[0.60091846 0.61626344 0.59296595 0.59117384 0.61281649]
Promedio: 0.6028276351474529
             precision    recall  f1-score   support

          0       0.59      0.70      0.64     22319
          1       0.63      0.50      0.56     22319

avg / total       0.61      0.60      0.60     44638

Accuracy: 0.6028271875980107
In [30]:
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.80937535 0.80761818 0.82873257 0.8361204  0.84068363]
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

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.

Decision Tree

In [31]:
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.84667271 0.84828818 0.85480671 0.86024035 0.8716626 ]
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
In [32]:
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)) 
[0.58501344 0.59935036 0.59229391 0.57549283 0.59612368]
Promedio: 0.5896548442510583
             precision    recall  f1-score   support

          0       0.59      0.61      0.60     22319
          1       0.60      0.57      0.58     22319

avg / total       0.59      0.59      0.59     44638

Accuracy: 0.590685066535239
In [33]:
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)) 
[0.71546187 0.7117977  0.70532887 0.71540375 0.71864821]
Promedio: 0.7133280801607865
             precision    recall  f1-score   support

          0       0.82      0.82      0.82     88208
          1       0.30      0.31      0.30     22319

avg / total       0.72      0.71      0.72    110527

Accuracy: 0.7145131958706922

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.

¿Cómo se tomaron en cuenta los comentarios del Hito 1?

Una vez revisado el feedback entregado por el cuerpo docente y compañeros/as, las mejoras y/o modificaciones en el proyecto fueron:

  • Se profundizó en la exploración de datos.
  • Se tuvo mayor cautela con las proporciones de los gráficos de la exploración.
  • Se agregan a los datos de interés factores no considerados previamente, como la hora,el día de la semana, la distancia entre fechas, el tráfico y el clima (junto con la lluvia), sin embargo la obtención de estos datos no se ha logrado en su totalidad en lo que se lleva de proyecto. La información sobre la hora de las reservas no se encuentra disponible de la fuente de los datos. La información del clima se obtuvo, pero los datos al ser antiguos eran muy generales, y se prefirió omitir hasta encontrar datos que sean un real aporte para el predictor. En cuanto al tráfico, dicho atributo se desechó por la dificultad de obtener dicha información, y la dificultad de la estimación de dicho atributo a partir de otros presentes en el dataset por no contar con información sobre la hora de las reservas.
  • Se desechó la hipótesis de que los pacientes en Chile tendrán un comportamiento similar al de los pacientes de Brasil, y por ende el proyecto estará enfocado sólo en los pacientes de dicho lugar.

Planificación Futura

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:

  • Agregar información sobre el clima del día de las reservas médicas: Dada la dificultad de obtener datos correspondientes a la región de estudio (Victoria), se usará un registro histórico, es decir, se buscará la temperatura y precipitaciones promedio de cada mes en la región de interés, y se trabajará con dichos datos, pues la información precisa de cada día es muy difícil de unir al dataset.
  • Incluir el factor sorpresa en el set de datos, o bien, estimarlo, y agregar la probabilidad de ocurrencia del factor sorpresa como una nueva columna: Esto se puede obtener de una correlación de los datos, por ejemplo podría ser una combinación entre la distancia entre fechas, el día de la reserva y la edad del paciente.
  • Mejorar las métricas obtenidas del predictor en su primera versión.
  • Buscar una forma de implementar el predictor sin utilizar oversampling para disminuir la probabilidad de overfitting al aplicarlo. Si bien se intentará mejorar este aspecto, no hay señales de overfitting hasta el momento.
  • Incluir las técnicas correspondiente a Clustering, que se estudiará durante las próximas semanas en el laboratorio 3.

Conclusión

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.