Geoinformática - Práctica 1

Transformación de los Datos

Las bases de datos provenientes de situaciones reales son problemáticas, no están limpias, pueden tener datos faltantes, codoficar los datos de formas extrañas, combinar tipos de datos, etcétera.

Es bien sabido que dentro de la Ciencia de Datos, se tenga la componente espacial o no, el trabajo no sólo se enfoca a generar modelos sofisticados o al análisis mismo, sino también a tareas mucho más básicas y menos exóticas como obtener los datos, procesarlos y modificarlos de forma que puedan ser examinados y explorados para entender sus propiedades básicas.

Dado lo extenuantes y relevantes que son estas tareas, es sorprendente encontrar que existen muy pocas publicaciones referentes a los patrones, técnicas y buenas prácticas existentes para una eficiente limpieza, manipulación y transformación de los datos.

En este taller nos vamos a enfocar en utilizar una base de datos del mundo real para aprender, utilizando Python, tćnicas para manipularlas, transformarlas y lim,piarlas de forma que podamos hacer análisis.

La base de datos que vamos a utilizar son los datos abiertos que publica diarimente la Dirección General de Epidemiología de la Secretaría de Salud. Esta es una base muy grande y compleja que incluye el seguimiento de todos los casos confirmados de COVID en México.

Antes de empezar a analizar la base de datos, vamos a importar las librerías que utilizaremos en el taller.

In [1]:
import os # hablar con el sistema operativo
import glob # listar directorios y ese tipo de operaciones
import itertools # herramientas para iterar objetos
from pathlib import Path # manipular rutas a directorios
import zipfile # comprimir y descomprimir archivos
import numpy as np # operaciones vectorizadas
import pandas as pd # DataFrames
from datetime import timedelta, date, datetime # Manejar fechas
import openpyxl # leer/escribir archivos de exel
import requests # Hablar con direcciones web
import logging

Descargar datos

El primer paso es, evidentemente, descargar los datos. Es claro que podríamos ir a la página de la Dirección General de Epidemiología (DGE) y descargarlos, sin embargo, lo vamos a hacer por el camino difícil de bajarlos usando Python.

Para eso, vamos a definir una función que tome como argumento la fecha para la que queremos descargar los datos y el path en donde los vamos a guardar. La función es relativamente simple, sólo hace un request al archivo histórico de la DGE y guarda los datos en la ruta configurada.

Para trabajar vamos siempre a descargar tres archivos:

  • Los datos COVID-19
  • El catálogo de campos
  • El archivo de descripción

El primer archivo contiene la serie de tiempo de seguimiento de casos hasta la fecha configurada, los dos archivos restantes sirven para entender la información contenida en los datos.

Entonces, primero definimos la función

In [6]:
def bajar_datos_salud(directorio_datos='data/', fecha='10-01-2022'):
    '''
        Descarga el archivo de datos y los diccionarios para la fecha solicitada.
    '''
    fecha = datetime.strptime(fecha, "%d-%m-%Y")
    url_salud_historicos = 'http://datosabiertos.salud.gob.mx/gobmx/salud/datos_abiertos/historicos/'    
    archivo_nombre = f'{fecha.strftime("%y%m%d")}COVID19MEXICO.csv.zip'
    archivo_ruta = os.path.join(directorio_datos, archivo_nombre)
    url_diccionario = 'http://datosabiertos.salud.gob.mx/gobmx/salud/datos_abiertos/diccionario_datos_covid19.zip'
    diccionario_ruta = os.path.join(directorio_datos, 'diccionario.zip')
    if os.path.exists(archivo_ruta):
        logging.debug(f'Ya existe {archivo_nombre}')
    else:
        print(f'Bajando datos {fecha.strftime("%d.%m.%Y")}')
        url_dia = "{}/{}/datos_abiertos_covid19_{}.zip".format(fecha.strftime('%Y'),
                                                                fecha.strftime('%m'),
                                                                fecha.strftime('%d.%m.%Y'))
        url = url_salud_historicos + url_dia
        r = requests.get(url, allow_redirects=True)
        open(archivo_ruta, 'wb').write(r.content)
        r = requests.get(url_diccionario, allow_redirects=True)
        open(diccionario_ruta, 'wb').write(r.content)
        with zipfile.ZipFile(diccionario_ruta, 'r') as zip_ref:
          zip_ref.extractall(directorio_datos)

Con la función que acabamos de definir, podemos bajar los datos hasta la última fecha disponible

In [7]:
ayer = datetime.now() - timedelta(1)
bajar_datos_salud(fecha=ayer.strftime('%d-%m-%Y'))

Exploración del contenido

Antes de empezar a manipular los datos, lo primero que tenemos que hacer es explorarlos brevemente y entender cómo están organizados. Leamos los datos en un DataFrame.

Fíjense en la ruta en donde la función de arriba descarga los datos, con la barra exploradora del lado izquierdo pueden navegar hasta esa ruta y copiar el path.

Para leer los datos vamos a utilizar la función read_csv(), esta función (como pueden ver) acepta que el csv venga comprimido en un zip.

