Escribir pruebas tiene muchas ventajas:
from maxima import find_maxima
find_maxima([1, 2, 3, 2, 4, 1])
[2, 4]
%run test_maxima.py
find_maxima([1, 2, 1, 2, 3])
[1, 4]
find_maxima([1, 1, 1])
[]
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.
import numpy as np
a = np.random.randn(100, 200)
b = np.random.randn(100, 200)
c = np.zeros_like(a)
%%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
%%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:
from IPython.display import HTML
HTML('<iframe src="http://en.m.wikipedia.org/wiki/Kepler%27s_equation" width="800" height="400"></iframe>')
El código sería algo así:
%matplotlib inline
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!
%%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.