Predicción de salud infantil en Argentina - parte 1/2
¿Qué pasaría si un médico pudiera saber de antemano si un niño va a tener problemas de salud?
Digamos que una madre con su bebé llega al consultorio del médico para hacerse un chequeo de rutina. El médico hace su trabajo y todo se ve bien. Ingresa toda la nueva información del bebé en la computadora pero ésta dice que el bebé probablemente va a estar en riesgo de salud… dentro de cuatro meses.
Hoy en día, esto sigue siendo ciencia ficción, pero no estamos TAN lejos de una situación así. Hoy voy a escribir sobre una competencia de la que participé, que proponía un problema similar: dadas algunas medidas como el peso o la talla de un bebé, ¿serán esos valores peligrosamente bajos en el futuro?
El problema a resolver
En el contexto de la semana de la ECI (Escuela de Ciencias Informáticas) se organizó una competencia en Kaggle referida a temáticas de salud en niños. El problema contenía un dataset de chequeos de rutina para diferentes bebés en diferentes regiones de Argentina, y tuvimos que predecir si los z-scores de altura, peso y masa corporal (HAZ, WAZ, BMIZ de ahora en adelante) estarán por debajo de un umbral determinado en el próximo chequeo.
La competencia era abierta a todos los que quisieran participar, es un problema interesante y un buen clasificador tendría un impacto positivo en la medicina nacional.
Además, pasó mucho tiempo desde que no competía, por lo que me vino al pelo. sentiment_very_satisfied
Veamos los datos
El dataset para esta competencia es bastante sencillo y lo suficientemente pequeño para correr varios experimentos interesantes. El conjunto de training tiene 43933 filas y el conjunto de test 6275 filas. Ambos conjuntos tienen 23 columnas (features).
Acá están las primeras filas de los datos originales del training set:
-
[[{col.field}]]
[[{user[col.field]}]] |
Y acá tenemos la descripción de cada feature:
Nombre | Descripción |
---|---|
BMIZ (float) | body-mass-index-for-age-Z-scores (referencia estandarizada del BMI por edad y género) |
HAZ (float) | height-for-age Z-scores (referencia estandarizada del la altura por edad y género) |
WAZ (float) | weight-for-age Z-scores (referencia estandarizada del peso por edad y género) |
… | … |
Nombre | Descripción |
---|---|
BMIZ (float) | body-mass-index-for-age-Z-scores (referencia estandarizada del BMI por edad y género) |
HAZ (float) | height-for-age Z-scores (referencia estandarizada del la altura por edad y género) |
WAZ (float) | weight-for-age Z-scores (referencia estandarizada del peso por edad y género) |
Individuo (int) | Identificador asignado a cada individuo |
Bmi (float) | bmi = peso / (altura^2) |
Departmento_indec_id (int) | código de departamento del efector. Coincide con el código obtenido del INDEC |
Departmento_lat (float) | promedio de la latitud en la cual se encuentran los efectores de dicho departamento |
Departmento_long (float) | promedio de la longitud en la cual se encuentran los efectores de dicho departamento |
Fecha_control (string) | fecha en que el individuo fue atendido |
Fecha_nacimiento (string) | fecha en que el individuo nació. |
Fecha_proximo_control (string) | fecha en que el individuo será atendido por próxima vez |
Genero | género del individuo (M = masculino, F = femenino) |
Nombre_provincia (string) | nombre de la provincia en donde se atendió el individuo |
Nombre_region (string) | nombre de la región en donde se atendió el individuo |
Perimetro_encefalico (float) | Mmedición de perímetro encefálico obtenida en la atención (cm) |
Peso (float) | medición de peso obtenida en la atención (kg) |
Talla (float) | medición de talla obtenida en la atención (cm) |
Provincia_indec_id (int) | código de provincia (S = sí pertenece, N = no pertenece) |
Zona_rural (string) | Código que indica si el efector se encuentra en una zona rural |
Var_BMIZ | variación que tendrá BMIZ en la siguiente atención respecto al valor actual |
Var_HAZ | variación que tendrá HAZ en la siguiente atención respecto al valor actual |
Var_WAZ | variación que tendrá WAZ en la siguiente atención respecto al valor actual |
Ahora veamos lo que tenemos que predecir. Nuestra variable objetivo “decae” se compone de tres condiciones:
Decae |
Toma el valor "true" si se cumple alguna de éstas tres condiciones:
{
HAZ >= -1 and proximo_HAZ < -1
WAZ >= -1 and proximo_WAZ < -1
BMIZ >= -1 and proximo_BMIZ < -1
|
Esto es, si el valor actual para el z-score es mayor que -1 y en el próximo chequeo es menor que -1 podemos decir que el valor bajó por debajo de un rango aceptable, y el bebé está posiblemente en riesgo (y “decae” es true). Hay que tener en cuenta que si el z-score del chequeo anterior estaba por debajo de -1, el valor del siguiente chequeo no importa ya que la definición de nuestras condiciones dice que estos casos siempre seran “false”.
Un par de ejemplos:
HAZ chequeo Jun '16 | HAZ chequeo Nov '16 | decae |
0.12 | -1.02 | true True, HAZ era >= -1 y ahora es < -1 |
-0.5 | -0.99 | false Falso, HAZ es siempre >= -1 |
-1.5 | -1.8 | false Ambos chequeos son menores a -1, la condición requiere que el anterior sea >= -1 |
HAZ respecto a dos chequeos en diferentes fechas
HAZ Jun '16 | BMIZ Jun '16 | HAZ Nov '16 | BMIZ Nov '16 | decae |
---|---|---|---|---|
0.1 | 1.0 | -1.5 | 1.0 | true HAZ cayó por debajo de -1 |
0.1 | -1.1 | -1.5 | -1.1 | true BMIZ está por debajo de -1 pero esto era verdad también en el chequeo anterior. Pero HAZ es menor a -1 y esta es la razón de por qué tenemos un true. |
BMIZ y HAZ respecto a dos chequeos en diferentes fechas
La fila resaltada es falsa independientemente de estar por debajo de -1.0 porque HAZ también estaba por debajo de -1.0 en el chequeo anterior. Acá se ve una dependencia, la variable objetivo depende del valor del chequeo visto anteriormente, y éste tiene que ser >= -1 para que “decae” pueda ser verdadero. Este es un umbral/límite fuerte y exploraremos más adelante que sucede en algunos de los modelos que entrenemos si simplemente ignoramos este hecho.
Una última cosa a mencionar es que la métrica de scoring que vamos a usar es ROC AUC (interactivo)
Primero lo primero
Digamos que Alice y Bob descargan el training set y empiezan a trabajar. La idea de Alice es tener un buen modelo, limpio, con buenas features y buenos hiperparámetros antes de mandar una solución. Bob por otro lado piensa que es mejor ser capaz de enviar una solución rápido y después mejorarla.
Alice trabaja durante una semana antes de poder mandar una solución, guiandose en los resultados de su propio set de validación. Pero el día en que manda su solución, algo sale mal y el score final es menor de lo que ella esperaba. Esto quizá sea un bug en el código, o overfittió en algún punto y ahora tiene que volver a un código posiblemente complejo y debuggearlo.
Bob hace el trabajo mínimo para tener una solución lo más rápido posible. Hizo una limpieza básica del dataset, corrió cualquier modelo con parámetros por defecto y generó la solución. Se presentó y obtuvo una puntuación no tan buena, pero ahora tiene una base sobre la cual mejorar. Sabe que su pipeline funciona de punto a punto, solamente tiene que mejorar la solución.
No digo que alguien que hace lo que Alice hizo va a cometer un error antes de tener una solución, pero creo que es mejor a largo plazo tener una solución que funcione, por más mala que sea, y luego mejorarla. Esto es cierto en las competencias y también en la industria.
Así que hice exactamente eso, limpié un poco el dataset (imputación básica y labelling), corrí un modelo de Gradient Boosting con valores por default y subí una solución el mismo día que empecé a competir. El puntaje inicial fue de 0.77043 y fue suficiente para estar entre los diez primeros en ese momento!
Explorando los datos
Antes de seguir con modelos y parámentros y demás, es muy buena idea ver realmente los datos y pensar cómo pueden ayudarnos a resolver el problema. Si fuéramos médicos, ¿cómo podemos saber intuitivamente si hay algo malo con un bebé? Probablemente midiendo su peso, altura, y otras cosas podrían ayudar. Podemos ver su entorno socioeconómico, ¿vive en una zona con mayor probabilidad de conducir a problemas de salud? Idealmente, también podríamos ver la situación de su madre, ¿es sana? ¿Cuida bien de su bebé?
No tenemos esa última información, pero tenemos algo sobre las dos primeras partes. Tenemos los pesos, alturas y z-scores y algunos datos geográficos que podemos extrapolar con datos socioeconómicos externos.
Comencemos con los z-scores. Acá podemos ver algunos ejemplos y cómo se relacionan con la variable “decae”:
<a class='fancybox-thumb ' id="HAZ" title="HAZ"
data-thumb="/assets/eci/haz.png" href="/assets/eci/haz.png" rel="density">
<img alt="HAZ" src="/assets/eci/haz.png">
<span class="fancy-caption">HAZ</span>
</a>
<a class='fancybox-thumb ' id="WAZ" title="WAZ"
data-thumb="/assets/eci/waz.png" href="/assets/eci/waz.png" rel="density">
<img alt="WAZ" src="/assets/eci/waz.png">
<span class="fancy-caption">WAZ</span>
</a>
<a class='fancybox-thumb ' id="BMIZ" title="BMIZ"
data-thumb="/assets/eci/bmiz.png" href="/assets/eci/bmiz.png" rel="density">
<img alt="BMIZ" src="/assets/eci/bmiz.png">
<span class="fancy-caption">BMIZ</span>
</a>
Vemos que, entre ciertos puntos, tenemos más densidad de casos con “decae” en “true”. Esto podría ser útil más adelante si podemos meter esta información en nuestro modelo.
También en dos minutos se puede generar una exploración descriptiva simple de las features:
Feature | top | freq | mean | std | min | 25% | 50% | 75% | max |
---|---|---|---|---|---|---|---|---|---|
BMIZ | — | — | 0.491 | 1.277 | -4.935 | -0.307 | 0.524 | 1.323 | 4.997 |
HAZ | — | — | -0.662 | 1.323 | -5.996 | -1.450 | -0.612 | 0.173 | 5.997 |
WAZ | — | — | -0.021 | 1.177 | -5.853 | -0.717 | 0.030 | 0.742 | 4.872 |
individuo | — | 4 | — | — | — | — | — | — | — |
bmi | — | — | 17.333 | 2.210 | 8.3298 | 15.938 | 17.301 | 26.874 | |
departamento_lat | — | — | -29.672 | 3.278 | -38.976 | -32.527 | -27.551 | -26.838 | -25.402 |
departamento_long | — | — | -62.0619 | 4.864 | -69.58 | -65.259 | -64.55 | -59.08 | -50 |
fecha_control | 2014-04-21 | 496 | — | — | — | — | — | — | — |
fecha_nacimiento | 2013-09-09 | 295 | — | — | — | — | — | — | — |
fecha_proximo_control | 2014-08-26 | 523 | — | — | — | — | — | — | — |
genero | M | 22010 | — | — | — | — | — | — | — |
nombre_provincia | Tucuman | 17467 | — | — | — | — | — | — | — |
nombre_region | NOA | 23084 | — | — | — | — | — | — | — |
perimetro_encefalico | — | — | 42.446 | 4.365 | 0 | 40 | 42 | 44.5 | 97 |
peso | — | — | 7.442 | 2.435 | 1.92 | 5.9 | 7.1 | 8.6 | 23 |
talla | — | — | 64.762 | 9.677 | 41 | 59 | 63 | 68 | 129 |
var_BMIZ | — | — | 0.0806 | 1.032 | -8.541 | -0.446 | 0.070 | 0.612 | 7.877 |
var_HAZ | — | — | 0.0537 | 1.016 | -10.005 | -0.438 | 0.0185 | 0.502 | 8.989 |
var_WAZ | — | — | 0.0900 | 0.693 | -7.50 | -0.243 | 0.070 | 0.393 | 6.00 |
zona_rural | N | 42970 | — | — | — | — | — | — | — |
decae | False | 37031 | — | — | — | — | — | — | — |
Ahora tenemos una idea de los datos. Recordemos que tenemos 43933 filas en el training set. Tenemos 22010 varones, esto es la mitad de los datos y la otra mitad son mujeres, así que está equilibrado por esta variable. También tenemos 17467 casos de Tucumán (es mucho) y esto posiblemente influirá en los resultados que dependan de las ubicaciones. La feature “Zona_rural” tiene 42970 filas en “false”, esto es la mayor parte del dataset. Esta feature no tiene casi varianza y es una posible candidata para ser eliminada. Una curiosidad para ver es que hay valores muy extremos para el min y max de nuestras features de z-score. Tener un HAZ de -5 debe significar un caso muy complicado de tratar para el médico.
Por último, nuestra variable objetivo “decae” tiene 37031 valores en “false”. Esto es casi el 80% del dataset por lo que tenemos un problema con las clases desequilibradas y esto va a afectar el rendimiento de nuestros predictores.
También tenemos las coordenadas de los diferentes hospitales. Es razonable pensar que hay zonas con mayor riesgo que otras. Dibujé todos los hospitales en el mapa y vemos que quizá sea una buena idea definir nuevas subregiones en algunas provincias. Por ejemplo, podemos dividir Buenos Aires en Sur/Centro-Norte/Gran Buenos Aires.
Usé algo de jitter y alpha blending en los puntos para poder ver mejor las zonas con más o con menos densidad de pacientes.
En este caso agrupamos los hospitales sólo geográficamente, pero podemos intentar una cosita más. Podemos agrupar los hospitales por la proporción de casos sanos/no sanos según “decae”. Por cada hospital tenemos el número de pacientes que tienen “decae” en “true” versus los que son falsos, dividimos esos dos números y obtenemos una proporción. Por ejemplo, si el hospital #123 tiene 80 pacientes con “decae” en “false” y 20 con “decae” en “true”, nuestra nueva feature es r = 0.8. Voy a mostrar en el próximo post que este enfoque fue mejor que el geográfico.
Una última cosa (y muy importante), podemos ver que los datos del training se componen de un chequeo por cada fila y varios pacientes se repiten. De hecho, la mayoría de los pacientes tienen tres o cuatro filas en el training set. No sólo eso, los pacientes que tienen tres filas corresponden a los mismos pacientes que están en el test set. Entonces tenemos la historia de los pacientes en el test set!
En el próximo post, sigo un poco con estas ideas y voy a contar las nuevas features que generé y los modelos predictivos que terminé usando en la competencia.
¡Gracias por leer!