In [2]:
df = pd.read_csv('data/220110COVID19MEXICO.csv.zip', dtype=object, encoding='latin-1')
df.head()
Out[2]:
FECHA_ACTUALIZACION ID_REGISTRO ORIGEN SECTOR ENTIDAD_UM SEXO ENTIDAD_NAC ENTIDAD_RES MUNICIPIO_RES TIPO_PACIENTE ... OTRO_CASO TOMA_MUESTRA_LAB RESULTADO_LAB TOMA_MUESTRA_ANTIGENO RESULTADO_ANTIGENO CLASIFICACION_FINAL MIGRANTE PAIS_NACIONALIDAD PAIS_ORIGEN UCI
0 2022-01-10 zz7202 1 12 16 2 16 16 112 1 ... 1 1 2 2 97 7 99 México 97 97
1 2022-01-10 z58ed3 2 12 18 1 18 18 017 1 ... 2 1 2 2 97 7 99 México 97 97
2 2022-01-10 z3be8c 2 12 07 1 07 07 101 1 ... 2 1 2 2 97 7 99 México 97 97
3 2022-01-10 z526b3 2 12 09 1 09 09 012 1 ... 1 2 97 1 1 3 99 México 97 97
4 2022-01-10 z3d1e2 2 12 09 1 09 09 005 1 ... 1 1 1 2 97 3 99 México 97 97

5 rows × 40 columns

Cada renglón en la base de datos corresponde a un caso en seguimiento, el resultado de cada caso se puede actualizar en sucesivas publicaciones de la base de datos. Las columnas describen un conjunto de variables asociadas al seguimiento de cada uno de los casos. Las dos primeras columnas corresponden a la fecha en la que se actualizó el caso y a un id único para cada caso respectivamente, en este taller no vamos a usar esas dos columnas.

Luego vienen un conjunto de columnas que describen la unidad médica de reporte y, después, las columnas que nos interesan más, que son las que describen al paciente.

Para entender un poco mejor los datos, conviene leer el archívo de catálogo. Pueden descargarlo en el explorador de archivos del lado izquierdo, pero aquí vamos a abrirlo y explorarlo un poco con Pandas. Como el catálogo es un archivo de excel con varias hojas, lo vamos a leer usando openpyxl que nos va a devolver un diccionario de DataFrames que relacionan el nombre de la hoja con los datos que contiene.

In [3]:
catalogos = 'data/201128 Catalogos.xlsx'
nombres_catalogos = ['Catálogo de ENTIDADES', # Acá están los nombres de las hojas del excel
                      'Catálogo MUNICIPIOS',
                      'Catálogo SI_NO',
                      'Catálogo TIPO_PACIENTE',
                      'Catálogo CLASIFICACION_FINAL',
                      'Catálogo RESULTADO_LAB'
                     ]
# read_excel nos regresa un diccionario que relaciona el nombre de cada hoja con 
# el contenido de la hoja como DataFrame
dict_catalogos = pd.read_excel(catalogos,
                          nombres_catalogos,
                          dtype=str,
                          engine='openpyxl')
clasificacion_final = dict_catalogos['Catálogo CLASIFICACION_FINAL']
# Aquí le damos nombre a las columnas porque en el excel se saltan dos líneas
clasificacion_final.columns = ["CLAVE", "CLASIFICACIÓN", "DESCRIPCIÓN"] 
clasificacion_final
Out[3]:
CLAVE CLASIFICACIÓN DESCRIPCIÓN
0 NaN NaN NaN
1 CLAVE CLASIFICACIÓN DESCRIPCIÓN
2 1 CASO DE COVID-19 CONFIRMADO POR ASOCIACIÓN CLÍ... Confirmado por asociación aplica cuando el cas...
3 2 CASO DE COVID-19 CONFIRMADO POR COMITÉ DE DIC... Confirmado por dictaminación solo aplica para ...
4 3 CASO DE SARS-COV-2 CONFIRMADO Confirmado aplica cuando:\nEl caso tiene muest...
5 4 INVÁLIDO POR LABORATORIO Inválido aplica cuando el caso no tienen asoci...
6 5 NO REALIZADO POR LABORATORIO No realizado aplica cuando el caso no tienen a...
7 6 CASO SOSPECHOSO Sospechoso aplica cuando: \nEl caso no tienen ...
8 7 NEGATIVO A SARS-COV-2 Negativo aplica cuando el caso:\n1. Se le tomo...

Lo que estamos viendo aquí es el catálogo de datos de la columna CLASIFICACION_FINAL. Este catálogo relaciona el valor de la CLAVE con su significado. En particular, la columna CLASIFICACION_FINAL es la que nos permite identificar los casos positivos como veremos más adelante.

El resto de los catálogos funciona de la misma forma, en este momento sólo vamos a utilizar la clasificación de los pacientes, pero más adelante podemos utilzar algunas de las columnas restantes.

Aplanado de datos

Como acabamos de ver, de alguna forma la información viene distribuida en tres archivos, uno con los datos, otro con las categorías que usa y un tercero con sus descripciones. Para utilizar los datos más fácilmente, sobre todo para poder hablarle a las cosas por su nombre en lugar de referirnos a sus valores codificados, vamos a realizar un conjunto de operaciones para aplanar los datos.

En el bajo mundo del análisis de datos, aplanar una base de datos es la operación de substituir los valores codificados a partir de un diccionario. En este caso, los datos que leímos traen valores codificados, entonces la primera misión es substituir esos valores por sus equivalentes en el diccionario.

Como la base de datos es muy grande, vamos a trabajar sólo con un estado de la república, en este caso la Ciudad de México (pero ustedes podrían elegir otro cualquiera).

Para seleccionar un estado, tenemos que elegir las filas del DataFrame que contengan el valor que queremos en la columna ENTIDAD, para eso vamos a aprender a usar nuestro primer operador de Pandas, el operador loc que nos permite seleccionar filas a partir de los valores de una o más columnas.

