2  Limpieza y transformación de datos de COVID-19 en México

En el capítulo anterior hicimos una introducción a las herramientas básicas de Python para manipular datos. Ahora, en este capítulo, vamos a trabajar con una base de datos más compleja y trataremos de seguir un flujo de trabajo real, basado en experiencias de trabajo con equipos de apoyo a la toma de decisiones.

La base de datos que vamos a utilizar son los datos abiertos sobre COVID-19 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. El tratamiento que vamos a dar a los datos, es el usado en diferentes flujos de trabajo de los grupos que estuvieron trabajando con la Secretaría de Salud para generar productos de análisis durante los primeros dos años de atención de la pandemia.

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

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

2.1 Exploración del contenido

Lo primero que vamos a hacer es explorar los datos publicados por la Secretaría de Salud para entender cómo están organizados. En la carpeta de datos del libro puedes encontrar un ejemplo de la base de datos para el 9 de enero de 2023 bajo el nombre datos_abiertos_covid19.zip.

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.

df = pd.read_csv('datos/datos_abiertos_covid19.zip', dtype=object, encoding='latin-1')
df.head()
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 2023-01-03 01e27d 2 9 25 2 25 25 001 1 ... 2 2 97 1 2 7 99 México 97 97
1 2023-01-03 180725 2 9 09 2 09 09 012 2 ... 2 2 97 1 2 7 99 México 97 2
2 2023-01-03 06fce8 1 12 07 1 07 07 059 1 ... 2 2 97 1 2 7 99 México 97 97
3 2023-01-03 1a4a8d 1 12 23 2 27 23 008 1 ... 2 2 97 1 2 7 99 México 97 97
4 2023-01-03 1933c0 1 12 09 2 09 09 007 1 ... 2 2 97 1 2 7 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. Lo pueden descargar del sitio de datos abiertos o bien usar el que viene en la carpeta de datos del libro bajo el nombre 201128 Catalogos.xlsx. 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.

catalogos = 'datos/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
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.

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

# 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()
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
1 2023-01-03 180725 2 9 09 2 09 09 012 2 ... 2 2 97 1 2 7 99 México 97 2
4 2023-01-03 1933c0 1 12 09 2 09 09 007 1 ... 2 2 97 1 2 7 99 México 97 97
8 2023-01-03 0741e4 2 6 09 2 09 09 016 2 ... 2 1 4 2 97 2 99 México 97 2
13 2023-01-03 1c4d2e 2 9 09 1 09 09 012 1 ... 99 2 97 1 1 3 99 México 97 97
15 2023-01-03 0a6cd6 2 6 09 1 18 09 007 1 ... 2 2 97 1 2 7 99 México 97 97

5 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).

df['MUNICIPIO_RES'] = df['ENTIDAD_RES'] + df['MUNICIPIO_RES']
df.head()
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
1 2023-01-03 180725 2 9 09 2 09 09 09012 2 ... 2 2 97 1 2 7 99 México 97 2
4 2023-01-03 1933c0 1 12 09 2 09 09 09007 1 ... 2 2 97 1 2 7 99 México 97 97
8 2023-01-03 0741e4 2 6 09 2 09 09 09016 2 ... 2 1 4 2 97 2 99 México 97 2
13 2023-01-03 1c4d2e 2 9 09 1 09 09 09012 1 ... 99 2 97 1 1 3 99 México 97 97
15 2023-01-03 0a6cd6 2 6 09 1 18 09 09007 1 ... 2 2 97 1 2 7 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.

# Como estamos usando explícitamente el parámetro columns, 
# no necesitamos especificar el eje
df = df.rename(columns={'OTRA_COM': 'OTRAS_COM'})
df.columns
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.

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:

clasificacion_final
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:

l1 = ['a', 'b', 'c']
l2 = [1, 2, 3]
l3 = list(zip(l1,l2))
l3
[('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:

clasificacion_final = dict(zip(clasificacion_final['CLAVE'], clasificacion_final['CLASIFICACIÓN']))
clasificacion_final
{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.

df['CLASIFICACION_FINAL'] = df['CLASIFICACION_FINAL'].map(clasificacion_final.get)
df['CLASIFICACION_FINAL'].head()
1                                 NEGATIVO A SARS-COV-2
4                                 NEGATIVO A SARS-COV-2
8     CASO DE COVID-19 CONFIRMADO POR COMITÉ DE  DIC...
13                       CASO DE SARS-COV-2  CONFIRMADO
15                                NEGATIVO A SARS-COV-2
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:

descriptores = pd.read_excel('datos/201128 Descriptores_.xlsx',
                             index_col='Nº',
                             engine='openpyxl')
descriptores
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:

df['OBESIDAD'].unique()
array(['2', '98', '1'], 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)
descriptores.columns = list(map(lambda col: col.replace(' ', '_'), descriptores.columns))
descriptores.head()
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:

descriptores['FORMATO_O_FUENTE'] = descriptores.FORMATO_O_FUENTE.str.strip()
descriptores['FORMATO_O_FUENTE'].head()
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).

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:

datos_si_no = descriptores.query('FORMATO_O_FUENTE == "CATÁLOGO: SI_ NO"')
datos_si_no
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

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:

cat_si_no = dict_catalogos['Catálogo SI_NO']
cat_si_no
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
# 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]
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
1 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
8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
13 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
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
6393642 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
6394417 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
6394626 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
6394988 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
6395781 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

1896084 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

2.3 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:

datetime_object = datetime.strptime('Jun 1 2005  1:33PM', '%b %d %Y %I:%M%p')
datetime_object
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:

datetime_object = datetime.strptime('06-01-2005  1:33PM', '%m-%d-%Y %I:%M%p')
datetime_object
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 transformar la columna FECHA_INGRESO de los datos originales en objetos de tipo datetime podemos hacer:

pd.to_datetime(df['FECHA_INGRESO'].head())
1    2022-01-19
4    2022-03-09
8    2022-02-20
13   2022-01-01
15   2022-06-28
Name: FECHA_INGRESO, dtype: datetime64[ns]

Vean la diferencia con el tipo de datos original:

df['FECHA_INGRESO'].head()
1     2022-01-19
4     2022-03-09
8     2022-02-20
13    2022-01-01
15    2022-06-28
Name: FECHA_INGRESO, dtype: object

Pandas intenta transformar los datos al tipo fecha usando formatos comunes. En general hace un buen trabajo, sin embargo, si nosotros conocemos el formato en el que están escritas las fechas, siempre es mejor ser explícito y usarlo para la transformación. En el caso de nuestros datos, el formato es: %Y-%m-%d, es decir, el año en cuatro caractéres, dos para el mes y dos para los días, separados por guiones medios. Para pasar el formato utilizamos la opción format de pd.to_datetime()

pd.to_datetime(df.FECHA_INGRESO, format="%Y-%m-%d")
1         2022-01-19
4         2022-03-09
8         2022-02-20
13        2022-01-01
15        2022-06-28
             ...    
6393642   2022-01-23
6394417   2022-11-17
6394626   2022-11-16
6394988   2022-12-01
6395781   2022-12-16
Name: FECHA_INGRESO, Length: 1896084, dtype: datetime64[ns]

Aunque el resultado debería ser el mismo, ser explícito nos ayuda a entender mejor el código y a asegurarnos de que nuestros datos se comportan como nosotros esperamos. Por ejemplo, ¿qué sucedería si algún registro no se contiene datos en el formato que especificamos? Veamos el campo FECHA_DEF que contiene registros intencionalmente inválidos.

pd.to_datetime(df['FECHA_DEF'], format="%Y-%m-%d")
ValueError: time data "9999-99-99" at position 0 doesn't match format specified

!Tenemos un ERROR! Pandas no puede transformar algunos datos utilizando el formato que le especificamos. En estos casos hay que especificar el copmportamiento que queremos cuando Pandas encuentra una fecha que no se ajusta al formato. El comportamiento por defecto es arrojar una excepción, es decir, detenerse al encontrar un error y reportárnoslo. Eso puede resultar útil en algunos casos, sin embargo no en el nuestro en el que una fecha que no se ajusta al formato significa que el paciente no ha fallecido, es decit las fechas codificadas como 9999-99-99 corresponden a valores nulos en el campo. Para que pandas regrese un valor nulo cuiando encuentre un error en la conversión de fechas, usamos la opción coerce:

pd.to_datetime(df['FECHA_DEF'], format="%Y-%m-%d", errors='coerce')
1                NaT
4                NaT
8         2022-02-21
13               NaT
15               NaT
             ...    
6393642          NaT
6394417          NaT
6394626          NaT
6394988          NaT
6395781          NaT
Name: FECHA_DEF, Length: 1896084, dtype: datetime64[ns]

Ahora los registros que no se pueden convertir en fechas con el formato que especificamos regresan NaT (Not a Time) en lugar de error.

Una vez que entendimos las formas en las que queremos convertir las columnas con fechas, podemos transformar todas:

df['FECHA_INGRESO'] = pd.to_datetime(df['FECHA_INGRESO'], format="%Y-%m-%d")
df['FECHA_SINTOMAS'] = pd.to_datetime(df['FECHA_SINTOMAS'], format="%Y-%m-%d")
df['FECHA_DEF'] = pd.to_datetime(df['FECHA_DEF'], format="%Y-%m-%d", errors='coerce')
df[['FECHA_INGRESO', 'FECHA_SINTOMAS', 'FECHA_DEF']].head()
FECHA_INGRESO FECHA_SINTOMAS FECHA_DEF
1 2022-01-19 2022-01-17 NaT
4 2022-03-09 2022-03-09 NaT
8 2022-02-20 2022-02-13 2022-02-21
13 2022-01-01 2022-01-01 NaT
15 2022-06-28 2022-06-28 NaT

2.4 Exportar datos

Ya que tenemos procesados los datos, es muy posible que los querramos guardar para usarlos más adelante. La forma más sencilla de exportar los datos es guardarlos como un csv. Para esto Pandas tiene el método to_csv

df.to_csv("datos/covid_enero_2023_procesados.csv")

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.

2.4.1 Tarea

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