S&P500を組み合わせて最適ポートフォリオを探索する【コード解説編】

5月 2, 2021

この記事は,S&P500(米国優良企業約500社)の組合せの中から最適ポートフォリオを見つけるPythonコードの解説を行うものです.

今回見つける最適ポートフォリオは,

・リスク(ボラティリティ)が最小になるポートフォリオ
・シャープレシオが最大になるポートフォリオ

とします.※シャープレシオとは,期待リターンをボラティリティで割った量です.

コード全文は本記事の一番下にありますので,実行だけしたいという人はコード全文をコピペしてご使用ください.

また,実行結果の詳細をまとめたものを以下の記事に書いています.細かい構成比も載せてあるので,気になる方はご覧ください.

S&P500を組み合わせて最適ポートフォリオを探索する【実行結果編】

コードの流れ

コードの大まかな構成は以下の通りです.

1. S&P500の株価データを取得してCSVファイルに保存
2. 株価データを元に各銘柄の期待リターンとリスクを求める
3. 色々な割合のポートフォリオでその期待リターンとリスクを計算し,効率的フロンティアを作成する
4. リスクが最小,シャープレシオが最大になる最適ポートフォリオを各々探す
5. 結果を可視化
6. 結果を見やすくするため,セクターごとのポートフォリオに直す

必要なモジュールのインポート

今回は以下のモジュールを使用するので,適宜インストールしておいてください.全てpipでインストールできます.

# import needed modules
import pandas_datareader.data as web
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import csv

S&P500の株価データを取得してCSVファイルに保存

まずは,S&P500銘柄の質(期待リターン,リスク)を計算するために,銘柄の株価を取得する必要があります.

S&P500銘柄のティッカーコードは,以下のURLにあるCSVから取得することにします.

https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv

S&P500銘柄のティッカーコードの取得は,以下でできます.

import pandas as pd
url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv"
sp500 = pd.read_csv(url, encoding="SHIFT_JIS")

sp500 = sp500['Symbol'] #sp500ティッカーコードのデータフレーム
sp500_tickers = sp500.values.tolist() #データフレームをリストに変換

そして,取得した銘柄の株価を取得するには,pandas-datareaderなどを用います.

import datetime
import pandas_datareader.data as web
start = datetime.date(2014,1,1)
end = datetime.date.today()

data = web.DataReader(sp500_tickers, 'yahoo', start, end)["Adj Close"] #sp500の株価データを取得

ただし,開始日(start)以前に上場していない銘柄については,株価は取得されないことに注意して下さい.
また,何度も株価を取得すると時間もかかり,APIのサーバーにも良くないようなのでCSVファイルに保存しておきます.

data.to_csv('sp500_stocks_data.csv')

株価をCSVに保存,読み出しする方法についての詳しい説明は,以下の記事をご覧ください.

【Python, pandas】株価時系列データを取得してCSVファイルに保存する方法と呼び出す方法

次に,保存した株価データを呼び出します.

#get sp500 data
data= pd.read_csv('drive/MyDrive/Colab Notebooks/sp500_stocks_data_from2014.csv')

data = data.set_index('Date')

この時点では,2014年以降にIPO(上場)した銘柄の株価は取得されておらず,CSVでは空セル,pandasのDataFrameではNaNになっています.

そこで,取得していない銘柄のカラム(NaN)については削除します.

#NaNのカラムを削除する
del_list = []
for i in range(len(data.columns)):
    if pd.isna(data.iat[1,i]):
        del_list.append(data.columns.values[i])
data.drop(del_list, axis=1, inplace=True)

sp = list(data.columns)

また,最後に取得した銘柄をリストとしてspに保存しています.

株価データを元に各銘柄の期待リターンとリスクを求める

以下で,年平均リターンを求めます.

# calculate daily and annual returns of the stocks
returns_daily = data.pct_change()
returns_annual = returns_daily.mean() * 250

ポートフォリオの期待リターンはこの年平均リターンに割合ベクトル(各銘柄の割合を並べたもの)をかけることで求めます.期待リターンは後で求めます.