In [4]:
# el copy() nos asegura tener una copia de los datos en lugar de una referencia, 
# con eso podemos liberar la memoria más fácil
df = df.loc[df['ENTIDAD_RES'] == '09'].copy()
df.head()
Out[4]:
FECHA_ACTUALIZACION ID_REGISTRO ORIGEN SECTOR ENTIDAD_UM SEXO ENTIDAD_NAC ENTIDAD_RES MUNICIPIO_RES TIPO_PACIENTE ... OTRO_CASO TOMA_MUESTRA_LAB RESULTADO_LAB TOMA_MUESTRA_ANTIGENO RESULTADO_ANTIGENO CLASIFICACION_FINAL MIGRANTE PAIS_NACIONALIDAD PAIS_ORIGEN UCI
3 2022-01-10 z526b3 2 12 09 1 09 09 012 1 ... 1 2 97 1 1 3 99 México 97 97
4 2022-01-10 z3d1e2 2 12 09 1 09 09 005 1 ... 1 1 1 2 97 3 99 México 97 97
11 2022-01-10 z29dac 2 12 09 1 09 09 010 1 ... 2 1 4 2 97 5 99 México 97 97
15 2022-01-10 z45dcb 2 12 09 2 09 09 003 1 ... 1 1 2 2 97 7 99 México 97 97
22 2022-01-10 z23c2e 2 12 09 1 09 09 005 1 ... 2 2 97 2 97 6 99 México 97 97
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
12731369 2022-01-10 m00073e 2 12 15 2 99 09 015 1 ... 99 2 97 1 2 7 99 Japón 97 97
12731784 2022-01-10 m030623 2 12 15 2 15 09 011 1 ... 99 2 97 1 2 7 99 México 97 97
12731821 2022-01-10 m049633 2 12 15 1 09 09 005 1 ... 99 1 4 2 97 6 99 México 97 97
12732221 2022-01-10 m160d02 2 12 15 2 09 09 005 1 ... 99 2 97 1 2 7 99 México 97 97
12732863 2022-01-10 m0da9ec 2 12 15 1 09 09 005 1 ... 99 2 97 1 2 7 99 México 97 97

4139178 rows × 40 columns

Fíjense que lo que hicimos fue reescribir en la variable df el resultado de nuestra selección, de forma que df ahora sólo contiene resultados para la CDMX.

Ahora ya con los datos filtrados y, por lo tanto, con un tamaño más manejable, vamos a empezar a trabajarlos. Lo primero que vamos a hacer es cambiar los valores de la columna MUNICIPIO_RES por la concatenación de las claves de estado y municipio, esto porque nos hará más adelante más fácil el trabajo de unir los datos con las geometrías de los municipios y porque además así tendremos un identificador único para estos (claro que esto sólo tiene sentido al trabajar con varios estados al mismo tiempo).

In [5]:
df['MUNICIPIO_RES'] = df['ENTIDAD_RES'] + df['MUNICIPIO_RES']
df.head()
Out[5]:
FECHA_ACTUALIZACION ID_REGISTRO ORIGEN SECTOR ENTIDAD_UM SEXO ENTIDAD_NAC ENTIDAD_RES MUNICIPIO_RES TIPO_PACIENTE ... OTRO_CASO TOMA_MUESTRA_LAB RESULTADO_LAB TOMA_MUESTRA_ANTIGENO RESULTADO_ANTIGENO CLASIFICACION_FINAL MIGRANTE PAIS_NACIONALIDAD PAIS_ORIGEN UCI
3 2022-01-10 z526b3 2 12 09 1 09 09 09012 1 ... 1 2 97 1 1 3 99 México 97 97
4 2022-01-10 z3d1e2 2 12 09 1 09 09 09005 1 ... 1 1 1 2 97 3 99 México 97 97
11 2022-01-10 z29dac 2 12 09 1 09 09 09010 1 ... 2 1 4 2 97 5 99 México 97 97
15 2022-01-10 z45dcb 2 12 09 2 09 09 09003 1 ... 1 1 2 2 97 7 99 México 97 97
22 2022-01-10 z23c2e 2 12 09 1 09 09 09005 1 ... 2 2 97 2 97 6 99 México 97 97

5 rows × 40 columns

Ahora vamos a corregir el nombre de una columna en la base de datos para que coincida con el nombre en el diccionario y después podamos buscar automáticamente. Pra corregir el nombre de la columna vamos a utilizar la función rename de Pandas. Esta función nos sirve para renombrar filas (el índice del DataFrame, que vamos a ver más adelante) o columnas dependiendo de qué eje seleccionemos. El eje 0 son las filas y el 1 las columnas.

In [6]:
# Como estamos usando explícitamente el parámetro columns, 
# no necesitamos especificar el eje
df = df.rename(columns={'OTRA_COM': 'OTRAS_COM'})
df.columns
Out[6]:
Index(['FECHA_ACTUALIZACION', 'ID_REGISTRO', 'ORIGEN', 'SECTOR', 'ENTIDAD_UM',
       'SEXO', 'ENTIDAD_NAC', 'ENTIDAD_RES', 'MUNICIPIO_RES', 'TIPO_PACIENTE',
       'FECHA_INGRESO', 'FECHA_SINTOMAS', 'FECHA_DEF', 'INTUBADO', 'NEUMONIA',
       'EDAD', 'NACIONALIDAD', 'EMBARAZO', 'HABLA_LENGUA_INDIG', 'INDIGENA',
       'DIABETES', 'EPOC', 'ASMA', 'INMUSUPR', 'HIPERTENSION', 'OTRAS_COM',
       'CARDIOVASCULAR', 'OBESIDAD', 'RENAL_CRONICA', 'TABAQUISMO',
       'OTRO_CASO', 'TOMA_MUESTRA_LAB', 'RESULTADO_LAB',
       'TOMA_MUESTRA_ANTIGENO', 'RESULTADO_ANTIGENO', 'CLASIFICACION_FINAL',
       'MIGRANTE', 'PAIS_NACIONALIDAD', 'PAIS_ORIGEN', 'UCI'],
      dtype='object')

