Gustavo Campanelli: Eve Online: Ayudas externas

Una de las particularidades del Eve Online es el grado de respeto que tiene CCP, la compañía, con sus usuarios. A la vez, el juego pose estructura y reglas, pero además de eso es un gran sandbox (arenenro) donde cada cual inveta su juego. Esto se traduce en una base de usuarios tremendamente fiel que además de generar eventos dentro del juego, construye cosas alrededor del juego, afuera del juego

Gustavo Campanelli: Los mecanismos predictores

Vengo a confesar algo: no me gusta la mayoría de los mecanismos predictores. Para aclarar, un mecanismos predictor es aquel que mediante algún algoritmo intenta predecir lo que vamos a precisar a continuación. Realmente me parecen muy feos en los smartphones, donde son una ayuda porque el sistema de entrada de datos de esos teléfono es realmente feo. Sea esto debido a que el teclado es muy

Gustavo Campanelli: Eve Online: mercado y actualizaciones de juego

Si bien este artículo habla principalmente del juego Eve Online, en realidad es aplicable a varios juegos distintos, todos aquellos juegos que además de poseer mercado tiene un sistema de construcción de items. Los mercados donde los jugadores son los que proponen los precios son variables en el tiempo, esto quiere decir que un mineral que hoy me cuesta 4,50 la unidad (moneda del juego) puede

Mariano Reingart: Aplicación libre para recuento de votos / escrutinio: recuento_web2py


Recientemente he liberado una aplicación web, usada originalmente para el Recuento definitivo de las elecciones provinciales de Rio Negro 2011:


En el Primer Hackaton de Datos Públicos,  que incluyó juegos de datos de elecciones pasadas, trabajé en dicha aplicación para armar y publicar el código de la misma, a modo de demostración que sirviera para procesar los resultados de distintas elecciones.

La aplicación es básica y simple, permite consultar/cargar datos de las planillas (actas / telegramas de cada mesa) y tiene una página de resultados totalizados. Está escrita en el lenguaje de programación Python (framework web2py) y utiliza PostgreSQL como base de datos.

El esquema de base de datos ha sido generalizado para que pueda ser utilizado en cualquier tipo de elección, ya que soporta diferentes cargos, listas internas / partidos, y un árbol jerárquico de ubicaciones (soportando clasificaciones variadas, por ej. en País, Provincia, Departamento, Circuito y Mesa).

Mi idea de publicar el código fuente, fue más que nada por una cuestión educativa / experimental, para poder compartirlo con alumnos y otros interesados, ya que quizás sea de utilidad para tomarlo como base para analizar resultados de elecciones, hacer pruebas de carga y rendimiento, optimización de bases de datos, etc.

Hasta el momento, se procesaron los datos públicos de dos elecciones nacionales:
Esta subido a un repositorio público bajo licencia AGPLv3+ (licencia de software libre similar a la de Linux pero con consideraciones adicionales para aplicaciones web): https://github.com/MSA-Argentina/recuento_web2py
Instalación Rápida:
  • Descargar web2py desde: http://www.web2py.com/ 
  • Descargar la aplicación en un archivo comprimido desde GitHub: master.zip
  • Descomprimir la aplicaión en la carpeta application de web2py (por ej. bajo el nombre recuento)
  • Crear una base de datos en PostgreSQL y ejecutar el esquema.sql para crear las tablas
  • Seguir los pasos siguientes de cada juego de datos (ver carga.sql y migracion.sql)
  • Configurar la cadena de conexión a la base de datos en el modelo app_settings.py

Juegos de Datos Públicos


Ultimamente trabajé en procesar los datos de las P.A.S.O. 2013 -elección primarias para senadores y diputados nacionales en las 24 provincias-, publicados en el Portal de Datos Públicos

