Cream-Kuchen

投資信託で最適なポートフォリオを構築する - PyPortofolioOptで投資比率決定

はじめに

複数の投資信託を組み合わせて最適なポートフォリオを構築するにはどうすれば良いのでしょう?

最小のリスク期待運用利回りを達成する投資比率に決めたいですよね。

PyPortfolioOptを用いることで容易かつ視覚的に検討できます。


Google Colaboratoryで全コードを公開しているので、ご参考にして下さい。
↓コードはこちらです↓
colab.research.google.com

目次


1, 想定する読者の疑問や悩み

主に以下のような疑問や悩みを抱えた方の参考になれば幸いです。

 ・複数の投資信託で資産運用を始めたい
 ・資産クラス別の投資比率が決められない
 ・最小のリスクで期待リターンを達成したい
 ・投資信託の時系列データをpythonで取得したい
 ・PyPortfolioOptを使いたい、何ができるか知りたい

これから具体的な事例を基に、できるだけ平易に書き記します。

※ 本記事の前提となる分散投資の効果や、後述する効率的フロンティア
  その背景にある現代ポートフォリオ理論は、分かりやすい専門家の解説に譲ります。



2, データ準備

2-1, 投資信託の時系列データを取得

まず、購入を検討している投資信託の時系列データを取得しましょう。

例えば、下記8つの資産クラスを対象とします。(※ リンク先はMORNINGSTARのファンド紹介ページです。)

 ・国内株式:ニッセイ TOPIXインデックスファンド
 ・国内債券:ニッセイ 国内債券インデックスファンド
 ・国内REITニッセイ Jリートインデックスファンド
 ・先進国株式:ニッセイ 外国株式インデックスファンド
 ・先進国債券:ニッセイ 外国債券インデックスファンド
 ・先進国REITニッセイ Gリートインデックスファンド
 ・新興国株式:eMAXIS 新興国株式インデックス
 ・新興国債券:iFree新興国債券インデックス

新興国株式について、本心では「eMAXIS Slim新興国株式インデックス」を選びたいのですが、
  後述するデータの抽出期間の制約から上記を挙げています。実際に投資する際には低コストな"Slim"です。




投資信託協会の投信総合ライブラリーから、下記コードで投資信託の時系列データを取得します。

import pandas as pd
from datetime import datetime

# 投資信託のデータを取得する関数
def get_data(ISIN, FUND):
  """
  引数
    ISIN ... ファンドのISINコード (ファンド検索して[ISIN]を探す: https://www.jasdec.com/reading/itmei.php)
    FUND ... ファンドコード (ファンド検索して[ファンドの特色]右上を探す: https://www.morningstar.co.jp/FundData/DetailSearchResult.do?mode=1)
  戻値
    df ... 基準価額や純資産総額のpandasデータフレーム
  """
  # csvダウンロードするURLを作成し読み込む
  BASEURL = "https://toushin-lib.fwg.ne.jp/FdsWeb/FDST030000/csv-file-download?"
  ISINcd  = "isinCd="+ISIN
  FUNDcd  = "associFundCd="+FUND
  DOWNURL = BASEURL+ISINcd+"&"+FUNDcd
  # 日付カラムを加工してデータ取得
  DATE_PARSE = lambda date: datetime.strptime(date, "%Y年%m月%d日")
  df = pd.read_csv(DOWNURL, engine="python", encoding="shift-jis", index_col="年月日", parse_dates=True, date_parser=DATE_PARSE)
  return df

# 8つの資産クラスのデータ取得
df_jpn_stck = get_data(ISIN="JP90C000BRT6", FUND="29312154")  # 国内株式     (ニッセイTOPIX)
df_jpn_bond = get_data(ISIN="JP90C000B9V6", FUND="29314151")  # 国内債券     (ニッセイ内債)
df_jpn_reit = get_data(ISIN="JP90C00099G5", FUND="29314136")  # 国内リート   (ニッセイJリート)
df_dvp_stck = get_data(ISIN="JP90C0009VE0", FUND="2931113C")  # 先進国株式   (ニッセイ外株インデックス)
df_dvp_bond = get_data(ISIN="JP90C0009VF7", FUND="2931213C")  # 先進国債券   (ニッセイ先進国債券)
df_dvp_reit = get_data(ISIN="JP90C0009VG5", FUND="2931313C")  # 先進国リート (ニッセイグローバルリート)
df_emg_stck = get_data(ISIN="JP90C0006LK4", FUND="0331809A")  # 新興国株式   (MUFJ eMAXIS インデックス)
df_emg_bond = get_data(ISIN="JP90C000DVU2", FUND="0431U169")  # 新興国債券   (大和 iFree 新興国債券)



