El padronazo cordobés

Open Data Córdoba es un colectivo abierto y multidisciplinario sin fines de lucro cuyo objetivo principal es difundir y transparentar el uso de la tecnología y los datos masivos para beneficio del conjunto de la sociedad, especialmente en la provincia de Córdoba.

El grupo nació en época de elecciones, intentando echar un manto de luz a las sospechas fundadas de desprolijidades en el escrutinio provisorio de las legislativas de 2013. Luego de muchas otras iniciativas y bastante perseverancia docente de algunos de nuestros compañeros, nuestra contribución ha empezado a ser reconocida desde los medios locales, algunas agrupaciones políticas y otras organizaciones de la sociedad civil que abogan por la transparencia y el fortalecimiento de las instituciones.

En este contexto, el portal de noticias Cba24N, perteneciente al grupo de Servicios de Radio y Televisión (SRT) de la Universidad Nacional de Córdoba, nos ha invitado a realizar análisis de datos "en vivo" el próximo 25 de octubre, día de las elecciones nacionales.

Hay muchas ideas para ese día que ojalá podamos llevar adelante, aunque vale resaltar (atajarse) que se trata de una tarea totalmente voluntaria . Mientras tanto, el domingo pasado me dediqué a jugar con algunas agregaciones sobre el padrón de Córdoba para encontrar "notas de color" analizando nombres y apellidos de los votantes

Leer más…

Curso de Python para ciencias e ingeniería, nueva edición

/images/Newsletter4-Banner_20120705_12-44-50-800.jpg

Attention!

¿Estás interesado en que dicte este curso, de manera intensiva, en tu laboratorio o empresa? Contactame.

Está abierta la inscripción para el nuevo dictado de mi curso de introducción a Python para ciencias e ingenierías, en la Facultad de Ciencias Exáctas, Físicas y Naturales de la Universidad Nacional de Córdoba (aprobado como curso de extensión - Resolución 1272/2015, bajo el nombre "Python como herramienta para la ingeniería") desde el miércoles 14 de ocubre de 18 a 22hs, durante 5 semanas.

Este curso una versión mejorada del que dicté en la Facultad de Matemática, Física y Astronomía en mayo pasado, avalado como curso de extensión (Resolución HCD 107/2015)

El precio está absolutamente subsidiado por las ganas de que más ingenieros e investigadores programen Python. No te demores en anotarte , los cupos son muy limitados!

Se entregarán certificados a quienes completen asistencia.

Leer más…

Curso de Python para ñoños

¡Los cupos irán a sorteo! Anotate. Si no entrás para en esta edición, quedás anotado para la próxima que será muy pronto.

La demanda nos desbordó. En menos de 2 dias tenemos inscriptos para llenar dos veces el laboratorio que tenemos disponible! Algunos ya están averiguando la disponibilidad del Estadio Kempes para hacer el próximo.

Como recién hoy (martes 7 de abril) se realizó la difusión oficial desde FaMAF, la decisión de los organizadores es permitir la inscripción de todos los interesados y hacer un sorteo de las 25 plazas aranceladas y otro para las 15 plazas gratuitas reservadas para estudiantes de grado de FaMAF.

La justificación de un sorteo en vez de tomar el orden de inscripción la dió el Dr. Nicolás Wolovick, que junto al Dr. Pedro Pury fueron los gestores para que el curso se oficializara, con un argumento democráticamente ñoño:

Estar conectado 24/7 por 3G, recibir el tweet, e inscribirse, no es justo, es una condición de posibilidad que no todos tienen. La distribución uniforme es la que mayor entropía tiene :)

Si estás interesado, es importante que te inscribas a través del formulario. Así tendremos una lista bien grande de "argumentos" para reeditar el curso lo más pronto posible.

Otra posibilidad es que averigües hagas lobby en tu empresa o laboratorio sobre la posibilidad de realizar el curso in house y, si tienen interés, lo charlamos.


/images/Newsletter4-Banner_20120705_12-44-50-800.jpg

A partir del 29 de abril voy a dar el curso Introducción a Python para ciencias e ingenierías en la Facultad de Matemática, Astronomía y Física (FaMAF) de la Universidad Nacional de Córdoba.

Este curso es una versión revisada y extendida del que dí en la ScipyCon Argentina 2014 y durará 8 clases de 2hs cada una. Será los miércoles de 18hs a 20hs en el laboratorio de computación de la facultad. No hace falta contar con equipo propio y el único pre-requisito es tener nociones básicas de programación en cualquier lenguaje.

El costo del curso es $400. Hay becas para estudiantes de grado de FaMAF.

