Prespuesto con Diagramas de Sankey

presupuesto
codigo
python
gráficos
plotly
Un poco de presupuesto con dibujitos.
Author

Matías

Published

March 29, 2024

¿Cómo se llaman esos gráficos?

sankey

Hace un tiempo necesitaba hacer unos gráficos con presupuesto.

No tenía mucha ídea que quería graficar, pero si sabía que tenia que usar datos de presupuesto de la Administración Pública Nacional.

No tenía mucha idea que quería demostrar, pero si sabía que tenía que mostrar ciertos flujos de créditos.

Sin mucha más precisión que esa, me puse a pensar, seguramente mal, y de atrás para adelante, en algún tipo de herramienta que me permita ayudar a mostrar lo que todavía no sabía que era.

Me acordaba que en uno de mis subreddits preferidos, Data is Beautiful, siempre mostraban un tipo de gráfico muy particular. Fui al subreddit, scrollee a lo loco, y obviamente, Murphy no falla, no encontré nada. Pero por suerte, muchos kilómetros de página para abajo encontré uno. Era sólo el gráfico, con una muy breve descripción de lo que mostraba. Pero no decía que tipo de gráfico usaba.

Pregunté en ese post, sin respuesta. Pero por suerte siempre está la búsqueda inversa de google, donde le subí la imagen del gráfico y me tiró muchisimas similares, y en alguna decís “Sankey Diagram”. EN TU CARA (introducir nombre que corresponda)

No desesperen, hoy se le tiran la imagen a cualquier motor de AI y te devuelve hasta el código…

Ya con el nombre en la cabeza, una búsqueda en google para ver que librerias de python eran capaces de reproducirlo. Encontré algunas:

(debe existir alguna más que se me pasó por alto en la búsqueda)

Me quedé con Plotly, aunque siempre prefiero Matplolib porque es mucho mas personalizable. En general, cuando el gráfico no es para integrar a un dashboar, Matplotlib cumple. Pero en este caso, el resultado era demasiado simple, y no mostraba lo que necesitaba.

Holoviews no me convenció, Floweaver lo vi tarde…y me incliné por Plotly. Siguiendo este artículo pude ir armando la configuración.

Para lograr el resultado esperado, es necesario tener en cuenta que el Diagrama de Sankey está compuesto por dos tipos de elementos:

  • Nodos: Bloques estáticos con un nombre.
  • Links: conección entre nodos que lleva cierto valor de un nodo a otro.

En plotly, lo traducimos como:

  • ´source´: Es el nodo de inicio. No es necesario tener multiples ´sources´ para tener mas profundidad.
  • ´target´: Es el nodo al que ´source´ se conecta.
  • ´value´: Es el volumen del flujo que conecta ´source´ y ´target´. Va a marcar el grosos de las líneas conectoras del diagrama.

Como hay nodos que cumplen las dos funciones (los nodos intermedios, que reciben y envían información), el mismo nodo aparece en las columnas source y target.

En el ejemplo que vamos a trabajar, el flujo arranca desde la Finalidad-Función (source) hacia las Jurisdicciones (target). Luego, desde las Jurisdicciones (source) hacia los Servicios (target). Y por último, de los Servicios (source) a los Programas (target). Todas estas etapas tienen un valor asignado de crédito. El primero, el que sale de la Finalidad-Función hacía la Jurisdicción, incluye el total del crédito para esa Finalidad-Función en esa jurisdicción, que luego se desgrana entre servicios y programas. En el código, esos valores (crédito) lo obtenemos con los diferentes groupby para cada etapa.

A continuación dejo el código con algunos comentarios. La base que subí ya tiene un pre proceso de ordenamiento y limpieza. Trabajé sobre las que la Secretaría de Hacienda disponibiliza en Presupuesto Abierto. Está actualizada a la ejecución del 31/12/2023.

La base resultante está disponible en Github

Código

import pandas as pd
import plotly.graph_objects as go
df = pd.read_parquet('datos_sankey.parquet')
# Filtramos la partida (la finalidad-función ya está filtrada en la base que cargamos)
df = df.loc[(df['inciso_id']==5) & (df['principal_id']==8) & (df['parcial_id']==1)]
df_grafico = df.groupby(['fin-fun','jurisdiccion_desc','servicio_desc','programa_desc']).agg(**{
                                'credito_vigente_sum': ('credito_vigente', 'sum')
                                }).reset_index()

# 1-[fin-fun]=>[jurisdiccion_desc]
df1 = df_grafico.groupby(['fin-fun', 'jurisdiccion_desc'])['credito_vigente_sum'].sum().reset_index()
df1.columns = ['source', 'target', 'value']

# 2-[jurisdiccion_desc]=>[servicio_desc]
df2 = df_grafico.groupby(['jurisdiccion_desc', 'servicio_desc'])['credito_vigente_sum'].sum().reset_index()
df2.columns = ['source', 'target', 'value']


# 3-[servicio_desc]=>[programa_desc]
df3 = df_grafico.groupby(['servicio_desc', 'programa_desc'])['credito_vigente_sum'].sum().reset_index()
df3.columns = ['source', 'target', 'value']

# Juntamos toda la información en un DF: 
all_links = pd.concat([
    df1, 
    df2,
    df3
], axis=0)
all_links_desc = all_links.copy()

# Para usar el parámetro 'label'
# https://sparkbyexamples.com/pandas/pandas-find-unique-values-from-columns
unique_source_target = list(pd.unique(all_links[['source', 'target']].values.ravel('K')))

# Asignamos un número único a cada source y target
mapping_dict = {k: v for v, k in enumerate(unique_source_target)}

# Mapeamos todos los datos
all_links['source'] = all_links['source'].map(mapping_dict)
all_links['target'] = all_links['target'].map(mapping_dict)

# Convertimos el dataframe a una lista para poder utilizarlo en plotly
links_dict = all_links.to_dict(orient='list')

# Código del Diagrama de Sankey Diagram 
fig = go.Figure(data=[go.Sankey(
    node = dict(
        pad = 100, # Espacio vertical entre los nodos terminales
        thickness = 10, # Ancho del rectángulo de los nodos
        line = dict(color = "black", width = 1), # Línea que rodea el rectángulo de los nodos
        label = unique_source_target,
        color =['#6db9aa', '#d9ffe5', '#92fff8', '#a6e6b4', '#56d1c7','#73cc83','#8dd99c', '#37b9ae', '#19a295']
    ),
    link = dict(
        source = links_dict["source"],
        target = links_dict["target"],
        value = links_dict["value"],
        color = ['#d9ffe5', '#92fff8', '#a6e6b4', '#56d1c7','#73cc83','#8dd99c', '#37b9ae', '#19a295']
    )
)])

fig.update_layout(
    title='<span style="font-size: 30px;">Ejecución de Partidas</span>' + '<br>' +  '<span style="font-size: 12px;">5.8.1 Serv. Sociales - Vivienda y Urbanismo - Diferentes jurisdicciones<br></span>')

fig.show()