pianofisica

Mathematics & Physics, Maxima, a bit Python & Wolfram, and Arts

Pythonで学ぶアセットアロケーション投資の効用(相関のある乱数生成)

数学の具体的な問題にPythonを使って、数学もPythonも同時に学んでしまいましょう。今回はPythonを使った相関のある乱数生成の例として、アセット(資産)アロケーション(配分)による投資リターンの期待値分布を求める問題を扱います。(※乱数の性質上、記事内で述べている計算結果の数値については再現性のあるものではなく、おおよその値であることに注意してください)


アセットクラスには、株式のほかに、債券、REITコモディティなどがあります。長期的な視点で資産を形成しようとするとき、どのような資産配分(アセットアロケーション)で投資をするのか決めることは悩ましい問題です。

投資に絶対はないと言いますが、過去のデータに基づいてどのようなリスク&リターンになるのかを確率・統計的に検証することはできます。それで未来を予測することはできませんが、数値的データによる裏付けは投資戦略を決めるうえでの手助けになるでしょう。



正規分布に従う乱数

まず手始めに、正規分布に従う乱数を4つ、10,000個ずつ生成してみます。

import numpy as np
import numpy.random as rand
import matplotlib.pyplot as plt

# 変数(乱数)の個数
n = 4

# 生成数
size = int(1e4) 

# 標準正規分布(平均値0、標準偏差1)に従う相関のない乱数の生成
x = rand.randn(n, size)

x1, x2, x3, x4 = x.tolist()

として生成されます。これらが正規分布に従っていることは、そのヒストグラムおよび期待値・標準偏差からわかります。

import statistics as st

plt.hist(x1,bins=100)
plt.show()
print("mean value:\n{}\n".format(st.mean(x1)))
print("standard deviation:\n{}\n".format(st.stdev(x1)))

x2、x3、x4についても同様です。
 



相関のある乱数

いま4つの独立な乱数 x1、x2、x3、x4 を用意しました。これらの間に一定の相関係数を持たせます。いま相関係数として

 \ \displaystyle{\qquad  C=
	\begin{pmatrix}
	1.000 & 0.290 & -0.158 & 0.105 \\
        0.290 & 1.000 & 0.060 & 0.585 \\
        -0.158& 0.060 & 1.000 & 0.643 \\
       0.105 & 0.585 & 0.643 & 1.000
	\end{pmatrix}  }

を考えてみます。これらの数値はGPIF(年金積立金管理運用独立行政法人)のwebページから引用したもので

相関係数 国内債券 国債 国内株式 外国株式
国内債券 1.000
国債 0.290 1.000
国内株式 -0.158 0.060 1.000
外国株式 0.105 0.585 0.643 1.000

という状況設定に沿ったものです。詳細については他の記事に任せますが、相関行列に従う乱数は、相関行列のコレスキー分解との積から生成することができます。

C = np.matrix([
    [1.000, 0.290, -0.158, 0.105],
    [0.290, 1.000, 0.060, 0.585],
    [-0.158, 0.060, 1.000, 0.643],
    [0.105, 0.585, 0.643, 1.000]
    ]) # 相関行列

# 相関行列に従う乱数の生成
l = np.linalg.cholesky(C) # 相関行列のコレスキー分解
z = l * x

z1, z2, z3, z4 = z.tolist()

これらが相関を持っていることは、散布図にして見比べてみることでわかります。例えば

plt.scatter(x1, x3)
plt.scatter(z1, z3)
plt.show()

ここで青点は相関のなかった場合、オレンジ点は相関を持たせた場合です。いま相関係数がそれほど大きくないので差異がわかりづらいですが、オレンジ点の集合が、青点の集合を全体的に時計回りに傾けたものになっていることがわかります。

このようにして得られた新たな変数 z1、z2、z3、z4 もまた正規分布に従います。

plt.hist(z3,bins=100)
plt.show()
print("mean value:\n{}\n".format(st.mean(z3)))
print("standard deviation:\n{}\n".format(st.stdev(z3)))

各アセットクラスのリスクとリターン

与えられた相関係数を持った乱数を得ることができました。これらは正規分布に従うので、たとえば(以下の数値はGPIFの「基本ポートフォリオの変更について」という資料にあるリスク・名目リターンの表から引用したもので、国内債券=NOMURA-BPI「除くABS」、外国債券=FTSE世界国債インデックス(除く日本、円ベース)、国内株式=TOPIX(配当込み)、外国株式=MSCI ACWI(除く日本、円ベース、配当込み)という内容です※相関係数も同様です)

国内債券 国債 国内株式 外国株式
リスク(%) 2.56 11.87 23.14 24.85
リターン(%) 0.7 2.6 5.6 7.2

という状況に則した変数にするためには、標準偏差倍だけ掛け、平均値分だけ足す必要があります。

Z1 = np.zeros(size)
Z2 = np.zeros(size)
Z3 = np.zeros(size)
Z4 = np.zeros(size)

for n in range(size):
    Z1[n] = 2.56*z1[n] + 0.7
    Z2[n] = 11.87*z2[n] + 2.6
    Z3[n] = 23.14*z3[n] + 5.6
    Z4[n] = 24.85*z4[n] + 7.2

確かめてみると

plt.hist(Z4,bins=100)
plt.show()
print("mean value:\n{}\n".format(st.mean(Z4)))
print("standard deviation:\n{}\n".format(st.stdev(Z4)))

などとなっています。


アセットアロケーションとリスク&リターン

GPIFでは、各運用資産の構成比率を

国内債券 国債 国内株式 外国株式
構成比率(%) 25 25 25 25

という4資産に均等に配分したポートフォリオを採用しています。

上記の数値に則して、このアセットバランスがどれくらいのリスク&リターンを持つのか調べてみます。

Y = np.zeros(size)
for n in range(size):
    Y[n] = 0.25*Z1[n]+0.25*Z2[n]+0.25*Z3[n]+0.25*Z4[n]

[st.mean(Y), st.stdev(Y)]

より、おおよそ期待リターン4%、リスク12%と求まります。上記GPIFの資料内でも、基本ポートフォリオの属性として期待リターン4.0%、リスク(標準偏差)12.32%と記載されているので、ここでの計算が正しいことがわかります。



アセットバランスと効率的フロンティア

個人が資産運用するうえで気になってくるのは、資産比率を変えていったときに、リスク&リターンがどのように変化していくのかということだと思います。原則として、大きなリターンには大きなリスクがともないます。しかしながら、各アセットの相関係数が1でない以上、適切な組み合わせによって効率のより良い資産運用を追求することができます。ここでは4つの各資産への重みづけをランダムに振り分け、それぞれの場合でリスク・リターンを求め、その分布の様子を観察してみます。また、比較のために、4資産のうちの両極端にある国内債券と外国株式のみの2資産からなる場合についても同様の分布を求め、あわせて図示してみます。

(計算に少し時間がかかるかもしれませんが)

test_number = int(1e3)

s1 = np.zeros(test_number)
s2 = np.zeros(test_number)
s3 = np.zeros(test_number)
s4 = np.zeros(test_number)
r1 = np.zeros(test_number)
r2 = np.zeros(test_number)
r3 = np.zeros(test_number)
r4 = np.zeros(test_number)

R = np.zeros((test_number,size))
S = np.zeros((test_number,size))
Rret = np.zeros(test_number)
Rrisk = np.zeros(test_number)
Sret = np.zeros(test_number)
Srisk = np.zeros(test_number)

for k in range(test_number):
    s1[k] = rand.randint(1,11)
    s2[k] = rand.randint(1,11)
    s3[k] = rand.randint(1,11)
    s4[k] = rand.randint(1,11)
    r1[k] = s1[k]/(s1[k]+s2[k]+s3[k]+s4[k])
    r2[k] = s2[k]/(s1[k]+s2[k]+s3[k]+s4[k])
    r3[k] = s3[k]/(s1[k]+s2[k]+s3[k]+s4[k])
    r4[k] = s4[k]/(s1[k]+s2[k]+s3[k]+s4[k])
    for n in range(size):
        R[k,n] = r1[k]*Z1[n]+r2[k]*Z2[n]+r3[k]*Z3[n]+r4[k]*Z4[n]
        S[k,n] = s1[k]/(s1[k]+s4[k])*Z1[n]+s4[k]/(s1[k]+s4[k])*Z4[n]
    Rret[k] = st.mean(R[k])
    Rrisk[k] = st.stdev(R[k])
    Sret[k] = st.mean(S[k])
    Srisk[k] = st.stdev(S[k])

plt.scatter(Rrisk,Rret)
plt.scatter(Srisk,Sret)
plt.show()

より

となります。ここで横軸がリスク、縦軸がリターン、青点の集合が4資産の比率をランダムに割り振って得られたもので、オレンジ点の集合が2資産のものです。ポイントは青点のなかに、オレンジ点より左側に来ているものが存在していることです。つまり、アセットをミックスすることで、より小さなリスクで同じ期待リターンを実現できることがわかります。また、青点集合の左側の境界は、同じリターンでリスクを最小にするもので、このラインを効率的フロンティアといいます。翻って、GPIFの資産配分ですが、期待リターン4%に対しリスク12%ほどだったわけで

plt.plot(12, 4, 'rX')
plt.scatter(Rrisk,Rret)
plt.scatter(Srisk,Sret)
plt.show()