Acá está el formulario de pre-inscripción (LOS CUPOS IRAN A SORTEO).

El curso está reconocido como Curso de Extensión de FaMAF (Res. HCD 107/2015) y se entregarán certificados oficiales a quienes completen asistencia y participación.

Atención Los cupos son muy limitados!

Leer más…

Sergio Massa y #LaGente

Anoche me reí mucho con el hashtag #LaGente, que se viralizó mientras Alejandro Fantino entrevistaba, una vez más, al inefable candidato presidencial Sergio Massa.

Me acordé entonces de un post de Zulko, cuyo blog es un compilado de gemas ñoñamente divertidas. Allí muestra cómo recortar automáticamente los pedacitos de un video que mencionen una palabra o frase, basándose en las marcas de tiempo del archivo de subtítulos, utilizando su maravillosa biblioteca Moviepy y un poco de Python. Más o menos lo que hace videogrep, pero más prolijo.

La herramienta youtube-dl (que también es genial y hecha en Python), permite no sólo bajar videos de youtube y los subtitulos existentes, sino que también puede bajar el "subtitulo automático". En general son bastante malos pero es suficientemente efectivo para encontrar pequeñas frases.

Todo sea por "la gente": manos a la obra

Lo primero que necesitamos es una lista de videos donde Sergio Massa hable. Hice una búsqueda, decidí ignorar algunos (parodias, por ejemplo) y generé una lista. Hay varias maneras de obtener este listado de las primeras paginas de resultados, yo utilicé el rústico y efectivo webscrapping:

In [1]:
from pyquery import PyQuery
links = []
skip = ('M0yuFHbhYLY','TLmMh9Qvmic', 'rY4Hwvn6GlA')

for page in range(1, 5):
    pq = PyQuery('https://www.youtube.com/results?search_query=entrevista+sergio+massa&page=%s' % page)
    pq.make_links_absolute()
    links.extend([pq(a).attr('href') for a in pq('a.yt-uix-tile-link') if pq(a).attr('href').split('v=')[1] not in skip])
