Mi nombre es Martín Gaitán y soy ingeniero en computación. Quienes me conocen saben que soy kirchnerista y fui fiscal de Carolina Scotto, pero antes de eso soy ciudadano y como tal, mi deber cívico es defender la democracia. En este caso, exigiendo un escrutinio transparente.
El domingo pasado, todas las boca de urna daban a Liliana Olivero, candidata del Frente de Izquierda y los Trabajadores (FIT), como la 9na diputada por Córdoba. Ese pronóstico se iba confirmando a lo largo del escrutinio hasta alrededor de las 23hs, cuando la tendencia empezó a cambiar en favor del tercer candidato de la UCR, Diego Mestre.
Al ser el marge tan estrecho (calculé 0.06%) el partido de Olivero exige el recuento de los votos y la justicia electoral se niega.
Aún sin el recuento voto a voto, se han encontrado mesas con votos al FIT mal computados. Algunos ejemplos
El sistema de publicación de telegramas electorales (www.resultados.gob.ar) es un gran progreso hacia la transparencia, pero está muy incompleto. Por ejemplo, no sólo carece de buscador, sino que no hay una validación entre la cantidad de votos computados (sumatoria de votos a los partidos, más blancos, nulos y recurridos) con los votos emitidos/cantidad de sobres que figura en el telgrama.
Más aun, los datos deberian estar no sólo visualizables sino en un formato abierto para poder procesarlos con otras herramientas.
Cómo soy informático, realicé un simple programa para extraer los datos de los resultados electorales de Córdoba (aunque bien serviría para otros distritos), sistematizarlos en una base de datos y hacer una búsqueda de mesas "sospechosas", por ejemplo, aquellas en las que el FIT obtuvo 0 voto (patrón de los 2 primeros ejemplos)
Si bien este proceso es automatizable, determinar cuándo ese dato es realmente erroneo es mucho más complejo (en parte por la falta de datos que menciono) y, dada la urgencia, es mejor hacerlo de forma manual, colaborativamente. Por ello necesito ayuda: para todas las mesas listadas, comparar la cantidad de votos emitios con el total computado.
from pyquery import PyQuery
def parse_telegrama(url):
pq = PyQuery(url)
resultado_mesa = {}
for tr in pq('.tablon tbody tr, .TVOTOS tbody tr'):
resultado_mesa[pq('th', tr).text()] = int(pq('td', tr).text().replace('.', ''))
return resultado_mesa
escrutinio = parse_telegrama('http://www.resultados.gob.ar/telegramas/04/001/0013J/040010013J2669.htm')
escrutinio
{'COALICION CIVICA - AFIRMACION PARA UNA REPUBLICA IGUALITARIA (ARI)': 8, 'ENCUENTRO VECINAL CORDOBA': 4, 'FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES': 0, 'FRENTE PARA LA VICTORIA': 35, 'FRENTE PROGRESISTA CIVICO Y SOCIAL': 20, 'UNION CIVICA RADICAL': 36, 'UNION POR CORDOBA': 56, 'UNION PRO': 40, 'VECINALISMO INDEPENDIENTE': 33, 'Votos en blanco': 4, 'Votos nulos': 8, 'Votos recurridos': 0}
sum(escrutinio.values())
244
Si nos fijamos el total de votos dice 277 (dato que no es digitalizado!, por qué?), es decir, no se han computado 33 votos del FIT.
En este programa voy a hacer un intento de buscar mesas sospechosas, para que las revisemos entre todos.
Autor: Martín Gaitán gaitan@gmail.com twitter: http://twitter/tin_nqn_
licencia: CC-by
un crawler que devuelve un arbol de diccionarios anidados.
def crawl(base_url):
"""
Devuelve un arbol con los resultados de cada mesa
Puede tardar mucho.
>>> crawl("http://www.resultados.gob.ar/telegramas/IMUN04.htm")
"""
arbol = {}
pq = PyQuery(base_url)
pq = pq.make_links_absolute(base_url='http://www.resultados.gob.ar/telegramas/')
for anchor in pq('a'):
url = pq(anchor).attr('href')
if '04/' in url:
# mesa
data = parse_telegrama(url)
else:
# abrir nueva rama
data = crawl(url)
arbol[pq(anchor).text()] = data
return arbol
Tener los datos en forma de árbol en memoria no es muy útil. Mejor es modelar una base de datos para poder hacer consultas o agregaciones. Por ejemplo, "obtener todas las mesas donde el FIT haya sacado 0 voto".
%install_ext https://gist.github.com/mgaitan/7207448/raw/ce87e68226ef4ad12cb1aabfa42ebacaf66b3bce/django_orm_magic.py
Installed django_orm_magic.py. To use it, type: %load_ext django_orm_magic
%load_ext django_orm_magic
%%django_orm
from django.db import models
from django.db.models import Sum
class Municipio(models.Model):
nombre = models.CharField(max_length=100)
def __unicode__(self):
return self.nombre
class Circuito(models.Model):
numero = models.CharField(max_length=100)
municipio = models.ForeignKey('Municipio', null=True)
def __unicode__(self):
return u"Circuito %s (%s)" % (self.numero, self.municipio)
class Mesa(models.Model):
circuito = models.ForeignKey('Circuito', null=True)
numero = models.CharField(max_length=100, unique=True)
url = models.URLField()
@property
def computados(self):
return self.votomesa_set.aggregate(Sum('votos'))['votos__sum']
def __unicode__(self):
return u"Mesa %s (%s)" % (self.numero, self.circuito)
class Opcion(models.Model):
# partido, blanco, etc.
nombre = models.CharField(max_length=100, unique=True)
def __unicode__(self):
return self.nombre
class VotoMesa(models.Model):
mesa = models.ForeignKey('Mesa')
opcion = models.ForeignKey('Opcion')
votos = models.IntegerField()
def __unicode__(self):
return u"%s: %d" % (self.opcion, self.votos)
class Meta:
unique_together = ('mesa', 'opcion')
/tmp/tmphcN0Ar/orm_magic
Eso deja nuestros modelos listos para usar.
Ahora refactorizo el motor que "chupa" los datos y los mete en la base de datos.
from django.db import IntegrityError
def crawl2(base_url, nivel='prov', parent=None):
pq = PyQuery(base_url)
pq = pq.make_links_absolute(base_url='http://www.resultados.gob.ar/telegramas/')
anchors = pq('a')
if nivel == 'circuito' and parent and parent.mesa_set.count() == len(anchors):
return
for anchor in anchors:
url = pq(anchor).attr('href')
text = pq(anchor).text()
if nivel == 'circuito':
# mesa
try:
mesa = Mesa.objects.create(numero=text, circuito=parent, url=url)
except IntegrityError:
continue
for opcion, votos in parse_telegrama(url).iteritems():
opcion, _ = Opcion.objects.get_or_create(nombre=opcion)
try:
VotoMesa.objects.create(mesa=mesa, opcion=opcion, votos=votos)
except: #not unique
continue
elif nivel == 'prov':
municipio, _ = Municipio.objects.get_or_create(nombre=text)
crawl2(url, nivel="muni", parent=municipio)
elif nivel == 'muni':
circuito, _ = Circuito.objects.get_or_create(numero=text, municipio=parent)
crawl2(url, nivel="circuito", parent=circuito)
Esa función realiza el proceso de "scrapping" y llena la base de datos.
Por ejemplo, para extraer todos los datos de Córdoba, ejecuto:
crawl2("http://www.resultados.gob.ar/telegramas/IMUN04.htm")
Ese proceso es muy lento (varias horas) porque tiene que acceder al telegrama de cada una de las mesas.
Como yo ya lo hice, comparto la base de datos resultante. El hash MD5 del archivo es d75d744b6ce1f40f3fa411f16938db2c
.
Entonces descargamos la base con datos (y sobreescribimos la vacia).
!wget -r http://lab.nqnwebs.com/descargas/db.sqlite
--2013-10-30 13:06:42-- http://lab.nqnwebs.com/descargas/db.sqlite Resolviendo lab.nqnwebs.com (lab.nqnwebs.com)... 69.163.186.135 Conectando con lab.nqnwebs.com (lab.nqnwebs.com)[69.163.186.135]:80... conectado. Petición HTTP enviada, esperando respuesta... 200 OK Longitud: 5779456 (5,5M) [text/plain] Grabando a: “lab.nqnwebs.com/descargas/db.sqlite” 100%[======================================>] 5.779.456 657K/s en 11s 2013-10-30 13:06:54 (495 KB/s) - “lab.nqnwebs.com/descargas/db.sqlite” guardado [5779456/5779456] COMPLETADO --2013-10-30 13:06:54-- Tiempo total: 12s Descargados: 1 files, 5,5M in 11s (495 KB/s)
Ya tenemos todos los datos. Por ejemplo,
# todas las opciones de voto
for opcion in Opcion.objects.all():
print opcion
FRENTE PROGRESISTA CIVICO Y SOCIAL VECINALISMO INDEPENDIENTE UNION CIVICA RADICAL Votos recurridos Votos nulos FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES UNION POR CORDOBA UNION PRO ENCUENTRO VECINAL CORDOBA Votos en blanco COALICION CIVICA - AFIRMACION PARA UNA REPUBLICA IGUALITARIA (ARI) FRENTE PARA LA VICTORIA
#total votos blancos en Capital
from django.db.models import Sum
cap = Municipio.objects.get(nombre='001 - Capital')
VotoMesa.objects.filter(mesa__circuito__municipio=cap, opcion__nombre='Votos en blanco').aggregate(Sum('votos'))
{'votos__sum': 7401}
Fit = Opcion.objects.get(nombre='FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES')
from IPython.display import display_html, HTML
for mesa in Mesa.objects.filter(votomesa__votos=0, votomesa__opcion=Fit):
if mesa.computados != 0:
display_html(HTML("<a href='%s'>%s</a> - Total computados: %d" % (mesa.url, mesa, mesa.computados)))
from django.db.models import Avg
for circuito in Circuito.objects.all():
promedio_fit_circuito = VotoMesa.objects.filter(mesa__circuito=circuito, opcion=Fit).aggregate(Avg('votos'))['votos__avg']
if not promedio_fit_circuito:
continue
for mesa in Mesa.objects.filter(circuito=circuito, votomesa__votos__gt=0,
votomesa__votos__lte=promedio_fit_circuito/2,
votomesa__opcion=Fit):
display_html(HTML("<a href='%s'>%s</a> - Total computados: %d, Promedio FIT/circ = %f" % (mesa.url, mesa,
mesa.computados, promedio_fit_circuito)))
Por favor, dejá comentarios si encontrás algun error en las mesas en