Para más información ver carpeta 2013-primarias:
El juego de datos es bastante interesante ya que está a nivel de mesa (fuente de la información), por lo que incluye 88.463 mesas procesadas con 1.846.369 registros de resultado (votos por mesa, por cada lista para los cargos de diputados + sendadores).
Recordar que es un recuento provisorio por lo que los datos no son definitivos y no están todas las planillas cargadas (90.670 mesas habilitadas en total). El total de electores habilitado fue de 30.573.183.
Para ver los datos oficiales, dirigirse a http://www.elecciones.gov.ar/ (recuento definitivo) y http://www.resultados.gob.ar (recuento provisional).

Manejo de Imágenes de actas de mesa (telegramas / faxes):


La página de Consulta de Telegrama / Planilla permite revisar, cargar y corregir los votos por cargo por lista en cada mesa escrutada.
Para el procesamiento de las imágenes se desarrollaron dos programas auxiliares (scripts):
  • faxes.sh: para extraer las imágenes del PDF y convertirlas al formato TIFF optimizado (CCITT grupo 4) se utilizaron las herramientas pdfimages y ppm2tiff
  • faxes.py: para incorporar las imágenes a PostgreSQL (campo BYTEA), usando python, psycopg2 (adaptador BINARY) y una consulta para determinar la mesa según el nombre de archivo.
NOTA: si se utiliza una versión antigua de PIL (Python Imaging Library), es necesario convertir las imágenes a PNG, perdiendo un poco de eficiencia en la compresión, pero ganando compatibilidad.

Para mostrar las imágenes, se obtiene la misma desde la base de datos y se genera una miniatura con PIL (ver thumbnail en definitivo.py) para ser descargada por el navegador web.

El tamaño total de la base de datos, incluyendo las imágenes de las planillas, es de 6.4 GB

Consultas no triviales con el "ORM" de web2py


Gracias a características declarativas del framework web2py, se pueden realizar consultas relativamente complejas y arbitrarias, en este caso con agrupaciones, tablas anidadas y criterios de búsquedas particulares, de manera programada en Python, sin necesidad de recurrir a escribir SQL puro.
A continuación se extrae un ejemplo de código para totalizar los votos dada una ubicación (por la URL se especifican los identificadores de ubicación, cargo y estado de las planillas):

# constantes (jerarquía de ubicaciones)
CLASES = ['Pais', 'Provincia', 'Departamento', 'Circuito', 'Mesa']

# alias de tablas:       

p = msa.planillas
d = msa.planillas_det
l = msa.listas

# armo la consulta base con los criterios de búsqueda
query = p.id_planilla == d.id_planilla
query &= d.id_lista == l.id_lista
query &= p.id_estado == request.args[3]
query &= d.id_cargo == request.args[0]
query &= l.positivo == True

# armo la consulta "recursiva" (árbol) para tener las ubicaciones y planillas

# (up -> ubicación padre, uh -> ubicación hija)
# p/ el alias de cada tabla se usa el nombre de la clase (depto, mesa, etc.)
up = msa.ubicaciones.with_alias(ubicacion.clase)
query &= up.id_ubicacion == id_ubicacion
for clase in CLASES[CLASES.index(ubicacion.clase)+1:]: 
      uh = msa.ubicaciones.with_alias(clase)
      query &= uh.id_ubicacion_padre == up.id_ubicacion
      up = uh
query &= p.id_ubicacion == up.id_ubicacion
        
# campo suma total:
suma_votos = d.votos_definitivos.sum()
    
# ejecuto la consulta:
resultado = msa(query).select( 
              d.id_lista.with_alias("id_lista"),
              l.nro_lista.with_alias("nro_lista"), 
              l.descripcion.with_alias("descripcion"), 
              l.idx_fila.with_alias("idx_fila"), 
              l.descripcion_corta.with_alias("descripcion_corta"),
              l.color.with_alias("color"),
              suma_votos.with_alias("suma_votos"),
              groupby=(d.id_lista |
                        l.nro_lista | 
                        l.descripcion |
                        l.idx_fila |
                        l.descripcion_corta |
                        l.color),
              orderby= ~suma_votos | l.idx_fila
             )