また,リスク計算のために共分散行列を計算します.

# get daily and covariance of returns of the stock
cov_daily = returns_daily.cov()
cov_annual = cov_daily * 250

実際にはリスク(ボラティリティ)はこの共分散行列をポートフォリオの割合ベクトルで挟み,平方根を取ることで求めます.リスクも後で求めるので,ここではこのままにしておきましょう.

今求めた年平均リターンと共分散行列をリストとして保存しておきます.

annual_data = [returns_annual, cov_annual]

色々な割合のポートフォリオでその期待リターンとリスクを計算し効率的フロンティアを作成する

各銘柄の割合をランダムにポートフォリオを生成して,各ポートフォリオの期待リターン,ボラティリティ(リスク),シャープレシオを計算します.

とりあえず,各ポートフォリオの各成績を格納する空リストを用意します.

# empty lists to store returns, volatility and weights of imiginary portfolios
port_returns = []
port_volatility = []
sharpe_ratio = []
stock_weights = []

ポートフォリオの銘柄数及びランダムに生成するポートフォリオの数を決めます.num_portfoliosgが少なすぎると可能性が減って最適なポートフォリオのパフォーマンスが悪くなります.多すぎるとメモリが足りなくなります.

# set the number of combinations for imaginary portfolios
num_assets = len(sp)
num_portfolios = 100000

num_portfoliosの回数分,以下を行います.

割合をランダムで生成
→割合を規格化(合計を1にする)
→そのポートフォリオの期待リターンを求める
→そのポートフォリオのボラティリティを求める
→そのポートフォリオのシャープレシオを求める
→期待リターン,ボラティリティ,シャープレシオ,ポートフォリオの割合をリストに格納する

#set random seed for reproduction's sake
np.random.seed(101)

print('計算中…')

# populate the empty lists with each portfolios returns,risk and weights
for single_portfolio in range(num_portfolios):
   weights = np.random.random(num_assets)
   weights /= np.sum(weights)
   returns = np.dot(weights, returns_annual)
   volatility = np.sqrt(np.dot(weights.T, np.dot(cov_annual, weights)))
   sharpe = returns / volatility
   sharpe_ratio.append(sharpe)
   port_returns.append(returns)
   port_volatility.append(volatility)
   stock_weights.append(weights)

これで,色々なポートフォリオの成績が求められました.

あとは,以降で扱いやすいようにデータ構造を少し加工します.

以下で辞書としてポートフォリオの期待リターン,ボラティリティ,シャープレシオを保存します.

# a dictionary for Returns and Risk values of each portfolio
portfolio = {'Returns': port_returns,
            'Volatility': port_volatility,
            'Sharpe Ratio': sharpe_ratio}

以下で,ポートフォリオをDataFrameとして保存します.DataFrameには期待リターン,ボラティリティ,シャープレシオの3列と各銘柄の割合の列が格納されます.

# extend original dictionary to accomodate each ticker and weight in the portfolio
for counter,symbol in enumerate(sp):
   portfolio[symbol] = [Weight[counter] for Weight in stock_weights]

# make a nice dataframe of the extended dictionary
df = pd.DataFrame(portfolio)

# get better labels for desired arrangement of columns
column_order = ['Returns', 'Volatility', 'Sharpe Ratio'] + [stock for stock in sp]

# reorder dataframe columns
df = df[column_order]

リスクが最小,シャープレシオが最大になる最適ポートフォリオを各々探す

最後に,先ほど生成したポートフォリオの中から,リスク(ボラティリティ)が最小になるポートフォリオとシャープレシオが最大になるポートフォリオを探します.

以下で探すことができます.min_volatilityがdfの中で最小のボラティリティ,max_sharpeがdfの中で最大のシャープレシオです.また,min_variance_portがボラティリティ最小のポートフォリオ,sharpe_portfolioがシャープレシオ最大のポートフォリオです.

# find min Volatility & max sharpe values in the dataframe (df)
min_volatility = df['Volatility'].min()
max_sharpe = df['Sharpe Ratio'].max()