Fíjense cómo otra vez reescribimos la variable df. La mayor parte de las operaciones en Pandas regresan un DataFrame con el resultado de la operación y no modifican el DataFrame original, entonces para guardar los resultados, necesitamos reescribir la variable (o guardarla con otro nombre)

Ahora sí podemos empezar a aplanar los datos. Vamos a empezar por resolver las claves de resultado de las pruebas COVID. En los datos originales estos vienen codificados en la columna RESULTADO_LAB, pero en el diccionario ese velor se llama RESULTADO, entonces otra vez vamos a empezar por renombrar una columna.

In [7]:
df = df.rename(columns={'RESULTADO_LAB': 'RESULTADO'})

Para sustituir los valores en nuestros datos originales vamos a usar la función map que toma una serie (una serie es una columna de un dataframe) y mapea sus valores de acuerdo a una correspondencia que podemos pasar como un diccionario. Veamos poco a poco cómo hacer lo que queremos.

Lo primero que necesitamos es un diccionario que relacione los valores en nuestros datos con los nombres en el diccionario. Recordemos cómo se ve el diccionario:

In [8]:
clasificacion_final
Out[8]:
CLAVE CLASIFICACIÓN DESCRIPCIÓN
0 NaN NaN NaN
1 CLAVE CLASIFICACIÓN DESCRIPCIÓN
2 1 CASO DE COVID-19 CONFIRMADO POR ASOCIACIÓN CLÍ... Confirmado por asociación aplica cuando el cas...
3 2 CASO DE COVID-19 CONFIRMADO POR COMITÉ DE DIC... Confirmado por dictaminación solo aplica para ...
4 3 CASO DE SARS-COV-2 CONFIRMADO Confirmado aplica cuando:\nEl caso tiene muest...
5 4 INVÁLIDO POR LABORATORIO Inválido aplica cuando el caso no tienen asoci...
6 5 NO REALIZADO POR LABORATORIO No realizado aplica cuando el caso no tienen a...
7 6 CASO SOSPECHOSO Sospechoso aplica cuando: \nEl caso no tienen ...
8 7 NEGATIVO A SARS-COV-2 Negativo aplica cuando el caso:\n1. Se le tomo...

Necesitamos un diccionario {CLASIFICACION:CLAVE} (ya sé que hay unos valores espurios, pero no nos importan porque simplemente esos no los va a encontrar en nuestra base de datos).

Para construir este diccionario, vamos a empezar por construir la tupla que mantiene la relación que buscamos, para eso vamos a utilizar la función zip que toma dos iteradores como entrada y regresa un iterador que tiene por elementos las tuplas hechas elemento a elemento entre los dos iteradores de inicio. Veamoslo con calma:

In [9]:
l1 = ['a', 'b', 'c']
l2 = [1, 2, 3]
l3 = list(zip(l1,l2))
l3
Out[9]:
[('a', 1), ('b', 2), ('c', 3)]

Lo que nos regresa zip es un iterado con las tuplas formadas por los pares ordenados de los iteradores de entrada. En Python un iterador es cualquier cosa que se pueda recorrer en orden, a veces estos iteradores, como en el caso de zip no regresan todas las entradas sino, para ahorrar memoria, las generan conforme se recorren, por eso hay que hacer list(zip) para que se generen las entradas.

Ahora sí podemos entonces crear el diccionario con el que vamos a actualizar los datos:

In [10]:
clasificacion_final = dict(zip(clasificacion_final['CLAVE'], clasificacion_final['CLASIFICACIÓN']))
clasificacion_final
Out[10]:
{nan: nan,
 'CLAVE': 'CLASIFICACIÓN',
 '1': 'CASO DE COVID-19 CONFIRMADO POR ASOCIACIÓN CLÍNICA EPIDEMIOLÓGICA',
 '2': 'CASO DE COVID-19 CONFIRMADO POR COMITÉ DE  DICTAMINACIÓN',
 '3': 'CASO DE SARS-COV-2  CONFIRMADO',
 '4': 'INVÁLIDO POR LABORATORIO',
 '5': 'NO REALIZADO POR LABORATORIO',
 '6': 'CASO SOSPECHOSO',
 '7': 'NEGATIVO A SARS-COV-2'}

Y entonces pasarlo como argumento a la función map. Hay un truco aquí, map toma como argumento una función que, para cada llave, regresa el valor correspondiente, entonces no es propiamente el diccionario lo que vamos a pasar, sino la función get del diccionario que hace justo lo que queremos. Esto nos revela una prpiedad curiosa de Python, los argumentos de una función pueden ser funciones.

In [11]:
df['CLASIFICACION_FINAL'] = df['CLASIFICACION_FINAL'].map(clasificacion_final.get)
df['CLASIFICACION_FINAL'].head()
Out[11]:
3     CASO DE SARS-COV-2  CONFIRMADO
4     CASO DE SARS-COV-2  CONFIRMADO
11      NO REALIZADO POR LABORATORIO
15             NEGATIVO A SARS-COV-2
22                   CASO SOSPECHOSO
Name: CLASIFICACION_FINAL, dtype: object