La salida puede observarse en la Página de Resultados por Ubicación  que permite obtener la suma total de votos agrupada por lista y cargo, para un País / Dpto / Mesa):
Esta consulta en Python generará una consulta SQL similar a la siguiente:

SELECT planillas_det.id_lista, listas.nro_lista, listas.descripcion, 
       SUM(planillas_det.votos_definitivos) AS suma_votos 
FROM planillas, planillas_det, listas, 
     ubicaciones AS provincia, 
     ubicaciones AS departamento, 
     ubicaciones AS circuito, 
     ubicaciones AS mesa 
WHERE planillas.id_planilla = planillas_det.id_planilla
  AND planillas_det.id_lista = listas.id_lista 
  AND planillas.id_estado = 'Publicada' 
  AND planillas_det.id_cargo = 1
  AND listas.positivo = 'T' 
  AND provincia.id_ubicacion = 1 
  AND departamento.id_ubicacion_padre = provincia.id_ubicacion
  AND circuito.id_ubicacion_padre = departamento.id_ubicacion 
  AND mesa.id_ubicacion_padre = circuito.id_ubicacion 
  AND planillas.id_ubicacion = mesa.id_ubicacion
GROUP BY planillas_det.id_lista, listas.nro_lista, listas.descripcion 
ORDER BY SUM(planillas_det.votos_definitivos) DESC, listas.nro_lista;

Consultas Recursivas con PostgreSQL:


La consulta anterior, podría escribirse de manera más flexible en PostgreSQL utilizando Expresiones de Tablas Comunes (CTE) para consultas recursivas (clausula WITH).
En el siguiente ejemplo, se utiliza un termino no recursivo para obtener la ubicación base (id_ubicacion =1), unido luego con el termino recursivo que busca las ubicaciones hijas de cada padre:

WITH RECURSIVE U(id_ubicacion, descripcion, clase) AS (
    SELECT UP.id_ubicacion, UP.descripcion, UP.clase 
      FROM ubicaciones UP 
     WHERE UP.id_ubicacion = 1
  UNION ALL
    SELECT UH.id_ubicacion, UH.descripcion, UH.clase
      FROM U, ubicaciones UH 
     WHERE UH.id_ubicacion_padre = U.id_ubicacion 
  )
SELECT L.id_lista, P.id_partido, L.nro_lista, L.descripcion, P.descripcion,
       SUM(PD.votos_definitivos) AS suma_lista
FROM U, planillas PL, planillas_det PD, listas L, partidos P
WHERE PL.id_ubicacion = U.id_ubicacion
  AND PL.id_planilla = PD.id_planilla
  AND PD.id_lista = L.id_lista 
  AND L.id_partido = P.id_partido
  AND PD.id_cargo = 2
GROUP BY P.id_partido, L.id_lista, L.nro_lista, L.descripcion, P.descripcion 
ORDER BY 6 DESC;

Si bien en este caso no cambia el rendimiento ni el resultado, podría ser útil para otras elecciones en que las jerarquías de ubicaciones no tengan los mismos niveles de agrupación (por ej., hay provincias que podrían estar divididas por secciones electorales, municipios, comunas, departamentos, con mayor o menor de detalle, generalmente hasta el establecimiento y mesa)

Es interesante ver con el comando EXPLAIN el análisis del plan de ejecución de la consulta para detectar posibles optimizaciones , como podría ser agregar el siguiente indice:

CREATE INDEX planillas_det_id_cargo_id_idx ON planillas_det(id_cargo, id_planilla)


Funciones de Ventana con PostgreSQL:


Otra característica de PostgreSQL que puede ser útil en estos casos, son las funciones que calculan en base a una partición de los datos (similar a un agrupamiento).
Por ejemplo, para obtener la cantidad de votos por cada partido (además de la suma de cada lista interna), se puede agregar la expresión SUM(SUM(PD.votos_definitivos)) OVER (PARTITION BY L.id_partido) AS suma_partido  a la consulta anterior:

WITH RECURSIVE U(id_ubicacion, descripcion, clase) AS (
    SELECT UP.id_ubicacion, UP.descripcion, UP.clase 
      FROM ubicaciones UP 
     WHERE UP.id_ubicacion = 1
  UNION ALL
    SELECT UH.id_ubicacion, UH.descripcion, UH.clase
      FROM U, ubicaciones UH 
     WHERE UH.id_ubicacion_padre = U.id_ubicacion 
  )
SELECT L.id_lista, P.id_partido, L.nro_lista, L.descripcion, P.descripcion,
       SUM(PD.votos_definitivos) AS suma_lista,
       SUM(SUM(PD.votos_definitivos)) OVER (PARTITION BY L.id_partido) AS suma_partido
FROM U, planillas PL, planillas_det PD, listas L, partidos P
WHERE PL.id_ubicacion = U.id_ubicacion
  AND PL.id_planilla = PD.id_planilla
  AND PD.id_lista = L.id_lista 
  AND L.id_partido = P.id_partido
  AND PD.id_cargo = 2
GROUP BY P.id_partido, L.id_lista, L.nro_lista, L.descripcion, P.descripcion 
ORDER BY 7 DESC, 6 DESC;

Cuyo resultado es:


Agradecimientos y trabajo a futuro


Esta aplicación fue posible gracias a la colaboración de la empresa MSA y al grupo de trabajo de datos públicos / software libre del foro de la Agenda Digital, por haber hecho posible la publicación del código fuente y el acceso a los datasets respectivamente.

En el futuro posiblemente publicaremos un análisis más exhaustivo, extendiendo el apunte sobre optimización de consultas y estadísticas en el sitio del Grupo de Usuarios de PostgreSQL Argentina: http://www.postgresql.org.ar/trac/wiki/OptimizarRendimiento

Seguramente se presentará el trabajo en la próxima jornada PostgreSQL: PgDay Argentina 2013, donde se preparará un taller para experimentar con los datos, analizar el rendimiento y proponer mejoras.

Hernán Grecco: Make your functions units-aware with Pint 0.3

Pint is Python package to define, operate and manipulate physical quantities: the product of a numerical value and a unit of measurement. It allows arithmetic operations between them and conversions from and to different units.

>>> from pint import UnitRegistry
>>> ureg = UnitRegistry()
>>> q1 = 24. * ureg.meter
>>> q2 = 10. * ureg.centimeters
>>> q1 + q2
<Quantity(24.1, 'meter')>
It provides a comprehensive and extensive list of physical units, prefixes and constants defined in a standalone text file. The registry can parse prefixed and pluralized forms of units resulting in a much shorter and maintainable unit definition list.

It also provides great NumPy integration, with implicit unit conversion and an emphasis on correctness.
>>> import numpy as np
>>> np.cos([0, 45] * ureg.degrees)
<Quantity([ 1. 0.70710678], 'dimensionless')>
>>> np.cos([0, 45] * ureg.meter)
Traceback (most recent call last):
...
pint.unit.DimensionalityError: Cannot convert from 'meter' to 'radian'

Wrapping your functions with Pint

