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 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.
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.
= pd.read_csv('datos/datos_abiertos_covid19.zip', dtype=object, encoding='latin-1')
df 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.
= 'datos/201128 Catalogos.xlsx'
catalogos = ['Catálogo de ENTIDADES', # Acá están los nombres de las hojas del excel
nombres_catalogos '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
= pd.read_excel(catalogos,
dict_catalogos
nombres_catalogos,=str,
dtype='openpyxl')
engine= dict_catalogos['Catálogo CLASIFICACION_FINAL']
clasificacion_final # Aquí le damos nombre a las columnas porque en el excel se saltan dos líneas
= ["CLAVE", "CLASIFICACIÓN", "DESCRIPCIÓN"]
clasificacion_final.columns 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.loc[df['ENTIDAD_RES'] == '09'].copy()
df 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).
'MUNICIPIO_RES'] = df['ENTIDAD_RES'] + df['MUNICIPIO_RES']
df[ 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.rename(columns={'OTRA_COM': 'OTRAS_COM'})
df 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.rename(columns={'RESULTADO_LAB': 'RESULTADO'}) df
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:
= ['a', 'b', 'c']
l1 = [1, 2, 3]
l2 = list(zip(l1,l2))
l3 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:
= dict(zip(clasificacion_final['CLAVE'], clasificacion_final['CLASIFICACIÓN']))
clasificacion_final 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.
'CLASIFICACION_FINAL'] = df['CLASIFICACION_FINAL'].map(clasificacion_final.get)
df['CLASIFICACION_FINAL'].head() df[
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:
= pd.read_excel('datos/201128 Descriptores_.xlsx',
descriptores ='Nº',
index_col='openpyxl')
engine descriptores
NOMBRE DE VARIABLE | DESCRIPCIÓN DE VARIABLE | FORMATO O FUENTE | |
---|---|---|---|
Nº | |||
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:
'OBESIDAD'].unique() df[
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)
= list(map(lambda col: col.replace(' ', '_'), descriptores.columns))
descriptores.columns descriptores.head()
NOMBRE_DE_VARIABLE | DESCRIPCIÓN_DE_VARIABLE | FORMATO_O_FUENTE | |
---|---|---|---|
Nº | |||
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 DataFramemap(lambda col: col.replace(' ', '_'), descriptores.columns)
la funciónmap
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:
'FORMATO_O_FUENTE'] = descriptores.FORMATO_O_FUENTE.str.strip()
descriptores['FORMATO_O_FUENTE'].head() descriptores[
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:
= descriptores.query('FORMATO_O_FUENTE == "CATÁLOGO: SI_ NO"')
datos_si_no datos_si_no
NOMBRE_DE_VARIABLE | DESCRIPCIÓN_DE_VARIABLE | FORMATO_O_FUENTE | |
---|---|---|---|
Nº | |||
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
'FORMATO_O_FUENTE'] = descriptores.FORMATO_O_FUENTE.str.strip() descriptores[
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:
= dict_catalogos['Catálogo SI_NO']
cat_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
= datos_si_no.NOMBRE_DE_VARIABLE
campos_si_no # sustituimos en el catálogo de acuerdo a lo que nos interesa
'DESCRIPCIÓN'] = list(map(lambda val: 1 if val == 'SI' else 0, cat_si_no['DESCRIPCIÓN']))
cat_si_no[# sustituimos en los datos originales
= df[datos_si_no.NOMBRE_DE_VARIABLE].replace(
df[campos_si_no] =cat_si_no['CLAVE'].values,
to_replace=cat_si_no['DESCRIPCIÓN'].values)
value 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.strptime('Jun 1 2005 1:33PM', '%b %d %Y %I:%M%p')
datetime_object 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.strptime('06-01-2005 1:33PM', '%m-%d-%Y %I:%M%p')
datetime_object 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:
'FECHA_INGRESO'].head()) pd.to_datetime(df[
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:
'FECHA_INGRESO'].head() df[
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()
format="%Y-%m-%d") pd.to_datetime(df.FECHA_INGRESO,
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.
'FECHA_DEF'], format="%Y-%m-%d") pd.to_datetime(df[
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
:
'FECHA_DEF'], format="%Y-%m-%d", errors='coerce') pd.to_datetime(df[
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:
'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() df[[
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
"datos/covid_enero_2023_procesados.csv") df.to_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