Cada mes, cientos de establecimientos de salud en Bolivia envían información sobre la prevalencia de enfermedades que el Estado considera de relevancia epidemiológica. Esta información es consolidada y presentada en tableros que creo podrían ser mucho más amigables de usar. Hay tantos usos como visualizaciones, modelos de proyección o simples índices de incidencia, que en el estado actual serían demasiado tediosos de producir. Por eso, en este trabajo produzco una forma reproducible y escalable de descargar todos estos datos a nivel municipal y mensual para cada condición registrada y año entre 2001 y 2021.
# dependencias
import pandas as pd
import requests
import datetime as dt
from bs4 import BeautifulSoup
import unicodedata
from slugify import slugify
from IPython.display import clear_output
from itables import show, init_notebook_mode
init_notebook_mode()
from IPython.display import display, IFrame
import itertools
import re
import json
Antes que nada, los datos. En este tablero puedes explorar algunas vistas interesantes:
display(IFrame(src='https://observablehq.com/embed/@mauforonda/vigilancia-epidemiologica/2?cells=barplot%2Ctext1%2Cviewof+nombre%2Cviewof+column%2Cheading1%2Clegend%2Cviewof+munselect%2Cheading2%2Clineplot%2Cgenero_h%2Cviewof+sex_compare%2Cgenero_plot%2Ctext3', width="100%", height="3050"))
Esta tabla muestra los datos para casos clasificados en una variable y año, con enlaces a documentos csv:
def draw_table(df):
def link(dfi, name, link):
return dfi[[link, name]].apply(lambda row: '<a href="{}">{}</a>'.format(row[0], row[1]), axis=1)
dfi = df.copy()
url_base = 'https://mauforonda.github.io/vigilancia-epidemiologica/datos/'
dfi['filename'] = dfi.filename.apply(lambda f: '{}{}'.format(url_base, f))
dfi['variable'] = link(dfi, name='variable', link='filename')
dfi = dfi[['year', 'grupo', 'variable']]
dfi.columns = ['Año', 'Grupo', 'Variable']
show(dfi,
order = [],
hover = True,
compact=True,
scrollY="900px",
lengthMenu=[50,100],
scrollCollapse=True,
search={"caseInsensitive": True},
paging=True,
language={
'lengthMenu': 'Mostrar _MENU_ filas',
'search': '🔎︎',
'processing': 'creando tabla ...',
'info': '',
'infoEmpty': '',
'infoFiltered':'_TOTAL_ documentos',
'paginate': {
'first': "Primero",
'previous': "Anterior",
'next': "Siguiente",
'last': "Último"
},
},
maxBytes=0,
columnDefs=[
{"width": "5px", "targets": [0]},
{"width": "20px", "targets": [1]},
{"width": "100px", "targets": [2]}
]
)
draw_table(indice)
Año | Grupo | Variable |
---|
Loading... (need help?) |
Y esta tabla muestra los datos para casos en una variable entre múltiples periodos, donde era posible agruparlos:
def draw_group_table(df):
def link(dfi, name, link):
return dfi[[link, name]].apply(lambda row: '<a href="{}">{}</a>'.format(row[0], row[1]), axis=1)
dfi = df.copy()
url_base = 'https://mauforonda.github.io/vigilancia-epidemiologica/agrupados/'
dfi['filename'] = dfi.filename.apply(lambda f: '<a href="{}{}">csv</a>'.format(url_base, f))
dfi['Nombre'] = link(dfi, name='nombre_comun', link='filename')
dfi = dfi[['nombre_comun', 'filename']]
dfi.columns = ['Nombre', '']
show(dfi,
order = [],
hover = True,
compact=True,
scrollY="900px",
lengthMenu=[50,100],
scrollCollapse=True,
search={"caseInsensitive": True},
paging=True,
language={
'lengthMenu': 'Mostrar _MENU_ filas',
'search': '🔎︎',
'processing': 'creando tabla ...',
'info': '',
'infoEmpty': '',
'infoFiltered':'_TOTAL_ documentos',
'paginate': {
'first': "Primero",
'previous': "Anterior",
'next': "Siguiente",
'last': "Último"
},
},
maxBytes=0,
columnDefs=[
{"width": "100px", "targets": [0]},
{"width": "5px", "targets": [1]}
]
)
draw_group_table(indice_agrupado)
Nombre |
---|
Loading... (need help?) |
A continuación detallo el proceso de construcción de estos datos, que consiste en 3 pasos:
Qué información está disponible?
El ministerio construye una página diferente para mostrar los datos de cada año. Es necesario consultar cada página y navegar los menús que ofrece para listar las variables y grupos de variables que exhibe. Algo interesante de estos tableros es que para entregar información, además de los parámetros que describen la consulta e identidad del usuario, requieren valores que describen el estado de navegación del usuario en el tablero durante la consulta anterior. Es decir que para mostrar una tabla en particular es necesario no sólo ejecutar la consulta correcta, sino la secuencia previa de consultas correctas que el servicio espera. Por suerte, estas limitaciones son fáciles de resolver.
# Información que el servidor espera de un usuario normal
cookies = {
'ASP.NET_SessionId': 'es15y505g3h0e14ooob5ebns', # Place your own 🍪 🍪 🍪
}
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Origin': 'https://estadisticas.minsalud.gob.bo',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
}
# Some helper dicts
years = [
{'year': 2001, 'pagina': 'Form_Vigi_2001.aspx'},
{'year': 2002, 'pagina': 'Form_Vigi_2001.aspx'},
{'year': 2003, 'pagina': 'Form_Vigi_2001.aspx'},
{'year': 2004, 'pagina': 'Form_Vigi_2001.aspx'},
{'year': 2005, 'pagina': 'Form_Vigi_2007.aspx'},
{'year': 2006, 'pagina': 'Form_Vigi_2007.aspx'},
{'year': 2007, 'pagina': 'Form_Vigi_2007.aspx'},
{'year': 2008, 'pagina': 'Form_Vigi_2008.aspx'},
{'year': 2009, 'pagina': 'Form_Vigi_2009.aspx'},
{'year': 2010, 'pagina': 'Form_Vigi_2010.aspx'},
{'year': 2011, 'pagina': 'Form_Vigi_2011.aspx'},
{'year': 2012, 'pagina': 'Form_Vigi_2012.aspx'},
{'year': 2013, 'pagina': 'Form_Vigi_2013.aspx'},
{'year': 2014, 'pagina': 'Form_Vigi_2014_2.aspx'},
{'year': 2015, 'pagina': 'Form_Vigi_2015_302a.aspx'},
{'year': 2016, 'pagina': 'Form_Vigi_2016_302a.aspx'},
{'year': 2017, 'pagina': 'Form_Vigi_2017_302a.aspx'},
{'year': 2018, 'pagina': 'Form_Vigi_2018_302a.aspx'},
{'year': 2019, 'pagina': 'Form_Vigi_2019_302a.aspx'},
{'year': 2020, 'pagina': 'Form_Vigi_2020_302a.aspx'},
{'year': 2021, 'pagina': 'Form_Vigi_2021_302a.aspx'}
]
state = {}
paginas = {y['year']:y['pagina'] for y in years}
# Functions to build a list of available datasets
def get_subvars(url, year, grvar):
"""
Makes a list of every variable under a variable group and year.
"""
data = {
'ctl00$MainContent$WebPanel2_hidden': '',
'__EVENTTARGET': 'ctl00$MainContent$WebPanel2$List_grvar',
'__EVENTARGUMENT': '',
'__LASTFOCUS': '',
'ctl00$MainContent$WebPanel3_hidden': '%3CWebPanel%20Expanded%3D%22false%22%3E%3C/WebPanel%3E',
'ctl00$MainContent$WebPanel2$List_gestion': str(year),
'ctl00$MainContent$WebPanel2$List_fomulario': '302',
'ctl00$MainContent$WebPanel2$Grupo': 'nomDepto',
'ctl00$MainContent$WebPanel2$seleccion': '0',
'ctl00$MainContent$WebPanel2$List_grvar': grvar
}
data = {**data, **state}
response = requests.post(url, cookies=cookies, headers=headers, data=data)
html = BeautifulSoup(response.text, 'html.parser')
for node in ['__VIEWSTATE', '__VIEWSTATEGENERATOR', '__EVENTVALIDATION']:
state[node] = html.select('#{}'.format(node))[0]['value']
subvars = [{'subvar': option['value'], 'variable': option.get_text().strip()} for option in html.select('#MainContent_WebPanel2_Lista_subvar option')]
return subvars
def get_variables(year, pagina):
"""
Makes a list of all variables and variable groups for a year.
"""
data = {
'ctl00$MainContent$WebPanel2_hidden': '',
'__EVENTTARGET': 'ctl00$MainContent$WebPanel2$List_gestion',
'__EVENTARGUMENT': '',
'__LASTFOCUS': '',
'ctl00$MainContent$WebPanel3_hidden': '%3CWebPanel%20Expanded%3D%22false%22%3E%3C/WebPanel%3E',
'ctl00$MainContent$WebPanel2$List_gestion': str(year),
'ctl00$MainContent$WebPanel2$List_fomulario': '302',
'ctl00$MainContent$WebPanel2$List_grvar': '01',
'ctl00$MainContent$WebPanel2$Grupo': 'nomDepto',
'ctl00$MainContent$WebPanel2$seleccion': '0',
}
url = 'https://estadisticas.minsalud.gob.bo/Reportes_Vigilancia/{}'.format(pagina)
variables[year] = []
response = requests.get(url, cookies=cookies, headers=headers)
html = BeautifulSoup(response.text, 'html.parser')
for node in ['__VIEWSTATE', '__VIEWSTATEGENERATOR', '__EVENTVALIDATION']:
state[node] = html.select('#{}'.format(node))[0]['value']
response = requests.post(url, cookies=cookies, headers=headers, data={**data, **state})
html = BeautifulSoup(response.text, 'html.parser')
for node in ['__VIEWSTATE', '__VIEWSTATEGENERATOR', '__EVENTVALIDATION']:
state[node] = html.select('#{}'.format(node))[0]['value']
for option in html.select('#MainContent_WebPanel2_List_grvar option'):
grupo = option.get_text().strip()
grvar = option['value']
subvars = get_subvars(url, year, grvar)
for v in subvars:
variables[year].append({'grvar': grvar,
'grupo': grupo,
'subvar': v['subvar'],
'variable': v['variable']})
def format_variables(variables):
"""
Format variables and variable groups so that they're meaningful and easier to harmonize in the future.
"""
dfv = []
for y in variables.keys():
dfi = pd.DataFrame(variables[y])
dfi.insert(0, 'year', y)
dfv.append(dfi)
dfv = pd.concat(dfv)
dfv.variable = dfv.variable.str.replace('^[0-9\.\-]+ ', '', regex=True)
dfv.variable = dfv.variable.str.lower()
dfv.variable = dfv.variable.apply(lambda x: unicodedata.normalize('NFKD', x).encode('ascii', 'ignore').decode('ascii'))
dfv.grupo = dfv.grupo.str.lower()
dfv.grupo = dfv.grupo.apply(lambda x: unicodedata.normalize('NFKD', x).encode('ascii', 'ignore').decode('ascii'))
return dfv.reset_index(drop=True)
# Make a list of all variables and variable groups for every year and format them right
variables = {}
for year in years:
print(year['year'])
get_variables(year['year'], year['pagina'])
dfv = format_variables(variables)
# save
dfv.to_csv('resources/variables.csv', index=False)
Encuentro más de 1500 variables que podría consultar entre 2001 y 2021. El inventario se ve así:
dfv
year | grvar | grupo | subvar | variable | |
---|---|---|---|---|---|
0 | 2001 | 00 | clasificacion general sistemica | 01 | enfermedades del sistema nervioso |
1 | 2001 | 00 | clasificacion general sistemica | 02 | enfermedades del ojo y anexos |
2 | 2001 | 00 | clasificacion general sistemica | 03 | enfermedades del oido y apofisis mastoides |
3 | 2001 | 00 | clasificacion general sistemica | 04 | enfermedades del sistema cardio circulatorio |
4 | 2001 | 00 | clasificacion general sistemica | 05 | enfermedades del sistema respiratorio |
... | ... | ... | ... | ... | ... |
1565 | 2021 | 16 | eventos - notificacion inmediata parte ii | 03 | otros de excepcion |
1566 | 2021 | 17 | enfermedades transmitidas por vectores (etv) | 01 | leishmaniasis |
1567 | 2021 | 17 | enfermedades transmitidas por vectores (etv) | 02 | chagas agudo |
1568 | 2021 | 17 | enfermedades transmitidas por vectores (etv) | 04 | malaria |
1569 | 2021 | 18 | mortalidad materna | 01 | sospecha de muerte materna |
1570 rows × 5 columns
En esta tabla, los valores grvar
y subvar
son códigos que describen un grupo de variables y variables en el tablero de un año year
. Estos códigos serán útiles para solicitar datos al servidor en la próxima sección. Las condiciones, códigos y forma de los datos varían demasiado entre distintos años. Por eso, para tener un proceso que funcione a través de todas estas condiciones y años es importante sostener la menor cantidad de supuestos sobre cómo realizar consultas correctas o cómo se ve una tabla específica.
Descargar todos los datos disponibles a nivel mensual y municipal.
Con el inventario en mano, el próximo paso es la descarga. Realizo consultas para cada variable, año y mes, que almaceno en documentos csv, uno para cada variable y año. Como no puedo predecir la forma que tendrá la tabla que produce el sistema, y éste tiende a nombrar columnas de maneras idiosincráticas, por ejemplo desagregando valores entre distintos rangos de edad, usando ocasionalmente sexo, etc., simplemente tomo todo lo que ofrece. Las únicas columnas sobre las que tengo control son las que creo, que describen las variables, años y municipios. Las variables y años están bien armonizadas gracias al índice. Y respecto al municipio, para tener datos que puedo cruzar fácilmente con otros sets de datos en la próxima sección construyo un diccionario que me permite armonizar códigos para municipios a través de todos los años.
def get_state(url, year):
"""
Inicializa parámetros de estado para persuadir al sistema a que nos entregue datos
"""
data = {
'ctl00$MainContent$WebPanel2_hidden': '',
'__EVENTTARGET': 'ctl00$MainContent$WebPanel2$List_gestion',
'__EVENTARGUMENT': '',
'__LASTFOCUS': '',
'ctl00$MainContent$WebPanel3_hidden': '%3CWebPanel%20Expanded%3D%22false%22%3E%3C/WebPanel%3E',
'ctl00$MainContent$WebPanel2$List_gestion': str(year),
'ctl00$MainContent$WebPanel2$List_fomulario': '302',
'ctl00$MainContent$WebPanel2$List_grvar': '01',
'ctl00$MainContent$WebPanel2$Grupo': 'nomDepto',
'ctl00$MainContent$WebPanel2$seleccion': '0',
}
response = requests.get(url, cookies=cookies, headers=headers)
html = BeautifulSoup(response.text, 'html.parser')
for node in ['__VIEWSTATE', '__VIEWSTATEGENERATOR', '__EVENTVALIDATION']:
state[node] = html.select('#{}'.format(node))[0]['value']
response = requests.post(url, cookies=cookies, headers=headers, data={**data, **state})
html = BeautifulSoup(response.text, 'html.parser')
for node in ['__VIEWSTATE', '__VIEWSTATEGENERATOR', '__EVENTVALIDATION']:
state[node] = html.select('#{}'.format(node))[0]['value']
def update_state(url, year, grvar):
"""
Actualiza parámetros de estado para persuadir al sistema a que nos entregue datos
"""
data = {
'ctl00$MainContent$WebPanel2_hidden': '',
'__EVENTTARGET': 'ctl00$MainContent$WebPanel2$List_grvar',
'__EVENTARGUMENT': '',
'__LASTFOCUS': '',
'ctl00$MainContent$WebPanel3_hidden': '%3CWebPanel%20Expanded%3D%22false%22%3E%3C/WebPanel%3E',
'ctl00$MainContent$WebPanel2$List_gestion': str(year),
'ctl00$MainContent$WebPanel2$List_fomulario': '302',
'ctl00$MainContent$WebPanel2$Grupo': 'nomMunicip',
'ctl00$MainContent$WebPanel2$seleccion': '0',
'ctl00$MainContent$WebPanel2$List_mes': '1',
'ctl00$MainContent$WebPanel2$List_grvar': grvar
}
data = {**data, **state}
response = requests.post(url, cookies=cookies, headers={**headers, **{'Referer': url}}, data=data)
html = BeautifulSoup(response.text, 'html.parser')
for node in ['__VIEWSTATE', '__VIEWSTATEGENERATOR', '__EVENTVALIDATION']:
state[node] = html.select('#{}'.format(node))[0]['value']
def get_month(url, year, mes, grvar, subvar):
"""
Descarga datos para un mes en una variable y año.
"""
data = {
'ctl00$MainContent$WebPanel2_hidden': '',
'__EVENTTARGET': '',
'__EVENTARGUMENT': '',
'__LASTFOCUS': '',
'ctl00$MainContent$WebPanel3_hidden': '%3CWebPanel%20Expanded%3D%22false%22%3E%3C/WebPanel%3E',
'ctl00$MainContent$WebPanel2$List_gestion': str(year),
'ctl00$MainContent$WebPanel2$List_fomulario': '302',
'ctl00$MainContent$WebPanel2$Grupo': 'nomMunicip',
'ctl00$MainContent$WebPanel2$seleccion': '0',
'ctl00$MainContent$WebPanel2$Button1': ' Procesar',
'ctl00$MainContent$WebPanel2$List_mes': str(mes),
'MainContentxWebPanel3xmydatagrid': '',
'MainContentxWebPanel3xmydatagrid2': '',
'ctl00$MainContent$WebPanel2$List_grvar': grvar,
'ctl00$MainContent$WebPanel2$Lista_subvar': subvar
}
data = {**data, **state}
response = requests.post(url, cookies=cookies, headers={**headers, **{'Referer': url}}, data=data)
html = BeautifulSoup(response.text, 'html.parser')
table = html.select('#G_MainContentxWebPanel3xmydatagrid')[0]
df = pd.read_html(str(table))[0]
df = df.set_index(df.columns[0])
df = df[~df.index.str.lower().str.contains('total')]
return df
def get_dataset(year, grvar, subvar, grupo, variable):
"""
Descarga datos para cada mes en una variable y año.
"""
year_data = []
url = 'https://estadisticas.minsalud.gob.bo/Reportes_Vigilancia/{}'.format(paginas[year])
get_state(url, year)
update_state(url, year, grvar)
for mes in range(1,13):
mes_data = get_month(url, year, mes, grvar, subvar)
mes_data.insert(0, 'mes', dt.date(year, mes, 1))
year_data.append(mes_data)
year_data = pd.concat(year_data)
year_data.insert(0, 'variable', variable)
year_data.insert(0, 'grupo', grupo)
filename = 'dirty/{}_{}_{}.csv'.format(year, slugify(grupo), slugify(variable))
indice.append(dict(year=year, filename=filename, grvar=grvar, grupo=grupo, subvar=subvar, variable=variable))
year_data.to_csv(filename)
indice = []
# Descargar datos para cada fila en el inventario y simultáneamente construir un índice
for i, row in dfv.iterrows():
clear_output(wait=True)
print('{}/{} : {} en {}'.format(i+1, len(dfv), row['variable'], row['year']))
get_dataset(row['year'], row['grvar'], row['subvar'], row['grupo'], row['variable'])
indice = pd.DataFrame(indice)
indice.to_csv('snis/resources/indice.csv', index=False)
Además de seguir las reglas mínimas que el servicio demanda para acceder a datos, no hago nada fancy en estas consultas. Son consultas secuenciales, en parte porque no quiero arriesgar degradar la disponibilidad del sitio y también porque es el proceso más sencillo de programar y evaluar. En total debe haber tomado alrededor de 9 horas de consultas y, si bien el proceso es reproducible, espero que no sea algo que deba hacerse de manera rutinaria.
Que cada tabla tenga una forma predecible y municipios armonizados.
Finalmente, pongo los datos en una estructura simple y extensible. Las primeras columnas de todos los sets de datos describen el código INE del municipio, el nombre del municipio que ofrece el tablero, el grupo de variable, el nombre de la variable y el mes de los valores. Para tener los códigos INE, construyo un diccionario que mapea cada nombre de municipio con su código, utilizando las tablas que publica el ministerio de salud sobre la estructura de establecimientos entre 2005 y 2021. Por suerte, los nombres en estas tablas y la base de datos de vigilancia epidemiológica son las mismas, debido a que el reporte se suele realizar mediante un sistema que consume listas similares publicadas periódicamente. El resto de las columnas consisten en todos los datos que ofrece el sistema, a veces desagregados por rangos de edad, sexo, si la atención fue prestada en el establecimiento o fuera, etc. No hago más que la limpieza más simple sobre estas columnas, suficiente como para que sean utilizadas rápidamente en el futuro y sin muchos juicios sobre cómo deberían verse. Con esta información es fácil agregar valores de condiciones a través de varios años y cruzarlos con información a nivel municipal, como por ejemplo indicadores de los Objetivos de Desarrollo Sostenible.
def format_vigilancia(indice_seleccion, municipio_dict):
errores = []
len_seleccion = len(indice_seleccion)
for i, f in enumerate(indice_seleccion.filename):
clear_output(wait=True)
print('{} / {}'.format(i+1, len_seleccion))
try:
dfi = pd.read_csv('dirty/{}'f)
if dfi.mes.isna().sum() > 0:
header = dfi[dfi.mes.isna()][dfi.columns[4:]].to_dict()
header = ['_'.join([slugify(k)] + [slugify(header[k][kk]) for kk in header[k].keys() if type(header[k][kk]) == str ]) for k in header.keys()]
dfi = dfi[dfi.mes.notna()]
dfi.columns = ['municipio', 'grupo', 'variable', 'mes'] + header
else:
dfi = dfi.rename(columns={'Municipio':'municipio'})
dfi.insert(0, 'codigo_municipio', dfi.municipio.map(municipio_dict))
dfi.to_csv('datos/{}'.format(f.split('/')[1]), index=False)
except Exception as e:
errores.append({'filename': f, 'error': e})
return errores
def listar_municipios1(f):
municipios_snis = []
establecimientos = pd.ExcelFile(f)
for s in establecimientos.sheet_names:
e = pd.read_excel(establecimientos, sheet_name=s, header=2)
e = e[['COD_MUNICIPIO', 'MUNICIPIO']]
e = e.drop_duplicates()
municipios_snis.append(e)
municipios_snis = pd.concat(municipios_snis)
municipios_snis = municipios_snis[municipios_snis.COD_MUNICIPIO.notna()]
municipios_snis['COD_MUNICIPIO'] = municipios_snis.COD_MUNICIPIO.astype(int)
municipios_snis['MUNICIPIO'] = municipios_snis.MUNICIPIO.str.strip()
municipios_snis = municipios_snis.drop_duplicates()
return municipios_snis
def listar_municipios2(f):
e = pd.read_excel(f, sheet_name='BASE DE DATOS', header=3)
e = e[['COD_MUN', 'MUN']]
e.columns = ['COD_MUNICIPIO', 'MUNICIPIO']
e = e[e.MUNICIPIO.notna()]
e['COD_MUNICIPIO'] = e.COD_MUNICIPIO.astype(int)
e['MUNICIPIO'] = e.MUNICIPIO.str.strip()
e = e.drop_duplicates()
return e
# Construir un diccionario para armonizar municipios
municipios_snis = pd.concat([
listar_municipios1('snis/resources/Establecimientos 2005_2017.xlsx'),
listar_municipios2('snis/resources/ESTRUCTURA DE EE.SS. GESTION 2021_DASHBOARDcerrado oficial.xlsx')
]).drop_duplicates()
municipios_snis['MUNICIPIO'] = municipios_snis.MUNICIPIO.apply(lambda x: ' '.join(x.split()))
municipios_dict = municipios_snis.set_index('MUNICIPIO').COD_MUNICIPIO.to_dict()
errores = format_vigilancia(indice, municipios_dict)
Para terminar, así es como se ve uno de los más 1500 sets de datos escogido aleatoriamente:
pd.read_csv('datos/{}'.format(indice.sample()['filename'].iloc[0]))
codigo_municipio | municipio | grupo | variable | mes | < de 1 año-H | < de 1 año-M | 1 a 4 años-H | 1 a 4 años-M | 5 a 14 años-H | 5 a 14 años-M | 15 a 59 años-H | 15 a 59 años-M | 60 y más-H | 60 y más-M | TOTAL-H | TOTAL-M | TOTAL | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 51302 | ACACIO | clasificacion general sistemica | otras causas | 2002-01-01 | 1 | 0 | 0 | 0 | 0 | 0 | 5 | 0 | 2 | 0 | 8 | 0 | 8 |
1 | 20201 | ACHACACHI | clasificacion general sistemica | otras causas | 2002-01-01 | 1 | 0 | 3 | 0 | 5 | 0 | 27 | 0 | 8 | 0 | 44 | 0 | 44 |
2 | 20104 | ACHOCALLA | clasificacion general sistemica | otras causas | 2002-01-01 | 1 | 0 | 3 | 0 | 6 | 0 | 10 | 0 | 6 | 0 | 26 | 0 | 26 |
3 | 30201 | AIQUILE | clasificacion general sistemica | otras causas | 2002-01-01 | 18 | 0 | 13 | 0 | 10 | 0 | 66 | 0 | 8 | 0 | 115 | 0 | 115 |
4 | 31303 | ALALAY | clasificacion general sistemica | otras causas | 2002-01-01 | 0 | 0 | 1 | 0 | 0 | 0 | 5 | 0 | 1 | 0 | 7 | 0 | 7 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1935 | 31201 | TOTORA(CBBA) | clasificacion general sistemica | otras causas | 2002-12-01 | 0 | 0 | 0 | 0 | 1 | 0 | 18 | 0 | 1 | 0 | 20 | 0 | 20 |
1936 | 41301 | TOTORA(ORR) | clasificacion general sistemica | otras causas | 2002-12-01 | 0 | 0 | 1 | 0 | 5 | 0 | 1 | 0 | 0 | 0 | 7 | 0 | 7 |
1937 | 31302 | VILA VILA | clasificacion general sistemica | otras causas | 2002-12-01 | 1 | 0 | 1 | 0 | 0 | 0 | 2 | 0 | 1 | 0 | 5 | 0 | 5 |
1938 | 10801 | VILLA SERRANO | clasificacion general sistemica | otras causas | 2002-12-01 | 4 | 0 | 23 | 0 | 11 | 0 | 41 | 0 | 4 | 0 | 83 | 0 | 83 |
1939 | 11001 | VILLA VACA GUZMAN (MUYUPAMPA) | clasificacion general sistemica | otras causas | 2002-12-01 | 5 | 0 | 36 | 0 | 19 | 0 | 115 | 0 | 12 | 0 | 187 | 0 | 187 |
1940 rows × 18 columns
Cada set de datos representa el número de casos clasificados en una variable para un año, desagregado por meses y municipios. Si bien una misma variable es monitoreada en múltiples periodos, sus nombres y las formas de la tabla que produce el sistema suelen cambiar. No quiero hacer juicios de valor sobre qué nombres representan la misma variable o cómo debería armonizar tablas muy diferentes. Sin embargo, sería muy útil tener información que represente periodos más largos que 1 sólo año. Para ésto, agrupo datos para una misma variable cuya tabla tenga la misma forma a través de múltiples años. Este agrupamiento reduce el número de sets de datos de 1570 series anuales a 469 documentos.
indice = pd.read_csv('resources/indice.csv')
unicos = indice[['grvar', 'subvar', 'grupo', 'variable']].drop_duplicates(subset=['grvar', 'subvar', 'variable']).reset_index(drop=True)
unicos = unicos[unicos.variable.notna()]
indice_agrupado = []
for i, condicion in unicos.iterrows():
schemas = []
for e, row in indice[(indice.grvar == condicion.grvar) & (indice.subvar == condicion.subvar) & (indice.variable == condicion.variable)].iterrows():
schemas.append({**row, **{'columns': ', '.join(pd.read_csv('datos/' + row.filename).columns.tolist())}})
schemas = pd.DataFrame(schemas)
print('{}. {} {} {} : {} schemas'.format(i, condicion.grvar, condicion.subvar, condicion.variable, len(schemas['columns'].drop_duplicates())))
for group in [g for c, g in schemas.groupby('columns')]:
group_df = pd.concat([pd.read_csv('datos/' + fn) for fn in group.filename])
group_dict = {**group[['grvar', 'subvar', 'variable']].iloc[0].to_dict(), **{'desde': group.iloc[0].year, 'hasta': group.iloc[-1].year}}
group_fn = '{}_{}_{}_{}_{}.csv'.format(*[slugify(str(group_dict[k])) for k in group_dict.keys()])
group_dict['filename'] = group_fn
group_df.to_csv('agrupados/' + group_fn, index=False)
indice_agrupado.append(group_dict)
indice_agrupado = pd.DataFrame(indice_agrupado)
indice_agrupado['nombre_comun'] = indice_agrupado.apply(lambda r: '{} entre {} y {}'.format(r['variable'][0].upper() + r['variable'][1:], r['desde'], r['hasta']) if r['desde'] != r['hasta'] else '{} en {}'.format(r['variable'][0].upper() + r['variable'][1:], r['hasta']), axis=1)
indice_agrupado.to_csv('resources/indice_agrupado.csv', index=False)
Un problema con el proceso hasta este momento es cómo las columnas en cada set de datos tienen nombres difíciles de interpretar, resultado de aplanar las tablas que produce el sistema del snis en una forma uniforme. Para tener columnas más fáciles de interpretar infiero los atributos que representan y construyo un mejor nombre. Cada columna puede describir casos que ocurren dentro o fuera de establecimientos de salud, hacer referencia a hombres o mujeres, representar distintos rangos de edad y referirse a poblaciones o eventos particulares. Identifico estos atributos y construyo mejores nombres de columna que aplico a todos los documentos producidos.
patterns = {
'dentro':{
'dentro de establecimientos': '(dentro)|(\-d[\-\_])|(Dentro)|(DENTRO)',
'fuera de establecimientos': '(fuera)|(\-f[\-\_])|(Fuera)|(FUERA)'
},
'sexo': {
'hombres': '(-H$)|(_masculino$)|(-m$)|(_masculino-1$)|(H1$)',
'mujeres': '(-M$)|(_femenino$)|(-f$)|(_femenino-1$)|(M1$)|(mujer)'
},
'edad': {
'menores a 1 año': '(< de 1)',
'de 1 a 4 años': '(1 a 4)|(1-a-4-anos)|(1-4-anos)',
'de 5 a 14 años': '(5 a 14)',
'de 15 a 59 años': '(15 a 59)',
'de 60 y más años': '(60 y más)|(60-y-mas)|(60-anos-y-mas)',
'de 5 a 9 años': '(5 a 9)|(5-a-9-anos)|(5-9-anos)',
'de 10 a 20 años': '(10 a 20)|(10-a-20-anos)',
'de 21 a 59 años': '(21 a 59)|(21-a-59-anos)',
'de 1 año': '(de-1-ano)',
'menores a 2 años': '(< de 2)',
'de 2 a 4 años': '(2 a 4)',
'de 1 a 2 años': '(1-a-menor-de-2)',
'de 2 a 5 años': '(2-a-menor-de-5)',
'menores a 6 meses': '(menor-de-6-meses)',
'de 6 meses a 1 año': '(6-meses-a-menor-de-1)|(6-m-a-menor-de-1-ano)',
'de 10 a 14 años': '(10-14-anos)|(10-a-14-anos)',
'de 15 a 19 años': '(15-19-anos)',
'de 20 a 39 años': '(20-39-anos)',
'de 40 a 49 años': '(40-49-anos)',
'de 50 a 59 años': '(50-59-anos)',
'de 6 meses': '(de-6-meses_)'
},
'especial': {
'total': '(TOTAL)|(total)',
'cantidad': '(Cantidad)',
'embarazadas según IMC': '(imc_mujer-embarazada)',
'población general según IMC': '(imc-1_)',
'número de eventos': '(^Nro.$)|(Nro. Eventos$)',
'número de afectados': '(^Nro. Afectados)|(Nro. Personas afectadas$)',
'número de fallecidos': '(^Nro. Fallecidos)|(Nro. Personas fallecidas$)',
}
}
def infer_category(text, patterns, lower=False):
if lower:
text = text.lower()
category = None
for k in patterns.keys():
if len(re.findall(patterns[k], text)) > 0:
category = k
return category
def field_format(text):
if text == None:
return ''
else:
return text
## Contruyo una lista con todos los nombres de columnas en todos los sets de datos
indice = pd.read_csv('resources/indice.csv')
unicos = indice[['grvar', 'subvar', 'grupo', 'variable']].drop_duplicates(subset=['grvar', 'subvar', 'variable']).reset_index(drop=True)
unicos = unicos[unicos.variable.notna()]
schemas = []
for i, condicion in unicos.iterrows():
for e, row in indice[(indice.grvar == condicion.grvar) & (indice.subvar == condicion.subvar) & (indice.variable == condicion.variable)].iterrows():
schemas.append({**row, **{'columns': pd.read_csv('datos/' + row.filename).columns.tolist()}})
schemas = pd.DataFrame(schemas)
columnas = pd.Series(list(itertools.chain.from_iterable(schemas['columns'].drop_duplicates().tolist()))).drop_duplicates().tolist()
## Infiero atributos en estos nombres: si se refiere a hombres o mujeres, si describe casos que ocurren dentro o fuera de establecimientos de salud, qué rangos de edad representan y si indican alguna población en particular
column_classes = pd.DataFrame({c: {k: infer_category(c, patterns[k]) for k in patterns.keys()} for c in columnas[5:]}).T
## Compongo una descripción con estos atributos
column_descriptions = column_classes.apply(lambda row: ' '.join(' '.join([field_format(field) for field in row]).split()), axis=1).to_dict()
## Guardo una tabla con los atributos y un diccionario con descripciones
column_classes.to_csv('resources/column_atributes.csv')
with open('resources/column_descriptions.json', 'w+') as f:
json.dump(column_descriptions, f)
## Remplazo los nombres de columnas en cada documento producido
for fn in indice.filename.tolist():
filename = 'datos/' + fn
dfi = pd.read_csv(filename)
dfi.columns = [column_descriptions[c] if c in column_descriptions.keys() else c for c in dfi.columns]
dfi.to_csv(filename, index=False)
for fn in indice_agrupado.filename.tolist():
filename = 'agrupados/' + fn
dfi = pd.read_csv(filename)
dfi.columns = [column_descriptions[c] if c in column_descriptions.keys() else c for c in dfi.columns]
dfi.to_csv(filename, index=False)
En la mayoría de sets de datos existen cortes de género que podrían usarse para realizar comparaciones. Utilizo la clasificación de columnas del anterior paso para encontrar en cada documento los pares de columnas donde existen datos para cada género en una misma población.
## Qué pares de columnas en cada documento representan a la misma población en sexos distintos?
indice_agrupado = pd.read_csv('resources/indice_agrupado.csv')
column_classes = pd.read_csv('resources/column_atributes.csv', index_col=[0])
column_classes = column_classes.drop_duplicates()
column_descriptions = column_classes.fillna('').apply(lambda row: ' '.join(' '.join([field_format(field) for field in row]).split()), axis=1).to_dict()
column_classes.insert(0, 'desc', column_classes.index.map(column_descriptions))
column_classes = column_classes.set_index('desc')
compare_sexo = {}
for i, row in indice_agrupado.iterrows():
cols = pd.read_csv('agrupados/' + row.filename).columns.tolist()[5:]
cols = [c for c in cols if c in column_classes.index]
colc = column_classes.loc[cols]
compare_sexo[row.nombre_comun] = [[group[group.sexo == s].desc.values[0] for s in ['hombres', 'mujeres']] for i, group in colc.reset_index().fillna('').groupby(['dentro', 'edad', 'especial']) if 'hombres' in group.sexo.tolist() and 'mujeres' in group.sexo.tolist()]
with open('resources/column_compare_sexo.json', 'w+') as f:
json.dump(compare_sexo, f)