Microentradas: Unicode/látex en la consola IPython/notebook Jupyter

[Este es un TIL que vi el otro día. Lo dejo aquí por si otros no lo sabían y les resulta útil.]

En la consola de IPython o en el notebook Jupyter podéis usar unicode escribiendo símbolos látex.

Por ejemplo, si escribes lo siguiente (en la consola o en una celda del notebook):

In [1]: \alpha

y pulsáis después la tecla tab veréis que se transforma a su símbolo látex y lo podéis usar fácilmente en vuestro código.

El resultado sería algo como lo siguiente (antes y después de pulsar la tecla tab):

  • Antes:

  • Después:

Esto puede ser útil para scripts propios, demos, formación,..., pero os lo desaconsejo en código en producción o a compartir 😉

Saludos.

Microentradas: Evitar ciertas etiquetas en la leyenda en Matplotlib

A veces, me llegan ficheros de datos con datos cada hora o cada día y los quiero representar en un plot. Para ello, podría acumular los ficheros en uno solo y luego pintarlo pero como lo debo hacer en 'tiempo casi-real' se puede meter todo en un bucle while que espera los ficheros cada hora/día/lo que sea y va pintando cada variable por tramos. Por ejemplo, una aproximación podría ser la siguiente:

import numpy as np
import matplotlib.pyplot as plt
plt.style.use('bmh')
%matplotlib inline

plt.figure(figsize = (12, 6))
for i in range(10):
    x = np.arange(i * 10, i * 10 + 10)
    y_var1 = np.random.randint(1, 5, 10)
    y_var2 = np.random.randint(5, 8, 10)
    plt.plot(x, y_var1, color = 'k', label = 'variable1')
    plt.plot(x, y_var2, color = 'g', label = 'variable2')
    plt.legend()
    plt.ylim(0, 9)

Como véis, en la gráfica anterior hay varios problemas pero como esta es una MicroEntrada solo nos vamos a centrar en el problema de las etiquetas repetidas en la leyenda.

¿Cómo podríamos evitar el meter tantas veces una etiqueta repetida?

Mi problema es que el bucle es o podría ser 'infinito' y tengo que inicializar las etiquetas de alguna forma. Si miro en esta respuesta encontrada en Stackoverflow dice que en la documentación se indica que "If label attribute is empty string or starts with “_”, those artists will be ignored." pero si busco aquí o en el enlace que indican en la respuesta en Stackoverflow no veo esa funcionalidad indicada en ningún sitio. Eso es porque aparecía en la versión 1.3.1 pero luego desapareció... Sin embargo podemos seguir usando esa funcionalidad aunque actualmente no esté documentada:

plt.figure(figsize = (12, 6))
for i in range(10):
    x = np.arange(i * 10, i * 10 + 10)
    y_var1 = np.random.randint(1, 5, 10)
    y_var2 = np.random.randint(5, 8, 10)
    plt.plot(x, y_var1, color = 'k', label = 'variable1' if i == 0 else "_esto_no_se_pintará")
    plt.plot(x, y_var2, color = 'g', label = 'variable2' if i == 0 else "_esto_tampoco")
    plt.legend()
    plt.ylim(0, 9)
Espero que a alguien le resulte útil.

Microentrada: Rendimiento de scatterplots en matplotlib

Normalmente, si vas a dibujar pocos puntos en un scatter plot lo normal es usar scatter en matplotlib. Sin embargo, si tienes que dibujar una cantidad considerable de puntos, el rendimiento puede ser un factor importante. Como alternativa se puede usar plot en lugar de scatter.

Veamos un ejemplo muy sencillo de esto y el rendimiento obtenido. vamos a dibujar 100.000 puntos aleatorios y ver los tiempos que obtenemos usando scatter y plot.

Primero importamos las librerías a usar:

import numpy as np
import matplotlib.pyplot as plt
%%matplotlib inline

Creamos 100.000 puntos aleatorios:

x = np.random.rand(100000)
y = np.random.rand(100000)

Veamos lo que tarda un scatter plot

plt.scatter(x,y)
1 loops, best of 3: 598 ms per loop

Y ahora lo mismo pero con un plot normal

plt.plot(x,y, 'bo')
100 loops, best of 3: 9.09 ms per loop

La diferencia entre ambas opciones es:

\(Rendimiento = \frac{scatter}{plot} = \frac{598}{9.09} \approx 65 \)

Motivación de esta entrada: hoy en el trabajo he tenido que escribir unos cuantos paneles de figuras 8x8, es decir, 64 figuras en cada panel, con más de 50.000 datos en cada figura y me ha parecido interesante compartir este pequeño truco que tengo por ahí guardado para estos casos 🙂

MicroEntradas: scipy.constants