Ahora vamos a hacer una sustitución un poco más compleja, tenemos que encontrar todos los campos de tipo "SI - NO" y resolverlos (sustituir por valores que podamos manejar más fácil). Los campos que tienen este tipo de datos vienen en el excel de descriptores:

In [12]:
descriptores = pd.read_excel('data/201128 Descriptores_.xlsx',
                             index_col='Nº',
                             engine='openpyxl')
descriptores
Out[12]:
NOMBRE DE VARIABLE DESCRIPCIÓN DE VARIABLE FORMATO O FUENTE
1 FECHA_ACTUALIZACION La base de datos se alimenta diariamente, esta... AAAA-MM-DD
2 ID_REGISTRO Número identificador del caso TEXTO
3 ORIGEN La vigilancia centinela se realiza a través de... CATÁLOGO: ORIGEN ...
4 SECTOR Identifica el tipo de institución del Sistema ... CATÁLOGO: SECTOR ...
5 ENTIDAD_UM Identifica la entidad donde se ubica la unidad... CATALÓGO: ENTIDADES
6 SEXO Identifica al sexo del paciente. CATÁLOGO: SEXO
7 ENTIDAD_NAC Identifica la entidad de nacimiento del paciente. CATALÓGO: ENTIDADES
8 ENTIDAD_RES Identifica la entidad de residencia del paciente. CATALÓGO: ENTIDADES
9 MUNICIPIO_RES Identifica el municipio de residencia del paci... CATALÓGO: MUNICIPIOS
10 TIPO_PACIENTE Identifica el tipo de atención que recibió el ... CATÁLOGO: TIPO_PACIENTE
11 FECHA_INGRESO Identifica la fecha de ingreso del paciente a ... AAAA-MM-DD
12 FECHA_SINTOMAS Idenitifica la fecha en que inició la sintomat... AAAA-MM-DD
13 FECHA_DEF Identifica la fecha en que el paciente falleció. AAAA-MM-DD
14 INTUBADO Identifica si el paciente requirió de intubación. CATÁLOGO: SI_ NO ...
15 NEUMONIA Identifica si al paciente se le diagnosticó co... CATÁLOGO: SI_ NO ...
16 EDAD Identifica la edad del paciente. NÚMERICA EN AÑOS
17 NACIONALIDAD Identifica si el paciente es mexicano o extran... CATÁLOGO: NACIONALIDAD
18 EMBARAZO Identifica si la paciente está embarazada. CATÁLOGO: SI_ NO ...
19 HABLA_LENGUA_INDIG Identifica si el paciente habla lengua índigena. CATÁLOGO: SI_ NO ...
20 INDIGENA Identifica si el paciente se autoidentifica co... CATÁLOGO: SI_ NO ...
21 DIABETES Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO ...
22 EPOC Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO ...
23 ASMA Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO ...
24 INMUSUPR Identifica si el paciente presenta inmunosupre... CATÁLOGO: SI_ NO ...
25 HIPERTENSION Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO ...
26 OTRAS_COM Identifica si el paciente tiene diagnóstico de... CATÁLOGO: SI_ NO ...
27 CARDIOVASCULAR Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO ...
28 OBESIDAD Identifica si el paciente tiene diagnóstico de... CATÁLOGO: SI_ NO ...
29 RENAL_CRONICA Identifica si el paciente tiene diagnóstico de... CATÁLOGO: SI_ NO ...
30 TABAQUISMO Identifica si el paciente tiene hábito de taba... CATÁLOGO: SI_ NO ...
31 OTRO_CASO Identifica si el paciente tuvo contacto con al... CATÁLOGO: SI_ NO ...
32 TOMA_MUESTRA_LAB Identifica si al paciente se le tomó muestra d... CATÁLOGO: SI_ NO
33 RESULTADO_LAB Identifica el resultado del análisis de la mue... CATÁLOGO: RESULTADO_LAB
34 TOMA_MUESTRA_ANTIGENO Identifica si al paciente se le tomó muestra d... CATÁLOGO: SI_ NO
35 RESULTADO_ANTIGENO Identifica el resultado del análisis de la mue... CATÁLOGO: RESULTADO_ANTIGENO
36 CLASIFICACION_FINAL Identifica si el paciente es un caso de COVID-... CATÁLOGO: CLASIFICACION_FINAL
37 MIGRANTE Identifica si el paciente es una persona migra... CATÁLOGO: SI_ NO ...
38 PAIS_NACIONALIDAD Identifica la nacionalidad del paciente. TEXTO, 99= SE IGNORA
39 PAIS_ORIGEN Identifica el país del que partió el paciente ... TEXTO, 97= NO APLICA
40 UCI Identifica si el paciente requirió ingresar a ... CATÁLOGO: SI_ NO ...

Fíjense en alguno de estos campos en los datos:

In [20]:
df['OBESIDAD'].unique()
Out[20]:
array(['1', '2', '98'], dtype=object)

Tenemos tres valores diferentes que corresponden (vean el diccionario) a SI, NO y NO ESPECIFICADO. Para todos los análisis que vamos a hacer en general sólo nos van a interesar los casos que sabemos que son SI, entonces lo que más nos conviene es codificar todos estos como binarios, es decir, sólo SI o NO. Además, podemos mejor decirles 1,0 respectivamente y así vamos a poder hacer cuentas mucho más fácil