# use the min, max values to locate and create the two special portfolios
min_variance_port = df.loc[df['Volatility'] == min_volatility]
sharpe_portfolio = df.loc[df['Sharpe Ratio'] == max_sharpe]

結果を可視化

最後に,結果を表示します.

まず,任意のポートフォリオの体裁を整える関数opt_portfolioと,成績(リターン,ボラティリティ,シャープレシオ)だけを取得する関数calculate_performanceを用意します.

def opt_portfolio(port, sp):
    port.drop(port.index[[0,1,2]],inplace=True)
    port["ティッカー(仮)"] = sp
    port["保有数量[株]"] = np.nan
    port.columns=["割合", "ティッカーコード", "保有数量[株]"]
    port = port.loc[:, ["ティッカーコード", "割合", "保有数量[株]"]]
    for i in range(len(port)):
        port.rename(index={port.index[i]:str(i)}, inplace=True)
    return port
def calculate_performance(port, annual_data):
    portlist = port.values.tolist()
    weights = []
    for w in portlist:
        weights.append(w[1])
    weights = np.array(weights)
    #returns
    returns = np.dot(weights, annual_data[0])
    #volatility
    volatility = np.sqrt(np.dot(weights.T, np.dot(annual_data[1], weights)))
    #sharpe ratio
    sharpe = returns / volatility

    print('リターン' , returns)
    print('ボラティリティ' , volatility)
    print('シャープレシオ' , sharpe)
    return returns, volatility, sharpe

もうすでにポートフォリオの成績が得られているので改めて成績を計算する必要はありませんが,過去に作ってきたコードの都合上,このような形になっています.再度計算させるのが嫌だという方は,書き換えてください.

最適ポートフォリオの内約

では,最適ポートフォリオを表示します.

opt_portfolio1 = opt_portfolio(sharpe_portfolio.T, sp)
print('最適ポートフォリオ(シャープレシオ最大)')
print(opt_portfolio1)
returns1, volatility1, sharpe1 = calculate_performance(opt_portfolio1, annual_data)
print('')
opt_portfolio2 = opt_portfolio(min_variance_port.T, sp)
print('最適ポートフォリオ(ボラティリティ最小)')
print(opt_portfolio2)
returns2, volatility2, sharpe2 = calculate_performance(opt_portfolio2, annual_data)

rvs = [returns1, volatility1, sharpe1, returns2, volatility2, sharpe2]

これで,以下のような結果が表示されると思います.

最適ポートフォリオ(シャープレシオ最大)
    ティッカーコード        割合  保有数量[株]
0        MMM  0.001556      NaN
1        AOS  0.003068      NaN
2        ABT  0.002113      NaN
..       ...       ...      ...
475     ZION  0.002556      NaN
476      ZTS  0.003414      NaN

リターン 0.18500777598585483
ボラティリティ 0.18467233125069676
シャープレシオ 1.0018164320171097

最適ポートフォリオ(ボラティリティ最小)
    ティッカーコード        割合  保有数量[株]
0        MMM  0.003041      NaN
1        AOS  0.001196      NaN
2        ABT  0.001024      NaN
..       ...       ...      ...
475     ZION  0.001155      NaN
476      ZTS  0.002688      NaN

リターン 0.17754191457590954
ボラティリティ 0.1821459503904961
シャープレシオ 0.9747233698870815

結果をCSVファイルに保存

そこそこ計算時間がかかるものなので,結果はCSVファイルに保存しておきましょう.

# CSVファイルとして出力
with open('sp_portfolio.csv', 'w') as f:
    writer = csv.writer(f)
    writer.writerow(['リターン', 'ボラティリティ' , 'シャープレシオ','', \
                    'リターン', 'ボラティリティ' , 'シャープレシオ'])
    writer.writerow([returns1, volatility1, sharpe1,'',returns2, volatility2, sharpe2])
allportfolio = pd.concat([opt_portfolio1, opt_portfolio2], axis=1)
allportfolio.to_csv('sp_portfolio.csv', mode='a', header=False)

効率的フロンティア

効率的フロンティアを描画します.

