Manejo de atributos mediante propiedades.

En Python, cuando creamos una clase, definimos una serie de atributos (que indicarán su estado) y de métodos (que marcarán su comportamiento). El acceso a ambos se realiza de manera sencilla mediante el operador punto. Un ejemplo de ello (como nota decir que todo el código mostrado en esta entrada está escrito en Python 3 sobre el IDE PyScripter)  es el siguiente:

class MiClase:
    def __init__(self, midato):
        self.dato = midato
    def cuadrado(self):
        return self.dato ** 2
a = MiClase(12.72)
print("El cuadrado es:", a.cuadrado())
a.dato = 7.72
print("El cuadrado es:", a.cuadrado())

La salida sería:

>>> 
El cuadrado es: 161.79840000000002
El cuadrado es: 59.5984
>>>

Para ejemplos sencillos podría ser suficiente actuar de esta manera, pero en otros momentos necesitaremos más versatilidad. Imaginemos que queremos validar los atributos con una determinada condición. En nuestro caso podría ser que los valores de dato estuviesen entre 0 y 10. A los valores menores que 0 se les asignaría el 0 y a los mayores de 10, el 10. Todo ello se lograría fácilmente creando un método que manejase esa condición y ejecutándolo también en __init__():

class MiClase:
   def __init__(self, midato):
       self.set_dato(midato)
   def set_dato(self, midato):
       if isinstance(midato, (int, float)):
           if midato < 0:
               self.dato = 0
           elif midato > 10:
               self.dato = 10
           else:
               self.dato = midato
       else:
           raise ValueError("Dato no válido")
   def cuadrado(self):
       return self.dato ** 2
a = MiClase(12.72)
print("El cuadrado es:", a.cuadrado())
a.set_dato(7.72)
print("El cuadrado es:", a.cuadrado())

Con salida:

>>> 
El cuadrado es: 100
El cuadrado es: 59.5984
>>>

En el método set_dato() hacemos la comprobación que el dato es entero o real. De lo contrario lanzamos una excepción. Con ello nos aseguraríamos que el dato cumple las condiciones que queremos.

De la misma forma que hemos creado un método para manejar la escritura del atributo dato podríamos hacerlo para su lectura. Son los denominados, de forma genérica, métodos getter y setter, que nos permiten, respectivamente, obtener o modificar los atributos. El concepto de encapsulación de datos incluye, además del uso de estos métodos, el que los atributos sean privados (no accesibles desde fuera de la propia clase), algo que en Python se logra añadiendo un doble guión bajo al nombre del atributo. Tendríamos lo siguiente:

class MiClase:
    def __init__(self, midato):
        self.set_dato(midato)
    def get_dato(self):
        return self.__dato
    def set_dato(self, midato):
        if isinstance(midato, (int, float)):
            if midato < 0:
                self.__dato = 0
            elif midato > 10:
                self.__dato = 10
            else:
                self.__dato = midato
        else:
            raise ValueError("Dato no válido")
        return None
    def cuadrado(self):
        return self.__dato ** 2
a = MiClase(12.72)
print("El cuadrado es:", a.cuadrado())
a.set_dato(7.72)
print("El cuadrado de", a.get_dato(), "es", a.cuadrado())

La salida es:

>>> 
El cuadrado es: 100
El cuadrado de 7.72 es 59.5984
>>>

En Python es habitual que los atributos que en un principio son simples datos estáticos con el tiempo se conviertan en expresiones a calcular dinámicamente o, como ya hemos visto, haya que validarlos de alguna manera. Si actuamos con métodos getter y setter desde el principio podremos hacer esa nueva implementación sin cambiar la forma en la que accedemos a los datos, pero si lo hemos hecho simplemente mediante el operador punto, estaríamos en una situación delicada. Es para superar este escollo, o conseguir una forma mas pythónica que los métodos getter y setter, para lo que se usan las propiedades.

¿Qué son exactamente las propiedades?

Las propiedades son atributos que manejamos mediante métodos getter, setter y deleter, por lo que podríamos llamarlos "atributos manejados". Podemos considerarlos unos atributos "especiales". En realidad en Python los datos, métodos y propiedades de una clase son todos atributos. Los métodos serían atributos "llamables" y las propiedades atributos "personalizables". La única diferencia entre una propiedad y un atributo estándar es que las primeras pueden invocar código personalizado al ser obtenidas, modificadas o eliminadas. Las propiedades se asocian a los atributos de la clase y, como cualquiera de ellos, son heredados en subclases e instancias. Una propiedad maneja un solo y específico atributo, y siempre harán referencia a los métodos de la clase en la que estén definidas.
Por lo tanto, las propiedades parecen atributos estándar, pero al acceder a ellos se lanzan los métodos getter, setter o deleter correspondientes. Como éstos métodos reciben el argumento self, podremos acceder a todos los elementos de la clase o la instancia (sus atributos y métodos) y hacer uso de ellos.

¿Cuándo usar entonces las propiedades?

Respecto al uso desde el principio de simples métodos getter , setter y deleter, las propiedades nos aportan un código mas pythónico, siendo mas fácil la lectura y escritura de los atributos. La interfaz es mas homogénea ya que a todo se accede mediante el operador punto.

Respecto al uso de atributos estándar, las propiedades nos dan la posibilidad de incorporar código que se ejecute de forma dinámica cuando intentamos acceder a ellos (para obtenerlos, modificarlos o borrarlos). Imaginemos el caso comentado con anterioridad: tenemos hecho un programa donde accedemos a atributos estándar públicos. Posteriormente necesitamos que esos atributos estándar sean validados con unas determinadas condiciones. Mediante las propiedades lograremos nuestro objetivo sin modificar el formato del código ya escrito, algo que no lograríamos con los métodos.

¿Cómo crear las propiedades?

La función property(), integrada en el intérprete, nos permite canalizar la lectura o escritura de los atributos (además de interceptar el momento en el que son borrados o proporcionar documentación sobre ellos) mediante funciones o métodos. Su formato es el siguiente:

atributo_de_clase = property(fget=None, fset=None,fdel=None, doc)

Ninguno de los parámetros es obligatorio. Si no los pasamos su valor por defecto es None. La función fget() se encargará de interceptar la lectura del atributo, la fset() de hacerlo cuando se escriba, la fdel() a la hora de borrarlo y el argumento doc recibirá una cadena para documentar el atributo (si no lo recibe, se copia el docstring de fget(),que por defecto tiene valor None). Si alguna operación no está permitida(por ejemplo si intentamos borrar un atributo, algo no demasiado habitual, y no tenemos indicada fdel()) se lanzará una excepción. La función fget() devolverá el valor procesado del atributo y tanto fset() como fdel() devolverán None. La función property() devuelve un objeto de tipo propiedad que se asigna al nombre del atributo. Un primer ejemplo del uso de las propiedades podría ser el siguiente:

class Persona:
    def __init__(self, nombre):
        self.set_nombre(nombre)
    def get_nombre(self):
        try:
            print("Pedimos atributo:")
            return self.__nombre
        except AttributeError:
            print("Error. No existe el atributo indicado")
        except:
            print("Error al acceder al atributo")
    def set_nombre(self, nuevo_nombre):
        print("Asignamos el valor", nuevo_nombre,"al atributo 'nombre'")
        self.__nombre = nuevo_nombre
        return None
    def del_nombre(self):
        try:
            print("Borramos atributo", self.__nombre)
            del self.__nombre
        except AttributeError:
            print("Error. No existe el atributo que desea borrar")
        except:
            print("Error al intentar borrar el atributo")
        return None
    nombre = property(get_nombre, set_nombre, del_nombre, "Mi información")
def main():
    a = Persona("Pepe")
    a.nombre = "Juan"
    print(a.nombre)
    del a.nombre
    del a.nombre
    print(a.nombre)
    a.nombre = "Elena"
    print(a.nombre)
    print(help(Persona.nombre))
main()

Obtenemos la siguiente salida:

>>> 
Asignamos el valor Pepe al atributo 'nombre'
Asignamos el valor Juan al atributo 'nombre'
Pedimos atributo:
Juan
Borramos atributo Juan
Error. No existe el atributo que desea borrar
Pedimos atributo:
Error. No existe el atributo indicado
None
Asignamos el valor Elena al atributo 'nombre'
Pedimos atributo:
Elena
Help on property:
    Mi información
None
>>>

En él se ha creado una clase Persona con un atributo privado nombre y tres métodos de tipo getter, setter y deleter. Posteriormente se crea, mediante la función property(), la propiedad nombre vinculada a ellos, que reemplaza al atributo del mismo nombre. Se añade además una información de ayuda. Más adelante creamos una instancia de Persona a la que posteriormente cambiamos el atributo nombre, tras lo cual lo borramos. Al intentar  borrarlo de nuevo, generamos un error que manejamos. También se lanza un error manejado al intentar leer el atributo borrado. Es interesante ejecutar el código paso a paso para ver su funcionamiento exacto. En este ejemplo, por simplicidad, no hemos manejado posibles excepciones en el método setter, pero sería conveniente hacerlo en un código completo.