¿No te acuerdas de la constante de gravitación universal?, ¿no sabes cuanta área es un acre?, ¿por qué me pasan esos resultados en pulgadas?, ¿a qué altura estamos volando cuando me dicen que estamos a 10.000 pies?,... Puedes responder a todo eso usando el, creo que infrautilizado, módulo constants dentro del paquete scipy.

Primero de todo, vamos a importat el módulo en cuestión:

from scipy import constants as constantes

En este módulo disponemos de varias constantes físicas y matemáticas de uso muy habitual en determinados campos. El número pi, la constante de gravitación universal, la constante de Plank o la masa del electrón están en la punta de tus dedos.

print(constantes.pi,
      constantes.gravitational_constant,
      constantes.Plank,
      constantes.m_e)

Nos dará el siguiente resultado:

3.141592653589793 6.67384e-11 6.62606957e-34 9.10938291e-31

Podemos acceder a otras constantes (no tan constantes) usando un diccionario con el nombre de la constante según la base de datos del Committee on Data for Science and Technology (CODATA):

constantes.value('standard atmosphere')

Podemos obtener el valor de varias unidades en el sistema internacional simplemente poniendo su nombre

print(constantes.foot, constantes.inch)

Incluso tenemos funciones para hacer conversiones de unidades

print(constantes.C2K(10))

Nos daría el valor de 10ºC en grados Kelvin.

Saludos.

 

MicroEntradas: numpy.vectorize

Hoy vamos a ver numpy.vectorize que, como la documentación oficial indica, sirve para 'vectorizar' funciones que solo aceptan escalares como entrada. La entrada que podemos meter es una lista de objetos o un 'numpyarray' y nos devolverá como resultado un 'numpyarray'.

No os engañéis, normalmente, cuando hablamos de vectorizar pensamos en varios órdenes de magnitud de mejora en el rendimiento pero eso no es lo que hace esta función :-). Podéis pensar en esta función como algo parecido a usar map.

Por ejemplo, la función abs solo acepta un entero o un float. Por ejemplo, lo siguiente:

print(abs(-3))

Nos devolvería el valor 3. Hasta ahí bien. Pero si queremos hacer lo siguiente:

print(abs([-3, -2]))

Nos devolverá un TypeError: bad operand type for abs(): 'list'.

Podríamos usar map para ello pero no obtendríamos un 'numpyarray' como salida y en Python 3 devuelve un iterador que para transformarlo a, por ejemplo, una lista o un 'numpyarray' debemos añadir un paso más, la conversión a lista (en Python 2 devuelve directamente una lista) o dos pasos, la conversión a lista y esta a 'numpyarray'.

Vamos a ver cómo podemos 'vectorizar' la función abs sin usar map y usando numpy.vectorize (antes deberéis importar numpy como np):

vectabs = np.vectorize(abs)

que podemos usar de la siguiente forma:

print(vectabs([-3, -2]))

Et voilà. ya tenemos lo que queríamos. Veamos como va el rendimiento de lo que acabamos de hacer:

In [1]: import numpy as np
In [2]: kk = np.random.randn(1000000)
In [3]: timeit np.abs(kk)
100 loops, best of 3: 4.13 ms per loop
In [4]: vectabs = np.vectorize(abs)
In [5]: timeit vectabs(kk)
10 loops, best of 3: 87.6 ms per loop
In [6]: timeit np.array([abs(i) for i in kk])
1 loops, best of 3: 241 ms per loop

La última opción es equivalente a hacer np.array(list(map(abs, kk))) en tiempo. La versión vectorizada, vectabs, es 21 veces más lenta que la que podemos obtener usando numpy.abs por lo que no tendríamos una gran ganancia pero, sin embargo, vemos que es casi tres veces más rápida que la versión usando map (o una 'list comprehension') por lo que algo ganariamos respecto a Python 3 puro :-).

Como apuntes finales, si sabéis de alguna función en CPython que no existe equivalente en numpy y la necesitáis usar quizá podéis obtener una ganancia usando numpy.vectorize. Si tenéis que escribir la función vosotros, escribidla pensando en operaciones vectorizadas usando 'numpyarrays' y no os hará falta usar numpy.vectorize.

Como punto final. recordad que np.vectorize no es más que un decorador por lo que lo siguiente sería perfectamente válido:

@np.vectorize
def mi_funcion(*args):
    ...

Saludos.

[Editado: corrección de algún bug, disculpas!!]

MicroEntradas: numpy.unravel_index

¿Cuántas veces te has encontrado que tienes que conocer un valor concreto de una matriz n-dimensional y conocer la posición en la que se encuentra ese valor en esa matriz? Yo me he visto en esa situación muchas veces y siempre he acabado creando mi función que me dijera la posición,..., hasta que he descubierto numpy.unravel_index!!!

