今回は、CAPMの検証や、ファクターの有効性の検証によく用いられる手法であるFama-MacBeth回帰を見ていきます。
よく用いられる手法ですが、意外と情報が少なく、間違った理解をしているケースも多いのがこの手法です。
Fama-MacBeth回帰とは
ファーマ・マクベス回帰(Fama-MacBeth regression)は、CAPMの検証や、ファクターの有効性の検証に用いられる手法で、ユージン・ファマ(Eugene Fama)とジェームズ・マクベス(James MacBeth)によって1973年に提案されました。
詳しい内容は、「Risk, return, and equilibrium」という論文に記載されています。
Fama-MacBeth回帰の基本的な手順は以下のようになります。
- 各銘柄のリターンを時系列方向に回帰して、ベータを算出する
- 時点tのクロスセクション回帰を行う(tは1~Tまで。T回の回帰を行う)
- 2で行ったクロスセクション回帰の回帰係数の時系列について(T個の推定された回帰係数)符号の有意性を検定する
という流れが基本になります。
原論文では検証においては、個別の銘柄のベータを用いるのではなく、ポートフォリオのベータを用いているので、少し手順が増えて以下のようになっています。
- 各銘柄のリターンを時系列方向に回帰して、ベータを算出する。ベータの大きい順に20個のポートフォリオに分類する。
- 各ポートフォリオについて、ポートフォリオ内の個別銘柄のベータを計算し、平均をポートフォリオのベータとする
- 各ポートフォリオのベータを用いて、時点tのクロスセクション回帰を行う(tは1~Tまで。T回の回帰を行う)。
- 2で行ったクロスセクション回帰の回帰係数の時系列について(T個の推定された回帰係数)符号の有意性を検定する。
Pythonでの実装
FamaMacbeth回帰を実際にPythonで実装してみます。
TOPIXコア30のデータを使って、時系列方向に各銘柄のベータを計算、そのベータの値をクロスセクションで回帰し、係数が0でないか検定します。
#TOPIX銘柄コードの取得
import requests
import pandas as pd
import pandas_datareader as web
from datetime import date
from dateutil.relativedelta import relativedelta
# CSVファイルのURL
url = "https://www.jpx.co.jp/markets/indices/topix/tvdivq00000030ne-att/topixweight_j.csv"
# HTTPリクエストを送信してファイルをダウンロード
response = requests.get(url)
# ステータスコードが200(成功)の場合
if response.status_code == 200:
# ダウンロードしたCSVデータをファイルに保存
with open("topixweight_j.csv", "wb") as f:
f.write(response.content)
# ファイルをPandas DataFrameに読み込む
data_df = pd.read_csv("/content/topixweight_j.csv",encoding='shift-jis')
else:
print(f"Failed to download the file. Status code: {response.status_code}")
#Core30の銘柄を取り出す
topix_core30 = list(data_df[data_df['ニューインデックス区分']=='TOPIX Core30']['コード'].astype(int).astype(str))
ticker_list = [i + '.JP' for i in topix_core30]
#データ取得
date_e = date.today() - relativedelta(days=1)
date_s = date_e - relativedelta(years=1)
price = web.stooq.StooqDailyReader(ticker_list, start=date_s, end=date_e,).read().sort_index()
price_tpx = web.stooq.StooqDailyReader(['^TPX'], start=date_s, end=date_e,).read().sort_index()
rtn = price['Close'].pct_change().dropna() * 100
rtn_tpx = price_tpx['Close'].pct_change().dropna() * 100
まずはデータを準備します。
#ベータの算出
import pandas as pd
import statsmodels.api as sm
df = pd.DataFrame(index=rtn.columns, columns=['beta'])
for i in range(len(rtn.columns)):
# 切片を追加
X = sm.add_constant(rtn_tpx)
# OLSモデルの作成
model = sm.OLS(rtn.iloc[:,i], X)
# モデルのフィッティング
result = model.fit()
# 係数の取得
coefficients = result.params['^TPX']
df.loc[rtn.columns[i], 'beta'] = coefficients
df['beta'] = df['beta'].astype('float')
各銘柄のベータを算出します。
import pandas as pd
import statsmodels.api as sm
df2 = pd.DataFrame(index=rtn.index, columns=['beta'])
for i in range(len(rtn.index)):
# 切片を追加
X = sm.add_constant(df['beta'])
# OLSモデルの作成
model = sm.OLS(rtn.iloc[i,:],df['beta'])
# モデルのフィッティング
result = model.fit()
# 係数の取得
coefficients = result.params['beta']
df2.loc[rtn.index[i], 'beta'] = coefficients
クロスセクション方向の回帰を行います。
from scipy.stats import ttest_1samp
# 検定統計量とp値の計算
t_statistic, p_value = ttest_1samp(df2['beta'].astype(float), popmean=0) # popmeanは仮説の母平均
# 結果の表示
print(f"検定統計量: {t_statistic}")
print(f"p値: {p_value}")
# p値の有意水準での検定結果の表示
alpha = 0.05
if p_value < alpha:
print("帰無仮説を棄却します(統計的に有意)")
else:
print("帰無仮説を採択します(統計的に有意ではありません)")
検定を行います。
0 秒
from scipy.stats import ttest_1samp
# 検定統計量とp値の計算
t_statistic, p_value = ttest_1samp(df2['beta'].astype(float), popmean=0) # popmeanは仮説の母平均
# 結果の表示
print(f"検定統計量: {t_statistic}")
print(f"p値: {p_value}")
# p値の有意水準での検定結果の表示
alpha = 0.05
if p_value < alpha:
print("帰無仮説を棄却します(統計的に有意)")
else:
print("帰無仮説を採択します(統計的に有意ではありません)")
検定統計量: 1.4268115245159134
p値: 0.1549231308137875
帰無仮説を採択します(統計的に有意ではありません)
統計的に有意ではなかったようです。
CAPMの成立を確認することはできませんでした。
ここまでは自分で回帰を行いましたが、ライブラリを使うこともできます。
! pip install linearmodels
from linearmodels.asset_pricing import LinearFactorModel
model = LinearFactorModel(rtn, rtn_tpx)
res = model.fit()
print(res.full_summary)
検定の結果の数値は若干違いますが、平均値に関してはほぼ同じ値になっています。
今回のコードはこちら