Pensar en arreglos completos, no en elementos
La vectorización consiste en escribir un cálculo como operaciones sobre arreglos completos y dejar que NumPy ejecute el bucle en código compilado rápido. a + b suma dos arreglos elemento a elemento, a * b los multiplica, a ** 2 eleva al cuadrado cada elemento y np.sqrt(a) calcula cada raíz cuadrada. Son funciones universales (ufuncs). El resultado es más corto de escribir, más legible y, con datos grandes, a menudo cincuenta veces más rápido que el bucle for de Python equivalente, que paga la sobrecarga del intérprete en cada elemento.
import numpy as np
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])
print(a + b) # [11. 22. 33.]
print(a * b) # [10. 40. 90.]
print(a ** 2) # [1. 4. 9.]
print(np.sqrt(a)) # [1. 1.41421356 1.73205081]
# The loop version of a * b, which vectorisation replaces
result = np.empty(3)
for i in range(3):
result[i] = a[i] * b[i]
print(result) # [10. 40. 90.]Rendimientos sin bucle: desplazar con porciones
Un cálculo que compara cada elemento con su vecino necesita una segunda copia del arreglo desplazada una posición, y las porciones la dan gratis. p[1:] es el precio de hoy y p[:-1] el de ayer, ambos de longitud n - 1, así que los rendimientos simples son p[1:] / p[:-1] - 1. El mismo par da diferencias (p[1:] - p[:-1], o np.diff(p)). Las sumas y productos acumulados son np.cumsum y np.cumprod; una trayectoria de crecimiento es np.cumprod(1 + returns). Las comparaciones de arreglos completos (r > 0) y np.where(condición, si_cierto, si_falso) reemplazan el if dentro de un bucle.
import numpy as np
p = np.array([100.0, 102.0, 101.0, 103.0])
returns = p[1:] / p[:-1] - 1 # today / yesterday - 1, no loop
print(np.round(returns, 4)) # [ 0.02 -0.0098 0.0198]
print(np.diff(p)) # [ 2. -1. 2.]
print(np.cumprod(1 + np.array([0.01, 0.02]))) # [1.01 1.0302]
# np.where replaces if/else inside a loop
r = np.array([0.01, -0.02, 0.03, -0.01])
print(np.where(r > 0, "up", "down")) # ['up' 'down' 'up' 'down']
print(np.where(r > 0, r, 0.0)) # [0.01 0. 0.03 0. ]Agregaciones a lo largo de un eje y productos matriciales
Funciones como sum, mean, std, min y max colapsan un arreglo. Con un arreglo 2-D de rendimientos (una fila por día, una columna por activo), axis=0 colapsa las filas y da un valor por columna (el promedio de cada activo en el tiempo), mientras que axis=1 da un valor por fila (el promedio de cada día entre activos). El producto matricial R @ w, de un arreglo (días, activos) por un vector de pesos de longitud activos, da el rendimiento de cartera de cada día en una sola llamada: es el producto escalar del tema de vectores hecho para todas las filas a la vez.
import numpy as np
# 2 days x 3 assets
R = np.array([[0.02, -0.01, 0.03],
[0.01, 0.00, 0.02]])
w = np.array([0.5, 0.3, 0.2])
print(R.shape) # (2, 3)
print(R.mean(axis=0)) # [ 0.015 -0.005 0.025] one value per asset
print(R.sum(axis=1)) # [0.04 0.03] one value per day
print(R @ w) # [0.013 0.009] portfolio return each day
# Standardise: how many standard deviations from the mean?
z = np.array([1.0, 2.0, 3.0, 4.0])
print((z - z.mean()) / z.std()) # [-1.34164079 -0.4472136 0.4472136 1.34164079]Medias móviles con cumsum y los límites de vectorizar
Una media móvil sobre una ventana w parece necesitar un bucle, pero con un total acumulado no: pon un 0 delante de np.cumsum(x) y la suma de w valores consecutivos es una diferencia de dos totales acumulados, cs[w:] - cs[:-w]; divide entre w. El resultado tiene len(x) - w + 1 valores. Vectoriza la aritmética, no la lógica que depende del resultado anterior (una posición que depende de la ejecución de ayer, por ejemplo): eso se queda en un bucle o pasa más adelante a herramientas compiladas como Numba. Vigila también la memoria, porque cada arreglo intermedio es una copia completa.
import numpy as np
x = np.array([1.0, 2.0, 4.0, 8.0, 16.0])
w = 3
cs = np.concatenate(([0.0], np.cumsum(x))) # [0, 1, 3, 7, 15, 31]
print((cs[w:] - cs[:-w]) / w) # [2.33333333 4.66666667 9.33333333]
# Check against the plain definition
print([x[i:i + w].mean() for i in range(len(x) - w + 1)]) # three values, the samenp.allclose(rápida, lenta) sobre datos aleatorios antes de fiarte. Cuida también los bordes: un arreglo vacío, un solo elemento, una serie constante (desviación típica cero, así que un z-score divide por cero) y valores nan merecen una decisión, no un fallo ni un número erróneo en silencio.- Las operaciones sobre arreglos completos (
a + b,a ** 2,np.sqrt(a)) reemplazan los bucles; compara connp.where. - Porciones desplazadas:
p[1:] / p[:-1] - 1;cumsumycumprodpara totales acumulados y crecimiento. axis=0colapsa filas (por columna),axis=1colapsa columnas (por fila);R @ wes un producto escalar por fila.
A continuación: broadcasting, la regla que permite que arreglos de formas distintas trabajen juntos.
Lab: qué se califica
- Calcular todos los rendimientos de una vez con
simple_returns25 pts - Obtener el rendimiento de la cartera de cada día con
portfolio_returns25 pts - Suavizar una serie sin bucle con
moving_average25 pts - Estandarizar valores con
zscore25 pts