De estos descriptores nos interesan los que tienen CATÁLOGO: SI_ NO en el campo FORMATO O FUENTE. Para poder encontrar y sustituir de forma más sencilla y automática vamos a hacer un par de modificaciones a los datos:

  • Reemplazar los espacios en los nombres de columnas por guiones bajos (para poder "hablarles" más fácil a las columnas)
  • Quitar espacios al principio o al final de los valores de los campos (para asegurarnos de que siempre van a ser los mismos)
In [14]:
descriptores.columns = list(map(lambda col: col.replace(' ', '_'), descriptores.columns))
descriptores.head()
Out[14]:
NOMBRE_DE_VARIABLE DESCRIPCIÓN_DE_VARIABLE FORMATO_O_FUENTE
1 FECHA_ACTUALIZACION La base de datos se alimenta diariamente, esta... AAAA-MM-DD
2 ID_REGISTRO Número identificador del caso TEXTO
3 ORIGEN La vigilancia centinela se realiza a través de... CATÁLOGO: ORIGEN ...
4 SECTOR Identifica el tipo de institución del Sistema ... CATÁLOGO: SECTOR ...
5 ENTIDAD_UM Identifica la entidad donde se ubica la unidad... CATALÓGO: ENTIDADES

Poco a poco:

  • descriptores.columns nos regresa (o les da valor, cuando está del lado izquierdo de un =) los nombres de las columnas del DataFrame
  • map(lambda col: col.replace(' ', '_'), descriptores.columns) la función map regresa una asosiación, como ya vimos. En este caso esta asosiación se hace a través de una función anónima lambda que toma como argumento el nombre de una columna y regresa el mismo nombre pero con los espacios sustituidos por guines bajos

Al final, lo que hacemos es sustituir los nombres de las columnas por una lista hecha por nosotros, para que esto funcione la lista que pasamos debe ser de igual tamaño que la lista original de columnas.

Ahora vamos a hacer lo mismo pero con los valores de los campos:

In [16]:
descriptores['FORMATO_O_FUENTE'] = descriptores.FORMATO_O_FUENTE.str.strip()
descriptores['FORMATO_O_FUENTE'].head()
Out[16]:
Nº
1             AAAA-MM-DD
2                  TEXTO
3       CATÁLOGO: ORIGEN
4       CATÁLOGO: SECTOR
5    CATALÓGO: ENTIDADES
Name: FORMATO_O_FUENTE, dtype: object

Este fué más fácil. Fíjense cómo pedimos el campo del lado derecho: descriptores.FORMATO_O_FUENTE, esto es equivalente a descriptores['FORMATO_O_FUENTE'] y los pueden usar indistintamente (claro, el primero sólo funciona si el nombre del campo no tiene espacios).

Perfecto, filtremos ahora los descriptores para quedarnos sólo con los que nos interesan, para eso vamos a usar la función query de Pandas, que nos permite filtrar un DataFrame de forma conveniente usando una expresión booleana:

In [17]:
datos_si_no = descriptores.query('FORMATO_O_FUENTE == "CATÁLOGO: SI_ NO"')
datos_si_no
Out[17]:
NOMBRE_DE_VARIABLE DESCRIPCIÓN_DE_VARIABLE FORMATO_O_FUENTE
14 INTUBADO Identifica si el paciente requirió de intubación. CATÁLOGO: SI_ NO
15 NEUMONIA Identifica si al paciente se le diagnosticó co... CATÁLOGO: SI_ NO
18 EMBARAZO Identifica si la paciente está embarazada. CATÁLOGO: SI_ NO
19 HABLA_LENGUA_INDIG Identifica si el paciente habla lengua índigena. CATÁLOGO: SI_ NO
20 INDIGENA Identifica si el paciente se autoidentifica co... CATÁLOGO: SI_ NO
21 DIABETES Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO
22 EPOC Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO
23 ASMA Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO
24 INMUSUPR Identifica si el paciente presenta inmunosupre... CATÁLOGO: SI_ NO
25 HIPERTENSION Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO
26 OTRAS_COM Identifica si el paciente tiene diagnóstico de... CATÁLOGO: SI_ NO
27 CARDIOVASCULAR Identifica si el paciente tiene un diagnóstico... CATÁLOGO: SI_ NO
28 OBESIDAD Identifica si el paciente tiene diagnóstico de... CATÁLOGO: SI_ NO
29 RENAL_CRONICA Identifica si el paciente tiene diagnóstico de... CATÁLOGO: SI_ NO
30 TABAQUISMO Identifica si el paciente tiene hábito de taba... CATÁLOGO: SI_ NO
31 OTRO_CASO Identifica si el paciente tuvo contacto con al... CATÁLOGO: SI_ NO
32 TOMA_MUESTRA_LAB Identifica si al paciente se le tomó muestra d... CATÁLOGO: SI_ NO
34 TOMA_MUESTRA_ANTIGENO Identifica si al paciente se le tomó muestra d... CATÁLOGO: SI_ NO
37 MIGRANTE Identifica si el paciente es una persona migra... CATÁLOGO: SI_ NO
40 UCI Identifica si el paciente requirió ingresar a ... CATÁLOGO: SI_ NO

Por si acaso, quitémosle también los espacios al campo FORMATO_O_FUENTE

In [18]:
descriptores['FORMATO_O_FUENTE'] = descriptores.FORMATO_O_FUENTE.str.strip()

Ahora sí, vamos a sustituir los valores como queremos en los datos originales. Para eso, lo primero que tenemos que hacer es fijarnos en el catálogo de estos campos:

In [21]:
cat_si_no = dict_catalogos['Catálogo SI_NO']
cat_si_no
Out[21]:
CLAVE DESCRIPCIÓN
0 1 SI
1 2 NO
2 97 NO APLICA
3 98 SE IGNORA
4 99 NO ESPECIFICADO

Justo estos valores los queremos cambiar por claves binarias (acuérdense, para distinguirlos fácilmente). Entonces lo que necesitamos ahora es:

  • Una lista de los nombres de los campos en donde vamos a hacer la sustitución
  • Un mapeo de los valores con los que vamos a sustituir
  • Hacer la sustitución primero en el diccionario y a partir de eso en los datos originales
In [22]:
# lista de los nombres de los campos
campos_si_no = datos_si_no.NOMBRE_DE_VARIABLE
# sustituimos en el catálogo de acuerdo a lo que nos interesa
cat_si_no['DESCRIPCIÓN'] = list(map(lambda val: 1 if val == 'SI' else 0, cat_si_no['DESCRIPCIÓN']))
# sustituimos en los datos originales
df[campos_si_no] = df[datos_si_no.NOMBRE_DE_VARIABLE].replace(
                                            to_replace=cat_si_no['CLAVE'].values,
                                            value=cat_si_no['DESCRIPCIÓN'].values)
df[campos_si_no]
Out[22]:
INTUBADO NEUMONIA EMBARAZO HABLA_LENGUA_INDIG INDIGENA DIABETES EPOC ASMA INMUSUPR HIPERTENSION OTRAS_COM CARDIOVASCULAR OBESIDAD RENAL_CRONICA TABAQUISMO OTRO_CASO TOMA_MUESTRA_LAB TOMA_MUESTRA_ANTIGENO MIGRANTE UCI
3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
11 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
15 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
22 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
12731369 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
12731784 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
12731821 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
12732221 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
12732863 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

4139178 rows × 20 columns

Acá utilizamos para la última sustitución la función replace de Pandas que toma dos parámetros: la lista de valores a reemplazar y la lista de los valoresa de reemplazo. El reemplazo sucede elemento a elemento, es decir, se sustituye el primer elemento de la lista to_replace por el primer elemento de la lista value y así sucesivamente.

Hay más campos que podemos aplanar en la base de datos, como ejercicio pueden explorar algunos de ellos y sustituir como le hemos hecho aquí. Regresaremos a esto más adelante en el taller, pero por lo pronto nos vamos a mover a otra etapa del pre-procesamiento: el manejo de las fechas

Manejo de fechas

En Python las fechas son un tipo especial de datos, nosotros estamos acostumbrados a verlas como cadenas de caractéres: 20 de febrero de 2010, por ejemplo. Python puede hacer muchas cosas con las fechas, pero para eso tienen que estar codificados de la forma correcta.

En general el módulo datetime de Python provee las utilerías necesarias para manejar/transformar objetos del tipo fecha. Una de las cosas más útiles es transformar strings en objetos datetime:

In [26]:
datetime_object = datetime.strptime('Jun 1 2005  1:33PM', '%b %d %Y %I:%M%p')
datetime_object
Out[26]:
datetime.datetime(2005, 6, 1, 13, 33)

Acá usamos un formato de fecha, '%b %d %Y %I:%M%p', para convertir el string 'Jun 1 2005 1:33PM'. De esa misma forma podemos especificar formatos diferentes:

In [28]:
datetime_object = datetime.strptime('06-01-2005  1:33PM', '%m-%d-%Y %I:%M%p')
datetime_object
Out[28]:
datetime.datetime(2005, 6, 1, 13, 33)

Pandas tiene la interfase to_datetime para este tipo de operaciones que nos permite transformar campos de forma muy sencilla, por ejemplo, para trransformar la columna FECHA_INGRESO de los datos originales en objetos de tipo datetime podemos hacer:

In [29]:
pd.to_datetime(df['FECHA_INGRESO'].head())
Out[29]:
3    2020-12-21
4    2020-04-22
11   2020-09-12
15   2020-08-18
22   2020-02-26
Name: FECHA_INGRESO, dtype: datetime64[ns]

Vean la diferencia con el tipo de datos original:

In [30]:
df['FECHA_INGRESO'].head()
Out[30]:
3     2020-12-21
4     2020-04-22
11    2020-09-12
15    2020-08-18
22    2020-02-26
Name: FECHA_INGRESO, dtype: object

Esto va a funcionar perfecto cuando el campo tiene sólo datos válidos (todos se ajustan a algún formato de fecha), pero ¿qué pasa en campos en donde no todos los datos son fechas válidas?

In [31]:
pd.to_datetime(df['FECHA_DEF'].head())
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in parse(self, timestr, default, ignoretz, tzinfos, **kwargs)
    648         try:
--> 649             ret = self._build_naive(res, default)
    650         except ValueError as e:

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in _build_naive(self, res, default)
   1234 
-> 1235         naive = default.replace(**repl)
   1236 

ValueError: month must be in 1..12

The above exception was the direct cause of the following exception:

ParserError                               Traceback (most recent call last)
~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslib.pyx in pandas._libs.tslib.array_to_datetime()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslibs/parsing.pyx in pandas._libs.tslibs.parsing.parse_datetime_string()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in parse(timestr, parserinfo, **kwargs)
   1367     else:
-> 1368         return DEFAULTPARSER.parse(timestr, **kwargs)
   1369 

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in parse(self, timestr, default, ignoretz, tzinfos, **kwargs)
    650         except ValueError as e:
--> 651             six.raise_from(ParserError(str(e) + ": %s", timestr), e)
    652 

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/six.py in raise_from(value, from_value)

ParserError: month must be in 1..12: 9999-99-99

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslib.pyx in pandas._libs.tslib.array_to_datetime()

TypeError: invalid string coercion to datetime

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in parse(self, timestr, default, ignoretz, tzinfos, **kwargs)
    648         try:
--> 649             ret = self._build_naive(res, default)
    650         except ValueError as e:

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in _build_naive(self, res, default)
   1234 
-> 1235         naive = default.replace(**repl)
   1236 

ValueError: month must be in 1..12

The above exception was the direct cause of the following exception:

ParserError                               Traceback (most recent call last)
/tmp/ipykernel_10062/2373993693.py in <module>
----> 1 pd.to_datetime(df['FECHA_DEF'])

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/core/tools/datetimes.py in to_datetime(arg, errors, dayfirst, yearfirst, utc, format, exact, unit, infer_datetime_format, origin, cache)
    881                 result = result.tz_localize(tz)  # type: ignore[call-arg]
    882     elif isinstance(arg, ABCSeries):
--> 883         cache_array = _maybe_cache(arg, format, cache, convert_listlike)
    884         if not cache_array.empty:
    885             result = arg.map(cache_array)

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/core/tools/datetimes.py in _maybe_cache(arg, format, cache, convert_listlike)
    193         unique_dates = unique(arg)
    194         if len(unique_dates) < len(arg):
--> 195             cache_dates = convert_listlike(unique_dates, format)
    196             cache_array = Series(cache_dates, index=unique_dates)
    197             # GH#39882 and GH#35888 in case of None and NaT we get duplicates

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/core/tools/datetimes.py in _convert_listlike_datetimes(arg, format, name, tz, unit, errors, infer_datetime_format, dayfirst, yearfirst, exact)
    399     assert format is None or infer_datetime_format
    400     utc = tz == "utc"
--> 401     result, tz_parsed = objects_to_datetime64ns(
    402         arg,
    403         dayfirst=dayfirst,

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/core/arrays/datetimes.py in objects_to_datetime64ns(data, dayfirst, yearfirst, utc, errors, require_iso8601, allow_object, allow_mixed)
   2196             return values.view("i8"), tz_parsed
   2197         except (ValueError, TypeError):
-> 2198             raise err
   2199 
   2200     if tz_parsed is not None:

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/core/arrays/datetimes.py in objects_to_datetime64ns(data, dayfirst, yearfirst, utc, errors, require_iso8601, allow_object, allow_mixed)
   2178     order: Literal["F", "C"] = "F" if flags.f_contiguous else "C"
   2179     try:
-> 2180         result, tz_parsed = tslib.array_to_datetime(
   2181             data.ravel("K"),
   2182             errors=errors,

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslib.pyx in pandas._libs.tslib.array_to_datetime()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslib.pyx in pandas._libs.tslib.array_to_datetime()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslib.pyx in pandas._libs.tslib._array_to_datetime_object()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslib.pyx in pandas._libs.tslib._array_to_datetime_object()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/pandas/_libs/tslibs/parsing.pyx in pandas._libs.tslibs.parsing.parse_datetime_string()

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in parse(timestr, parserinfo, **kwargs)
   1366         return parser(parserinfo).parse(timestr, **kwargs)
   1367     else:
-> 1368         return DEFAULTPARSER.parse(timestr, **kwargs)
   1369 
   1370 

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/dateutil/parser/_parser.py in parse(self, timestr, default, ignoretz, tzinfos, **kwargs)
    649             ret = self._build_naive(res, default)
    650         except ValueError as e:
--> 651             six.raise_from(ParserError(str(e) + ": %s", timestr), e)
    652 
    653         if not ignoretz:

~/miniconda3/envs/geoinformatica/lib/python3.10/site-packages/six.py in raise_from(value, from_value)

ParserError: month must be in 1..12: 9999-99-99

ERROR!!!!! Efectívamente, pandas no puede transformar algunos datos en fechas verdaderas (porque no todos los registros tienen una fecha de defunción válida). Entonces hay que decirle a Pandas qué hacer con estos registros, lo que queremos en este caso es que los registros inválidos los regrese como nulos, para eso usamos la opción coerce

In [32]:
pd.to_datetime(df['FECHA_DEF'].head(), 'coerce')
Out[32]:
3    NaT
4    NaT
11   NaT
15   NaT
22   NaT
Name: FECHA_DEF, dtype: datetime64[ns]

Listo, los registros que no se pueden convertir en fechas ahopra regresan NaT (Not a Time) en lugar de error.

Con esto en realidad ya es muy simple convertir toda una columna en tipo fecha:

In [33]:
df['FECHA_INGRESO'] = pd.to_datetime(df['FECHA_INGRESO'])
df['FECHA_INGRESO'].head()
Out[33]:
3    2020-12-21
4    2020-04-22
11   2020-09-12
15   2020-08-18
22   2020-02-26
Name: FECHA_INGRESO, dtype: datetime64[ns]

Hasta aquí hemos cubierto más o menos todo el pre-proceso de los datos. Claro no vimos todas las columnas, sólo nos fijamos en algunas, pero eso basta para darnos una buena idea de cómo se hacen las demás.

Tarea

Sustituyan los valores de la columna TIPO_PACIENTE por sus valores en el catálogo correspondiente

In [ ]: