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
3 Automatización
En el taller anterior vimos toda una serie de pasos para preprocesar los datos de COVUD-19. En esta actividad lo único que vamos a hacer es definir un par de funciones que realizan todo el flujo de preproceso. De esta forma podemos repetir todo el procedimiento de forma fácil.
3.1 Bajar y guardar datos
En el taller anterior bajamos los datos directamente del sitio de la Secretaría de Salud, ahora vamos a automatizar el proceso de descarga de datos de forma que, desde Python, podamos descargar los datos y asegurarnos de que tenemos la última versión disponible.
Descargar y guardar archivos en Python es relativamente sencillo, vamos a usar tres módulos de la distribución base de Python:
- os. Este módulo provee herramientas para interactuar con el sistema operativo. La vamos a usar para construir los paths en donde vamos a guardar los datos y preguntar si el archivo ya existe.
- requests. Esta librería provee diferentes formas de interactuar con el protocolo HTTP. La vamos a usar para hacer las peticiones a la página y procesar la respuesta.
- zipfile. Esta librería sirve para trabajar con archivos comprimidos en formato zip. En nuestro caso la usaremos para descomprimir los diccionarios.
La parte complicada de entender es el uso de requests
pra comunicarse con la página en donde están los datos.
= requests.get("https://www.centrogeo.org.mx/")
r 0:500] r.content[
b'\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n<!DOCTYPE html>\r\n<html lang="es-es" dir="ltr" class=\'com_content view-featured itemid-101 home j31 mm-hover\'>\r\n<head>\r\n<base href="https://www.centrogeo.org.mx/" />\n\t<meta http-equiv="content-type" content="text/html; charset=utf-8" />\n\t<meta name="keywords" content="M\xc3\xa9xico, CONACYT, CentroGeo, Ciencias de Informaci\xc3\xb3n Geoespacial, Centro de Investigaci\xc3\xb3n, Ciencias de Informaci\xc3\xb3n, Investigaci\xc3\xb3n, Geoespacial" />\n\t<meta name="rights" content="Esta obra est\xc3\xa1 bajo una licencia d'
Como ven, una petición de tipo get simplemente nos regresa, a través de la propiedad content, el contenido de la respuesta del servidor. En el caso de la página de CentroGeo, el contenido es el HTML de la página (que podríamos ver mejor con un browser), pero en el caso de que la dirección apunte a un archivo de descarga, el contenido es el stream de datos del archivo. Este stream de datos lo podemos usar como entrada para escribir un archivo utilizando la función open.
La función open
va a tomar como entrada el path en donde queremos guardar el archivo, este path puede ser simplemente una cadena de caracteres, sin embargo esto haría que nuestro código no fuera interoperable entre sistemas operativos, entonces, en lugar de escribir el path como cadena de caracteres, vamos a escribirlo como un objeto de os
:
"datos", "datos_covid.zip") os.path.join(
'datos/datos_covid.zip'
Esta forma de construir el path nos asegura que va a funcionar en cualquier sistema operativo.
Ya con estas explicaciones, podemos escribir la función que descarga los datos:
def bajar_datos_salud(directorio_datos='data/'):
'''
Descarga el ultimo archivo disponible en datos abiertos y los diccionarios correspondientes.
'''
= datetime.now().date()
fecha_descarga = 'https://datosabiertos.salud.gob.mx/gobmx/salud/datos_abiertos/datos_abiertos_covid19.zip'
url_datos = f'{fecha_descarga.strftime("%y%m%d")}COVID19MEXICO.csv.zip'
archivo_nombre = os.path.join(directorio_datos, archivo_nombre)
archivo_ruta = 'https://datosabiertos.salud.gob.mx/gobmx/salud/datos_abiertos/diccionario_datos_covid19.zip'
url_diccionario = os.path.join(directorio_datos, 'diccionario.zip')
diccionario_ruta if os.path.exists(archivo_ruta):
f'Ya existe {archivo_nombre}')
logging.debug(else:
print(f'Bajando datos...')
= requests.get(url_datos, allow_redirects=True)
r open(archivo_ruta, 'wb').write(r.content)
= requests.get(url_diccionario, allow_redirects=True)
r open(diccionario_ruta, 'wb').write(r.content)
with zipfile.ZipFile(diccionario_ruta, 'r') as zip_ref:
zip_ref.extractall(directorio_datos)
Para utilizar la función hacemos:
'datos/') bajar_datos_salud(
Bajando datos...
3.2 Preproceso
Ahora, ya que tenemos los datos descargados, vamos a empaquetar en una función el flujo de preproceso que trabajamos en el taller anterior. Esta función va a tomar como entrada la carpeta en donde se encuentran los datos y dicionariosy el nombre del archivo de datos que queremos procesar. Toma dos parámetros adicionales, uno para decidir si queremos resolver o no las claves binarias y otro para definir la entidad que queremos procesar.
def carga_datos_covid19_MX(data_dir='datos/', archivo='datos_abiertos_covid19.zip', resolver_claves='si_no_binarias', entidad='09'):
"""
Lee en un DataFrame el CSV con el reporte de casos de la Secretaría de Salud de México publicado en una fecha dada. Esta función
también lee el diccionario de datos que acompaña a estas publicaciones para preparar algunos campos, en particular permite la funcionalidad
de generar columnas binarias para datos con valores 'SI', 'No'.
**Nota 2**: Por las actualizaciones a los formatos de datos, esta función sólo va a servir para archivos posteriores a 20-11-28
resolver_claves: 'sustitucion', 'agregar', 'si_no_binarias', 'solo_localidades'. Resuelve los valores del conjunto de datos usando el
diccionario de datos y los catálogos. 'sustitucion' remplaza los valores en las columnas, 'agregar'
crea nuevas columnas. 'si_no_binarias' cambia valores SI, NO, No Aplica, SE IGNORA, NO ESPECIFICADO por 1, 0, 0, 0, 0 respectivamente.
"""
='201128 Catalogos.xlsx'
catalogo_nombre = os.path.join(data_dir, catalogo_nombre)
catalogo_path = '201128 Descriptores.xlsx'
descriptores_nombre = os.path.join(data_dir, descriptores_nombre)
descriptores_path = os.path.join(data_dir, archivo)
data_file print(data_file)
= pd.read_csv(data_file, dtype=object, encoding='latin-1')
df if entidad is not None:
= df[df['ENTIDAD_RES'] == entidad]
df # Hay un error y el campo OTRA_COMP es OTRAS_COMP según los descriptores
={'OTRA_COM': 'OTRAS_COM'}, inplace=True)
df.rename(columns# Asignar clave única a municipios
'MUNICIPIO_RES'] = df['ENTIDAD_RES'] + df['MUNICIPIO_RES']
df['CLAVE_MUNICIPIO_RES'] = df['MUNICIPIO_RES']
df[# Leer catalogos
= ['Catálogo de ENTIDADES',
nombres_catalogos 'Catálogo MUNICIPIOS',
'Catálogo RESULTADO',
'Catálogo SI_NO',
'Catálogo TIPO_PACIENTE']
'Catálogo CLASIFICACION_FINAL')
nombres_catalogos.append(2] = 'Catálogo RESULTADO_LAB'
nombres_catalogos[
= pd.read_excel(catalogo_path,
dict_catalogos
nombres_catalogos,=str,
dtype='openpyxl')
engine
= dict_catalogos[nombres_catalogos[0]]
entidades = dict_catalogos[nombres_catalogos[1]]
municipios = dict_catalogos[nombres_catalogos[2]]
tipo_resultado = dict_catalogos[nombres_catalogos[3]]
cat_si_no = dict_catalogos[nombres_catalogos[4]]
cat_tipo_pac # Arreglar los catálogos que tienen mal las primeras líneas
2]].columns = ["CLAVE", "DESCRIPCIÓN"]
dict_catalogos[nombres_catalogos[5]].columns = ["CLAVE", "CLASIFICACIÓN", "DESCRIPCIÓN"]
dict_catalogos[nombres_catalogos[
= dict_catalogos[nombres_catalogos[5]]
clasificacion_final
# Resolver códigos de entidad federal
= ['ENTIDAD_RES', 'ENTIDAD_UM', 'ENTIDAD_NAC']
cols_entidad 'CLAVE_ENTIDAD_RES'] = df['ENTIDAD_RES']
df[= df[cols_entidad].replace(to_replace=entidades['CLAVE_ENTIDAD'].values,
df[cols_entidad] =entidades['ENTIDAD_FEDERATIVA'].values)
value
# Construye clave unica de municipios de catálogo para resolver nombres de municipio
'CLAVE_MUNICIPIO'] = municipios['CLAVE_ENTIDAD'] + municipios['CLAVE_MUNICIPIO']
municipios[
# Resolver códigos de municipio
= dict(zip(municipios['CLAVE_MUNICIPIO'], municipios['MUNICIPIO']))
municipios_dict 'MUNICIPIO_RES'] = df['MUNICIPIO_RES'].map(municipios_dict.get)
df[
# Resolver resultados
={'RESULTADO_LAB': 'RESULTADO'}, inplace=True)
df.rename(columns'DESCRIPCIÓN'].replace({'POSITIVO A SARS-COV-2': 'Positivo SARS-CoV-2'}, inplace=True)
tipo_resultado[
= dict(zip(tipo_resultado['CLAVE'], tipo_resultado['DESCRIPCIÓN']))
tipo_resultado 'RESULTADO'] = df['RESULTADO'].map(tipo_resultado.get)
df[= dict(zip(clasificacion_final['CLAVE'], clasificacion_final['CLASIFICACIÓN']))
clasificacion_final 'CLASIFICACION_FINAL'] = df['CLASIFICACION_FINAL'].map(clasificacion_final.get)
df[# Resolver datos SI - NO
# Necesitamos encontrar todos los campos que tienen este tipo de dato y eso
# viene en los descriptores, en el campo FORMATO_O_FUENTE
= pd.read_excel(f'{data_dir}201128 Descriptores_.xlsx',
descriptores ='Nº',
index_col='openpyxl')
engine= list(map(lambda col: col.replace(' ', '_'), descriptores.columns))
descriptores.columns 'FORMATO_O_FUENTE'] = descriptores.FORMATO_O_FUENTE.str.strip()
descriptores[
= descriptores.query('FORMATO_O_FUENTE == "CATÁLOGO: SI_ NO"')
datos_si_no 'DESCRIPCIÓN'] = cat_si_no['DESCRIPCIÓN'].str.strip()
cat_si_no[
= datos_si_no.NOMBRE_DE_VARIABLE
campos_si_no = campos_si_no
nuevos_campos_si_no
if resolver_claves == 'agregar':
= [nombre_var + '_NOM' for nombre_var in campos_si_no]
nuevos_campos_si_no elif resolver_claves == 'si_no_binarias':
= [nombre_var + '_BIN' for nombre_var in campos_si_no]
nuevos_campos_si_no 'DESCRIPCIÓN'] = list(map(lambda val: 1 if val == 'SI' else 0, cat_si_no['DESCRIPCIÓN']))
cat_si_no[
= df[datos_si_no.NOMBRE_DE_VARIABLE].replace(
df[nuevos_campos_si_no] =cat_si_no['CLAVE'].values,
to_replace=cat_si_no['DESCRIPCIÓN'].values)
value
# Resolver tipos de paciente
= dict(zip(cat_tipo_pac['CLAVE'], cat_tipo_pac['DESCRIPCIÓN']))
cat_tipo_pac 'TIPO_PACIENTE'] = df['TIPO_PACIENTE'].map(cat_tipo_pac.get)
df[
= procesa_fechas(df)
df
return df
def procesa_fechas(covid_df):
= covid_df.copy()
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['DEFUNCION'] = (df['FECHA_DEF'].notna()).astype(int)
df['EDAD'] = df['EDAD'].astype(int)
df[return df
3.3 Preprocesar usando nuestras funciones
= carga_datos_covid19_MX(entidad='09')
df df
datos/datos_abiertos_covid19.zip
FECHA_ACTUALIZACION | ID_REGISTRO | ORIGEN | SECTOR | ENTIDAD_UM | SEXO | ENTIDAD_NAC | ENTIDAD_RES | MUNICIPIO_RES | TIPO_PACIENTE | ... | CARDIOVASCULAR_BIN | OBESIDAD_BIN | RENAL_CRONICA_BIN | TABAQUISMO_BIN | OTRO_CASO_BIN | TOMA_MUESTRA_LAB_BIN | TOMA_MUESTRA_ANTIGENO_BIN | MIGRANTE_BIN | UCI_BIN | DEFUNCION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2023-01-03 | 180725 | 2 | 9 | CIUDAD DE MÉXICO | 2 | CIUDAD DE MÉXICO | CIUDAD DE MÉXICO | TLALPAN | HOSPITALIZADO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | 2023-01-03 | 1933c0 | 1 | 12 | CIUDAD DE MÉXICO | 2 | CIUDAD DE MÉXICO | CIUDAD DE MÉXICO | IZTAPALAPA | AMBULATORIO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
8 | 2023-01-03 | 0741e4 | 2 | 6 | CIUDAD DE MÉXICO | 2 | CIUDAD DE MÉXICO | CIUDAD DE MÉXICO | MIGUEL HIDALGO | HOSPITALIZADO | ... | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
13 | 2023-01-03 | 1c4d2e | 2 | 9 | CIUDAD DE MÉXICO | 1 | CIUDAD DE MÉXICO | CIUDAD DE MÉXICO | TLALPAN | AMBULATORIO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
15 | 2023-01-03 | 0a6cd6 | 2 | 6 | CIUDAD DE MÉXICO | 1 | NAYARIT | CIUDAD DE MÉXICO | IZTAPALAPA | AMBULATORIO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
6393642 | 2023-01-03 | m1cd235 | 2 | 12 | MÉXICO | 1 | MÉXICO | CIUDAD DE MÉXICO | GUSTAVO A. MADERO | HOSPITALIZADO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
6394417 | 2023-01-03 | m0dbc4c | 2 | 12 | MÉXICO | 1 | AGUASCALIENTES | CIUDAD DE MÉXICO | AZCAPOTZALCO | AMBULATORIO | ... | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
6394626 | 2023-01-03 | m13431e | 2 | 12 | MÉXICO | 1 | CIUDAD DE MÉXICO | CIUDAD DE MÉXICO | AZCAPOTZALCO | AMBULATORIO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
6394988 | 2023-01-03 | m1493ea | 2 | 12 | MÉXICO | 1 | MÉXICO | CIUDAD DE MÉXICO | GUSTAVO A. MADERO | AMBULATORIO | ... | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
6395781 | 2023-01-03 | m0a22b8 | 2 | 12 | MÉXICO | 2 | CIUDAD DE MÉXICO | CIUDAD DE MÉXICO | TLALPAN | AMBULATORIO | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1896084 rows × 63 columns
3.4 Guardando el resultado
Listo, con nuestras funciones tenemos ya nuestros datos preprocesados, ahora vamos a guardarlos para poder utlizarlos rápidamente en otros notebooks. En general tenemos muchas opciones para guardar los datos, csv, por ejemplo. En esta ocasión vamos a usar un formato nativo de Python el pickle, que es una forma de serializar un objeto de Python. Pandas nos provee una función para guardar directamente un dataframe como pickle:
"data/datos_covid_ene19.pkl") df.to_pickle(
En la documentación de to_pickle pueden ver las opcioones completas.
"datos/covid_enero_2023_procesados.csv") df.to_csv(