[Para la siguiente entrada se ha usado python 2.7.3 y numpy 1.6.1 aunque debería ser compatible con python 3.x y versiones de numpy superiores a la 1.6.0, antes de la versión 1.6.0 de numpy la versión unravel_index solo permitía que le pasáramos un único índice]

Por ejemplo, imagina que quieres encontrar el valor máximo en una matriz 2D como la siguiente:

import numpy as np
# Matriz bidimensional de números aleatorios distribuidos
# siguiendo una distribución normal de 100 x 100 elementos
matriz = np.random.randn(100,100)

Para encontrar el valor máximo de la matriz podemos usar numpy.max (o numpy.amax) y para encontrar el valor de la posición de ese máximo en la matriz podemos usar numpy.argmax.

posicion_maximo = np.argmax(matriz)
print(posicion_maximo)

EL anterior código nos dará un único número. Ese valor que hemos obtenido es la posición del máximo en una matriz que ha sido 'aplanada' a un vector de 1D. Pero yo quiero conocer el valor de la x y de la y en la matriz bidimensional. Para ello existe numpy.unravel_index. Le pasamos el número que hemos encontrado y las dimensiones de la matriz original y nos dirá en qué posición se encuentra en la posición bidimensional. Veamos como se hace:

posicion_maximo_2d = np.unravel_index(posicion_maximo, matriz.shape)
print(posicion_maximo_2d)

Et voilà, ya tenemos lo que estábamos buscando.

Antes he comentado que el código solo admite su uso en versiones de numpy superiores o iguales a la 1.6.0. Esto es debido a que si, por ejemplo, quiero buscar la posición de más de un índice de una sola vez lo puedo hacer y con versiones anteriores de numpy esto no era posible. Por ejemplo, si en lugar de solo buscar el máximo quiero el máximo y el mínimo puedo hacer lo siguiente:

posicion_maximo = np.argmax(matriz)
posicion_minimo = np.argmin(matriz)
posiciones_2d = np.unravel_index([posicion_minimo, posicion_maximo], matriz.shape)
print(posiciones_2d)

Y listo. Sencillo, limpio y rápido 😀

MicroEntradas: numpy.digitize

Con esta entrada voy a inaugurar una serie de entradas que serán cortas. La idea detrás de ellas sirve a dos propósitos, fundamentalmente:

  • Ver pequeñas y, quizás, desconocidas funcionalidades prácticas que están dentro de bibliotecas de uso típicas (numpy, scipy, matplotlib,...),
  • intentar aportar algo a Pybonacci dentro de mi apretada agenda (y que se apretará más en unos pocos meses, mi tiempo se ha reducido drásticamente en los últimos tiempos).

Y basta ya de preámbulos, que esto pretende ser corto y práctico.

Hoy vamos a ver numpy.digitize que, como la documentación oficial indica, sirve para devolver los índices de los intervalos de datos a los que pertenece cada valor de un array de datos. Las entradas de la función son solo tres, el array en cuestión a evaluar (array), los valores que establecen los intervalos (bins) y una una palabra clave opcional que establece si los valores a tener en cuenta como extremos del intervalo incluyen el extremo derecho o el izquierdo del intervalo (el valor por defecto es right==False, lo cual indica que el intervalo no incluye el extremo derecho, es decir, tendríamos el siguiente caso, bins[i-1] <= x < bins[i]).

El uso puede ser algo tan simple como lo siguiente:

Problema: Para un grupo de datos con valor comprendido entre 0 y 1 encontrar los datos asociados a los siguientes intervalos y saber qué función hay que aplicar en cada intervalo:

f(x) = x cuando x <= 0.25

f(x) = 2x cuando 0.25 > x >= 0.7

f(x) = 3x cuando x > 0.7

Una solución sencilla usando np.digitize podría ser la siguiente:

import numpy as np
import matplotlib.pyplot as plt
xarray = np.random.rand(100)
intervalos = [0, 0.25, 0.7, 1]
indices = np.digitize(xarray, intervalos, right=True)
funcs = [lambda x: x, lambda x: 2*x, lambda x: 3*x]
yarray = [funcs[indice-1](x) for x, indice in zip(xarray, indices)]
plt.scatter(xarray, yarray)

El resultado final debería ser una imagen como la siguiente:

(Imagen PNG, 382 × 253 píxeles)

Quizá lo más complicado de entender sea la línea 8 de la anterior porción de código, si no entiendes algo puedes preguntar en los comentarios.

La función numpy.digitize podría ser similar en su uso a numpy.histogram y a numpy.histogramdd. En la última función se usa internamente numpy.digitize.

Saludos.