Si no quisiésemos contravenir uno de los principios del Zen de Python ("Debería haber una, y preferiblemente solo una, forma obvia de hacer las cosas") los métodos get_nombre(), set_nombre() y del_nombre() deberían ser privados (algo que nuevamente logramos colocando un doble guión bajo antes de su nombre).

Pero tampoco es ésta la forma mas pythónica para tratar con las propiedades, algo que se consigue mediante el uso de decoradores. Recordemos que un decorador es básicamente una función que "envuelve" a otra dotándola (al añadir código) de alguna funcionalidad extra. El formato es el siguiente:

def mifunción(argumentos):
    ...
mifunción = decorador(mifunción)

También podríamos ponerlo así:

@decorador
def mifunción(argumentos)
    ...

Podemos ahora usar la función property() como decorador para que se ejecute cuando queramos acceder a uno de los atributos.

class Miclase:
    def mi_atributo(self):
        ...
    mi_atributo = property(mi_atributo)

O también colocarlo de la siguiente manera:

class Miclase:
     @property
     def mi_atributo(self):
         ...

De esta manera lograríamos pasar mi_atributo como el primer argumento de la función property(), que es el que se usa cuando intentamos leer. El objeto propiedad tiene métodos getter, setter y deleter que asignan los métodos de acceso de la propiedad y que devuelven una copia de la propia propiedad. Los podemos, a su vez, usar para decorar métodos con el mismo nombre mi_atributo que usaremos en el momento en que intentamos modificar o borrar el atributo. Es un poco lioso, por lo que el código nos puede aclarar un poco las cosas:

class Persona:
    def __init__(self, nombre):
        self.nombre = nombre
    @property
    def nombre(self):
        "Documentación del atributo 'nombre' "
        try:
            print("Pedimos atributo:")
            return self.__nombre
        except AttributeError:
            print("Error. No existe el atributo indicado")
        except:
            print("Error al leer el atributo")
    @nombre.setter
    def nombre(self, nuevo_nombre):
        print("Asignamos el valor",nuevo_nombre," al atributo 'nombre'")
        self.__nombre = nuevo_nombre
        return None
    @nombre.deleter
    def nombre(self):
        try:
            print("Borramos atributo", self.__nombre)
            del self.__nombre
        except AttributeError:
             print("Error. No existe el atributo que desea borrar")
        except:
             print("Error al borrar atributo")
        return None
def main():
    a = Persona("Pepe")
    a.nombre = "Juan"
    print(a.nombre)
    del a.nombre
    del a.nombre
    print(a.nombre)
    a.nombre = "Jaime"
    print(a.nombre)
    print(help(Persona.nombre))
main()

Salida:

>>> 
Asignamos el valor Pepe al atributo 'nombre'
Valor del atributo 'nombre':
Pepe
Asignamos el valor Juan al atributo 'nombre'
Valor del atributo 'nombre':
Juan
Borramos atributo Juan
Error. No existe el atributo que desea borrar
Valor del atributo 'nombre':
Error. No existe el atributo indicado
None
Asignamos el valor Jaime al atributo 'nombre'
Valor del atributo 'nombre':
Jaime
Help on property:
    Documentación del atributo 'nombre'
None
>>>

En el código definimos en  __init__() el atributo nombre como público. Eso nos permitirá que se ejecute, al crear la instancia de Persona, el método setter asociado a su propiedad y por tanto añadir el código que creamos conveniente. Nuevamente, por simplicidad, no se ha añadido manejo de excepciones ni condiciones de validación en él. Mediante la función property() decoramos el método getter que tiene el mismo nombre que nuestro atributo, es decir, nombre. Una vez hecho ésto, podemos usar los métodos setter y deleter del objeto propiedad para decorar los métodos correspondientes, de nombre nombre. Esta si es la forma mas pythónica de tratar las propiedades.

Los métodos getter de las propiedades pueden ser muy útiles si necesitamos calcular atributos sobre la marcha teniendo como base otros atributos. O también si queremos almacenar el resultado de un cálculo complejo para que sucesivas peticiones de ese cálculo no lo realicen de nuevo, sino que recuperen el resultado previo. Un ejemplo heterodoxo del uso de todo ello  es el siguiente: imaginemos que queremos crear una clase que almacene uno o una serie de valores. Podemos tener  o un número(entero o real) o una lista de números (reales y/o enteros). Como condición de validación tendremos que el número o números esté/n entre 0 y 10 (inclusive). De no ser así asignaremos al número el valor 0. Hay en la clase una propiedad llamada que simula (mediante el uso del módulo time) una operación pesada computacionalmente aplicada a los datos. Simulamos que nos cuesta tres segundos calcularla y el resultado es 123.79382.  Lo almacenamos en transformada  por si hay una posterior petición de su cálculo. Otra propiedad llamada media calcula el promedio de los datos ya filtrados mediante la validación. Se comprueba que los datos de entrada son correctos y si no se lanza una excepción de tipo ValueError.

El código y su correspondiente salida serían algo así, aconsejando una ejecución paso a paso para observar su funcionamiento detalladamente:

import time
class MiClase:
   def __init__(self, valor=None):
       self.valor = valor
       self.__transformada = None
   @property
   def valor(self):
       return self.__valor
   @valor.setter
   def valor(self, mivalor):
       if isinstance(mivalor, (int, float)):
           if 0 <= mivalor <=10:
               self.__valor = mivalor
           else:
               self.__valor = 0
       elif isinstance(mivalor, list):
           res = []
           for midato in mivalor:
               if isinstance(midato, (int, float)):
                   if 0 <= midato <=10:
                       res.append(midato)
                   else:
                       res.append(0)
               else:
                   raise ValueError ("El dato introducido no es válido")
           self.__valor = res
       else:
           raise ValueError("El dato introducido no es válido")
       return None
    @property
    def transformada(self):
        if not self.__transformada:
            print("Calculando transformada...")
            time.sleep(3)
            self.__transformada = 123.79382
        else:
            print("Transformada en caché. Valor:")
        return self.__transformada
    @property
    def media(self):
        if isinstance(self.valor, list):
            return sum(self.__valor) / len(self.__valor)
        else:
            return "\nNo se puede calcular la media al no ser una lista"
def main():
    datos = [12.21, 8.68, -2, 7.77]
    print("Los datos introducidos son: ", datos)
    a = MiClase(datos)
    print("Los datos filtrados son: ", a.valor)
    print("La media de los datos filtrados es:", a.media)
    print(a.transformada)
    print(a.transformada)
main()
>>> 
Los datos introducidos son: [12.21, 8.68, -2, 7.77]
Los datos filtrados son: [0, 8.68, 0, 7.77]
La media de los datos filtrados es: 4.1125
Calculando transformada...
123.79382
Transformada en caché. Valor:
123.79382
>>> 

Revisión del libro “Python 3, Curso Práctico” de Alberto Cuevas

Nos han pedido una revisión de un nuevo libro sobre Python en español. El libro se titula 'Python 3, Curso Práctico' y lo ha escrito Alberto Cuevas Álvarez. Si seguís leyendo podréis ver cómo ganar una copia en papel del mismo ;-D

Primero de todo, algunas características del libro:

  • Título: Python 3, Curso Práctico.
  • Autor: Alberto Cuevas Álvarez
  • Año de edición: 2016
  • Nº páginas: 560
  • ISBN: 978-84-9964-658-9
  • Editorial: RA-MA EDITORIAL
  • Encuadernación: Rústica
  • Idioma: Español (de España).
  • Versión de Python usada en el libro: 3.3
  • Link: http://www.ra-ma.es/libros/PYTHON-3-CURSO-PRACTICO/94627/978-84-9964-658-9
  • Precio: 31.90 € para la versión en papel.
  • Versión electrónica: No disponible de momento.

Le hemos pedido al autor que nos defina su intención al escribir el libro y esta es la frase resumen que nos ha enviado:

"Mi intención a la hora de realizar el libro ha sido explicar los fundamentos básicos de Python y las herramientas necesarias dentro de su ecosistema para conseguir crear aplicaciones gráficas completas en 2D ."

Como venimos haciendo, vamos a ir viendo el libro capítulo a capítulo para poder comentar de forma pormenorizada las pequeñas piezas que componen el mismo:

INTRODUCCIÓN

El libro empieza fuerte explicando, aunque sea muy por encima, cosas básicas de programación que pueden venir bien para fijar ciertos conceptos si no se dispone de cierto bagaje en ciencias de la computación.

EMPEZANDO A PROGRAMAR

