Millón de Monos

Weblog de Manuel Aristarán

Participación ciudadana online: y dale con Pernía

La Municipalidad de Bahía Blanca anunció un “sistema online de participación ciudadana”. Opino que es una mala idea, e intentaré explicar por qué.

Cuando opino de política, hablo porque es gratis. Salvo mi interés, y algún que otro libro que he leído, no tengo formación pertinente. Sobre tecnologías aplicadas a la actividad cívica, creo poder hablar con un poco de conocimiento del asunto.

Establezco mis credenciales: por interés personal, hace 10 años construí una herramienta que se llamó Gasto Público Bahiense. Tuvo mucha más repercusión de la que imaginaba, y me llevó por un camino que “terminó” en una maestría en el MIT Media Lab, donde me dediqué a investigar cuestiones relacionadas y construir tecnologías que se inscriben en estas áreas.

Hace 10 años, las tecnologías aplicadas a la participación cívica estaban en auge. Fui partícipe activo y a veces protagonista de esos movimientos durante su máxima popularidad. Participé como disertante y asistente en decenas de conferencias dedicadas al tema en Latinoamérica, Europa y Estados Unidos. Muchas de esas charlas están en YouTube, pueden buscarlas. Fui parte de varios equipos que construyeron “tecnología cívica”. A veces me pagaron, pero casi siempre lo hice ad-honorem y por puro interés profesional.

Me formé como programador de computadoras. Los técnicos solemos pecar de ingenuos cuando, con buena voluntad, ofrecemos desplegar nuestros artilugios en complejas cuestiones socio-políticas. La participación ciudadana es una de esas problemáticas. Durante la inevitable pérdida de inocencia que sufrí en mi devenir en el campo de la tecnología cívica, aprendí que el diseño de una herramienta aplicada a lo sociopolítico es un hecho político. Las computadoras ofrecen una ambigüedad delicada: apagadas, son maquinaria neutral. Encendidas, ejecutan programas que no lo son, pues fueron construídos por seres humanos.

No hablo de fraude o manipulación: casi siempre, esas son cosas de paranoicos (otra clase de inocentes). Aquí, incurrimos en una parcialidad involuntaria. Por ejemplo: ¿quiénes tienen acceso a los dispositivos necesarios para “participar” de manera online? ¿cuáles serán los mecanismos de participación? ¿Quién construyó los sistemas? ¿Serán vinculantes las consultas?

Evgeny Morozov, un autor muy antipático pero que suele ser bastante agudo, habla de “solucionismo”. Es decir, tirarle computadoras a un problema complejo. Esto que parece estar impulsando la Municipalidad —cuando veamos algo concreto intentaré volver a opinar—, es solucionismo de manual. Están tirándole computadoras a un problema que, sospecho, no entienden y no tienen voluntad de entender. Y a juzgar por los productos tecnológicos que construye la Municipalidad, tampoco entienden de computadoras.

La mentada “participación ciudadana” no se activa con un botón, ni con un “Me gusta”. La participación ciudadana se educa, se contruye, se permite. Dudo mucho que la gestión municipal que encabeza Hector Gay (que difícilmente quede en la historia por su vocación democrática y participativa) sea capaz de llevar adelante un proyecto de estas características.

Hoy ya no participo de los movimientos de Datos Abiertos y Gobierno Abierto. Descubrí que no tengo ni me interesa desarrollar el el temple necesario para lidiar con el sector público. Pero habité ese campo durante una parte importante de mi carrera profesional, por ende sigo el tema con interés.

En la cancha se verán los pingos. No obstante, habiendo visto varias de estas iniciativas en Argentina y en todo el mundo, es muy probable que esto que el Municipio anuncia con bombos, platillos y pocos detalles, desaparezca en muy poco tiempo.


Cómo son los royalties que paga Spotify en cada país?

Lo siguiente es un ejercicio de análisis de un dataset de reproducciones emitido por el servicio Distrokid, para un artista argentino con 2.5 millones de reproducciones en los últimos 9 meses, en el servicio de streaming Spotify.

import pandas as pd
import altair as alt
import numpy as np

data = pd.read_csv('DistroKid_1558988581759.tsv', sep='\t', encoding='iso-8859-1')
sales = data[(data.Store == 'Spotify')]

Las columnas que nos interesan son:

  • Store: sólo vamos a considerar Spotify
  • Quantity: la cantidad de streams que se reportan en esta fila
  • Country of Sale: El país en el que fue escuchado el stream
  • Earnings (USD): lo pagado para la cantidad de streams reportada en esta fila. Asumimos que el pago por un stream es Earnings (USD)/Quantity

Ingreso mensual

sales_per_month=sales.groupby(['Sale Month'])[['Quantity', 'Earnings (USD)']].sum().reset_index()

alt.Chart(sales_per_month).mark_bar().encode(
    x='Sale Month:O',
    y='Earnings (USD):Q',
    tooltip='Quantity:Q',
).properties(width=500, title="Ingreso (USD) por mes")\
 .configure(background='#FFFFFF')

png

Los meses de aumento en el ingreso corresponden a lanzamientos.

¿En qué países está la audiencia?

Por supuesto, nos interesa saber a dónde está la mayor cantidad de escuchas de nuestros tracks

by_country = sales.groupby('Country of Sale').sum().reset_index()
top_10_countries = by_country.sort_values('Quantity', ascending=False).head(10).reset_index()
top_10_countries[['Country of Sale', 'Quantity']]
Country of Sale Quantity
0 AR 1917972
1 CL 199124
2 UY 98114
3 US 37505
4 PE 31865
5 MX 30346
6 CO 14393
7 ES 12948
8 PY 10372
9 EC 9856

Vemos que Argentina es el país principal, muy por encima del resto de los países: un orden de magnitud superior al segundo puesto. Graficamos esta distribución en un gráfico con escala logarítmica en el eje vertical.

(Ya que estamos,agregamos las banderas de los países)

OFFSET = ord('🇦') - ord('A')
def flag(code):
    """ Retorna el emoji de la bandera de un país, dado su código ISO-3166 alpha-2"""
    return chr(ord(code[0]) + OFFSET) + chr(ord(code[1]) + OFFSET)

top_quantity = by_country.sort_values('Quantity', ascending=False).reset_index()
top_quantity.loc[:, 'country_name_and_flag'] = top_quantity['Country of Sale'].apply(lambda c: flag(c) + c)
alt.Chart(top_quantity.sort_values('Quantity', ascending=False)).mark_bar().encode(
    x=alt.X('country_name_and_flag:N', sort=list(top_quantity['country_name_and_flag'])),
    y=alt.Y('Quantity:Q',scale=alt.Scale(type='log')),
).properties(
    title='Number of streams per country (log scale)',    
)\
 .configure(background='#FFFFFF')

png

Promedio de pago por stream en cada país

Intentaremos, de manera muy poco rigurosa, estudiar la relación entre el importe promedio que Spotify paga al artista por cada reproducción, y el costo de la suscripción mensual en cada país.

Comenzamos calculando el valor promedio de cada reproducción (stream)

metrics = by_country[['Country of Sale', 'Quantity', 'Earnings (USD)']].sort_values('Quantity', ascending=False)
metrics = metrics.assign(per_stream_avg=metrics['Earnings (USD)']/metrics['Quantity'])
metrics[['Country of Sale', 'Quantity', 'per_stream_avg']].head(10)

Country of Sale Quantity per_stream_avg
2 AR 1917972 0.000965
11 CL 199124 0.001405
64 UY 98114 0.002186
63 US 37505 0.002802
50 PE 31865 0.001511
43 MX 30346 0.001261
12 CO 14393 0.001187
22 ES 12948 0.002075
54 PY 10372 0.001203
19 EC 9856 0.001744

Graficamos el promedio por reproducción para cada país

top_per_stream_avg = metrics.sort_values('per_stream_avg', ascending=False)
top_per_stream_avg.loc[:, 'country_name_and_flag'] = top_per_stream_avg['Country of Sale'].apply(lambda c: flag(c) + c)

alt.Chart(top_per_stream_avg).mark_bar().encode(
    x=alt.X('country_name_and_flag:N', type='nominal', sort=list(top_per_stream_avg['country_name_and_flag'])),
    y='per_stream_avg:Q',
    tooltip=['per_stream_avg:Q']
).properties(
    title='Per-stream payout average by Country\n(from a Spotify royalties report with %.2fM plays)' % (metrics.Quantity.sum() / 1e6)
)\
 .configure(background='#FFFFFF')

png

Suiza (CH) es el país que “mejor” paga por cada reproducción ($0.005 USD). Vemos también que los países en el tope del ranking pertecen al hemisferio norte. Gana peso nuestra hipótesis de que el per-stream average payout está relacionado con el costo de la suscripción al servicio Spotify

Costo mensual de Spotify en cada país

Necesitamos, por supuesto, una base de datos que contenga el costo de la suscripción a Spotify en cada país. Pensé en scrapearlo, pero por suerte un señor danés llamado Matias Singer construyó un Spotify International Pricing Index que contiene la información que necesitamos.

Los datos en la versión publicada están muy desactualizados, pero el buen Matias publicó el código fuente del scraper. Luego de unas modificaciones triviales, ejecuté ese código y obtuve la data que necesitaba.

spotify_monthly = pd.read_json('./spotify-pricing.jsonlines', lines=True)
spotify_monthly = spotify_monthly[spotify_monthly.convertedPrice > 0]
spotify_monthly.loc[:, 'country_upper']  = spotify_monthly.rel.str.upper()
spotify_monthly.head(5)
convertedPrice countryCode currency demonym internationalName link originalCurrency originalPrice originalRel price region rel subRegion title country_upper
0 4.990000 DZA DZD Algerian Algeria /dz-fr/premium/?checkout=false DZD USD 4.99/mois dz-fr 4.99 Africa dz Northern Africa Algérie (français) DZ
1 7.506266 CAN CAD Canadian Canada /ca-fr/premium/?checkout=false CAD 9,99 $ CAD/mois ca-fr 9.99 Americas ca Northern America Canada (français) CA
2 5.990000 GTM GTQ Guatemalan Guatemala /gt/premium/?checkout=false GTQ 5.99 USD al mes gt 5.99 Americas gt Central America Guatemala GT
3 5.990000 ECU USD Ecuadorean Ecuador /ec/premium/?checkout=false USD 5.99 USD al mes ec 5.99 Americas ec South America Ecuador EC
4 7.506266 CAN CAD Canadian Canada /ca-en/premium/?checkout=false CAD $9.99 CAD / month ca-en 9.99 Americas ca Northern America Canada (English) CA

Vamos a visualizar en un scatterplot las dos variables que nos interesan, valor promedio por reproducción y costo mensual del servicio.

El eje horizontal codificará el per-stream average payout, mientras que el vertical el costo mensual de la suscripción. El color de cada punto corresponde a la región del país, mientras que el tamaño codifica la cantidad de reproducciones.

Combinamos (merge) nuestro dataset de pago promedio por reproducción con el dataset obtenido por el scrapper, y construímos el scatterplot.

stream_avg_monthly_scatter = top_per_stream_avg.merge(spotify_monthly, left_on='Country of Sale', right_on='country_upper')
stream_avg_monthly_scatter.loc[:, 'convertedPriceFit'] = pd.Series(np.poly1d(fit)(stream_avg_monthly_scatter.per_stream_avg))

scatter = alt.Chart(stream_avg_monthly_scatter).mark_circle().encode(
    x='per_stream_avg:Q',
    y='convertedPrice:Q',
    tooltip=['internationalName:N', 'convertedPrice:Q', 'per_stream_avg:Q', 'Quantity:Q'],
    color='region:N',
    size=alt.Size('Quantity:Q', scale=alt.Scale(range=(50,1000))),
).properties(
title="Per-stream AVG payout VS Spotify monthly price by country (R²=%.2f)" % fit_rsq
)\
 .configure(background='#FFFFFF')
scatter

png

Vemos un atisbo de correlación lineal entre las dos variables. También vemos agrupaciones “horizontales”, que corresponden a importes como $5.99 (psychological pricing). Calculamos un fit lineal y también lo graficamos. También vamos a agregar líneas horizontales para enfatizar los países que comparten importes.

fit = np.polyfit(stream_avg_monthly_scatter.per_stream_avg, stream_avg_monthly_scatter.convertedPrice, 1)

fit_rsq = np.corrcoef(stream_avg_monthly_scatter.per_stream_avg, stream_avg_monthly_scatter.convertedPrice)[0,1]**2
scatter = alt.Chart(stream_avg_monthly_scatter).mark_circle().encode(
    x='per_stream_avg:Q',
    y='convertedPrice:Q',
    tooltip=['internationalName:N', 'convertedPrice:Q', 'per_stream_avg:Q', 'Quantity:Q'],
    color='region:N',
    size=alt.Size('Quantity:Q', scale=alt.Scale(range=(50,1000))),
).properties(
title="Per-stream AVG payout VS Spotify monthly price by country (R²=%.2f)" % fit_rsq
)

linear_fit = alt.Chart(stream_avg_monthly_scatter).mark_line().encode(
    x='per_stream_avg:Q',
    y='convertedPriceFit:Q'
)


# add fixed price points
fixed_price_points = pd.DataFrame(
    [
        {'text': '9.99€', 'rule': 11.20},
        {'text': '6.99€', 'rule': 7.83},
        {'text': '$5.99', 'rule': 5.99}
    ]
)

fixed_price_rules = alt.Chart(fixed_price_points).mark_rule(strokeDash=[1,1]).encode(
    y='rule:Q',
)

fixed_price_text = alt.Chart(fixed_price_points).mark_text(align='left', dy=-5, dx=5).encode(
    text='text:N',
    y='rule:Q',
    x=alt.X(value=0)
)

(scatter + linear_fit + fixed_price_rules + fixed_price_text).configure(background='#FFFFFF')

png

Qué averiguamos?

El resultado de este ejercicio informal y superficial sugiere que hay una relación entre el costo de la suscripción mensual al servicio Spotify, y lo que pagan a los artistas en concepto de royalties. También, el dato curioso de que Argentina es el país con el costo de suscripción más barato del mundo.

Si la relación que investigamos existe en efecto, podemos decir que pegar un hit en Argentina es un mal negocio en comparación al resto de los países.


Data-driven stylistic analysis of Charlie Parker solos

A quick exploration of the music21 toolkit for computational musicology using Charlie Parker’s solos, as transcribed in this MusicXML version of the venerable Omnibook.

No big findings here, just fooling around with the awesome music21 library. If you are interested in serious computational and statistical analysis of jazz solos, head over to the Jazzomat Project

import glob
import music21
import pandas as pd
import functools
import altair as alt

Distribution of notes (expressed as intervals from the chord root) by chord quality

What intervals from the chord root does Bird usually play, depending on the chord’s quality?

@functools.lru_cache(maxsize=4096)
def isChordTone(chord, tone_name):
    """ True if note """
    return tone_name in [p.name for p in chord.pitches]

notes = []
for fname in glob.glob('Omnibook/MusicXml/*.xml'):
    piece = music21.converter.parse(fname)
    for m in [m for m in piece.parts[0] if type(m) == music21.stream.Measure]:
        currentChord = None
        currentChordOffset = None
        for thing in m.notesAndRests:
            if type(thing) == music21.harmony.ChordSymbol:
                currentChord = thing
                currentChordOffset = thing.offset
            elif type(thing) == music21.note.Note:
                if currentChord is None:
                    continue
                interval = music21.interval.Interval(thing.pitch, currentChord.root())
                notes.append({
                    'score': fname,
                    'measure': m.measureNumber,
                    'offset': thing.offset,
                    'chord_kind': currentChord.chordKind,
                    'figure': currentChord.figure,
                    'note': thing.name,
                    'interval': interval.simpleName,
                    'interval_semitones': interval.chromatic.mod12,
                    'is_chord_tone': isChordTone(currentChord, thing.name)
                })

notes = pd.DataFrame(notes)

data = notes.groupby(['chord_kind', 'interval']).count().reset_index()
alt.Chart(data[data.chord_kind.isin(['dominant', 'major', 'minor'])]).mark_bar().encode(
    x='interval:N',
    y='figure:Q',
    row='chord_kind'
).configure(background='#fafafa') \
.properties(title="Frequency of notes as intervals from the root by chord quality")