links
Out[1]:
['https://www.youtube.com/watch?v=8pP8G3fSAcY',
 'https://www.youtube.com/watch?v=g6QSwxUo1aw',
 'https://www.youtube.com/watch?v=_9FN6CI8fD4',
 'https://www.youtube.com/watch?v=5wqwNDpkZOo',
 'https://www.youtube.com/watch?v=V865E4mBiHU',
 'https://www.youtube.com/watch?v=TPrGNJnMS9U',
 'https://www.youtube.com/watch?v=SVTl11hG9Gs',
 'https://www.youtube.com/watch?v=Df_dwb5XHQM',
 'https://www.youtube.com/watch?v=sptBkyfq1VU',
 'https://www.youtube.com/watch?v=tzjz1xrNu3k',
 'https://www.youtube.com/watch?v=k-CGbuOo8do',
 'https://www.youtube.com/watch?v=_L-B_wHsEec',
 'https://www.youtube.com/watch?v=iFOABIQdo9Q',
 'https://www.youtube.com/watch?v=WOlRIKGrBWY',
 'https://www.youtube.com/watch?v=a-mCgN6W9ek',
 'https://www.youtube.com/watch?v=x5vhchv3zAY',
 'https://www.youtube.com/watch?v=bi5eK7i59w0',
 'https://www.youtube.com/watch?v=VNHV3D_6o4E',
 'https://www.youtube.com/watch?v=MWVZ6JDU9V8',
 'https://www.youtube.com/watch?v=v-JmdgVZqVc',
 'https://www.youtube.com/watch?v=FBFHpdxsyYU',
 'https://www.youtube.com/watch?v=WXmTc83l1sQ',
 'https://www.youtube.com/watch?v=GfNgds5vS60',
 'https://www.youtube.com/watch?v=UHRa34A6rDg',
 'https://www.youtube.com/watch?v=xVU-EjnuksU',
 'https://www.youtube.com/watch?v=-IXymTZZM6o',
 'https://www.youtube.com/watch?v=tzvwDTPyTHQ',
 'https://www.youtube.com/watch?v=a19z6EVWpQ4',
 'https://www.youtube.com/watch?v=rAOvF8X_nzM',
 'https://www.youtube.com/watch?v=wtvl4esdMGU',
 'https://www.youtube.com/watch?v=1YPHDDH1Az0',
 'https://www.youtube.com/watch?v=w7TnghsrJUo',
 'https://www.youtube.com/watch?v=qBT-6HpSrwc',
 'https://www.youtube.com/watch?v=JM-xblTxLGc',
 'https://www.youtube.com/watch?v=kMymsVsmETY',
 'https://www.youtube.com/watch?v=K1-dfiVfbOI',
 'https://www.youtube.com/watch?v=VnoiHVlR-So',
 'https://www.youtube.com/watch?v=hMTzJyLiXE4',
 'https://www.youtube.com/watch?v=VGQPNQ1Bhkg',
 'https://www.youtube.com/watch?v=0oR4z7SsY14',
 'https://www.youtube.com/watch?v=Cl4r8h_Hlak',
 'https://www.youtube.com/watch?v=gJFmek-YgYo',
 'https://www.youtube.com/watch?v=9VQ7Ov5W_tM',
 'https://www.youtube.com/watch?v=rKwKImVrYu4',
 'https://www.youtube.com/watch?v=LJwj9SHC9EU',
 'https://www.youtube.com/watch?v=-08OEpFThiw',
 'https://www.youtube.com/watch?v=BPJBl5y2P2g',
 'https://www.youtube.com/watch?v=MvkXlg9ZbL4',
 'https://www.youtube.com/watch?v=7KgIa4fX_Ng',
 'https://www.youtube.com/watch?v=upNLrHtzeBI',
 'https://www.youtube.com/watch?v=Y-norf1BKAs',
 'https://www.youtube.com/watch?v=QMvAl_fxQSA',
 'https://www.youtube.com/watch?v=3os_uXUOvcM',
 'https://www.youtube.com/watch?v=ZE_aChIEELo',
 'https://www.youtube.com/watch?v=iKI-8ceuR-A',
 'https://www.youtube.com/watch?v=CASdYLquQII',
 'https://www.youtube.com/watch?v=5cvyi1CcpYs',
 'https://www.youtube.com/watch?v=NVEw-YIAy5A',
 'https://www.youtube.com/watch?v=yMXn04-GQTY',
 'https://www.youtube.com/watch?v=RCCzZGcGg5k',
 'https://www.youtube.com/watch?v=FqMFKGsXLOE',
 'https://www.youtube.com/watch?v=MVOvQb8KBm0',
 'https://www.youtube.com/watch?v=ENvWfMwnJ_0',
 'https://www.youtube.com/watch?v=bs7xGm293Vs',
 'https://www.youtube.com/watch?v=7OvrK-U-axI',
 'https://www.youtube.com/watch?v=VHeWqPqs4vo',
 'https://www.youtube.com/watch?v=nVOEi9FESn8',
 'https://www.youtube.com/watch?v=eikTWAvFwTE',
 'https://www.youtube.com/watch?v=BU2amn3QdWk',
 'https://www.youtube.com/watch?v=GiB1pOuEvqg',
 'https://www.youtube.com/watch?v=GAPN17lTJ9c',
 'https://www.youtube.com/watch?v=4Ja1uZbMM8E',
 'https://www.youtube.com/watch?v=F1dAfCR4rc0',
 'https://www.youtube.com/watch?v=334O9xh-CQY',
 'https://www.youtube.com/watch?v=KgNmw3sJ0g8',
 'https://www.youtube.com/watch?v=-SQSue4-PLk',
 'https://www.youtube.com/watch?v=HPE4PHlYySo']

Luego, el paso lento: bajar los videos. Al parecer, Youtube no genera un subtitulo automático para videos demasiado largo, así que limité hasta 30 minutos.

In [2]:
for link in links:
    !youtube-dl --write-auto-sub --sub-lang es --max-filesize 30.00m {link}

Con el material crudo disponible (aunque puede ser que no se hayan encontrado subtitulos para todos los videos), podemos copiar descaradamente partes del código de Zulko (levemente adaptado)

In [3]:
import re 
import os
import glob
import random
from moviepy.editor import VideoFileClip, concatenate, TextClip, CompositeVideoClip


def convert_time(timestring):
    """ Converts a string into seconds """
    nums = [float(t) for t in re.findall(r'\d+', timestring)]
    return 3600 * nums[0] + 60*nums[1] + nums[2] + nums[3]/1000


def get_time_texts(file):
    with open(file) as f:
        lines = f.readlines()

    times_texts = []
    current_times , current_text = None, ""
    for line in lines:
        times = re.findall("[0-9]*:[0-9]*:[0-9]*,[0-9]*", line)
        if times != []:
            current_times = [convert_time(t) for t in times]
        elif line == '\n':
            times_texts.append((current_times, current_text))
            current_times, current_text = None, ""
        elif current_times is not None:
            current_text = current_text + line.replace("\n"," ")
    return times_texts