En este punto se introducen conceptos importantes como lo que son las variables, cómo se asignan, ciertas funciones integradas (builtin) en el intérprete, operadores,etc. Cosas básicas introducidas en el momento oportuno. Está bien lo detallado de algún punto con múltiples ejemplos útiles. Se comenta la instalación de PyScripter, un IDE solo windows :-(

ELEMENTOS FUNDAMENTALES DE PROGRAMACIÓN: INSTRUCCIÓN CONDICIONAL Y BUCLES

El capítulo habla de forma extensa del control de flujo, bucles y condiciones. Muy detallada la explicación de las condiciones. En este mismo capítulo se introducen algunas cosas que no tienen mucha relación como el mecanismo del import (explicado muy brevemente), el uso de números aleatorios (que no tiene mucha relación con el resto de contenidos del capítulo), depuración con PyScripter (menos útil para personas fuera de Windows),...

PROGRAMACIÓN FUNCIONAL

El título del capítulo es algo desafortunado ya que no estamos hablando sobre programación funcional sino sobre el uso de funciones en Python y esto podría confundir a alguien. El capítulo es muy completo para aprender a usar funciones en Python, el 'scope' de las variables, los parámetros y argumentos,... Nuevamente, algún ejemplo podría resultar confuso pero, en general, está bastante completo. Echo en falta que se nombre a las funciones lambda.

PROGRAMACIÓN ORIENTADA A OBJETOS

Llegamos a la parte de clases y la programación orientada a objetos (POO). El capítulo es bastante extenso y se explican muchas cosas, algunas de ellas avanzadas. Sin duda, este es el capítulo del libro que me gusta menos. Los ejemplos que se usan no son muy ortodoxos, nuevamente se recurre a casos muy particulares que provocan, siempre en mi modesta opinión, que algunas cosas resulten en más complejas de lo que deberían en este punto. Una buena parte del capítulo se habla sobre como se hacen algunas cosas con PyScripter lo que le resta valor a usuarios fuera de Windows.

TIPOS DE DATOS EN PYTHON

Este capítulo es muy detallado. Se explica con mucha profundidad los tipos básicos en Python, cadenas, listas, tuplas, conjuntos y diccionarios. Podría, incluso, servir como guía de referencia en español del uso de estos tipos  por lo detallado (son unas 90 páginas). Además, se usan muchísimos ejemplos para explicar los conceptos.

FICHEROS Y EXCEPCIONES

Se habla sobre cómo poder usar ficheros para leer y escribir información. Esta parte es muy detallada y extensa con múltiples ejemplos útiles. La parte de excepciones es más que correcta con un alcance adecuado para introducirlos. Por último, se muestra el uso de with pero se despacha en menos de una página por lo que me parece insuficiente. Este capítulo y el anterior son los que más me gustan del libro.

PROGRAMACIÓN GRÁFICA EN PYTHON MEDIANTE PYQT

No es normal encontrar una introducción a interfaces gráficas en un libro introductorio. Como mucho, se muestra un ejemplo básico para dejar al lector que se introduzca por su cuenta en el tópico si tiene interés. En este caso se hace una introducción mucho más extensa que el ejemplo típico por lo que si tienes interés en crear interfaces gráficas estás de suerte. Sin embargo, si no tienes mucho interés en ello, se van muchas páginas en ello. Se explican de forma detallada muchos de los widgets disponibles en PyQt y está muy enfocado a crear las interfaces usando Qt Designer.

GENERACIÓN DE GRÁFICOS EN PYTHON MEDIANTE MATPLOTLIB

Nuevamente, se introduce otra librería que quizá no sea de interés para todo el mundo. En alrededor de 50 páginas se habla sobre cómo instalar la librería, cómo usar pyplot con muchos ejemplos, como usar matplotlib usando POO y cómo integrar matplotlib en una aplicación PyQt. Reitero, si tienes interés en hacer gráficas tienes suerte pero si no es así se te van otras 50 páginas de libro en ello.

Apuntes varios sobre el libro:

Me gusta:

  • Lo detallado de la explicación del if en el capítulo 3.
  • Que no haya referencias a Python 2.
  • Lo detallado de la explicación de las funciones.
  • Lo detallado de la explicación de los tipos básicos de Python pudiendo servir, incluso, como guía de referencia en español.
  • La parte del tratamiendo de ficheros y excepciones se hace con un alto nivel de detalle.
  • La extensión del libro, que permite poder desarrollar algunos temas de forma muy completa.
  • Que sea en español.

No me gusta:

  • El uso de eval en muchos ejemplos del libro lo considero una mala práctica.
  • No se respeta el PEP8, enseñando malas prácticas, lo cual no me parece adecuado en un curso introductorio.
  • Algunos ejemplos usan casos extremos para explicar ciertos conceptos. Estos casos extremos pueden ser detalles de la implementación y no los considero adecuados ya que en lugar de ayudar pueden resultar más confusos.
  • Está muy enfocado a Windows (uso de PyScripter, instalaciones) haciendo que una buena parte de las páginas sea menos útil para gente que use otros sistemas operativos.

Conclusión

El libro tiene algunos altibajos con partes que brillan con luz propia y partes que mejoraría.

Si de mi dependiera me gustaría que algunas cosas se explicasen un poco mejor, como el uso de with, las funciones lambda, el mecanismo del import, la parte de POO. Metería partes con muchas librerías útiles de la librería estándar (math, itertools, datetime, os, sys, collections,...), como crear paquetes y módulos,... Reduciría o eliminaría los dos últimos capítulos.

Como comentario general, considero que el libro está bien pero, como todo en esta vida, se podría mejorar en algunos aspectos.

Sorteamos una copia entre nuestros lectores

El autor nos envió varias copias del libro. Una de ellas la vamos a sortear entre todos los lectores de Pybonacci residentes en España. Para participar:

  • Solo tienes que escribir un tweet indicando porqué te gustaría tener este libro incluyendo un enlace a http://www.ra-ma.es/libros/PYTHON-3-CURSO-PRACTICO/94627/978-84-9964-658-9
  • Una vez enviado el tweet nos lo enlazas en los comentarios de más abajo para que no se nos escape el tweet.
  • Si no tienes cuenta en twitter, déjanos un comentario más abajo indicando porqué te gustaría tener este libro.

Tenéis hasta el miércoles, 2 de noviembre a las 21:00:00 (CET) para participar en el sorteo. Después de pasada la fecha indicaremos cómo se hará el sorteo (actualizando algunas cosas de aquí), usando el número ganador del sorteo de la ONCE de ese día (2016/11/02) y anunciaremos el ganador.

Actualización: Resultado del sorteo

El número de la ONCE fue el 69907. Si introducís el código aquí:

sale que el ganador ha sido @FlixUjo. He usado los participantes por orden de fecha en su comentario, del más antiguo al más nuevo:

participantes = ['FlixUjo', 'Javier @runjaj', 'Antonio Molina',
'Raúl', 'José Carlos Juanos', 'Christian',
'Kike', 'Eduardo Campillos']

Enhorabuena al vencedor, por favor, mándanos un DM por twitter o usa el formulario de contacto para mandarnos una dirección de correo/teléfono o lo que prefieras.

Saludos a todos.

Analizando datos sobre el Brexit con Pandas

Esta entrada apareció en inglés en mi blog.

Desde hace tiempo quería aprender Pandas. Por fin llegó la oportunidad: Brexit = datos.

Como siempre empecé con un ejercicio / objetivo práctico, en este caso procesar los datos del referéndum. Usé Pandas para analizar los datos (CSV) publicados por electoralcommission.org.uk.

Aunque quería responder a más preguntas este CSV era suficiente para estrenarme con Pandas (¡es inmenso!). Además aprendí a usar Jupyter notebook para documentar todo. El notebook lo puedes ver / bajar desde Github.

Consegui mi objetivo de representar los datos mostrados aquí. Aquí algunos pantallazos del notebook:

 

Añadiendo datos demográficos

Vinculé los datos del voto con los datos de censo públicamente disponibles (como sugirió Pybonacci), gracias). Encontré unas correlaciones interesantes (y aprendí algunas cosas de matplotlib usándolo), puedes ver el notebook aquí:

¿Cómo influye la edad en el voto por salir / quedar?

¿Cómo influye el porcentaje de paro?

¿Cómo influye un nivel más alto de estudios (educación)?

Y, ¿cómo influye el porcentaje de gente nacida fuera de Inglaterra?

Claramente, áreas con una población mayor y una tasa de paro más alta votan por salir. Por otro lado, áreas con un alto porcentaje de estudios superiores, y regiones donde más gente nacieron fuera de Inglaterra prefieren (por lo general) que Inglaterra se quede en la unión.

Lo dicho, para ver como llegué a estos resultados con Pandas el notebook está aquí.

Y por último: datos de ingresos por región

Los datos de ingresos (sueldo) eran más dificiles de obtener en los datos del censo entonces usé este enlace para comprobar la relación entre la mediana de ingresos y el voto. Encontré un patrón interesante:

(el parsing de los datos está documentado en el mismo notebook)

Se ve claramente que regiones con una mediana de ingresos más baja prefieren salir de la unión, aunque no es 100% consistente: Irlanda tiene una mediana relativamente baja pero vota por quedarse, y South East tiene un sueldo mediano más alto y, no obstante, vota por salir. Es interesante como se ve este tipo de tendencias combinando varias fuentes de datos.

Enlaces de referencia para aprender Pandas

* Pandas home y docs
* Python’s pandas make data analysis easy and powerful with a few lines of code - tutorial breve y fácil para empezar.
* Python for Data Analysis - libro del creador de Pandas Wes McKinney.
* Introduction to Pandas for Developers / Data Wrangling and Analysis with Python - ya he visto algunos videos, son buenos.

Como mejorar tu script fácilmente

Esta entrada apareció originalmente en inglés en mi blog.

Nos ha pasado a todos. Ese momento en el que descubres que sabes suficiente sobre un lenguage de programacion que quieres ponerlo en práctica y construir "algo", lo que sea.
Una de las mejores cosas de la comunidad de Python es no sólo su habilidad para construir cosas increíbles, sino también para compartirlas con todo el mundo, mejorando la comunidad en el proceso.

Sin embargo, llevo un tiempo fijándome en un patrón que se repite en algunos de estos proyectos. Seguro que has visto alguno así. Hablo de esos proyectos con 2 ó 3 componentes, donde el README tiene una pequeña descripción del proyecto, quizás un par de lineas explicando como ejecutar el proyecto, y frases del tipo, "Seguramente añadiré X o Y si tengo tiempo".

El caso es que muchos de estos proyectos son realmente interesantes, y tienen algún tipo de componentes que me gustaría usar sin tener que implementarlos yo mismo.

Te voy a mostrar 3 formas distintas de implementar uno de estos proyectos, cada una de ellas mejor (desde mi punto de vista) que la anterior:

Supongamos que queremos construir un script genial, donde la funcionalidad principal será que, dado un número entero por el usuario, realizará un calculo simple en base a ese entero, y devolverá el resultado.

Implementación 1

 
#!/usr/bin/env python

"""
Super awesome script
Asks the user for a number:
 - If the number is less or equal to 100, it returns the 1st tetration of the number (power of itself)
 - else, it returns the number squared
"""

__version__ = '0.1'

if __name__ == '__main__':

    while 1:
        user_number = input('Choose a number:\n') #raw_input() in python2
        if user_number.isdigit():
            user_number = int(user_number)
            break
        else:
            print('{} is not a valid number'.format(user_number))

    if user_number > 100:
        print(user_number**2)
    else:
        print(user_number**user_number)

Ésta suele ser la implementación de alquien que lleva poco tiempo en python. Funciona, pregunta al usuario por el input, realiza la operación, e imprime en pantalla el resultado.

Veo dos problemas en esta implementación:

1. No hay ningún tipo de separación entre la lógica de la interacción del usuario y la lógica del cálculo. Todo esta incluido en el mismo macro bloque. Pese a ser funcional, esta implementación hace que sea díficil el modificar o expandir este script (para hacerlo tendrías que leerte todo el código).

2. Estamos gestionando toda la validación por nuestra cuenta. Python tiene formas de hacer esto para que tú no te tengas que molestar en hacerlo :).

Para la siguiente implementación, usaremos el módulo mas simple de la libreria standard para trabajar con inputs del usuario, .

Implementación 2

 
#!/usr/bin/env python

"""
Super awesome script
Asks the user for a number:
 - If the number is less or equal to 100, it returns it to the power of itself
 - else, it returns the number squared
"""

import argparse

__version__ = '0.2'


if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('--number', required=True, type=int,
                        help='number to perform calculation')
    values = parser.parse_args()
    user_number = values.number
    if user_number > 100:
        print(user_number**2)
    else:
        print(user_number**user_number)

En esta implementación hemos eliminado el problema #2 de la implementación anterior. En esta ocasión usamos argparse, de esta forma dejamos que la libreria estándar se encargue de la validación del input. Esta implementación no funciona a menos que el input sea válido.

Todavía tenemos el problema #1, la separación entre la lógica del input y la lógica primaria (la función de calculo).

En la siguiente implementación vemos como podemos arreglar esto.

Implementación 3

 
#!/usr/bin/env python

"""
Super awesome script
Asks the user for a number:
 - If the number is less or equal to 100, it returns it to the power of itself
 - else, it returns the number squared
"""

import argparse

__version__ = '0.3'



def calculation(number):
    """Performs awesome calculation"""
    if number > 100:
        return number**2
    else:
        return number**number

if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('--number', required=True, type=int,
                        help='number to perform calculation')
    values = parser.parse_args()
    user_number = values.number
    calculation_result = calculation(user_number)
    print(calculation_result)

En esta implementación, hemos hecho dos cosas:

1. Hemos puesto la carga de la validación en un módulo bien mantenida como es argparse.
2. Hemos separado la lógica del input del usuario de la lógica del input de cálculo.

Éste último cambio tiene tres ventajas sobre #1 y #2.

- Ventaja 1: En primer lugar, si nos damos cuenta que por algún motivo queremos modificar el 100 por un 200, ahora podemos fácilmente modificar eso, sin tener que modificar ni leer todo el código. Siempre y cuando la función calculation siga teniendo los mismos inputs y outputs, el resto de código seguirá funcionando sin problemas.

- Ventaja 2: Otro efecto, y para mi el más significativo, es que si ahora yo leo este script que otra persona ha escrito, y me gusta tanto que quiero añadirlo a un proyecto mio, ¡ahora puedo importarlo sin problemas!.

En las implementacines #1 y #2, la única manera de usar el script era haciendo:

python calculation_script.py --number INTEGER

Ahora, en la implementación #3, tenemos una manera mucho mas útil de usar la lógica mas importante (la del cálculo). Si yo tengo otro script en el que quiero usar la funcion de cálculo, puedo usarla de la forma:

 
from calculation_script import calculation

number = 10
calculation_result = calculation(number)

¿Increíble, no? Simplemente haciendo una pequeña modificación a la estructura del proyecto, ahora cualquier persona se puede beneficiar del mismo.

- Ventaja 3: Supongamos que este simple proyecto empieza a crecer, más desarrolladores se interesan y empiezan a colaborar. El código empieza a crecer y alguien comenta que tendría sentido empezar a trabajar en el suite de testing. (si no sabes lo que es el testing, te recomiendo este artículo.)

Con la implementación #3, testear la funcionalidad de calculation es super fácil (gracias a /u/choffee en reddit por el apunte):

 
import pytest
from calculation_script import calculation

class TestCalculation:
    """Calculation function does funky things to number
    More above 100 than below
    """
    def test_zero():
        x = 0
        assert calculation(x) == 0

    def test_border():
        x = 100
        assert calculation(x) == 10000

    def test_one():
        x = 1
        assert calculation(x) == 1

Piensa en ello la próxima vez, no cuesta nada y hace que tu script sea mejor :)

El producto de matrices y el nuevo operador @

Introducción.

El 13 de septiembre de 2015 fue lanzada la versión 3.5 de Python. Entre las novedades podemos encontrar la inclusión del PEP 465 que trata sobre el nuevo operador @ para la multipliación matricial y del que hablaremos en este post. Como bien sabrán los lectores de este blog, los arrays son la piedra angular de numerosísimas áreas de la programación científica y sirven para realizar operaciones de forma masiva y mucho más eficiente. Esto, sumado a la posibilidad de utilizarlos como matrices, proporciona una herramienta muy potente para llevar a cabo operaciones algebraicas. NumPy es la librería que nos permite utilizar esta maravillosa estructura de datos y según figura en el ya citado PEP, podría ser la librería fuera de la librería estándar más importada del mundo Python.

Continue reading

Por qué dar una charla *ahora* y no luego

Nota: Esto iba a ser un email para la lista de PyLadies España, pero me ha parecido más divertido compartirlo en público :)

Contexto: Como sabéis, dentro de una semana se cierra el plazo para presentar charlas a la PyData Madrid 2016, que se celebrará en abril. Es un momento importante porque, si bien no es la primera vez que tenemos presencia de «trazas de PyData» en nuestro país (la primera fue en la EuroPython 2015) es la primera vez que se organiza como evento independiente. Yo tuve el privilegio de asistir a la primera que se hizo en Europa en un tiempo en el que me podía permitir el lujo de hacer estas cosas. Mucha gente se piensa que soy una especie de experto nacional de Python así que, inspirado por un intercambio que hemos tenido con una chica en privado por Twitter, me gustaría aclarar que mis inicios fueron bastante tortuosos, para así haceros reír un poco y de paso animaros a que participéis en este evento ;) A continuación, mi email tal y como lo empecé:

Yo ya estoy animando a amigas y gente de mi entorno y empiezo a ver un patrón en las respuestas: «todavía no». Os animo a que hagáis piña, os volváis locas y presentéis charla - aunque penséis que «no sois expertas (¿todavía?)» o mejor: con más motivo si lo pensáis. Por tres razones:

La primera: no todos los que vamos a presentar charlas somos «expertos» ¡ni de coña! Es más, yo muchas veces he presentado charlas sobre temas que no dominaba, pero ha sido una excusa perfecta para estudiarlos.

La segunda: si no queréis presentar una charla «experta», las charlas introductorias son súper informativas, muy útiles y suelen gustar a un rango de público bastante amplio. En Internet hay demasiada información, pero vosotras en media hora podéis separar el grano de la paja e iluminar a quienes quieran seguir un determinado camino.

Y la tercera: esa gente que pensáis que es «experta» también la caga. Y a veces la caga bastante, y pasa vergüenzita y quieren que se los trague la tierra. En primicia, cómo me llevé unas diapos a medio preparar a la primera PyConES y luego para arreglarlo enseñé mi contraseña de PyPI en directo

He tardado dos años y medio en volver a ver este vídeo porque me daba pánico, y dos años y medio después me he dado cuenta de que desde fuera no fue tan horrible. Me encuentro sinceramente sorprendido :) (y también de todo el pelo que he perdido en tan poco tiempo, ¿será la radiación de mi portátil?)

De mi segundo fallo no hay vídeo: sucedió en la PyData Londres 2014. Era mi primer evento en inglés, solo conocía a mi colega Fran (con quien volaba a Alicante al acabar) y en un momento dado me volví loco y apunté mi nombre en el tablón de lightning talks.

¡¡Me cago en todo, iba a dar una charla en inglés delante de un huevo de expertos sobre Python científico y análisis de datos!! La experiencia fue trepidante porque fui incapaz de hacer funcionar el proyector con mi portátil en dos ocasiones, así que estaba taquicárdico perdido. Ian Ozsvald me dio una última oportunidad y di la última lightning talk de esa conferencia.

Hubo un momento divertido cuando me puse a explorar la galería de ejemplos con widgets de IPython notebook, que entonces estaban aún en beta y a punto de salir, y no se me ocurre otra cosa que saltarme la sección de machine learning haciendo scroll a toda leche mientras decía al micrófono este comentario:

Machine learning, data science, blah blah blah...

Ni qué decir tiene que la gente soltó una buena carcajada y que yo me puse bastante rojo.

Lo mejor de todo esto es que... no pasa ná :) Me recuperé sin terapia ni nada, mi lightning de los notebooks gustó mucho, he seguido dando charlas en un montón de países, y encima me invitaron a la PyData London 2015 y lo peté. Pero si nunca me hubiese atrevido a empezar a dar malas charlas, a medio preparar o directamente sudoroso y tembloroso como un flan nunca habría llegado a donde estoy.

Así que por favor: mandad charlas aunque no seáis expertas, mandadlas aunque penséis que todos os van a juzgar y mandadlas aunque tengáis miedo de hacerlo mal. Porque la realidad es que al principio tal vez lo haréis mal, os juzgarán menos de lo que pensáis y sois más expertas de lo que imagináis. Pero nada de esto importa en realidad :)

Si necesitáis ayuda, consejo de cualquier tipo o un empujoncito, no dudéis en escribirme a mi nombre acortado arroba este blog.

Desde aquí un aplauso a las personas de esta comunidad que trabajan duro por hacer de Python algo más que un simple lenguaje de programación y convertirlo en una herramienta de cambio social.

¡Un saludo y os veo en la PyData 2015!

Joyas Python del 2015

Este es un resumen de algunas joyas que he descubierto este 2015 dentro del mundo Python. Que las haya descubierto en el 2015 no significa que necesariamente sean cosas novedosas pero la mayoría siguen de actualidad. Tampoco es un resumen ordenado. de hecho, es un pequeño cajón de sastre. Tampoco es temático sobre ciencia, aunque la mayoría están relacionadas con ciencia ya que es a lo que me dedico. En las siguientes líneas nombro muchas cosas pero solo incluyo enlaces sobre las joyas de las que quiero hablar.

WEB:

  • En el pasado he trasteado algo con Django para hacer cosas que se puedan compartir con mucha otra gente. Django es un framework web muy completo o, como se suele decir, con baterías incluidas y el de más amplio uso dentro del mundo Python. El hecho de incluir tantas cosas de uso habitual en un desarrollo web es su fuerte para la mayoría de desarrolladores pero también su talón de Aquiles para gente que solo lo usa de vez en cuando para hacer cosas simples. Demasiado sobrecargado para acordarte de todo ello cuando lo usas muy eventualmente y demasiado condicionante para hacer cosas simples sin un guión claro. Es por ello que este año he empezado a trastear con Flask. Lo recomiendo para gente que quiere convertir una idea simple en algo usable en poco tiempo. He prestado algo de atención a wagtail y me gustaría encontrar un tutorial para gente especialmente lerda en desarrollo web (que no se lo vendan a un desarrollador Django, vamos) y que no tiene tiempo.
  • Relacionado con el trasteo anterior, he empezado a trastear también con SQLAlchemy. Como Flask no te aporta de serie su propia idea de ORM, como sí hace Django, puedes acoplar el ORM que elijas, usar SQL a capón, Mongo,... Facilita mucho el trabajo con BBDD. Y aquí puedes encontrar una serie de recursos relacionados con SQLAlchemy.
  • También relacionado con el uso de Flask, he estado trasteando con Babel para internacionalizar 'cosas' (poder hacer uso de distintos idiomas). Es increible la facilidad de uso pero he encontrado ciertos problemas que no he sabido resolver (aun no sé muy bien porqué, seguramente mi poca experiencia con la biblioteca).

GRÁFICAS:

  • ¿Quieres hacer un mapa interactivo con Python? Hasta ahora había usado mis propias soluciones. Mis soluciones son fáciles de usar y fácilmente portables a la web de forma independiente pero requieren aprender, por ejemplo, OpenLayers o Leaflet y para hacer algo simple puede resultar excesivo. Pero para otros casos de uso hay otras soluciones que pueden resultar más convenientes. Es por ello que en los últimos tiempos he estado usando Folium. Es muy simple de usar y para según que necesidad es muy apropiado. Por otra parte, quizá su diseño limite un poco las posibilidades reales. Es por ello que, después de investigar un poco, descubrí mplleaflet. Esta última librería sigue la misma filosofía que mpld3, usa matplotlib y exporta el código en algo que es capaz de interpretar la librería javascript de turno (d3js para el caso de mpld3 o leaflet para el caso de mplleaflet). Las posibilidades de uso que se me ocurren para mplleaflet son muchas.
  • Otra joyita para hacer análisis estadístico y visualización es Seaborn. Es una delicia ver como con tan poco se puede hacer tanto. Una capa sobre otra capa sobre otra capa,..., dan un gran poder con un mínimo esfuerzo. Se pierde poder de personalización pero se gana inmediatez y, en el mundo del 'loquieroparahoy', es una gran ventaja eso de la inmediatez.
  • Una pequeña tontería pero que te puede resultar útil en algún sistema donde es difícil usar un interfaz gráfico o quieres tener algo ligero para hacer gráficas de ¿baja calidad? sería bashplotlib (hasta el nombre mola).
  • He empezado a trastear algo con Plotly pero los resultados no han sido especialmente buenos (le tendré que dar una nueva oportunidad en 2016):

UTILIDADES:

  • Una pequeña tontada para el día a día sería tqdm, que te añade una barra de progreso a los bucles de tu código.

RENDIMIENTO:

  • La depuración y optimización de código siempre es algo árido y gris. La optimización prematura es la raiz de todo mal. Juntamos las churras con las merinas y nos sale que tienes que probar line_profiler sin ningún género de dudas. Date un paseo por tú código paso a paso y descubre qué es lo que está haciendo que tooooodo sea tan lento.
  • Para correr código más rápido en Python mis opciones de hoy en día serían, por orden de qué es lo que intentaría antes, numba (si es código científico) o pypy (si solo uso numpy mezclado con cosas más estándar que no dependan de bibliotecas que usan la C-API de CPython). Si Numba no funciona y pypy no resuleve la papeleta valoro si el código lo voy a necesitar ejecutar muchas veces y el tiempo que tarda es inasumible en la mayoría de ocasiones y, si es así, tiro de Cython que, con un poco de esfuerzo y afeando un poco el código Python original, permite obtener velocidades cercanas a o del mismo orden que C/C++/Fortran.

LIBRERÍA ESTÁNDAR Y ALGUNAS ALTERNATIVAS:

  • De la librería estándar he estado usando bastante argparse, collections e itertools. Los tres no tienen nada que ver, los tres son muy potentes y, sabiendo usarlos, los tres se hacen imprescindibles. Quizá para el año que viene me ponga como deberes mirar más a fondo click como mejora a argparse y functools, toolz y/o CyToolz en combinación con collections e itertools.

AÑO 2016 (DEBERES QUE ME PONGO):

  • dask
  • Más PyTables.
  • Creo que le voy a dar bastante más a d3js (por dictadura del navegador).
  • scikit-extremes, mi propia solución al análisis de extremos univariantes en Python (se aceptan ayudas).
  • PyMC y/o PyMC3.