png


Matrices y nombres

Esta semana, la Dirección Nacional de Datos e Información Pública publicó “Tu nombre en los últimos 100 años”, un sitio muy divertido que permite consultar frecuencias de uso de nombres propios. Parecido al Popular Baby Names de la Social Security Administration de Estados Unidos. Junto con el sitio, el equipo de datos públicos subió el dataset al portal de datos públicos.

El diario La Nación publicó un enlace al sitio de nombres en su home page…y se vino abajo por el tráfico.

Para poner a andar un ratito la croqueta, me puse a pensar cómo hacer un método eficiente de consulta de esta información. Dado uno o varios nombres, quiero obtener la serie temporal de sus frecuencias. No es nada del otro mundo, y es apenas un prototipo.

Preparando el dataset

cat historico-nombres.csv | uconv  -t ASCII -x nfd -c | tr '[:upper:]' '[:lower:]' | tr -s ' ' | sed -e 's/^ *//' -e 's/ *$//' | csvfix sort -smq -rh -f 1:S,3:N > sorted-ascii-historico-nombres.csv 

Ese pipeline de comandos procesa el archivo original aplicando las siguientes transformaciones:

  • uconv -t ASCII -x nfd -c: Aplicar la forma de normalización Canonical Decomposition de Unicode (NFD). En criollo, sacarle acentos a los caracteres
  • tr '[:upper:]' '[:lower:]': pasar todo a minísculas
  • tr -s ' ': convertir espacios repetidos a uno sólo.
  • sed -e 's/^ *//' -e 's/ *$//': sacar espacios del principio y final de cada línea.
  • csvfix sort -smq -rh -f 1:S,3:N: ordenar la tabla según nombre y luego año.

Nos queda algo así:

nombre cantidad anio
aage tomasen 2 1931
aago peter 1 1987
aakash 2 1985
aalam yamir 2 2013
aale rene 1 1987
aalejandro daniel 1 2002
aaleyah nayara 2 2013

Una estructura eficiente

Lo más simple que se me ocurrió es pivotear ese dataset, para convertirlo en una matriz donde cada fila es un nombre y cada columna es un año. Tenemos 3044402 nombres únicos y un período de 94 años. Es decir, una matriz de 3044402 x 94.

Para poder obtener la fila correspondiente al nombre que nos interesa, también construimos un diccionario NAMES cuyas claves son los nombres y sus valores el índice de la fila de la matriz que contiene la serie temporal de frecuencias.

El siguiente script construye esas estructuras de datos.

# coding: utf-8
import csv, sys, pickle
import numpy as np

YEAR_MIN, YEAR_MAX = 1922, 2015
YEARS_Q = YEAR_MAX - YEAR_MIN
NAMES_Q = 3044402 # count-distinct on the name column
NAMES = {}

FREQS = np.zeros((NAMES_Q, YEARS_Q+1), int)

reader = csv.reader(sys.stdin)
next(reader) # skip header

# Pivotear el dataset de nombres:
# a partir de una tabla de (nombre, frecuencia, año), construir una matriz
# de frecuencias |nobmres| x |años|
cur_name, cur_row, i = None, None, -1
for row in reader:
    if row[0] != cur_name:
        i += 1
        NAMES[row[0]] = i
        
    if i % 2000 == 0:
        print("%d names processed" % i)

    FREQS[i, int(row[2]) - YEAR_MIN] += int(row[1])
    cur_name = row[0]

# save FREQS
np.save('freqs', FREQS)
# save NAMES
with open('names.pickle', 'wb') as f:
    pickle.dump(NAMES, f)

Consultando las frecuencias

Cómo consultamos esto? Fácil. Obtenemos el índice del nombre que nos interesa, y con él, la fila correspondiente en la matriz:

import numpy as np
import pickle

FREQS = np.load('freqs.npy')
with open('names.pickle', 'rb') as f:
    NAMES = pickle.load(f)