def find_word(word, times_texts, padding=.4):
    """ Finds all 'exact' (t_start, t_end) for a word """
    matches = [re.search(word, text)
               for (t,text) in times_texts]
    return [(t1 + m.start()*(t2-t1)/len(text) - padding,
             t1 + m.end()*(t2-t1)/len(text) + padding)
             for m,((t1,t2),text) in zip(matches, times_texts)
             if (m is not None)]


def get_subclips(video_path, cuts):   
    video = VideoFileClip(video_path)
    return [video.subclip(start, end) for (start,end) in cuts]


def get_all_subclips_for(word, pattern='*.mp4', sub_ext='.es.srt', shuffle=True):
    subclips = []
    for mp4 in glob.glob(pattern):
        sub = os.path.splitext(mp4)[0] + sub_ext
        try:
            times = find_word(word, get_time_texts(sub))
        except IOError:
            # ignore video if it hasn't subtitle
            continue
        cuts = get_subclips(mp4, times)
        subclips.extend(cuts)
    if shuffle:
        random.shuffle(subclips)
    return subclips

La función get_all_subclip recibe la frase a buscar y devuelve un listado de segmentos donde, muy probablemente, se pronuncia.

In [4]:
gente = get_all_subclips_for('la gente')
len(gente)
Out[4]:
77

El problema es que aunque es muy probable que sea Sergio Massa el que diga "la gente" en sus entrevistas, a veces es el entrevistador, a veces youtube entendió mal al desgrabar y a veces el código recortador la pifia. Por este motivo hay que descartar los segmentos que no sirven.

Se me ocurrió hacerlo visualmente: los pegué todos, superponiendo el índice al que corresponde cada segmento, para luego anotar los que no sirven y filtrarlos en otra pasada.

In [5]:
def make_preview(subclips):
    subclips_ = []
    for (i, clip) in enumerate(subclips):
        txt_clip = TextClip(str(i),fontsize=70, color='white')
        txt_clip = txt_clip.set_pos('center').set_duration(clip.duration)
        clip = CompositeVideoClip([clip, txt_clip])
        subclips_.append(clip)

    final = concatenate(subclips_, method='compose')
    final.write_videofile('preview.webm', codec='libvpx', fps=24)    
In [6]:
make_preview(gente)
[MoviePy] >>>> Building video preview.webm
[MoviePy] Writing audio in previewTEMP_MPY_wvf_snd.ogg
[MoviePy] Done.
[MoviePy] Writing video preview.webm
[MoviePy] Done.
[MoviePy] >>>> Video ready: preview.webm 

El resultado me permitió hacer el tamizado

In [7]:
ignore = [2, 3, 8, 12, 17, 19, 25, 28, 32, 36, 38, 40, 41, 44, 49, 55, 56, 61, 62, 66, 73, 74]
subclips_cleaned = [i for j, i in enumerate(gente) if j not in ignore]

Aunque no tengo idea de edición de videos, y porque de verdad creo que es un demamogo impresentable que no debería presidir ni una junta vecinal, quería darle un toque final, con una pequeña frase

In [8]:
import numpy as np
from moviepy.video.tools.segmenting import findObjects

def arrive(screenpos,i,nletters):
    v = np.array([-1,0])
    d = lambda t : max(0, 3-3*t)
    return lambda t: screenpos-400*v*d(t-0.2*i)

screensize = (640,360)
txtClip = TextClip('Yn tragr ab rf obyhqn'.decode('rot13'), color='white', font="Amiri-Bold", kerning=5, fontsize=50)
cvc = CompositeVideoClip( [txtClip.set_pos('center')],
                        size=screensize)

letters = findObjects(cvc) # a list of ImageClips

def moveLetters(letters, funcpos):
    return [ letter.set_pos(funcpos(letter.screenpos,i,len(letters)))
              for i,letter in enumerate(letters)]

ending = CompositeVideoClip(moveLetters(letters, arrive), size=screensize).subclip(0, 10)
In [9]:
# le damos una mezcladita más
random.shuffle(subclips_cleaned)
subclips_cleaned.append(ending)
make_final(subclips_cleaned, 'massa_lagente_final.webm')
[MoviePy] >>>> Building video massa_lagente_final.webm
[MoviePy] Writing audio in massa_lagente_finalTEMP_MPY_wvf_snd.ogg
[MoviePy] Done.
[MoviePy] Writing video massa_lagente_final.webm
[MoviePy] Done.
[MoviePy] >>>> Video ready: massa_lagente_final.webm 

Y este es el resultado:

In [ ]:
 