Scientific code usually needs to interface with services, libraries or data structures in which physical parameters must be given in specific units. It is common to specify the units in the docstrings like this:
>>> def somefun(length, force):
... """
... :param length: length in meters.
... :param force: applied force in Newton.
... :return: elapsed time in seconds.
... """
... # Do something
Later in the code, you will have to remember the required units and make sure that you are doing the right conversions. This is very inconvenient and error prone. Pint 0.3 helps you to write units-aware functions:
>>> from pint import UnitRegistry
>>> ureg = UnitRegistry()
>>> @ureg.wraps('second', ('meter', 'newton'))
... def somefun(length, force):
... # Do something
and you will use it like this:
>>> somefun(200 * ureg.cm, 8e+5 ureg.dyne)
8 second
The first argument in the decorator factory indicates the return units and the second argument indicates the input units. The decorator will convert input quantities to the required units and then call the wrapped function using their magnitudes as arguments. The return value will be used as the magnitude of a new quantity with the specified output units.
This is very convenient to wrap functions from other packages. For example, the Polymode package provides the index of refraction as a function of the wavelength (given in micrometers) for a variety of optical materials.
>>> from Polymode import Material
>>> nbk7 = Material.BK7().index
>>> nbk7(.5) # The index of refraction for a wavelength given in micrometer
1.5214144757734767
Instead of remembering the input units, you can just wrap it using Pint:
>>> nbk7 = ureg.wraps(None, ['micrometer', ])(Material.BK7().index)
>>> nbk7(500 * ureg.nanometer)
1.5214144757734767

Calling the wrapped function with an argument with incompatible units (e.g. in this case, seconds) will raise a `DimensionalityError`. There is more information in the documentation.

IPython integration

Pint now integrates better with IPython providing pretty printing and autocomplete.

In the qtconsole:



and in the notebook:


Thanks to the people that contributed bug reports, suggestions and patches in this release. In particular to: Eric Lebigot, Nate Bogdanowicz, Daniel Sokolowski, 

Interested? Install it and give it a try!

Submit your bug reports, comments and suggestions in the Issue Tracker. There are already some ideas for version 0.4. Check them out, comment and add yours.

Read the docs: https://pint.readthedocs.org/
or fork the code: https://github.com/hgrecco/pint

alecu: Selección de charlas para PyCon

Este año tenemos solamente dos salas para PyCon Argentina 2013, asi que desde el comité de selección de charlas tuvimos que descartar charlas a lo loco. Otros años pudimos incluir el 60 o 70% de las propuestas, pero este año va a quedar menos del 50%.

Elegir las charlas es un proceso exhaustivo, y desde el comité revisamos cada una con respecto a la experiencia de los oradores, relevancia del tema y claridad de la propuesta. Y luego tuvimos algunas reuniones donde terminamos de decidir la grilla.

El criterio que usamos para evaluar a los oradores que nunca habían presentado charla en algún PyDay o en PyCon o que no conocíamos fue ver si tenían alguna charla grabada en internet. A los oradores que están empezando, nuestra recomendación es dar una Charla Relámpago durante el evento, y luego proponer la charla durante algún PyDay donde la revisión suele ser más permisiva.

También tuvimos que rechazar varias charlas que hablaban sobre algún framework o biblioteca, sobre todo si dicho código estaba publicado hace poco, significando que tenía pocos o inexistentes usuarios; y en algunas charlas también, si el código no estaba publicado. En ambos casos tratamos de priorizar lo que es general y útil a la mayoría de la audiencia, versus lo que es muy nuevo y poco usado. Estos temas recomendamos también presentarlos como Charla Relámpago durante la conferencia.

Y también rechazamos charlas de temas repetidos, y tratamos de minimizar la cantidad de charlas de cada orador. Salvo en casos muy puntales, cada orador aparece en una sóla charla.

Como siempre, es probable que nos hayamos equivocado, y muy probable también es que haya cosas para criticar. Los invito a hacerlo en los comentarios de este blog, pero sobre todo los invito también a participar en la organización de esta y las siguientes conferencias para que las cosas salgan más a su gusto.

De lo que si estamos seguros es que las charlas que elegimos van a armar una conferencia buenísima, asi que los esperamos en menos de dos meses en Rosario!

Alejandro Santos: Ping por Packet AX25

