Penser en tableaux entiers, pas en éléments
La vectorisation consiste à écrire un calcul comme des opérations sur des tableaux entiers et à laisser NumPy exécuter la boucle en code compilé rapide. a + b additionne deux tableaux élément par élément, a * b les multiplie, a ** 2 met chaque élément au carré et np.sqrt(a) extrait chaque racine carrée. Ce sont des fonctions universelles (ufuncs). Le résultat est plus court à écrire, plus lisible et, sur de grandes données, souvent cinquante fois plus rapide que la boucle Python for équivalente, qui paie le coût de l'interpréteur pour chaque élément.
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.]Des rendements sans boucle : décalage par tranches
Un calcul qui compare chaque élément à son voisin a besoin d'une seconde copie du tableau décalée d'un cran, et les tranches la fournissent gratuitement. p[1:] est le prix d'aujourd'hui et p[:-1] celui d'hier, tous deux de longueur n - 1 : les rendements simples sont donc p[1:] / p[:-1] - 1. La même paire donne les différences (p[1:] - p[:-1], ou np.diff(p)). Les sommes et produits cumulés sont np.cumsum et np.cumprod ; une trajectoire de croissance est np.cumprod(1 + returns). Les comparaisons de tableaux entiers (r > 0) et np.where(condition, si_vrai, si_faux) remplacent le if dans une boucle.
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. ]Agrégations selon un axe, et produits matriciels
Des fonctions comme sum, mean, std, min et max réduisent un tableau. Avec un tableau 2-D de rendements (une ligne par jour, une colonne par actif), axis=0 réduit les lignes et donne une valeur par colonne (la moyenne de chaque actif dans le temps), tandis que axis=1 donne une valeur par ligne (la moyenne de chaque jour sur les actifs). Le produit matriciel R @ w, d'un tableau (jours, actifs) par un vecteur de poids de longueur actifs, donne le rendement du portefeuille de chaque jour en un seul appel : c'est le produit scalaire du sujet sur les vecteurs, fait pour toutes les lignes à la fois.
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]Moyennes mobiles avec cumsum, et les limites de la vectorisation
Une moyenne mobile sur une fenêtre w semble exiger une boucle, mais avec un total cumulé ce n'est pas le cas : placez un 0 devant np.cumsum(x) et la somme de w valeurs consécutives est une différence de deux totaux cumulés, cs[w:] - cs[:-w] ; divisez par w. Le résultat contient len(x) - w + 1 valeurs. Vectorisez l'arithmétique, pas la logique qui dépend du résultat précédent (une position qui dépend de l'exécution d'hier, par exemple) : celle-ci reste dans une boucle, ou passe plus tard à des outils compilés comme Numba. Surveillez aussi la mémoire, car chaque tableau intermédiaire est une copie complète.
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(rapide, lent) sur des données aléatoires avant de vous y fier. Soignez aussi les bords : un tableau vide, un seul élément, une série constante (écart type nul, donc un z-score divise par zéro) et des valeurs nan méritent une décision, pas un plantage ni un nombre faux silencieux.- Les opérations sur tableaux entiers (
a + b,a ** 2,np.sqrt(a)) remplacent les boucles ; comparez avecnp.where. - Tranches décalées :
p[1:] / p[:-1] - 1;cumsumetcumprodpour les totaux cumulés et la croissance. axis=0réduit les lignes (par colonne),axis=1réduit les colonnes (par ligne) ;R @ west un produit scalaire par ligne.
À suivre : le broadcasting, la règle qui permet à des tableaux de formes différentes de travailler ensemble.
Lab : ce qui est noté
- Calculer tous les rendements d'un coup avec
simple_returns25 pts - Obtenir le rendement du portefeuille de chaque jour avec
portfolio_returns25 pts - Lisser une série sans boucle avec
moving_average25 pts - Standardiser des valeurs avec
zscore25 pts