すると、例えば国内株式ではこのようなデータが取得できます。

f:id:Cream-Kuchen:20210503202135p:plain



2-2, 投資信託の時系列データを加工

次に、取得した複数の時系列データを直近5年分だけ抽出し、日付を揃えて横方向に結合します。

# 取得データを設定期間だけ抽出し結合する関数
def join_data(df_part, df_join, KEYWORD, str_date, end_date):
  """
  引数
    df_part ... 各資産クラスのデータフレーム
    df_join ... 複数の資産クラスを結合したデータフレーム
    KEYWORD ... 資産クラス名
    str_date ... 抽出期間の開始日
    end_date ... 抽出期間の終了日
  戻値
    df_join ... 複数の資産クラスを結合したデータフレーム
  """
  # 各資産クラスのデータフレームを期間抽出
  df_part_fil = df_part.loc[(df_part.index>=str_date)&(df_part.index<=end_date), :]
  df_part_fil = df_part_fil.rename(columns={"基準価額(円)":KEYWORD})[[KEYWORD]]
  # 複数の資産クラスを結合
  df_join = pd.merge(df_join, df_part_fil, left_index=True, right_index=True, how="inner") if df_join is not None else df_part_fil
  return df_join

# 資産クラス名とデータフレームの変数を組み合わせる
dict_assets = {
  "国内株式"     : df_jpn_stck,
  "国内債券"     : df_jpn_bond,
  "国内リート"   : df_jpn_reit,
  "先進国株式"   : df_dvp_stck,
  "先進国債券"   : df_dvp_bond,
  "先進国リート" : df_dvp_reit,
  "新興国株式"   : df_emg_stck,
  "新興国債券"   : df_emg_bond,
}

# 取得データを直近過去5年間だけ抽出し結合
# (※注:  データの日付が抽出期間未満の場合、結合データも少なくなる)
df_join = None
str_date = "2017-04-01"  # 抽出開始日
end_date = "2021-03-31"  # 抽出終了日
# 資産クラス名とデータフレームの変数の組み合わせを横方向に結合
for KEYWORD, df_part in dict_assets.items():
  df_join = join_data(df_part, df_join, KEYWORD, str_date, end_date)



すると、このような結合データが出来上がります。

f:id:Cream-Kuchen:20210503203755p:plain


これでデータの準備が完了しました。


3, リターン, リスク, 相関を確認

各資産クラスの過去のリターン, リスク, 相関を順番に確認します。

3-1, リターン
length = len(df_join)-1  # データ数 - 1
days = 250  # 年間営業日数
df_return = 100*((pd.DataFrame((df_join.pct_change()[1:]+1).prod()).T)**(days/length)-1)

f:id:Cream-Kuchen:20210504081741p:plain

年率リターン(%)は、先進国株式, 新興国株式, 国内株式の順に高いですね。


3-2, リスク(標準偏差)
days = 250  # 年間営業日数
df_vola = (pd.DataFrame((df_join.pct_change()[1:]*100).std()).T)*(days**(1/2))

f:id:Cream-Kuchen:20210504082010p:plain

年率リスク(%)は、先進国リート, 国内リート, 先進国株式の順に高いですね。

株式に比べてリートは、リターンのわりにリスクが高く投資効率が悪いようです。


3-3, 相関係数
df_corr = df_join.corr()

f:id:Cream-Kuchen:20210504113444p:plain

国内株式と新興国株式が強い正の相関(0.91)を持つ一方、

国内債券と新興国株式や、国内株式は、やや弱い負の相関(-0.21, -0.20)を持つようですね。

後者の組み合わせはポートフォリオのリスク・リターン特性を改善してくれそうです。


4, ポートフォリオの構築

データを基に投資比率を決めましょう。その際、値動きについて以下の特徴を念頭に置いて下さい。

 ・リターン:計測期間が異なると、リターンも大きく異なる
 ・リスク :計測期間が異なっても、リスクは安定性がある
 ・相関関係:計測期間が異なっても、相関にも安定性がある