# plot frontier, max sharpe & min Volatility values with a scatterplot
plt.style.use('seaborn-dark')
df.plot.scatter(x='Volatility', y='Returns', c='Sharpe Ratio',
               cmap='RdYlGn', edgecolors='black', figsize=(8, 5), grid=True)
plt.scatter(x=sharpe_portfolio['Volatility'], y=sharpe_portfolio['Returns'], c='red', marker='s', s=200, label='sharpe ratio max')
plt.scatter(x=min_variance_port['Volatility'], y=min_variance_port['Returns'], c='blue', marker='s', s=200, label='volatility min' )

# bboxの作成
boxdic = {
    "facecolor" : "white",
    "edgecolor" : "darkred",
    "boxstyle" : "Square",
    "linewidth" : 2
}
max_volatility = df['Volatility'].max()
plt.xlabel('Volatility (Std. Deviation)')
plt.ylabel('Expected Returns')
plt.title('Efficient Frontier')
plt.legend(loc='upper left')
#plt.show()

赤い点がシャープレシオ最大のポートフォリオ,青い点がボラティリティ最小のポートフォリオです.また,各ポートフォリオ点の色はシャープレシオの度合いを意味しており,緑であるほどシャープレシオが大きいです.

ポートフォリオを円グラフで描画

以下で,ポートフォリオを円グラフで表示します.

def get_weights(port):
    weights = []
    for i in range(0,len(port)):
        p = port.values.tolist()
        weights.append(p[i][1])
    return weights

fig_cir = plt.figure()
ax1 = fig_cir.add_subplot(1, 2, 1)
ax2 = fig_cir.add_subplot(1, 2, 2)
w1 = get_weights(opt_portfolio1)
w2 = get_weights(opt_portfolio2)
colors = []
color_list = ["r", "g", "b", "c", "m", "y", "k", "gray", "lightpink", "slateblue", "lightcoral", "orange", "springgreen", "Navy", "plum"]
for i in range(len(sp)):
    colors.append(color_list[i%len(color_list)])
ax1.pie(w1, labels=sp, counterclock=False, startangle=90, colors=colors)
ax2.pie(w2, labels=sp, counterclock=False, startangle=90, colors=colors)

circle1 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
circle2 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
ax1.add_patch(circle1)
ax2.add_patch(circle2)
ax1.set_title("Maximize Sharpe Ratio", fontsize = 22)
ax2.set_title("Minimize Volatility", fontsize = 22)
plt.tight_layout()
plt.show()

たくさんあるのは分かりますが,全然参考になりませんね.

そこで,セクター別の割合で表示してみます.

結果を見やすくするため,セクターごとのポートフォリオに直す

セクター別にまとめるには,各銘柄のセクターを取得する必要があります.

一番初めにS&P500銘柄のティッカーコードを取得したURL

https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv

には,セクターも記述されていますので,これを利用します.

url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv"
sp500 = pd.read_csv(url, encoding="SHIFT_JIS")

sp500_ticker = sp500['Symbol'] #sp500ティッカーコードのデータフレーム
tickers = sp500_ticker.values.tolist() #データフレームをリストに変換
sp500_sector = sp500['Sector'] #sp500セクターのデータフレーム
sectors = sp500_sector.values.tolist() #データフレームをリストに変換

tic_sec = dict(zip(tickers, sectors))
#print(tic_sec)

ティッカーコードとセクターが結びついた辞書tic_secができました.

この辞書を以下でDataFrameに直し,先ほどと同じように円グラフを表示します.

def make_sector_frame(dec, sp):
    sec_port = pd.DataFrame(index=[], columns=['セクター', '割合'])

    for s in sp:
        tmp_se = pd.Series(dec[s], index=sec_port.columns)
        sec_port = sec_port.append(tmp_se, ignore_index=True)

    sec_port.drop_duplicates(subset='セクター', inplace=True)
    sec_port = sec_port.reset_index(drop=True)
    sec_port['割合'] = 0.0
    return sec_port

