Parte 5 - Buenas prácticas

Corrección de código y pruebas

Escribir pruebas tiene muchas ventajas:

  • Comprobación de que lo que funcionaba antes sigue funcionando ahora
  • Dan una idea de cómo se usa el código
  • Ayudan a descubrir casos raros y fallos ocultos
In [1]:
from maxima import find_maxima
In [2]:
find_maxima([1, 2, 3, 2, 4, 1])
Out[2]:
[2, 4]
In [3]:
%run test_maxima.py
In [4]:
find_maxima([1, 2, 1, 2, 3])
Out[4]:
[1, 4]
In [5]:
find_maxima([1, 1, 1])
Out[5]:
[]

Nociones de optimización de código

Hay un par de trucos que podemos utilizar en IPython para medir el rendimiento del código. El primero es usa la magic function %%timeit, que mide cuánto tiempo tarda en ejecutarse una celda.

In [14]:
import numpy as np
In [15]:
a = np.random.randn(100, 200)
b = np.random.randn(100, 200)
c = np.zeros_like(a)
In [16]:
%%timeit
assert a.shape == b.shape
N = a.shape[0]
M = a.shape[1]
for i in range(N):
    for j in range(M):
        c[i, j] = a[i, j] + b[i, j]
100 loops, best of 3: 17.2 ms per loop
In [17]:
%%timeit
c = a + b
100000 loops, best of 3: 18.4 µs per loop

Observamos que la versión vectorizada con NumPy es, en mi ordenador, cerca de 1000 veces más rápida. Aun así, en cuanto empecemos a escribir código numérico de cierta magnitud nuestro programa empezará a ir lento. Puede ser que lleguemos a unos tiempos de ejecución inaceptables (8 horas para ejecutar un programa de un trabajo de clase por ejemplo). ¿Cómo hacer que vaya más rápido?

Premature optimization is the root of all evil”.

Donald E. Knuth

Existen unas herramientas llamadas profilers que ejecutan nuestro programa y analizan cuánto tiempo tarda cada función dentro de nuestro programa. Podemos usar esta valiosa información para escoger qué partes de nuestro programa tenemos que optimizar y qué partes no tenemos que optimizar.

Por ejemplo, supongamos que quiero reproducir la gráfica de la ecuación de Kepler que aparece en la Wikipedia:

In [18]:
from IPython.display import HTML
HTML('<iframe src="http://en.m.wikipedia.org/wiki/Kepler%27s_equation" width="800" height="400"></iframe>')
Out[18]:

El código sería algo así:

In [19]:
%matplotlib inline
In [20]:
import numpy as np
from scipy import optimize
import matplotlib.pyplot as plt

Utilizando la magic function %%prun y ordenando los resultados por tiempo acumulado -s cumtime obtenemos un análisis de cuáles son las partes más lentas del código. Si utilizas un valor de N alto (por ejemplo 10000) la celda tardará unos segundos en ejecutarse, y la mayor parte del tiempo se irá en resolver la ecuación para cada uno de los puntos. Sin embargo, si utilizas un N bajo (por ejemplo 10) la gráfica es de peor calidad ¡y en representarla es donde se pierde la mayor parte del tiempo!

In [21]:
%%prun -s cumtime
def kepler(E, e, M):
    """Kepler's equation."""
    return E - e * np.sin(E) - M

fig, axes = plt.subplots(figsize=(6, 6))

N = 10000
M_domain = np.linspace(0, 2 * np.pi, N)
for ecc in 0.0167, 0.249, 0.432, 0.775, 0.967:
    E_domain = np.zeros_like(M_domain)
    ii = 0
    for M in M_domain:
        sol = optimize.root(kepler, E_domain[ii], args=(ecc, M))
        E_domain[ii] = sol.x
        ii += 1

    axes.plot(M_domain, E_domain)

axes.set_xlim(0, 2 * np.pi)
axes.set_ylim(0, 2 * np.pi)
axes.set_xlabel("$M$", fontsize=15)
axes.set_ylabel("$E$", fontsize=15)
axes.set_aspect(1)
axes.grid(True)
axes.legend(["Earth", "Pluto", "Comet Holmes", "28P/Neujmin", "Halley's Comet"], loc=2)
axes.set_title("Kepler's equation solutions")
 

Por eso es fundamental analizar nuestro progama antes de optimizarlo, no sea que lo que nosotros pensamos que puede ser lento en realidad no lo sea o sea menos importante que otras partes del programa.