ここから、下記3種類のポートフォリオ構築方法を提示します。

 1, 過去のデータを参考にして、任意に投資比率を決める
 2, 過去のデータを基に効率的フロンティアを描画し、期待運用利回りを達成する投資比率を求める
 3, 専門家が予測するリターンと、過去のリスク・相関係数を基に、効率的フロンティアを描画し、
   期待運用利回りを達成する投資比率を求める

では、上から順に見ていきましょう。

※ 前述[3-2]でデータを直近過去5年分に絞ったのは上記の特徴を考慮し、任意に設定しました。



4-1, 試行: 各資産均等バランス型ポートフォリオ

過去のデータを参考にして、任意に投資比率を決めましょう。

例えば、各資産に均等に投資すると、そのリターンとリスクは過去どうなっていたでしょう?

# ポートフォリオのリターンとリスクを算出
N = len(df_return.T)   # 資産クラス数
x = np.array([1/N]*N)  # ウェイト変数例
# リターンの計算(年率%)
RETURN = (x@df_return.T.to_numpy()).item()
print("リターン(年率%)", f": {RETURN:.3}")

# リスクの計算(年率%)
# 資産別ウェイト行列 (N × N)
df_multi_weight = pd.DataFrame((x.reshape(N, 1))@(x.reshape(1, N)), index=df_corr.index, columns=df_corr.columns)
# 資産別ボラティリティ行列 (N × N)
df_multi_vola   = pd.DataFrame((df_vola.to_numpy().reshape(N, 1))@(df_vola.to_numpy().reshape(1, N)), index=df_corr.index, columns=df_corr.columns)
# 分散・共分散行列
df_multi_cova   = df_multi_weight*df_multi_vola*df_corr
# リスク(年率%)
RISK = (np.sum(df_multi_cova.to_numpy().reshape(1, -1)))**(1/2)
print("リスク (年率%)", f": {RISK:.3}")

上記コードを実行すると、下記の出力を確認できます。

 ・リターン (年率) : 6.27%
 ・リスク  (年率) : 11.1%

国内リートに近いリターン(7.38%)を得ながらリスク(21.6%)を半減できている、または、

新興国債券に近いリスク(10.5%)を負いながら数倍のリターン(1.4%)を得られたようですね。



4-2, 最適化: 過去のリターン, リスク, 相関を用いるポートフォリオ

過去のデータを基に効率的フロンティアを描画し、期待運用利回りを達成する投資比率を求めましょう。

このポートフォリオ最適化のために、PyPortfolioOptを用います。

※ 実装時の参考ブログはこちらです。感謝申し上げます。

!pip install PyPortfolioOpt
from pypfopt.efficient_frontier import EfficientFrontier
from pypfopt import risk_models, expected_returns
import math
import numpy as np
import plotly.graph_objects as go
days = 250  # 年間営業日数
mu = expected_returns.mean_historical_return(df_join, frequency=days) # リターンの計算
S = risk_models.sample_cov(df_join, frequency=days) # 分散・共分散行列の計算
risks = (df_join.pct_change().dropna(how='all')).std() # リスク(標準偏差)の計算
risks = ((risks*risks)*days).apply(math.sqrt)
# 資産クラス別のリスク・リターンデータフレームを作成
df_plot = pd.DataFrame()
df_plot['資産クラス名'] = mu.index
df_plot['リスク'] = risks.values
df_plot['リターン'] = mu.values
# 効率的フロンティアの計算
ef = EfficientFrontier(mu, S)  # 右記カギ括弧を引数に入れると、ウェイトの上下限が設定可能「,weight_bounds=(0.002, 0.5)」
# 計算するポートフォリオリターンの範囲
trets = np.arange(round(np.amin(mu),3), round(np.amax(mu),3), 0.001)
tvols = []
res_ret = []
res_risk = []
df_weight = pd.DataFrame(columns=list(df_join.columns), index=trets)
# 目標リターン刻みでウェイト算出
for tr in trets:
    try:
        w = ef.efficient_return(target_return=tr)
        w = ef.clean_weights()
        pref = ef.portfolio_performance()
        res_ret += [pref[0]]
        res_risk += [pref[1]]
        df_weight.loc[tr,:] = list(w.values())
    except:
        print('算出不可:', tr)