より(少し見づらいですが、赤いバツ印がGPIFのベンチマーク

GPIFの資産比率はほぼほぼ効率的フロンティア上にあると言えます。

20年後の期待リターン分布

さて、これまでの計算では統計的平均をとるために10,000回の試行を繰り返しました。つまり、上に与えられた相関係数、およびリスク&リターンの仮定のもとで1万年間過ごしたときに、統計的にリスクとリターンはどのような値に落ち着くかということで、私たちは1万年も生きられないわけですから、あまり現実的ではありません。

現行のつみたてNISAの運用可能期間は20年間ということになっています。そこで、うえで行った1万回の試行の中からランダムに20個の試行を取り出して1年ごと20年分のリターンを決め、累積させて最終的に得られる値を求める、という試行を10,000回行ってみます。

trial = int(1e4)
initial = np.zeros(trial)

for k in range(trial):
    year = 20
    T = np.zeros(year)
    Ret = np.zeros(year)
    for y in range(year):
        # Tは乱数のリストのうち何番目を使うかをランダムに決める
        T[y] = rand.randint(0,size)
        # ここではGPIFのリターンYで計算
        Ret[y] = 1+Y[int(T[y])]/100

    initial[k] = 100   # 初期投資額
    for y in range(year):
         initial[k] = Ret[y]*initial[k]

[st.mean(initial), st.median(initial), st.stdev(initial)]

すると、おおよそ期待値220、中央値190、標準偏差120という値を得ます。ヒストグラムを図示すると

plt.hist(initial, bins=200)
plt.show()

といった分布を得ます。

negative = np.zeros(trial)

for k in range(trial):
     if (initial[k] <= 100):
        negative[k] = 1

int(sum(negative))

より、初期投資額の100を下回った回数は1,200程度で、おおよそ9割の確率で投資元本を上回る運用結果になることがわかります。また

twice = np.zeros(trial)

for k in range(trial):
     if (initial[k] >= 200):
        twice[k] = 1

int(sum(twice))

より、運用結果が200を上回った回数は4,800程度で、おおよそ5割の確率で投資元本の2倍位以上の運用結果になることがわかります。



長期投資による効用

上の計算では20年の運用期間を考えましたが、人生100年時代とも言われていて、投資期間をより長く設定することも非現実的なことではありません。そこで、30年の運用期間を考えたときに同様の計算を行ってみます。

trial = int(1e4)
initial30 = np.zeros(trial)

for k in range(trial):
    year = 30
    T = np.zeros(year)
    Ret = np.zeros(year)
    for y in range(year):
        T[y] = rand.randint(0,size)
        Ret[y] = 1+Y[int(T[y])]/100

    initial30[k] = 100   # 初期投資額
    for y in range(year):
         initial30[k] = Ret[y]*initial30[k]

[st.mean(initial30), st.median(initial30), st.stdev(initial30)]

より、平均値320、中央値260、標準偏差240ほどとなります。

negative30 = np.zeros(trial)

for k in range(trial):
     if (initial30[k] <= 100):
        negative30[k] = 1

int(sum(negative30))

すると、投資元本の100を下回った回数は約800で、元本割れは90%以上の確率で避けることができることになります。

twice30 = np.zeros(trial)

for k in range(trial):
     if (initial30[k] >= 200):
        twice30[k] = 1

int(sum(twice30))

そして注目すべきは投資元本の2倍以上の運用結果になる回数が約6,500にもなることで、6割以上の確率で2倍以上のリターンが得られることがわかります。20年運用の場合には5割程度だったので、時間を味方につけることの大切さがわかります。



外国株式100%の場合

極端なアセットバランスの例として投資額の100%を外国株式で30年間運用した場合を考えてみます。

trial = int(1e4)
initial30gaikoku = np.zeros(trial)

for k in range(trial):
    year = 30
    T = np.zeros(year)
    Ret = np.zeros(year)
    for y in range(year):
        T[y] = rand.randint(0,size)
        Ret[y] = 1+Z4[int(T[y])]/100

    initial30gaikoku[k] = 100   # 初期投資額
    for y in range(year):
         initial30gaikoku[k] = Ret[y]*initial30gaikoku[k]

[st.mean(initial30gaikoku), st.median(initial30gaikoku), st.stdev(initial30gaikoku)]

より、平均値770、中央値320、標準偏差1,470ほどとなります。平均値、中央値ともに4資産均等の場合より大きな値をとりますが、それ以上に標準偏差(リスク)が大きいことが数値的にもわかります。さらに

negative30gaikoku = np.zeros(trial)

for k in range(trial):
     if (initial30gaikoku[k] <= 100):
        negative30gaikoku[k] = 1

int(sum(negative30gaikoku))

より、30年後に元本を下回った回数は2,000近くにのぼり、2割の確率で元本割れしてしまうことがわかります。他方で、倍額以上になる回数は

twice30gaikoku = np.zeros(trial)

for k in range(trial):
     if (initial30gaikoku[k] >= 200):
        twice30gaikoku[k] = 1

int(sum(twice30gaikoku))

より6,500ほどとなって、4資産均等の場合とさほど変わらないことがわかります。以上の結果から、資産形成の基本と言われる、長期・分散で投資することの効用を数値的にも納得することができるのではないでしょうか。



いかがだったでしょうか。今回は、相関を持った乱数の生成、その応用としてアセットアロケーション投資のリスク&リターンについて考察してみました。


キーワードPython、投資