Así me está funcionando el ping por AX25, nada mal!
# ping -i 15 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_req=1 ttl=64 time=4002 ms
64 bytes from 10.0.0.1: icmp_req=2 ttl=64 time=2689 ms
64 bytes from 10.0.0.1: icmp_req=4 ttl=64 time=2851 ms
64 bytes from 10.0.0.1: icmp_req=5 ttl=64 time=3382 ms
64 bytes from 10.0.0.1: icmp_req=7 ttl=64 time=3258 ms
64 bytes from 10.0.0.1: icmp_req=9 ttl=64 time=2753 ms
64 bytes from 10.0.0.1: icmp_req=12 ttl=64 time=4070 ms
64 bytes from 10.0.0.1: icmp_req=13 ttl=64 time=3859 ms
64 bytes from 10.0.0.1: icmp_req=15 ttl=64 time=4178 ms
64 bytes from 10.0.0.1: icmp_req=17 ttl=64 time=4264 ms
64 bytes from 10.0.0.1: icmp_req=18 ttl=64 time=3541 ms
64 bytes from 10.0.0.1: icmp_req=20 ttl=64 time=4856 ms
64 bytes from 10.0.0.1: icmp_req=22 ttl=64 time=4018 ms
64 bytes from 10.0.0.1: icmp_req=23 ttl=64 time=5313 ms
64 bytes from 10.0.0.1: icmp_req=25 ttl=64 time=3723 ms
64 bytes from 10.0.0.1: icmp_req=27 ttl=64 time=2809 ms
64 bytes from 10.0.0.1: icmp_req=28 ttl=64 time=4391 ms
64 bytes from 10.0.0.1: icmp_req=29 ttl=64 time=6268 ms
64 bytes from 10.0.0.1: icmp_req=30 ttl=64 time=6058 ms
64 bytes from 10.0.0.1: icmp_req=31 ttl=64 time=4279 ms
64 bytes from 10.0.0.1: icmp_req=32 ttl=64 time=5625 ms
64 bytes from 10.0.0.1: icmp_req=33 ttl=64 time=3763 ms
64 bytes from 10.0.0.1: icmp_req=34 ttl=64 time=2911 ms
64 bytes from 10.0.0.1: icmp_req=35 ttl=64 time=2853 ms
64 bytes from 10.0.0.1: icmp_req=36 ttl=64 time=2718 ms
64 bytes from 10.0.0.1: icmp_req=37 ttl=64 time=2789 ms
64 bytes from 10.0.0.1: icmp_req=38 ttl=64 time=2944 ms
64 bytes from 10.0.0.1: icmp_req=39 ttl=64 time=3699 ms
64 bytes from 10.0.0.1: icmp_req=41 ttl=64 time=3798 ms
64 bytes from 10.0.0.1: icmp_req=44 ttl=64 time=2918 ms
^C
--- 10.0.0.1 ping statistics ---
44 packets transmitted, 30 received, 31% packet loss, time 645337ms
rtt min/avg/max/mdev = 2689.509/3819.856/6268.794/986.186 ms

Alejandro Santos: Medidor de ROE digital con Arduino

Arduino SWR Meter

Tal como adelanté en mi anterior entrada, una mejora al puente de ROE de XQ2FOD que le hice fue la de reemplazar los medidores analógicos por un display LCD controlado con un Arduino (ATmega328p).

Arduino SWR Meter

El circuito es demasiado simple como para dibujarlo, la idea es usar la librería LiquidCristal de Arduino y reemplazar en el circuito de XQ2FOD la entrada a los medidores analógicos por las entradas Analog0 y Analog1 en el Arduino. Se pueden ver más fotos en mi galería de Flickr.

El código fuente del programa cargado en el Arduino se puede encontrar acá. En programa se encarga de leer constantemente las entradas 0 y 1 analógicas, y hacer los cálculos de SWR (sacados del capítulo de Lineas de Transmisión del ARRL Handbook 2010).

p = sqrt(Pr/Pf);
SWR = (1+p) / (1-p);

Además como se puede ver en el codigo fuente, se calcula el promedio de los datos leídos para evitar que el display LCD parpadee cuando los datos de entrada fluctúen en el tiempo.

LU4EXT