# 可視化
flont_line = pd.DataFrame({'リスク':res_risk,'リターン':res_ret,})
layout = go.Layout(
    xaxis = {'title': 'リスク [%]'},
    yaxis = {'title': 'リターン [%]',},
    )
fig = go.Figure(layout=layout)
fig.add_trace(go.Scatter(x=df_plot['リスク']*100, y=df_plot['リターン']*100, mode='markers+text', text=df_plot['資産クラス名'], textposition='top center'))
fig.add_trace(go.Scatter(x=flont_line['リスク']*100, y=flont_line['リターン']*100, mode='lines'))
fig.show()



上記コードを実行すると、下図のような効率的フロンティア(赤線)を描画できます。

過去において、期待運用利回りを達成する最小のリスクが一目瞭然ですね。

f:id:Cream-Kuchen:20210504141652p:plain


さらに下記コードを実行すると、期待運用利回りに対する各資産別の投資比率が分かります。

layout = go.Layout(
    xaxis = {'title': 'リターン[%]'},
    yaxis = {'title': '比率[%]',},
    )
fig = go.Figure(layout=layout)

for col in df_weight:
    fig.add_trace(go.Scatter(
        x=df_weight.index*100,
        y=df_weight[col]*100,
        hoverinfo='x+y',
        mode='lines',
        line=dict(width=0.5,),
        name=col,
        stackgroup='one' # define stack group
    ))
fig.show()

f:id:Cream-Kuchen:20210504144108p:plain

リターンが高くなるほど、債券の投資比率が低下する一方で株式が上昇しています。

また、リートはリターンに関わらず比率が僅少です。前述の投資効率が悪いという気付きの通りです。



具体的な一意のリターン(例: 5%)に対する各資産クラスの投資比率は、下記のように確認できます。

f:id:Cream-Kuchen:20210504150258p:plain

大まかに、資産種別では「株 : 債券 : リート = 29 : 69 : 2」、
     地域別には「日本 : 先進国 : 新興国 = 45 : 50 : 5」が良いという示唆が得られました。

過去のデータを参考にしているためか、リターン(5%)の高さに対し債券比率が高いポートフォリオです。



4-3, 最適化: 専門家の予測リターン, 過去のリスク, 相関を用いるポートフォリオ

専門家が予測するリターンを用いて、期待運用利回りを達成する投資比率を求めてみましょう。

ここでは、J.P.Morgan AssetManagementが毎年公表している超長期市場予測を参考にします。

資産クラス別に予測リターンを設定し、変数muの入力以外は前述[4-2]と同じコードを実行します。

exp_return = {
  '国内株式'     : 5.25,
  '国内債券'     :  0.4,
  '国内リート'   : 5.00,
  '先進国株式'   : 3.40,
  '先進国債券'   : 0.30,
  '先進国リート' : 5.40,
  '新興国株式'   : 5.80,
  '新興国債券'   : 3.55,
}
mu = pd.Series(exp_return)/100
from pypfopt.efficient_frontier import EfficientFrontier
from pypfopt import risk_models, expected_returns
import math
import numpy as np
import plotly.graph_objects as go
days = 250  # 年間営業日数
# mu = expected_returns.mean_historical_return(df_join, frequency=days) # リターンの計算
S = risk_models.sample_cov(df_join, frequency=days) # 分散・共分散行列の計算
risks = (df_join.pct_change().dropna(how='all')).std() # リスク(標準偏差)の計算
risks = ((risks*risks)*days).apply(math.sqrt)
# 資産クラス別のリスク・リターンデータフレームを作成
df_plot = pd.DataFrame()
df_plot['資産クラス名'] = mu.index
df_plot['リスク'] = risks.values
df_plot['リターン'] = mu.values
# 効率的フロンティアの計算
ef = EfficientFrontier(mu, S)  # 右記カギ括弧を引数に入れると、ウェイトの上下限が設定可能「,weight_bounds=(0.002, 0.5)」
# 計算するポートフォリオリターンの範囲
trets = np.arange(round(np.amin(mu),3), round(np.amax(mu),3), 0.001)
tvols = []
res_ret = []
res_risk = []
df_weight = pd.DataFrame(columns=list(df_join.columns), index=trets)
# 目標リターン刻みでウェイト算出
for tr in trets:
    try:
        w = ef.efficient_return(target_return=tr)
        w = ef.clean_weights()
        pref = ef.portfolio_performance()
        res_ret += [pref[0]]
        res_risk += [pref[1]]
        df_weight.loc[tr,:] = list(w.values())
    except:
        print('算出不可:', tr)
