Pensar em arrays inteiros, não em elementos
Vetorização significa escrever um cálculo como operações sobre arrays inteiros e deixar o NumPy executar o laço em código compilado rápido. a + b soma dois arrays elemento a elemento, a * b os multiplica, a ** 2 eleva cada elemento ao quadrado e np.sqrt(a) calcula cada raiz quadrada. Essas são funções universais (ufuncs). O resultado é mais curto de escrever, mais legível e, em dados grandes, muitas vezes cinquenta vezes mais rápido que o laço for equivalente do Python, que paga o custo do interpretador em 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.]Retornos sem laço: deslocar com fatias
Um cálculo que compara cada elemento com o vizinho precisa de uma segunda cópia do array deslocada de uma posição, e as fatias a dão de graça. p[1:] é o preço de hoje e p[:-1] o de ontem, ambos de comprimento n - 1, então os retornos simples são p[1:] / p[:-1] - 1. O mesmo par dá diferenças (p[1:] - p[:-1], ou np.diff(p)). Somas e produtos acumulados são np.cumsum e np.cumprod; uma trajetória de crescimento é np.cumprod(1 + returns). Comparações de arrays inteiros (r > 0) e np.where(condição, se_verdadeiro, se_falso) substituem o if dentro de um laço.
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. ]Agregações ao longo de um eixo e produtos matriciais
Funções como sum, mean, std, min e max colapsam um array. Com um array 2-D de retornos (uma linha por dia, uma coluna por ativo), axis=0 colapsa as linhas e dá um valor por coluna (a média de cada ativo no tempo), enquanto axis=1 dá um valor por linha (a média de cada dia entre os ativos). O produto matricial R @ w, de um array (dias, ativos) por um vetor de pesos de comprimento ativos, dá o retorno da carteira de cada dia numa só chamada: é o produto escalar do tópico de vetores feito para todas as linhas de uma 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]Médias móveis com cumsum e os limites de vetorizar
Uma média móvel sobre uma janela w parece precisar de um laço, mas com um total acumulado não precisa: ponha um 0 na frente de np.cumsum(x) e a soma de w valores consecutivos é uma diferença de dois totais acumulados, cs[w:] - cs[:-w]; divida por w. O resultado tem len(x) - w + 1 valores. Vetorize a aritmética, não a lógica que depende do resultado anterior (uma posição que depende da execução de ontem, por exemplo): isso fica num laço ou passa depois para ferramentas compiladas como o Numba. Atenção à memória também, pois cada array intermediário é uma cópia 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) em dados aleatórios antes de confiar. Cuide também das bordas: um array vazio, um único elemento, uma série constante (desvio padrão zero, então um z-score divide por zero) e valores nan merecem uma decisão, não uma falha nem um número errado em silêncio.- Operações sobre arrays inteiros (
a + b,a ** 2,np.sqrt(a)) substituem laços; compare comnp.where. - Fatias deslocadas:
p[1:] / p[:-1] - 1;cumsumecumprodpara totais acumulados e crescimento. axis=0colapsa linhas (por coluna),axis=1colapsa colunas (por linha);R @ wé um produto escalar por linha.
A seguir: broadcasting, a regra que permite que arrays de formatos diferentes trabalhem juntos.
Lab: o que é avaliado
- Calcular todos os retornos de uma vez com
simple_returns25 pts - Obter o retorno da carteira de cada dia com
portfolio_returns25 pts - Suavizar uma série sem laço com
moving_average25 pts - Padronizar valores com
zscore25 pts