要素ではなく配列全体で考える
ベクトル化とは、計算を配列全体に対する演算として書き、ループを高速なコンパイル済みコードで NumPy に実行させることです。a + b は2つの配列を要素ごとに足し、a * b は掛け、a ** 2 は各要素を2乗し、np.sqrt(a) はすべての平方根を取ります。これらはユニバーサル関数(ufunc)です。短く書けて読みやすく、大きなデータでは同等の Python の for ループより50倍ほど速いこともよくあります。for ループは要素ごとにインタープリターのオーバーヘッドを払うからです。
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.]ループなしのリターン:スライスでずらす
各要素を隣と比べる計算には、1つずらした配列のコピーがもう1つ必要で、スライスならそれがただで得られます。p[1:] は今日の価格、p[:-1] は昨日の価格で、どちらも長さ n - 1 なので、単純リターンは p[1:] / p[:-1] - 1 です。同じ組から差分も得られます(p[1:] - p[:-1] または np.diff(p))。累積和と累積積は np.cumsum と np.cumprod で、成長経路は np.cumprod(1 + returns) です。配列全体の比較(r > 0)と np.where(条件, 真のとき, 偽のとき) が、ループ内の if に取って代わります。
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. ]軸方向の集計と行列の積
sum、mean、std、min、max などの関数は配列を集約します。リターンの2次元配列(行が日、列が資産)では、axis=0 は行方向に集約して列ごとに1つの値(各資産の時間平均)を、axis=1 は行ごとに1つの値(各日の資産平均)を返します。(日数, 資産数) の配列とウェイトのベクトルの行列積 R @ w は、毎日のポートフォリオリターンを1回の呼び出しで返します。ベクトルのトピックの内積を、全行に対して一度に行ったものです。
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]cumsum による移動平均と、ベクトル化の限界
ウィンドウ w の移動平均はループが必要に見えますが、累積和を使えば不要です。np.cumsum(x) の先頭に 0 を置くと、連続する w 個の値の和は2つの累積和の差 cs[w:] - cs[:-w] になるので、w で割ります。結果は len(x) - w + 1 個です。ベクトル化するのは算術であり、直前の結果に依存するロジック(たとえば前日の約定に依存するポジション)ではありません。そうしたものはループに残すか、後で Numba のようなコンパイル型のツールに移します。中間配列はそれぞれ完全なコピーなので、メモリにも注意しましょう。
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(速い版, 遅い版) と照合しましょう。端のケースにも気を配ります。空配列、要素が1つ、定数の系列(標準偏差が0なので z スコアは0で割ることになる)、nan の値には、クラッシュや黙って誤った数値ではなく、方針の決定が必要です。- 配列全体の演算(
a + b・a ** 2・np.sqrt(a))がループの代わりになる。条件はnp.where - ずらしたスライス:
p[1:] / p[:-1] - 1。累積和・累積積はcumsum・cumprod axis=0は行を集約(列ごと)、axis=1は列を集約(行ごと)。R @ wは行ごとの内積
次はブロードキャストです。形の異なる配列を一緒に扱える規則です。
ラボ:採点される内容
simple_returnsですべてのリターンを一度に計算する25点portfolio_returnsで毎日のポートフォリオリターンを求める25点moving_averageでループなしに系列を平滑化する25点zscoreで値を標準化する25点