¿Y vuestras joyitas? Animáos a compartirlas en los comentarios, independientemente que estén relacionadas con ciencia o no.

Saludos y feliz año!!

The Software Sustainability Institute

Y eso... ¿qué es?

El instituto de software sostenible (SSI por sus siglas del inglés) es una organización con sede en el Reino Unido cuyo objetivo es mejorar el software en el ámbito de la investigación, cubriendo áreas de ciencia, humanidades y arte. Su lema es claro:

"Un mejor software, una mejor investigación".

Según los resultados de una encuesta que realizaron el año pasado, siete de cada diez investigadores en Reino Unido no pueden llevar a cabo su investigación sin software. Lo que muestra que el software es una pieza imprescindible en la investigación de hoy en día.

Por otro lado, el término sostenible, en software, significa que lo que uses hoy puedas usarlo mañana, y que no se haya abandonado. Esto, que puede parecer una trivialidad, no es una cosa que se haya tenido en cuenta en muchas disciplinas por mucho tiempo. La mejor manera de ponerse en situación es pensar en esas películas que grabaste de la tele o de tu videocámara allá por los noventa, ¿quieres verlas ahora? ¿tienes un aparato de esos que solían estar debajo del televisor? ¿aún funciona? Bueno, pues en ciencia pasa tres cuartos de lo mismo... pero el problema es bastante más grave. Muchos de los datos, medidas y resultados de experimentos que nos han hecho avanzar hasta donde estamos hoy están perdidos, y con ellos los programas que se usaron. Razones varias, pero una de ellas es que cuando algo se publicaba el software o los datos no eran requeridos. El SSI intenta concienciar a la comunidad investigadora de estos problemas, y dispone de manuales y cursos para que los científicos no sigan tropezando en esa roca.

Continue reading

Breve introducción a los Sistemas de Recomendación

En este pequeño tutorial, vamos a hablar sobre Sistemas de Recomendación.

Es posible que no sepas que son, sin embargo interactúas constantemente con ellos en Internet.

amazon

Cada vez que Amazon te sugiere productos relacionados...

netflix

O cuando Netflix o Youtube te recomiendan contenido que te puede interesar...

La finalidad de un sistema de recommendación es predecir la valoración que un usuario va a hacer de un ítem que todavía no ha evaluado.

Esta valoración se genera al analizar una de dos cosas, o las características de cada item, o las valoraciones de cada usuario a cada item, y se usa para recomendar contenido personalizado a los usuarios.

Hay dos tipos principales de sistemas de recomendación:

  • Filtrado de Contenido. Las recomendaciones están basadas en las características de cada item.
  • Filtrado Colaborativo. Las recomendaciones están basadas en las valoraciones existentes de los usuarios.