def convert_to_sector(port, dec, sp):

    sec_port = make_sector_frame(dec, sp)

    for se in range(len(sec_port)):
        sector = sec_port.iat[se, 0]
        for i in range(len(port)):
            if sector == dec[port.iat[i,0]]:
                sec_port.iat[se,1] += port.iat[i,1]

    return sec_port

sector_port1 = convert_to_sector(opt_portfolio1, tic_sec, sp)
sector_port2 = convert_to_sector(opt_portfolio2, tic_sec, sp)

print(sector_port1)
print(sector_port2)

sectors_list = list(sector_port1['セクター'])
#print(sectors_list)

#print(sector_port['割合'].sum())

fig_cir2 = plt.figure()
ax3 = fig_cir2.add_subplot(1, 2, 1)
ax4 = fig_cir2.add_subplot(1, 2, 2)
w3 = get_weights(sector_port1)
w4 = get_weights(sector_port2)
colors = []
color_list = ["r", "g", "b", "c", "m", "y", "k", "gray", "lightpink", "slateblue", "lightcoral", "orange", "springgreen", "Navy", "plum"]
for i in range(len(sectors_list)):
    colors.append(color_list[i%len(color_list)])
ax3.pie(w3, labels=sectors_list, counterclock=False, startangle=90, colors=colors)
ax4.pie(w4, labels=sectors_list, counterclock=False, startangle=90, colors=colors)

circle1 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
circle2 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
ax3.add_patch(circle1)
ax4.add_patch(circle2)
ax3.set_title("Maximize Sharpe Ratio", fontsize = 22)
ax4.set_title("Minimize Volatility", fontsize = 22)
plt.tight_layout()
plt.show()

だいぶ見やすくなりました.セクター別でみてもバランスがいい感じですね.やはり分散は大事という感じでしょうか.

コード全文

株価データをCSVファイルに保存

import pandas as pd
import datetime
url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv"
sp500 = pd.read_csv(url, encoding="SHIFT_JIS")

sp500_ticker = sp500['Symbol'] #sp500ティッカーコードのデータフレーム
tickers = sp500_ticker.values.tolist() #データフレームをリストに変換

import pandas_datareader.data as web

start = datetime.date(2014,1,1)
end = datetime.date.today()

print('data取得中…')

sp500_data = web.DataReader(tickers, 'yahoo', start, end)["Adj Close"] #sp500の株価データを取得
sp500_data.to_csv('sp500_stocks_data_from2014.csv')

ポートフォリオ最適化コード

# import needed modules
import pandas_datareader.data as web
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

def opt_portfolio(port, sp):
    port.drop(port.index[[0,1,2]],inplace=True)
    port["ティッカー(仮)"] = sp
    port["保有数量[株]"] = np.nan
    port.columns=["割合", "ティッカーコード", "保有数量[株]"]
    port = port.loc[:, ["ティッカーコード", "割合", "保有数量[株]"]]
    for i in range(len(port)):
        port.rename(index={port.index[i]:str(i)}, inplace=True)
    return port
def calculate_performance(port, annual_data):
    portlist = port.values.tolist()
    weights = []
    for w in portlist:
        weights.append(w[1])
    weights = np.array(weights)
    #returns
    returns = np.dot(weights, annual_data[0])
    #volatility
    volatility = np.sqrt(np.dot(weights.T, np.dot(annual_data[1], weights)))
    #sharpe ratio
    sharpe = returns / volatility

    print('リターン' , returns)
    print('ボラティリティ' , volatility)
    print('シャープレシオ' , sharpe)
    return returns, volatility, sharpe
def get_weights(port):
    weights = []
    for i in range(0,len(port)):
        p = port.values.tolist()
        weights.append(p[i][1])
    return weights

def make_sector_frame(dec, sp):
    sec_port = pd.DataFrame(index=[], columns=['セクター', '割合'])

    for s in sp:
        tmp_se = pd.Series(dec[s], index=sec_port.columns)
        sec_port = sec_port.append(tmp_se, ignore_index=True)

    sec_port.drop_duplicates(subset='セクター', inplace=True)
    sec_port = sec_port.reset_index(drop=True)
    sec_port['割合'] = 0.0
    return sec_port

