【証券分析5-2】FamaMacbeth回帰とは

ファイナンス理論

今回は、CAPMの検証や、ファクターの有効性の検証によく用いられる手法であるFama-MacBeth回帰を見ていきます。
よく用いられる手法ですが、意外と情報が少なく、間違った理解をしているケースも多いのがこの手法です。

Fama-MacBeth回帰とは

ファーマ・マクベス回帰(Fama-MacBeth regression)は、CAPMの検証や、ファクターの有効性の検証に用いられる手法で、ユージン・ファマ(Eugene Fama)とジェームズ・マクベス(James MacBeth)によって1973年に提案されました。
詳しい内容は、「Risk, return, and equilibrium」という論文に記載されています。
Fama-MacBeth回帰の基本的な手順は以下のようになります。

  1. 各銘柄のリターンを時系列方向に回帰して、ベータを算出する
  2. 時点tのクロスセクション回帰を行う(tは1~Tまで。T回の回帰を行う)
  3. 2で行ったクロスセクション回帰の回帰係数の時系列について(T個の推定された回帰係数)符号の有意性を検定する

という流れが基本になります。
原論文では検証においては、個別の銘柄のベータを用いるのではなく、ポートフォリオのベータを用いているので、少し手順が増えて以下のようになっています。

  1. 各銘柄のリターンを時系列方向に回帰して、ベータを算出する。ベータの大きい順に20個のポートフォリオに分類する。
  2. 各ポートフォリオについて、ポートフォリオ内の個別銘柄のベータを計算し、平均をポートフォリオのベータとする
  3. 各ポートフォリオのベータを用いて、時点tのクロスセクション回帰を行う(tは1~Tまで。T回の回帰を行う)。
  4. 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)

結果

検定の結果の数値は若干違いますが、平均値に関してはほぼ同じ値になっています。

今回のコードはこちら

タイトルとURLをコピーしました