En este tutorial vamos a trabajar con el dataset de MovieLens. Este dataset contiene valoraciones de películas sacadas de la página web MovieLens (https://movielens.org/).

El dataset consiste en múltiples archivos, pero los que vamos a usar en este artículo son movies.dat y ratings.dat.

Primero nos descargamos el dataset:

 
wget http://files.grouplens.org/datasets/movielens/ml-1m.zip
unzip ml-1m.zip
cd ml-1m/

Filtrado de Contenido

Aquí están las primeras líneas del archivo movies.dat. El archivo tiene el formato:

movie_id::movie_title::movie genre(s)

 
head movies.dat

1::Toy Story (1995)::Animation|Children's|Comedy
2::Jumanji (1995)::Adventure|Children's|Fantasy
3::Grumpier Old Men (1995)::Comedy|Romance
4::Waiting to Exhale (1995)::Comedy|Drama
5::Father of the Bride Part II (1995)::Comedy
6::Heat (1995)::Action|Crime|Thriller
7::Sabrina (1995)::Comedy|Romance
8::Tom and Huck (1995)::Adventure|Children's
9::Sudden Death (1995)::Action
10::GoldenEye (1995)::Action|Adventure|Thriller

Los géneros de cada película están separados por un pipe |.

Cargamos el archivo movies.dat:

 
import pandas as pd
import numpy as np
movies_df = pd.read_table('movies.dat', header=None, sep='::', names=['movie_id', 'movie_title', 'movie_genre'])

movies_df.head()

Out[]:

movie_id movie_title movie_genre
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama

Para poder usar la columna movie_genre, tenemos que convertirla en un grupo de campos llamados dummy_variables.

Esta función convierte una variable categórica (por ejemplo, el genéro de la película puede ser Animation, Comedy, Romance...), en múltiples columnas (una columna para Animation, una columna para Comedy, etc).

Para cada película, éstas columnas dummy tendrán un valor de 0 excepto para aquellos géneros que tenga la película.

 
# we convert the movie genres to a set of dummy variables 
movies_df = pd.concat([movies_df, movies_df.movie_genre.str.get_dummies(sep='|')], axis=1)
movies_df.head()

Out[]:

movie_id movie_title movie_genre Action Adventure Animation Children's Comedy Crime Documentary ...
0 1 Toy Story (1995) Animation|Children's|Comedy 0 0 1 1 1 0 0 ...
1 2 Jumanji (1995) Adventure|Children's|Fantasy 0 1 0 1 0 0 0 ...
2 3 Grumpier Old Men (1995) Comedy|Romance 0 0 0 0 1 0 0 ...
3 4 Waiting to Exhale (1995) Comedy|Drama 0 0 0 0 1 0 0 ...
4 5 Father of the Bride Part II (1995) Comedy 0 0 0 0 1 0 0 ...

Por ejemplo, la película con una id de 1, Toy Story, pertenece a los géneros Animation, Children's y Comedy, y por lo tanto las columnas Animation, Children's y Comedy tienen un valor de 1 para Toy Story.

 
movie_categories = movies_df.columns[3:]
movies_df.loc[0]

Out[]:

movie_id                                 1
movie_title               Toy Story (1995)
movie_genre    Animation|Children&apos;s|Comedy
Action                                   0
Adventure                                0
Animation                                1
Children&apos;s                          1
Comedy                                   1
Crime                                    0
Documentary                              0
Drama                                    0
Fantasy                                  0
Film-Noir                                0
Horror                                   0
Musical                                  0
Mystery                                  0
Romance                                  0
Sci-Fi                                   0
Thriller                                 0
War                                      0
Western                                  0
Name: 0, dtype: object

El filtrado de contenidos es una manera bastante simple de construir un sistema de recomendación. En este método, los items (en éste ejemplo las películas), se asocian a un grupo de características (en este caso los géneros cinematográficos).

Para recomendar items a un usuario, primero dicho usuario tiene que especificar sus preferencias en cuanto a las características.

En el ejemplo de Movielens, el usuario tiene que especificar qué generos le gustan y cuánto le gustan.

Por el momento tenemos todas las columnas categorizadas por géneros.

Vamos a crear un usuario de ejemplo, con unos gustos cinematográficos enfocados a películas de acción, aventura y ficción:

 
from collections import OrderedDict

user_preferences = OrderedDict(zip(movie_categories, []))

user_preferences['Action'] = 5
user_preferences['Adventure'] = 5
user_preferences['Animation'] = 1
user_preferences["Children's"] = 1
user_preferences["Comedy"] = 3
user_preferences['Crime'] = 2
user_preferences['Documentary'] = 1
user_preferences['Drama'] = 1
user_preferences['Fantasy'] = 5
user_preferences['Film-Noir'] = 1
user_preferences['Horror'] = 2
user_preferences['Musical'] = 1
user_preferences['Mystery'] = 3
user_preferences['Romance'] = 1
user_preferences['Sci-Fi'] = 5
user_preferences['War'] = 3
user_preferences['Thriller'] = 2
user_preferences['Western'] =1

Ahora que tenemos las preferencias del usuario, para calcular la puntuación que dicho usuario daría a cada película sólo tenemos que hacer el producto vectorial de las preferencias del usuario con las características de cada película.

 
#En producción usaríamos np.dot, en vez de escribir esta función, la pongo como ejemplo.
def dot_product(vector_1, vector_2):
    return sum([ i*j for i,j in zip(vector_1, vector_2)])

def get_movie_score(movie_features, user_preferences):
    return dot_product(movie_features, user_preferences)

Ahora podemos computar la puntuación de la película Toy Story, una película de animación infantil, para el usuario del ejemplo.

 
toy_story_features = movies_df.loc[0][movie_categories]
toy_story_features
>Action         0
Adventure      0
Animation      1
Children&apos;s     1
Comedy         1
Crime          0
Documentary    0
Drama          0
Fantasy        0
Film-Noir      0
Horror         0
Musical        0
Mystery        0
Romance        0
Sci-Fi         0
Thriller       0
War            0
Western        0
Name: 0, dtype: object
 
toy_story_user_predicted_score = dot_product(toy_story_features, user_preferences.values())
toy_story_user_predicted_score

Out[]:

5

Para este usuario, Toy Story tiene una puntuación de 5. Lo cual no significa mucho por sí mismo, sólo si comparamos dicha puntuación con la puntuación de las otras películas.

Por ejemplo,, calculamos la puntuación de Die Hard (La Jungla de Cristal), una película de acción.

 
movies_df[movies_df.movie_title.str.contains('Die Hard')]
movie_id movie_title movie_genre Action Adventure Animation Children's Comedy Crime Documentary ...
163 165 Die Hard: With a Vengeance (1995) Action|Thriller 1 0 0 0 0 0 0 ...
1023 1036 Die Hard (1988) Action|Thriller 1 0 0 0 0 0 0 ...
1349 1370 Die Hard 2 (1990) Action|Thriller 1 0 0 0 0 0 0 ...
 
die_hard_id = 1036
die_hard_features = movies_df[movies_df.movie_id==die_hard_id][movie_categories]
die_hard_features.T 

Out[]:

1023
Action 1
Adventure 0
Animation 0
Children's 0
Comedy 0
Crime 0
Documentary 0
Drama 0
Fantasy 0
Film-Noir 0
Horror 0
Musical 0
Mystery 0
Romance 0
Sci-Fi 0
Thriller 1
War 0
Western 0
  • Nota, 1023 es el índice interno del dataframe, no el índice de la película Die Hard en Movielens*
 
die_hard_user_predicted_score = dot_product(die_hard_features.values[0], user_preferences.values())
die_hard_user_predicted_score

Out[]:

8

Vemos que Die Hard tiene una puntuación de 8 y Toy Story de 5, asi que Die Hard sería recomendada al usuario antes que Toy Story. Lo cual tiene sentido teniendo en cuenta que a nuestro usuario de ejemplo le encantan las películas de acción.

Una vez sabemos como calcular la puntuación para una película, recomendar nuevas películas es tan fácil como calcular las puntuaciones de cada película, y luego escoger aquellas con una puntuación más alta.

 
def get_movie_recommendations(user_preferences, n_recommendations):
    #we add a column to the movies_df dataset with the calculated score for each movie for the given user
    movies_df['score'] = movies_df[movie_categories].apply(get_movie_score, 
                                                           args=([user_preferences.values()]), axis=1)
    return movies_df.sort_values(by=['score'], ascending=False)['movie_title'][:n_recommendations]
    
get_movie_recommendations(user_preferences, 10)    

Out[]:

2253                                       Soldier (1998)
257             Star Wars: Episode IV - A New Hope (1977)
2036                                          Tron (1982)
1197                              Army of Darkness (1993)
2559     Star Wars: Episode I - The Phantom Menace (1999)
1985                      Honey, I Shrunk the Kids (1989)
1192    Star Wars: Episode VI - Return of the Jedi (1983)
1111                                    Abyss, The (1989)
1848                                    Armageddon (1998)
2847                                  Total Recall (1990)
Name: movie_title, dtype: object</pre>

Asi que vemos que el sistema de recomendación recomienda películas de acción y de ciencia ficción.

El filtrado de contenidos hace que recomendar nuevas películas a un usuario sea muy fácil, ya que los usuarios simplemente tienen que indicar sus preferencias una vez. Sin embargo, este sistema tiene algunos problemas:

  • Hay que categorizar cada item nuevo manualmente en funcion a las características existentes.
  • Las recomendaciones son limitadas, ya que por ejemplo, los items existentes no se pueden clasificar en función de una nueva categoría.

Hemos visto que el filtrado de contenidos es quizás una manera demasiado simple de hacer recomendaciones, lo que nos lleva a...

Filtrado Colaborativo

El filtrado colaborativo es otro método distinto de predecir puntuaciones de usuarios a items. Sin embrago, en este método usamos las puntuaciones existentes de usuarios a items para predecir los items que no han sido valorados por el usuario al que queremos recomendar.

Para ello asumimos que las recomendaciones que le hagamos a un usuario serán mejores si las basamos en usuarios con gustos similares.

Para este ejemplo usaremos el archivo ratings.dat, que tiene el siguiente formato:

user_id::movie_id::rating::timestamp

head ratings.dat

1::1193::5::978300760
1::661::3::978302109
1::914::3::978301968
1::3408::4::978300275
1::2355::5::978824291
1::1197::3::978302268
1::1287::5::978302039
1::2804::5::978300719
1::594::4::978302268
1::919::4::978301368

El dataset de Movielens contiene un archivo con más de un millón de valoraciones de películas hechas por usuarios.

 
ratings_df = pd.read_table('ratings.dat', header=None, sep='::', names=['user_id', 'movie_id', 'rating', 'timestamp'])

#Borramos al fecha en la que el rating fue creado.
del ratings_df['timestamp']

#reemplazamos la id de la película por su titulo para tener una mayor claridad
ratings_df = pd.merge(ratings_df, movies_df, on='movie_id')[['user_id', 'movie_title', 'movie_id','rating']]

ratings_df.head()

Out[]:

user_id movie_title movie_id rating
0 1 One Flew Over the Cuckoo's Nest (1975) 1193 5
1 2 One Flew Over the Cuckoo's Nest (1975) 1193 5
2 12 One Flew Over the Cuckoo's Nest (1975) 1193 4
3 15 One Flew Over the Cuckoo's Nest (1975) 1193 4
4 17 One Flew Over the Cuckoo's Nest (1975) 1193 5

De momento tenemos una matriz de usuarios y películas, vamos a convertir ratings_df a una matriz con un usuario por fila y una película por columna.

 
ratings_mtx_df = ratings_df.pivot_table(values='rating', index='user_id', columns='movie_title')
ratings_mtx_df.fillna(0, inplace=True)

movie_index = ratings_mtx_df.columns

ratings_mtx_df.head()

Out[]:

movie_title $1,000,000 Duck (1971) 'Night Mother (1986) 'Til There Was You (1997) ...
user_id
1 0 0 0 ...
2 0 0 0 ...
3 0 5 0 ...
4 0 0 1 ...
5 0 0 0 ...

Nos queda una matriz de 6064 usuarios y 3706 películas.

Para computar la similaridad entre películas, una manera de hacerlo es calcular la correlación entre ellas en función de la puntuación que dan los usuarios.

Una manera fácil de calcular la similaridad en python es usando la función numpy.corrcoef, que calcula el coeficiente de correlación de Pearson(PMCC)](https://es.wikipedia.org/wiki/Coeficiente_de_correlaci%C3%B3n_de_Pearson) entre cada pareja de items.

El PMCC tiene un valor entre -1 y 1 que mide cuán relacionadas están un par de variables cuantitativas.

La matriz de correlación es una matriz de tamaño m x m, donde el elemento Mij representa la correlación entre el item i y el item j.

 
corr_matrix = np.corrcoef(ratings_mtx_df.T)
corr_matrix.shape

Out[]:

(3706, 3706)

*Nota: Usamos la matriz traspuesta de ratings_mtx_df para que la función np.corrcoef nos devuelva la correlación entre películas. En caso de no hacerlo nos devolvería la correlación entre usuarios.

Una vez tenemos la matriz, si queremos encontrar películas similares a una concreta, solo tenemos que encontrar las películas con una correlación alta con ésta.

 
favoured_movie_title = 'Toy Story (1995)'

favoured_movie_index = list(movie_index).index(favoured_movie_title)

P = corr_matrix[favoured_movie_index]

#solo devolvemos las películas con la mayor correlación con Toy Story
list(movie_index[(P>0.4) & (P<1.0)])

Out[]:

['Aladdin (1992)',
 "Bug's Life, A (1998)",
 'Groundhog Day (1993)',
 'Lion King, The (1994)',
 'Toy Story 2 (1999)']

Vemos que los resultados son bastante buenos.

Ahora, si queremos recomendar películas a un usuario, solo tenemos que conseguir la lista de películas que dicho usuario ha visto. Ahora, con dicha lista, podemos sumar las correlaciones de dichas películas con todas las demás y devolver las películas con una mayor correlación total.

 
def get_movie_similarity(movie_title):
    '''Devuelve el vector de correlación para una película'''
    movie_idx = list(movie_index).index(movie_title)
    return corr_matrix[movie_idx]

def get_movie_recommendations(user_movies):
    '''Dado un grupo de películas, devolver las mas similares'''
    movie_similarities = np.zeros(corr_matrix.shape[0])
    for movie_id in user_movies:
        movie_similarities = movie_similarities + get_movie_similarity(movie_id)
    similarities_df = pd.DataFrame({
        'movie_title': movie_index,
        'sum_similarity': movie_similarities
        })
    similarities_df = similarities_df[~(similarities_df.movie_title.isin(user_movies))]
    similarities_df = similarities_df.sort_values(by=['sum_similarity'], ascending=False)
    return similarities_df

Por ejemplo, vamos a seleccionar un usuario con preferencia por las películas infantiles y algunas películas de acción.

 
sample_user = 21
ratings_df[ratings_df.user_id==sample_user].sort_values(by=['rating'], ascending=False)

Out[]:

user_id movie_title movie_id rating
583304 21 Titan A.E. (2000) 3745 5
707307 21 Princess Mononoke, The (Mononoke Hime) (1997) 3000 5
70742 21 Star Wars: Episode VI - Return of the Jedi (1983) 1210 5
239644 21 South Park: Bigger, Longer and Uncut (1999) 2700 5
487530 21 Mad Max Beyond Thunderdome (1985) 3704 4
707652 21 Little Nemo: Adventures in Slumberland (1992) 2800 4
708015 21 Stop! Or My Mom Will Shoot (1992) 3268 3
706889 21 Brady Bunch Movie, The (1995) 585 3
623947 21 Iron Giant, The (1999) 2761 3
619784 21 Wild Wild West (1999) 2701 3
4211 21 Bug's Life, A (1998) 2355 3
368056 21 Akira (1988) 1274 3
226126 21 Who Framed Roger Rabbit? (1988) 2987 3
41633 21 Toy Story (1995) 1 3
34978 21 Aladdin (1992) 588 3
33432 21 Antz (1998) 2294 3
18917 21 Bambi (1942) 2018 1
612215 21 Devil's Advocate, The (1997) 1645 1
617656 21 Prince of Egypt, The (1998) 2394 1
440983 21 Pinocchio (1940) 596 1
707674 21 Messenger: The Story of Joan of Arc, The (1999) 3053 1
708194 21 House Party 2 (1991) 3774 1

Ahora podemos proporcionar nuevas recomendaciones para dicho usuario teniendo en cuenta las películas que ha visto como input.

 
sample_user_movies = ratings_df[ratings_df.user_id==sample_user].movie_title.tolist()
recommendations = get_movie_recommendations(sample_user_movies)

#Obtenemos las 20 películas con mejor puntuación
recommendations.movie_title.head(20)

Out[]:

1939                     Lion King, The (1994)
324                Beauty and the Beast (1991)
1948                Little Mermaid, The (1989)
3055    Snow White and the Seven Dwarfs (1937)
647                     Charlotte&apos;s Web (1973)
679                          Cinderella (1950)
1002                              Dumbo (1941)
301                              Batman (1989)
3250            Sword in the Stone, The (1963)
303                      Batman Returns (1992)
2252                              Mulan (1998)
2924                Secret of NIMH, The (1982)
2808                         Robin Hood (1973)
3026                    Sleeping Beauty (1959)
1781                   Jungle Book, The (1967)
260         Back to the Future Part III (1990)
259          Back to the Future Part II (1989)
2558                          Peter Pan (1953)
2347             NeverEnding Story, The (1984)
97                  Alice in Wonderland (1951)
Name: movie_title, dtype: object

Vemos que el sistema recomienda mayoritariamente películas infantiles y algunas películas de acción.

El ejemplo que he puesto sobre filtrado colaborativo es un ejemplo muy simple, y no tiene en cuenta las valoraciones que cada usuario ha hecho a cada película (solo si las ha visto).

Una manera más eficaz de hacer filtrado colaborativo es vía Descomposición en valores singulares (SVD). Es un tópico que da para otro artículo pero este artículo lo explica con bastante claridad.

El filtrado colaborativo se usa con frecuencia en la actualidad. Es capaz de recomendar nuevos items sin tener que clasificarlos manualmente en función de sus características. Además, es capaz de proporcionar recomendaciones basadas en características ocultas que no serían obvias a primera vista (por ejemplo, combinaciones de géneros o de actores).
Sin embargo, el filtrado colaborativo tiene un problema importante, y es que no puede recomendar items a un usuario a menos que dicho usuario haya valorado items, este problema se llama problema de Arranque en frío.

Una manera de solucionar ésto es usar un sistema híbrido de filtrado de contenido + filtrado colaborativo, usando el filtrado de contenidos para nuevos usuarios y filtrado colaborativo para usuarios de los que se tiene suficiente información.

Lista de lecturas

Aquí hay una lista de lecturas sobre sisetmas de recomendación (en inglés)

Aprende historia gracias a geocodificación inversa, mapping y wikipedia

El otro día, mientras esperaba en Juan Bravo a un amigo, tuve algo de tiempo para divagar y entre esas divagaciones junté Juan Bravo, Python, Internet, geolocalización, historia,... En fin, que estaba en Juan Bravo, no tenía ni idea de quien era ese señor (llamadme ignorante), tenía algo de tiempo y se me ocurrió poder obtener información de calles a partir de un mapa y de la wikipedia y, de aquellos polvos, estos lodos, y nació map2wiki.

¿Qué es map2wiki?

En pocas palabras, es una aplicación web que te permite buscar una calle/avenida/plaza/... en un mapa y obtener información sobre lo que le da nombre a esa dirección.

¿Por qué no buscarlo directamente en la wikipedia?

Porque eso no es tan divertido y no hubiera aprendido nada sobre Flask, Jinja2, geocodificación inversa, OpenStreetMap, Nominatim, OpenLayers, Javascript, Brython, la API de la wikipedia, mis nulas aptitudes para el diseño web (de forma expresa no quería usar cosas como bootstrap para mantenerlo lo más simple posible) aunque podría ser peor, el infierno CSS...

CSS hell

Pero vayamos por partes...

¿Qué es la geocodificación inversa?

La geocodificación inversa (reverse geocoding en inglés) permite, a partir de unas coordenadas geográficas, obtener información sobre una dirección, por ejemplo, u otras cosas que se encuentren en esas coordenadas o cerca.

OpenStreetMap ofrece una API, Nominatim, que permite hacer eso mismo, a partir de unas coordenadas geográficas se obtiene información de su base de datos.

¿Cómo funciona?

En este post voy a relatar un poco el Así se hizo sin ver código, que a veces es más interesante que la película en sí. En otro post comentaré un poco el código por si alguien quiere utilizar alguna parte para otros proyectos.

Existen varias partes que se conectan entre sí.

  • Accedes a una página servida por Flask que ofrece un mapa, gracias a openlayers + openstreetmap.
  • En el mapa, nos podemos mover hasta una dirección que debe estar en español, ya que solo está pensada para ser usada en español. En el frontend tenemos la dirección central del mapa almacenada en una variable para la latitud y otra para la longitud (gracias a Brython/JS).
  • Con la dirección definida, pulsamos sobre el botón de buscar en la wikipedia. Este botón conecta un formulario en HTML, en el cliente, con Flask, en el servidor. Tenemos un formulario con algunos campos ocultos al que le pasaremos las coordenadas obtenidas en el frontend para que sean manejadas en Python gracias a Flask.
  • Una vez que tenemos las coordenadas en Python hacemos varias cosas:
    • Primero, vamos a la API de Nominatim y le metemos las coordenadas para que nos devuelva un JSON con la información de la dirección relacionada con esas coordenadas.
    • De ese JSON extraemos la información relevante a la dirección y la 'limpiamos' (sanitizar no está en el diccionario de la RAE). En la dirección se eliminan los siguientes términos junto con los posibles preposiciones y/o artículos que pueden acompañar a esa dirección ('calle de ...', 'avenida de los ...',...)
 
["alameda", "avenida", "bulevar", "calle", "camino", 
 "carrera", "cuesta", "pasaje", "pasadizo", "paseo", "plaza", 
 "rambla", "ronda", "travesia", "via"]
  • Seguimos:
    • Con la dirección sanitizada solo nos debería quedar el nombre de la dirección ya limpio. Por ejemplo, 'Paseo de la Marquesa de Python' debería quedar como 'Marquesa de Python'. Esa dirección ya limpia se la pasamos a la API de la Wikipedia usando la librería wikipedia que podéis encontrar en pypi. Si es posible encontrar algo en la wikipedia usando el valor que le hemos pasado nos devolverá un objeto con la información relevante del artículo.
    • Con el objeto con la información de la wikipedia obtenido extraemos parte de la información y la formateamos para mostrarla en la página.
    • Una vez tenemos la información de Nominatim (el JSON con la información de la dirección) y la información devuelta por la Wikipedia tenemos todo listo para que, mediante Flask, pasar esa información a una plantilla Jinja2, que construirá el HTML final con la información del JSON obtenido y de la Wikipedia, en caso de que sea posible, o un mensaje de error, en el caso de que no sea posible.

Y este es, principalmente, todo el proceso.

En el próximo artículo nos meteremos un poco más en las tripas para poder entender mejor como se unen las piezas. Lo que veamos no pretenderá ser algo exhaustivo sobre Flask, Jinja2 u otras tecnologías.

Espero que a alguien le resulte útil:

  1. la aplicación en sí, para aprender algo de historia,
  2. la explicación del como se hizo la aplicación, para entender como se juntan las piezas del rompecabezas en una aplicación con una estructura extremadamente simple y sin base de datos detrás.

Hasta la próxima entrada.