def convert_to_sector(port, dec, sp):

    sec_port = make_sector_frame(dec, sp)

    for se in range(len(sec_port)):
        sector = sec_port.iat[se, 0]
        for i in range(len(port)):
            if sector == dec[port.iat[i,0]]:
                sec_port.iat[se,1] += port.iat[i,1]

    return sec_port

#get sp500 data
data= pd.read_csv('drive/MyDrive/Colab Notebooks/sp500_stocks_data_from2014.csv')

data = data.set_index('Date')

#NaNのカラムを削除する
del_list = []
for i in range(len(data.columns)):
    if pd.isna(data.iat[1,i]):
        del_list.append(data.columns.values[i])
data.drop(del_list, axis=1, inplace=True)

#print(data)

sp = list(data.columns)

# calculate daily and annual returns of the stocks
returns_daily = data.pct_change()
returns_annual = returns_daily.mean() * 250

# get daily and covariance of returns of the stock
cov_daily = returns_daily.cov()
cov_annual = cov_daily * 250

annual_data = [returns_annual, cov_annual]

# empty lists to store returns, volatility and weights of imiginary portfolios
port_returns = []
port_volatility = []
sharpe_ratio = []
stock_weights = []

# set the number of combinations for imaginary portfolios
num_assets = len(sp)
num_portfolios = 100000

#set random seed for reproduction's sake
np.random.seed(101)

print('計算中…')

# populate the empty lists with each portfolios returns,risk and weights
for single_portfolio in range(num_portfolios):
   weights = np.random.random(num_assets)
   weights /= np.sum(weights)
   returns = np.dot(weights, returns_annual)
   volatility = np.sqrt(np.dot(weights.T, np.dot(cov_annual, weights)))
   sharpe = returns / volatility
   sharpe_ratio.append(sharpe)
   port_returns.append(returns)
   port_volatility.append(volatility)
   stock_weights.append(weights)

# a dictionary for Returns and Risk values of each portfolio
portfolio = {'Returns': port_returns,
            'Volatility': port_volatility,
            'Sharpe Ratio': sharpe_ratio}

# extend original dictionary to accomodate each ticker and weight in the portfolio
for counter,symbol in enumerate(sp):
   portfolio[symbol] = [Weight[counter] for Weight in stock_weights]

# make a nice dataframe of the extended dictionary
df = pd.DataFrame(portfolio)

# get better labels for desired arrangement of columns
column_order = ['Returns', 'Volatility', 'Sharpe Ratio'] + [stock for stock in sp]

# reorder dataframe columns
df = df[column_order]

# find min Volatility & max sharpe values in the dataframe (df)
min_volatility = df['Volatility'].min()
max_sharpe = df['Sharpe Ratio'].max()

# use the min, max values to locate and create the two special portfolios
min_variance_port = df.loc[df['Volatility'] == min_volatility]
sharpe_portfolio = df.loc[df['Sharpe Ratio'] == max_sharpe]

opt_portfolio1 = opt_portfolio(sharpe_portfolio.T, sp)
print('最適ポートフォリオ(シャープレシオ最大)')
print(opt_portfolio1)
returns1, volatility1, sharpe1 = calculate_performance(opt_portfolio1, annual_data)
print('')
opt_portfolio2 = opt_portfolio(min_variance_port.T, sp)
print('最適ポートフォリオ(ボラティリティ最小)')
print(opt_portfolio2)
returns2, volatility2, sharpe2 = calculate_performance(opt_portfolio2, annual_data)

rvs = [returns1, volatility1, sharpe1, returns2, volatility2, sharpe2]

# CSVファイルとして出力
with open('sp_portfolio.csv', 'w') as f:
    writer = csv.writer(f)
    writer.writerow(['リターン', 'ボラティリティ' , 'シャープレシオ','', \
                    'リターン', 'ボラティリティ' , 'シャープレシオ'])
    writer.writerow([returns1, volatility1, sharpe1,'',returns2, volatility2, sharpe2])