Las cosas chiquitas que cambian el mundo

Note

Tengo dos weblogs. Este es en el que escribo artículos técnicos o que tienen que ver de alguna manera con mi profesión. Textos y pretextos es igual de personal, pero me sale —me sale cada vez menos, tristemente— de otras tripas, quizás para otro público.

Sin embargo, quiero hacer una excepción con este post, porque probablemente tenga más alcance aquí y es algo que me atraviesa todas las facetas: las ganas de hacer de este que pisamos, un mejor mundo.

Desde el 2005, a las poquitas semanas de haberme venido a vivir a Córdoba, hasta no sé que día del 2012, cada sábado de todas las semanas del año, me tomé dos bondis (o uno, cuando me iba a pata hasta el centro), para llegar a Campo de La Ribera. Mazamorra, la agrupación, fue mi espacio de militancia, mi usina de amistad, mi cobijo del dar y recibir. Sobre todo del recibir.

Leer más…

Los números del Mundial

Un amigo me dijo ayer que la fiebre del mundial ya no le afecta como antes. Cuando era chico, decía, la ansiedad por la navidad, los reyes y sobre todo, los mundiales, no lo dejaba dormir.

A mí Papá Noel y los Reyes me tenían bastante sin cuidado (que sólo ocasionalmente venían a mi casa, sobre todo cuando le hacíamos caso a mi hermano mayor, que nos instruía que a Baltazar había que dejarle una cerveza negra bien fría, para pasar el calor de las madrugadas de enero). Pero me sigue pasando lo mismo con el mundial de fútbol: fiebre. No hay acontecimiento que ansíe más que ese.

Leer más…

Scrapping de PDF con IPython y pdftotext

Pocos días después de mi análisis de datos del escrutinio provisorio en las últimas elecciones de Córdoba me llegó un correo que empezaba así:

Hola Martín, soy Franco Luque, profesor e investigador en Computación de la FaMAF. Con Jorge Sánchez, otro investigador de acá que trabaja en procesamiento de imágenes, vimos tu iniciativa, muy buena por cierto, y nos pareció muy interesante la posibilidad de procesar las imágenes de las actas para reconocer los números manuscritos. Tan interesante nos pareció que pensamos en la posibilidad de organizar una jornada de programación (lo que algunos llaman hackatón muy a nuestro pesar :P), posiblemente para sábado de la semana que viene.

Así fue que, en tiempo record, junto a Franco, Jorge, Jairo Trad, Andrés Vazquez y Marysol Farneda organizamos el evento Democracia con códigos en el que participaron 35 personas! Ese evento fue éxito en todo sentido y dio el puntapie inicial para armar el grupo Open Data Córdoba.

Abriendo datos para la democracia

Uno de los requisitos fundamentales para investigar datos es tenerlos. Si bien el sitio oficial datospublicos.gob.ar ya había publicado datasets oficiales de las elecciones, en el sitio resultados.gob.ar, donde se publicaron los telegramas en tiempo real, había más información.

En particular, hay una sección que muestra resúmemes los resultados provisorios por distrito que incluye un dato muy interesante: la hora en que fue computado cada centro de votación en el escrutinio provisorio. Lamentablemente, esa info atrapada en PDFs no es muy útil.

Si bien no alcazamos a utilizarlos en el evento (mi idea era agregar una línea de tiempo al mapa para ver cómo fue evolucionando), el dia anterior del hackatón dediqué un ratito a extraer esos datos para poder computarlos.

Lo publico ahora porque me parece útil no sólo como ejemplo de extracción de datos desde un PDF sino sobre las posibilidades de IPython Notebook (de paso, este artículo es un notebook) como entorno de "hackeo", pudiendo utilizar Python, muchísimos otros lenguajes y cualquier herramienta que tengamos en el sistema, de una manera integrada y fácil.

Leer más…

Lo siento por vos

A fines de enero, apenas volvimos de Ushuaia, en la góndola de los fideos del Vea de mi barrio decidí que iba a retomar Preciosa. Hice un aviso en la lista de correo y lo tuiteé, sin esperar mucha respuesta.

Cuando salieron las primeras aplicaciones para "Precios Cuidados", Luciano Ferrer me preguntó si eran lo mismo que lo que yo quería hacer. Le respondí que Preciosa era mucho más que eso, pero todavía tenía dudas sobre cómo concretarlo:

Tres meses después Preciosa tiene una primera versión con casi 2000 descargas, 400 usuarios activos, múltiples aparaciones en radio y TV y muchísimas ideas y ganas para seguir creciendo.

Leer más…