FREQS[NAMES['manuel']]
array([  56,    2,  119,  122,    2,    1,    4,  231,    6,    2,  326,
        268,  330,    1,  330,  332,    6,  356,    3,    3,    4,    2,
          3,  494,  464,  510,    8,    4,    1,  365,  374,    3,  317,
          3,    3,  308,    3,  253,    8,    5,    1,    1,    1,  180,
        167,    3,  161,  151,  193,  156,  144,  173,    5,  223,  269,
          4,  242,    2,    3,  238,  268,    8,    1,  436,  456,  415,
        487,  458,  566,  627,  555,  801, 1013, 1135, 1012,  783,  786,
        760,  729,  678,  736,  815,    2,    0,  718,  726,  705,  650,
        581,  600,  814,  789,  849,  711])

Visualizamos el resultado para verificar que al menos se parezca a lo que reporta el sitio oficial. Para esto, también vamos a calcular el pormilaje (?) del nombre de interés para cada año. Con los datos en esta matriz, es fácil: la cantidad de nombres en cada año es la suma de cada columna.

manuel_1000ct = (FREQS[NAMES['manuel']] / np.sum(FREQS, axis=0)) * 1000
from altair import Chart, Bin, X, Axis
import pandas as pd

data = pd.DataFrame({'year': list(range(1922,2016)), 'freq': manuel_1000ct})
chart = Chart(data).mark_line().encode(
    x='year:N',
    y='freq:Q',
)
chart

png

Es parecido, pero no igual 😔. El 0 en 2005 no coincide con la fuente, sospecho algun problema de comparacion de strings.

Mirá, mamá: sin base de datos.

El tamaño de la matriz FREQS es relativamente chico, apenas 2.13 gigabytes en memoria.

FREQS.nbytes / 1024**3
2.132160872220993

El diccionario de nombres (NAMES) tampoco ocupa mucho; 160 megabytes.

sys.getsizeof(NAMES) / 1024**2
160.0000991821289

Este pequeño ejercicio se puede exponer a través de un endpoint HTTP muy simple que mantenga esta matriz numpy en memoria y envíe los datos serializados en la respuesta.

Con eso, estimo, se pueden mejorar bastante la estabilidad y robustez del servicio