allportfolio = pd.concat([opt_portfolio1, opt_portfolio2], axis=1)
allportfolio.to_csv('sp_portfolio.csv', mode='a', header=False)


# plot frontier, max sharpe & min Volatility values with a scatterplot
plt.style.use('seaborn-dark')
df.plot.scatter(x='Volatility', y='Returns', c='Sharpe Ratio',
               cmap='RdYlGn', edgecolors='black', figsize=(8, 5), grid=True)
plt.scatter(x=sharpe_portfolio['Volatility'], y=sharpe_portfolio['Returns'], c='red', marker='s', s=200, label='sharpe ratio max')
plt.scatter(x=min_variance_port['Volatility'], y=min_variance_port['Returns'], c='blue', marker='s', s=200, label='volatility min' )

# bboxの作成
boxdic = {
    "facecolor" : "white",
    "edgecolor" : "darkred",
    "boxstyle" : "Square",
    "linewidth" : 2
}
max_volatility = df['Volatility'].max()
plt.xlabel('Volatility (Std. Deviation)')
plt.ylabel('Expected Returns')
plt.title('Efficient Frontier')
plt.legend(loc='upper left')
#plt.show()


fig_cir = plt.figure()
ax1 = fig_cir.add_subplot(1, 2, 1)
ax2 = fig_cir.add_subplot(1, 2, 2)
w1 = get_weights(opt_portfolio1)
w2 = get_weights(opt_portfolio2)
colors = []
color_list = ["r", "g", "b", "c", "m", "y", "k", "gray", "lightpink", "slateblue", "lightcoral", "orange", "springgreen", "Navy", "plum"]
for i in range(len(sp)):
    colors.append(color_list[i%len(color_list)])
ax1.pie(w1, labels=sp, counterclock=False, startangle=90, colors=colors)
ax2.pie(w2, labels=sp, counterclock=False, startangle=90, colors=colors)

circle1 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
circle2 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
ax1.add_patch(circle1)
ax2.add_patch(circle2)
ax1.set_title("Maximize Sharpe Ratio", fontsize = 22)
ax2.set_title("Minimize Volatility", fontsize = 22)
plt.tight_layout()
plt.show()



url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/master/data/constituents.csv"
sp500 = pd.read_csv(url, encoding="SHIFT_JIS")

sp500_ticker = sp500['Symbol'] #sp500ティッカーコードのデータフレーム
tickers = sp500_ticker.values.tolist() #データフレームをリストに変換
sp500_sector = sp500['Sector'] #sp500セクターのデータフレーム
sectors = sp500_sector.values.tolist()

tic_sec = dict(zip(tickers, sectors))
#print(tic_sec)

sector_port1 = convert_to_sector(opt_portfolio1, tic_sec, sp)
sector_port2 = convert_to_sector(opt_portfolio2, tic_sec, sp)

print(sector_port1)
print(sector_port2)

sectors_list = list(sector_port1['セクター'])
#print(sectors_list)

#print(sector_port['割合'].sum())

fig_cir2 = plt.figure()
ax3 = fig_cir2.add_subplot(1, 2, 1)
ax4 = fig_cir2.add_subplot(1, 2, 2)
w3 = get_weights(sector_port1)
w4 = get_weights(sector_port2)
colors = []
color_list = ["r", "g", "b", "c", "m", "y", "k", "gray", "lightpink", "slateblue", "lightcoral", "orange", "springgreen", "Navy", "plum"]
for i in range(len(sectors_list)):
    colors.append(color_list[i%len(color_list)])
ax3.pie(w3, labels=sectors_list, counterclock=False, startangle=90, colors=colors)
ax4.pie(w4, labels=sectors_list, counterclock=False, startangle=90, colors=colors)

circle1 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
circle2 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
ax3.add_patch(circle1)
ax4.add_patch(circle2)
ax3.set_title("Maximize Sharpe Ratio", fontsize = 22)
ax4.set_title("Minimize Volatility", fontsize = 22)
plt.tight_layout()
plt.show()

より詳細な結果は以下の記事をご覧ください.