Creación de una base de datos de créditos a través de 179 municipios y 600 actividades económicas entre 2012 y 2021.
# Funciones de ayuda para mostrar información de forma elegante
from IPython.display import clear_output, display, Markdown
repo_name = 'cartera_de_creditos_en_bolivia'
def md(text):
display(Markdown(text))
def caption(text):
display(Markdown('<p class="caption"> <em>{}</em> </p>'.format(text)))
def datalink(filename):
base = 'https://github.com/mauforonda/{}/blob/master/data/'.format(repo_name)
return "<a class='datalink' href={b}{f}>{f}</a>".format(b=base, f=filename)
Cada mes, las Entidades de Intermediación Financiera reportan información del estado de sus carteras de crédito a la Autoridad de Supervisión del Sistema Financiero (ASFI) (Autoridad de Supervisión del Sistema Financiero, 2021). El envío se realiza electrónicamente mediante el Sistema de Captura de Información Periódica (SCIP) (Autoridad de Supervisión del Sistema Financiero, 2021a) y es consolidado en la Central de Información Crediticia (Autoridad de Supervisión del Sistema Financiero, 2021c). Luego de unas semanas, la Dirección de Análisis Productivo del Ministerio de Desarrollo Productivo y Economía Plural utiliza esta información para actualizar un tablero público en el Sistema Integrado de Información Productiva (Dirección de Análisis Productivo (DAPRO), Ministerio de Desarrollo Productivo y Economía Plural, 2021). Específicamente, la información publicada consiste en
A continuación detallo las funciones y el proceso que utilizo para construir estos datos. El código está escrito en el lenguaje de programación python y corrió originalmente en un notebook Jupyter versión 6.4.0 sobre la versión 3.6.9 de python, en Ubuntu 18.04 con un kernel Linux 5.4. Sin embargo, ninguna operación en el código es específica a esta configuración y debería ser suficiente para reproducir los resultados en cualquier sistema GNU-Linux estándar con python 3.
Dependencias:
import pandas as pd
import requests
from bs4 import BeautifulSoup
import csv
from IPython.display import clear_output
pd.options.mode.chained_assignment = None
Funciones para descargar la lista de municipios:
def download_opciones():
"""
Descarga listas de departamentos, actividades y complejos productivos
del tablero de créditos en el Sistema Integrado de Información Productiva
"""
response = requests.get("https://siip.produccion.gob.bo/repSIIP2/formAsfi.php")
html = BeautifulSoup(response.text, "html.parser")
departamentos = [
{"codigo": option["value"], "nombre": option.get_text()}
for option in html.select("select#departamento option")
if option["value"] != "null"
]
actividades = [
option["value"]
for option in html.select("select#actividad option")
if option["value"] != "null"
]
complejos = [
{"codigo": option["value"], "nombre": option.get_text()}
for option in html.select("select#complejo option")
if option["value"] != "0"
]
return departamentos, actividades, complejos
def download_municipios(departamentos):
"""
Descarga la tabla de municipios para una lista de departamentos,
donde cada departamento es un diccionario con dos llaves:
- codigo: el código del departamento, descargado mediante la función download_opciones
- nombre: el nombre del departamento
Retorna un dataframe
"""
municipios_departamento = []
for dep in departamentos:
data = {"flag": "asfiMunicipiosXDepartamento", "departamento": dep["codigo"]}
response = requests.post(
"https://siip.produccion.gob.bo/repSIIP2/JsonAjaxAsfi.php", data=data
)
depdf = pd.DataFrame(response.json())
depdf.insert(0, "cod_dep", dep["codigo"])
depdf.insert(0, "desc_dep", dep["nombre"])
municipios_departamento.append(depdf)
return pd.concat(municipios_departamento)
def format_municipios(municipios):
"""
Ordena el dataframe de municipios:
- se asegura que no existen entradas que repiten códigos
- construye el código INE en base al código del sistema
- desambigua municipios con el mismo nombre
Retorna un dataframe
"""
def ine(cod):
cod = str(cod).strip()
if len(cod) == 3:
return cod[0] + "0" + cod[1] + "0" + cod[2]
elif len(cod) == 4:
return cod[0:2] + cod[2] + "0" + cod[3]
mun = municipios.copy()
mun = mun.drop_duplicates(subset=["cod_mun"], keep="last")
mun["desc_dep"] = mun.desc_dep.str.title()
mun["cod_ine"] = mun.cod_mun.apply(lambda x: ine(x)).astype(int)
reemplazos = {80102: "San Javier (Beni)", 80201: "Riberalta"}
for k in reemplazos.keys():
mun.loc[mun.cod_ine == k, "desc_mun"] = reemplazos[k]
return mun
Funciones para descargar los datos:
def print_clear(text):
clear_output(wait=True)
print(text)
def log(i0, i1, i2, i3, i4, i5, n0, n1, n2, n3, n4, n5):
"""
Muestra un indicador del estado de las descargas
"""
print_clear(
"municipio:{}/{}\nactividad:{}/{}\ncategoria:{}/{}\ndivision:{}/{}\ngrupo:{}/{}\nclase:{}/{}\n".format(
i0 + 1, n0, i1 + 1, n1, i2 + 1, n2, i3 + 1, n3, i4 + 1, n4, i5 + 1, n5
)
)
def get_item(
i0,
tabla,
nivel,
departamento,
municipio,
actividad="null",
categoria="null",
division="null",
grupo="null",
clase="null",
subclase="null",
tipo="resumen",
):
"""
Realiza una consulta de datos al tablero de créditos donde:
- i0: codigo del municipio para depuración
- tabla: cartera o prestatarios
- nivel: el nivel de la consulta
- departamento: código del departamento
- municipio: código del municipio
- actividad: código de la actividad
- categoría: código de la categoría
- división: código de la división
- grupo: código del grupo
- clase: código de la clase
- subclase: código de la subclase
- tipo: resumen (sólo los campos necesarios) o completo (todos los campos, para depuración)
Retorna una lista de diccionarios con datos de la consulta.
Si falla agrega información de la consulta y error a la lista `errores`,
que debe ser creada previamente en el scope global
"""
url = "https://siip.produccion.gob.bo/repSIIP2/JsonAjaxAsfi.php"
data = {
"flag": "asfiNiveles",
"complejo": "0",
"departamento": departamento,
"municipio": municipio,
"actividad": actividad,
"categoria": categoria,
"division": division,
"grupo": grupo,
"clase": clase,
"subclase": subclase,
"tabla": tabla,
"nivel": nivel,
}
try:
response = requests.post(url, data=data, timeout=20)
if tipo == "resumen":
return [
{"codigo": i["codigo"] if type(i["codigo"]) == str else "null", "name": i["name"].lower() if type(i["name"]) == str else "null"}
for i in response.json()["datos"]
]
elif tipo == "completo":
return response.json()["datos"]
except Exception as e:
errores.append({"data": data, "error": e, "municipio": i0, "response": response.json()["datos"]})
def get_data(municipio, tabla, i0, munlen):
"""
Descarga datos para un municipio y tabla recorriendo iterativamente
el árbol de niveles de actividades.
- municipio: el código del municipio
- tabla: cartera o prestatarios
- i0: codigo del municipio para depuración
- munlen: el número total de municipios para depuración
Agrega los datos a la lista `datos`, que debe ser creada
previamente en el scope global
"""
actividades = ["Productivo", "Comercio", "Servicios"]
for i1, actividad in enumerate(actividades):
categorias = get_item(
i0,
tabla,
"categoria",
municipio["cod_dep"],
municipio["cod_mun"],
actividad,
)
if categorias is None:
return
for i2, categoria in enumerate(categorias):
divisiones = get_item(
i0,
tabla,
"division",
municipio["cod_dep"],
municipio["cod_mun"],
actividad,
categoria["codigo"],
)
if divisiones is None:
return
for i3, division in enumerate(divisiones):
grupos = get_item(
i0,
tabla,
"grupo",
municipio["cod_dep"],
municipio["cod_mun"],
actividad,
categoria["codigo"],
division["codigo"],
)
if grupos is None:
return
for i4, grupo in enumerate(grupos):
clases = get_item(
i0,
tabla,
"clase",
municipio["cod_dep"],
municipio["cod_mun"],
actividad,
categoria["codigo"],
division["codigo"],
grupo["codigo"],
)
if clases is None:
return
for i5, clase in enumerate(clases):
log(
i0,
i1,
i2,
i3,
i4,
i5,
munlen,
len(actividades),
len(categorias),
len(divisiones),
len(grupos),
len(clases),
)
subclases = get_item(
i0,
tabla,
"subclase",
municipio["cod_dep"],
municipio["cod_mun"],
actividad,
categoria["codigo"],
division["codigo"],
grupo["codigo"],
clase["codigo"],
tipo="completo",
)
if subclases is None:
return
contexto = {
"departamento": municipio["desc_dep"],
"municipio": municipio["desc_mun"],
"cod_ine": municipio["cod_ine"],
"actividad": actividad.lower(),
"categoria": categoria["name"],
"division": division["name"],
"grupo": grupo["name"],
"clase": clase["name"],
}
datos.extend([{**contexto, **i} for i in subclases])
def get_all_data(municipios, tabla, desde=0, lista=None):
"""
Descarga datos para todos los municipios en el dataframe municipios
- municipios: dataframe de municipios
- tabla: cartera o prestatarios
- desde: desde qué índice de municipios realizar consultas,
por defecto desde el inicio
- lista: lista de índices de municipios para los cuales se quiere
realizar consultas
"""
if lista is None:
for i0, municipio in enumerate(municipios.to_dict(orient="records")[desde:]):
get_data(municipio, tabla, i0 + desde, len(municipios))
else:
for i in lista:
municipio = municipios.to_dict(orient="records")[i]
get_data(municipio, tabla, i, len(municipios))
Funciones para ordenar datos descargados en una forma que facilite el análisis:
def format_datos(datos, tabla):
"""
- Remueve entradas duplicadas ocasionadas por repetir descargas al lidiar con errores
- Convierte valores numéricos de cadenas de texto
- Multiplica valores en la cartera x 1 Millón para lidiar con unidades de dólar
- Crea columnas adicionales para valores bien formateados de categorías y
subclases de la CAEDEC en base a información oficial de paquetes de actualización
de sistemas de la ASFI
Retorna un dataframe
"""
def get_caedec():
categorias = pd.read_csv('data/caedec_categorias.csv')
subclases = pd.read_csv('data/caedec_subclases.csv')
subclases = subclases[subclases.CodigoGrupo != 'X']
subclases.loc[:,'Codigo'] = subclases.Codigo.astype(int)
return categorias, subclases
def format_caedec(df, categorias, subclases):
dataframe = df.copy()
dataframe.insert(10, 'subclase', dataframe.codigo.map(subclases.set_index('Codigo').Descripcion.apply(lambda x: x.lower()).to_dict()))
dataframe.insert(5, 'categoria_codigo', dataframe.codigo.map(subclases.set_index('Codigo').CodigoGrupo.to_dict()))
dataframe.insert(5, 'categoria_completo', dataframe.categoria_codigo.map(categorias.set_index('Codigo').Descripcion.apply(lambda x: x.lower()).to_dict()))
return dataframe
df = pd.DataFrame(datos)
df.drop_duplicates(inplace=True)
for year in [str(y) for y in range(2012, 2022)]:
if year in df.columns:
df[year] = pd.to_numeric(df[year])
if tabla == 'cartera':
df[year] = df[year].apply(lambda x: x * 1000000)
for col in ['cod_ine', 'codigo']:
df[col] = pd.to_numeric(df[col])
df['name'] = df.name.str.lower()
categorias, subclases = get_caedec()
df = format_caedec(df, categorias, subclases)
df.columns = df.columns[:14].tolist() + [int(col) for col in df.columns[14:]]
df = df[['departamento', 'municipio', 'cod_ine', 'actividad', 'categoria_completo', 'categoria_codigo', 'division', 'grupo', 'clase', 'codigo', 'subclase', 'nota', 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]]
df.columns = ['departamento', 'municipio', 'cod_ine', 'sector', 'categoria', 'categoria_codigo', 'division', 'grupo', 'clase', 'subclase_codigo', 'subclase', 'nota', 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]
# if tabla == 'cartera':
# df = df[['departamento', 'municipio', 'cod_ine', 'actividad', 'categoria_completo', 'categoria_codigo', 'division', 'grupo', 'clase', 'codigo', 'subclase', 'nota', 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]]
# df.columns = ['departamento', 'municipio', 'cod_ine', 'sector', 'categoria', 'categoria_codigo', 'division', 'grupo', 'clase', 'subclase_codigo', 'subclase', 'nota', 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]
# else:
# df = df[['departamento', 'municipio', 'cod_ine', 'actividad', 'categoria_completo', 'categoria_codigo', 'division', 'grupo', 'clase', 'codigo', 'subclase', 'nota', 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]]
# df.columns = ['departamento', 'municipio', 'cod_ine', 'sector', 'categoria', 'categoria_codigo', 'division', 'grupo', 'clase', 'subclase_codigo', 'subclase', 'nota', 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]
return df
def consolidar_datos(dataframes, names):
"""
Consolida los datos de cartera y prestatarios en un sólo dataframe
donde cada fila representa el valor para una actividad en un municipio
y año específicos.
"""
common_cols = 12
new_cols = {i: name for i, name in enumerate(names)}
new_cols["level_{}".format(common_cols)] = 'year'
for i, dataframe in enumerate(dataframes):
dataframe = dataframe.drop_duplicates(subset=dataframe.columns[:common_cols])
dataframe = dataframe[dataframe.subclase_codigo.notna()]
dataframe['subclase_codigo'] = dataframe.subclase_codigo.astype(int)
dataframes[i] = dataframe
creditos = (
pd.concat(
[
dataframe.set_index(dataframe.columns[:common_cols].tolist()).stack()
for dataframe in dataframes
],
axis=1,
)
.reset_index()
.rename(
columns=new_cols
)
)
creditos['year'] = creditos['year'].astype(int)
creditos = creditos.drop_duplicates(subset=['year','cod_ine', 'sector', 'subclase_codigo'])
return creditos
md(
"En estas funciones utilizo diccionarios de categorías y subclases del Código de Actividad Económica y Destino del Crédito (CAEDEC). Construí estos diccionarios con datos extraídos de paquetes de actualización del sistema SCIP que produce la ASFI para que Entidades de Intermediación Financiera envíen reportes:\n- {}\n- {}".format(
datalink('caedec_categorias.csv'),
datalink('caedec_subclases.csv')
)
)
En estas funciones utilizo diccionarios de categorías y subclases del Código de Actividad Económica y Destino del Crédito (CAEDEC). Construí estos diccionarios con datos extraídos de paquetes de actualización del sistema SCIP que produce la ASFI para que Entidades de Intermediación Financiera envíen reportes:
El proceso consiste en construir la lista de municipios disponibles, descargar datos de la créditos para cada uno, ordenar y guardar estos datos. La descarga es la parte más lenta y menos robusta del proceso porque depende del comportamiento del sistema del cual se toman los datos. Toma alrededor de 6 horas de descargas no concurrentes (para evitar dañar la disponibilidad del sistema), que deberían correr de forma ininterrumpida. Todos los datos y errores son almacenados en objetos del scope global, lo cual permite depurar y correr el proceso iterativamente sin perder datos en caso de fallas graves. Al terminar cada sesión de descarga, se reintentan las consultas que devuelven errores. Finalmente, se ordena los datos en una forma que facilite posteriores análisis.
Descarga una tabla con información de todos los municipios disponibles
departamentos, actividades, complejos = download_opciones()
municipios = format_municipios(download_municipios(departamentos))
Descarga datos de la cartera para todos los municipios
errores = []
datos = []
get_all_data(municipios, 'cartera')
Reintenta descargas fallidas
get_all_data(municipios, 'cartera', lista=pd.json_normalize(errores).municipio.tolist())
Ordena datos
cartera = format_datos(datos, 'cartera')
Descarga datos del número de prestatarios
errores = []
datos = []
get_all_data(municipios, 'prestatarios')
Reintenta descargas fallidas
get_all_data(municipios, 'prestatarios', lista=[e.municipio for e in errores])
Ordena datos
prestatarios = format_datos(datos, 'prestatarios')
Consolida los datos de cartera y prestatarios en una sóla tabla donde cada fila representa valores para un municipio, actividad y año.
cartera.to_csv('data/cartera.csv', index=False)
prestatarios.to_csv('data/prestatarios.csv', index=False)
creditos = consolidar_datos([cartera, prestatarios], ['cartera', 'prestatarios'])
md('Finalmente guarda los datos en dos documentos:\n- {} contiene los datos de la cartera de créditos\n- {} contiene un diccionario con metadatos de cada actividad en CAEDEC'.format(
datalink('creditos.csv'),
datalink('actividades.csv')
))
Finalmente guarda los datos en dos documentos:
creditos[['departamento', 'municipio', 'cod_ine', 'sector', 'categoria', 'categoria_codigo', 'subclase_codigo', 'subclase', 'year', 'cartera', 'prestatarios']].to_csv('data/creditos.csv', index=False)
creditos.groupby('subclase_codigo')[['categoria', 'categoria_codigo', 'division', 'grupo', 'clase', 'subclase_codigo', 'subclase', 'nota']].first().to_csv('data/actividades.csv', index=False)
creditos = pd.read_csv('data/creditos.csv')
md('El dataset tiene una forma de {} filas y {} columnas. Cada fila representa el valor de cartera en dólares americanos y el número de prestatarios para una actividad en un municipio y año específicos. Una fila aleatoria se ve así:'.format(len(creditos), len(creditos.columns)))
display(creditos.sample())
caption('Ejemplo de una entrada en el set de datos.')
El dataset tiene una forma de 321650 filas y 11 columnas. Cada fila representa el valor de cartera en dólares americanos y el número de prestatarios para una actividad en un municipio y año específicos. Una fila aleatoria se ve así:
departamento | municipio | cod_ine | sector | categoria | categoria_codigo | subclase_codigo | subclase | year | cartera | prestatarios | |
---|---|---|---|---|---|---|---|---|---|---|---|
209087 | Santa Cruz | El Torno | 70105 | servicios | servicios sociales comunales y personales | O | 85199 | otros servicios relacionados con la salud huma... | 2019 | 10541.95 | 2.0 |
Ejemplo de una entrada en el set de datos.
Existen datos anuales del valor de cartera a fin de diciembre para el periodo entre 2012 y 2020, y datos anuales del número de prestatarios a fin de diciembre para el periodo entre 2012 y 2019. Además existen datos del valor de cartera al final de Agosto de 2021. Cada municipio está descrito con su Código INE, nombre y departamento. Cada actividad económica está descrita con su valor en el Código de Actividad Económica y Destino del Crédito (CAEDEC), que clasifica actividades en una nomenclatura jerárquica de categorías que contienen divisiones, grupos, clases y subclases, de más general a más específico. La cantidad de valores únicos para cada parámetro es:
display(
pd.DataFrame(
[
{
"Departamentos": len(dataframe.departamento.unique()),
"Municipios": len(dataframe.cod_ine.unique()),
"Categorías": len(dataframe.categoria_codigo.unique()),
"Subclases": len(dataframe.subclase_codigo.unique()),
"Año inicial": dataframe.year.min(),
"Año final": dataframe.year.max()
}
for dataframe in [
creditos[creditos.cartera.notna()],
creditos[creditos.prestatarios.notna()],
]
],
index=["Valor de Cartera", "Número de Prestatarios"],
).T
)
caption("Número de valores únicos en columnas del set de datos de créditos para valor de cartera y número de prestatarios.")
Valor de Cartera | Número de Prestatarios | |
---|---|---|
Departamentos | 9 | 9 |
Municipios | 180 | 181 |
Categorías | 18 | 19 |
Subclases | 600 | 600 |
Año inicial | 2012 | 2012 |
Año final | 2021 | 2021 |
Número de valores únicos en columnas del set de datos de créditos para valor de cartera y número de prestatarios.
display(
pd.DataFrame(
[
{
"Departamentos": len(dataframe.departamento.unique()),
"Municipios": len(dataframe.cod_ine.unique()),
"Categorías": len(dataframe.categoria_codigo.unique()),
"Subclases": len(dataframe.subclase_codigo.unique()),
}
for dataframe in [
creditos[creditos.cartera.notna()],
creditos[creditos.prestatarios.notna()],
]
],
index=["Valor de Cartera", "Número de Prestatarios"],
).T
)
caption("Número de valores únicos en columnas del set de datos de créditos para valor de cartera y número de prestatarios.")
Valor de Cartera | Número de Prestatarios | |
---|---|---|
Departamentos | 9 | 9 |
Municipios | 180 | 181 |
Categorías | 18 | 19 |
Subclases | 600 | 600 |
Número de valores únicos en columnas del set de datos de créditos para valor de cartera y número de prestatarios.
Además, cada fila incluye una nota que describe la actividad y el sector en el que es clasificada. La designación de sector es una decisión política y ortogonal a la nomenclatura CAEDEC. Existen 3 sectores: productivo, comercio y servicios. Algunas actividades son cambiadas de un sector a otro para mejorar sus condiciones de acceso a crédito. Pero también, existen actividades que son simultáneamente clasificadas en dos sectores diferentes en los mismos periodos y municipios. Mantengo estas irregularidades porque podrían ser significativas.
sectores_por_entrada = pd.DataFrame(
[
{
"cod_ine": i[0],
"year": i[1],
"subclase": i[2],
"sectores": len(dfi),
}
for i, dfi in creditos.groupby(["cod_ine", "year", "subclase"])
]
)
doble_sector_sample = sectores_por_entrada[sectores_por_entrada.sectores > 1].sample().iloc[0].to_dict()
md('Existen {} entradas que incluyen 2 sectores. Un ejemplo:'.format(len(sectores_por_entrada[sectores_por_entrada.sectores > 1])))
display(creditos[(creditos.cod_ine == doble_sector_sample['cod_ine']) & (creditos.year == doble_sector_sample['year']) & (creditos.subclase == doble_sector_sample['subclase'])])
caption('Ejemplo de una entrada en un misma actividad, periodo y municipio que ocurre en dos sectores simultáneamente.')
Existen 7370 entradas que incluyen 2 sectores. Un ejemplo:
departamento | municipio | cod_ine | sector | categoria | categoria_codigo | subclase_codigo | subclase | year | cartera | prestatarios | |
---|---|---|---|---|---|---|---|---|---|---|---|
269889 | Beni | Riberalta | 80201 | productivo | servicios inmobiliarios empresariales y de alq... | L | 71110 | alquiler de equipo de transporte por via terre... | 2021 | 42790.82 | 3.0 |
272279 | Beni | Riberalta | 80201 | servicios | servicios inmobiliarios empresariales y de alq... | L | 71110 | alquiler de equipo de transporte por via terre... | 2021 | 0.00 | 0.0 |
Ejemplo de una entrada en un misma actividad, periodo y municipio que ocurre en dos sectores simultáneamente.
Por eso, al trabajar con los datos es importante decidir inicialmente entre utilizar la clasificación de sector o CAEDEC. Si se utiliza CAEDEC, la clasificación más técnica y fácil de interpretar, se debe considerar la existencia de múltiples filas para una misma actividad, municipio y año, y agregarlas.