[El código está disponible aquí: https://gist.github.com/jazzido/1050fd9169adb7fd9ff1d1002649fd16]


OpenRAFAM: Abriendo los Presupuestos Municipales

Han ocurrido muchas cosas desde los primeros esfuerzos para construir recursos de información basados en datos públicos gubernamentales. Desde Dinero y Política, esfuerzo fundacional de Gonzalo Iglesias y Poder Ciudadano, y Gasto Público Bahiense (GPB) —de quien escribe—, algunos gobiernos en Argentina adoptaron políticas de transparencia acompañadas de implementaciones de herramientas online. Pese a estos 8 años de historia y evolución en el país, los resultados son bastante pobres. Los proyectos surgidos de la sociedad civil tienen problemas de sustentabilidad, el sector privado no se interesa en el tema, y el sector público tiene un enorme déficit en su capacidad de construir servicios públicos digitales.

El uso de los recursos públicos es una de las fuentes de información más importantes que genera el gobierno en cualquiera de sus niveles. En particular, el presupuesto y su ejecución quizás sea el dataset más valioso que produce y mantiene una administración pública. Las prioridades de la poltica pública, en definitiva, pueden verse a través del uso de los recursos que hace un gobierno.

Un sistema que corre en al menos 135 municipios

Unas de las primeras veces que hablé públicamente sobre GPB fue hace casi 7 años en el Hackatón de Datos Públicos y Gobierno Abierto que organizamos con Garage Lab junto al Programa de Gobierno Electrónico de la Universidad de San Andrés. Hay video de esa charla. Durante el evento de dos días, que recuerdo como el mejor hackatón al que haya asistido, alguien me comentó que la información administrativa (presupuestos, personal, tasas, etc) de los 135 municipios bonaerenses se almacenaba en un sistema llamado RAFAM, que corre en todos los partidos de la provincia. Con la ingenuidad que tenemos los ingenieros con poca experiencia en política, dijimos —«Obvio! Hay que escribir software que extraiga los datos de esos 135 servidores Oracle, convencer a los intendentes que lo instalen, y ya está: información fiscal para todos y todas». Para eso, necesitábamos acceder a alguno de esos sistemas para poder hacer la ingeniería reversa correspondiente. No llegamos muy lejos; como suele suceder en muchos esfuerzos voluntaristas el entusiasmo se fue apagando luego del hackatón. Además, los contactos iniciales con algunos municipios nos hicieron pensar que ningún secretario de hacienda iba a darnos acceso irrestricto a su sistema contable para poder reversearlo.

Pero la idea de un componente de software que fuese capaz de extraer datos de 135 bases de datos municipales era demasiado potente como para dejarla ir así no más.

La conexión bahiense

Ya he contado la historia muchas veces: cuando lancé GPB, el gobierno municipal estaba abiertamente en contra de la iniciativa. La nueva administración que asumiría en 2012, en cambio, creó una de las primeras oficinas dedicadas a la innovación y gobierno abierto del país con la que tuve excelente relación desde el comienzo hasta el nuevo cambio de intendente a fines de 2015. Fue con esa dependencia, a través de su secretario Esteban Mirofsky, que organizamos otro hackatón del que surgieron proyectos pioneros como un sistema de información ambiental en tiempo real y un prototipo de plataforma de información basada en los datos de la tarjeta de transporte. Sobre este último, escribí un blog post y, junto a Cristian Jara Figueroa, un trabajo final para un curso de modelos de movilidad humana que tomé durante mi maestría en MIT.

El último proyecto en el que colaboré con la Agencia de Innovación y Gobierno Abierto fue una plataforma para visualizar prespuestos públicos, en el marco de mi tesis de maestría. Finalmente, 5 años después del mítico hackatón de 2010, un municipio bonaerense estuvo dispuesto a darme acceso a su sistema RAFAM para estudiarlo y poder extraer la información que almacena.

Levantando el capot de RAFAM

Pensaba que escribir las consultas SQL para extraer datos de RAFAM sería una tarea relativamente fácil, pero fui víctima una vez más del optimismo del que sufrimos los ingenieros. Cuando me conecté a esa base de datos, me encontré con un sistema con casi 2000 tablas, cientos de vistas y miles de stored procedures. No iba a ser tan fácil.

Muchas tablas

Por suerte, el Ministerio de Economía de la provincia de Buenos Aires publica en su sitio web actualizaciones del sistema RAFAM, que contienen archivos de Crystal Reports. Estos, a su vez, contienen las queries necesarias para generar diversos reportes. Entre ellos, estaban los que nos interesan: ejecución presupuestaria.

Armados con esta información, junto al Dr. Gastón Ávila escribimos una serie de queries que permitieron extraer tablas del estado de ejecución presupuestaria —tanto de gastos como de recursos— desagregadas según todos los criterios en los que se clasifica un presupuesto público (ver mi post anterior sobre el tema)

¿Y el código?

Siempre tuve la intención de liberar el código de Presupuesto Abierto y SpendView, ambos desarrollados dentro de mi tesis de maestría. El optimismo ingenieril otra vez al ataque: el código escrito frenéticamente durante una carrera de posgrado está lejos de ser publicable, pero quiero empezar con ese proceso más temprano que tarde. Hoy comienzo publicando el código del programa que sirve para extraer los datos de ejecución presupuestaria de una instancia de RAFAM. Consiste en dos módulos escritos en lenguaje Python.

  • rafam_db: encapsula las consultas SQL que escribimos basándonos en los reportes que genera el sistema RAFAM.
  • rafam_extract: comando para ejecutar las consultas del modulo rafam_db

Si este código aporta algún valor (espero que sí), está en las consultas SQL. Los comandos escritos en Python son bastante genéricos, y no es necesario usarlos.

El código está en mi perfil de GitHub: https://github.com/jazzido/openrafam

¿Para qué sirve todo esto?

Pese a los anuncios grandilocuentes, los eventos, los subsidios y los hackatones, los servicios públicos basados en datos avanzaron muy poco en los ~8 años de historia del movimiento de datos abiertos en Argentina. Estoy convencido de que la falta de voluntad política es la razón más importante, pero también la falta de capacidades técnicas en el sector público.

Quizás, ayudando desde afuera con lo que sabemos hacer (programar), animemos a los municipios a construir sistemas de información basados en datos tanto para consumo interno como externo.