# 可視化
flont_line = pd.DataFrame({'リスク':res_risk,'リターン':res_ret,})
layout = go.Layout(
    xaxis = {'title': 'リスク [%]'},
    yaxis = {'title': 'リターン [%]',},
    )
fig = go.Figure(layout=layout)
fig.add_trace(go.Scatter(x=df_plot['リスク']*100, y=df_plot['リターン']*100, mode='markers+text', text=df_plot['資産クラス名'], textposition='top center'))
fig.add_trace(go.Scatter(x=flont_line['リスク']*100, y=flont_line['リターン']*100, mode='lines'))
fig.show()



上記コードを実行すると、下図のような効率的フロンティア(赤線)を描画できます。

前述[4-2]より低リターン/高リスクになりました。保守的な見通しポートフォリオを構築できそうです。

f:id:Cream-Kuchen:20210504153039p:plain


さらに下記コードを実行すると、期待運用利回りに対する各資産別の投資比率が分かります。

layout = go.Layout(
    xaxis = {'title': 'リターン[%]'},
    yaxis = {'title': '比率[%]',},
    )
fig = go.Figure(layout=layout)

for col in df_weight:
    fig.add_trace(go.Scatter(
        x=df_weight.index*100,
        y=df_weight[col]*100,
        hoverinfo='x+y',
        mode='lines',
        line=dict(width=0.5,),
        name=col,
        stackgroup='one' # define stack group
    ))
fig.show()

f:id:Cream-Kuchen:20210504153535p:plain

リターンが高くなるほど、国内債券の比率が低下する一方、国内株式や新興国株式/債券が上昇します。

専門家の予測を参考にすることで、同じ債券でも国内と新興国で投資比率の推移が逆転しましたね。



具体的な一意のリターン(例: 5%)に対する各資産クラスの投資比率は、下記のように確認できます。

f:id:Cream-Kuchen:20210504154337p:plain

大まかに、資産種別では「株 : 債券 : リート = 63 : 22 : 15」、
     地域別には「日本 : 先進国 : 新興国 = 47 : 0 : 53」が良いという示唆が得られました。

前述[4-2]に対してリターン(5%)に対する債券比率は納得感がありますが、アンバランスな地域配分です。



4-4, 最適化: (追記) 上下限比率の設定

前述[4-2][4-3]を通して目標リターンに対する各資産クラス別の投資比率を確認しましたが、

いずれも資産種別や地域配分がアンバランスで納得感がありませんでしたね。

これは投資比率に上下限を設定することで調整可能です。

効率的フロンティアを算出する下記コードを修正し試してみて下さい。


<変更前>

ef = EfficientFrontier(mu, S)

<変更後>

lower_limit = 0.05  # 下限比率5%
upper_limit = 0.25  # 上限比率25%
ef = EfficientFrontier(mu, S, weight_bounds=(lower_limit, upper_limit))



5, 一般的なよくある注意

念のため、お決まりの文句を記します。

 ・本記事では個別の投資信託を紹介しておりますが、あくまでも参考です。その売買を推奨するものではありません。
 ・記事中に幾つか筆者の見解を示しておりますが、あくまでも参考です。その売買に伴う損失は保証致しません。
 ・期待運用利回りや背負えるリスクは、ライフステージや資産状況などによって変わります。上記はあくまでも一例です。



おわりに

複数の投資信託を組み合わせて最適なポートフォリオを構築する方法を紹介しました。

PyPortfolioOptは、最小のリスク期待運用利回りを達成する投資比率を求めるために有用ですね。

Google Colaboratoryのコードも参考にして、より良い資産運用ライフの一助になれると幸いです。


p.s.
目標とする期待運用利回りの算出や、運用開始後のリバランスについて、下記を参考にして下さい。

cream-kuchen.hatenablog.com

cream-kuchen.hatenablog.com