見出し画像

4章アルファ-ファクター研究: 第一節: 特徴量エンジニアリング

参考記事

こちらでは、特徴量のデータセットを作っていきます。様々な手法が紹介されているので参考になるかと思います。

本記事のnotebookで実行するためにはいくつか必要なモジュールとデータセットがございますので、もしデータセットで行き詰った場合は、こちらのノート(データセット準備編)をご参考くださいませ。
まずは普通にimportします。pyfinance等のモジュールは各自インストールをお願いいたします。

インポート

import warnings
warnings.filterwarnings('ignore')

%matplotlib inline

from datetime import datetime
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import pandas_datareader.data as web
from pyfinance.ols import PandasRollingOLS 
 #plotのスタイルsns .set_style('whitegrid')
idx = pd.IndexSlice

データ取得

 #データの位置 
DATA_STORE = '../data/assets.h5' #期間指定 
START = 2000
END = 2018
 #データ取得 
with pd.HDFStore(DATA_STORE) as store:
   prices = (store['quandl/wiki/prices']
             .loc[idx[str(START):str(END), :], 'adj_close']
             .unstack('ticker'))
   stocks = store['us_equities/stocks'].loc[:, ['marketcap', 'ipoyear', 'sector']]

画像1

pricesは2412銘柄で4706営業日あります。

画像2

stocksは2412銘柄の時価総額、IPO時期、セクターの情報です。

データの整理とフォーマット

stocksから重複しているインデックスを削除して、stockspricesの共通部分だけを取得します。今回のデータセットはちゃんとしていたが、他のデータセットを使うときには役に立ちます。

 #重複を削除 
stocks = stocks[~stocks.index.duplicated()]
stocks.index.name = 'ticker' #共通部分のみを取得 
shared = prices.columns.intersection(stocks.index)
stocks = stocks.loc[shared, :]
prices = prices.loc[:, shared]

本データセットは日次データとなっており、計算量の削減と戦略の保有時間を伸ばすために、日次を月次データに変換する。

monthly_prices = prices.resample('M').last()
ここから特徴量生成を行うが、たとえば、モメンタムのパターンをダイナミクスに反映させたい場合は.pct_change(n_periods)メソッドを使用して過去のリターンを計算します。つまり、ラグによって識別される月ごとのさまざまな期間のリターンを計算します。
次に、.stack()メソッドを使用してワイド結果をロングフォーマットに変換し、.pipe()を使用して.clip()メソッドを結果のDataFrameに適用し、[1%、99%]レベルでリターンをウィンソライズ(取り除く)します。つまり、これらのパーセンタイルで外れ値を除きます。
最後に、幾何平均を使用してリターンを正規化します。 .swaplevel()を使用してMultiIndexレベルの順序を変更した後、1か月から12か月の範囲の6つの期間の複合月次リターンを取得します。
outlier_cutoff = 0.01
data = pd.DataFrame()
lags = [1, 2, 3, 6, 9, 12]
for lag in lags:
   data[f'return_{lag}m'] = (monthly_prices
                          .pct_change(lag)
                          .stack()
                          .pipe(lambda x: x.clip(lower=x.quantile(outlier_cutoff),
                                                 upper=x.quantile(1-outlier_cutoff)))
                          .add(1)
                          .pow(1/lag)
                          .sub(1)
                          )
data = data.swaplevel().dropna()
data.info()

上場10年未満の株式を取り除きます。

min_obs = 120
nobs = data.groupby(level='ticker').size()
keep = nobs[nobs>min_obs].index

data = data.loc[idx[keep,:], :]

リターンの相関関係を見る。

sns.clustermap(data.corr('spearman'), annot=True, center=0, cmap='Blues');

画像3

概ね相関性は高い。株はやはりモメンタムがありそうだ。ここからは、様々な特徴量を作成し、本データセットに入れていく。

※相関が高いからと言って、モメンタムがあるとは限りません。こちらは私のミスです。失礼いたしました。こちらでは、クラスタリングを目的としており、そういった解釈はnotebookの方にも記載されておりません。

ローリングファクターベータ(Fama-French)

ここでFama-French法を紹介する。この手法は、一般的なリスク要因への資産のエクスポージャーを推定するためにある。詳しくは8章参照(Time Series Models.)

5つのFama-Frenchファクター、つまり市場リスク、サイズ、バリュー、営業収益性、および投資は、資産のリターンを説明するために経験的に示され、ポートフォリオのリスク/リターンのプロファイルを評価するために一般的に使用されます。したがって、将来のリターンを予測することを目的としたモデルに、過去の要因のエクスポージャーを財務上の特徴として含めるのは当然です。

次のように、pandas-datareaderを使用して履歴因子のリターンにアクセスし、pyfinanceライブラリのPandasRollingOLSローリング線形回帰機能を使用して履歴の露出を推定できます。

Fama-Frenchリサーチファクターを使用して、市場リスク、サイズ、バリュー、営業収益性、投資の5つのファクターに対するデータセット内の株式のファクターエクスポージャーを推定します。

factors = ['Mkt-RF', 'SMB', 'HML', 'RMW', 'CMA']
factor_data = web.DataReader('F-F_Research_Data_5_Factors_2x3', 'famafrench', start='2000')[0].drop('RF', axis=1)
factor_data.index = factor_data.index.to_timestamp()
factor_data = factor_data.resample('M').last().div(100)
factor_data.index.name = 'date'
factor_data = factor_data.join(data['return_1m']).sort_index()

次にこのモデルからベータを計算する。

T = 24
betas = (factor_data
        .groupby(level='ticker', group_keys=False)
        .apply(lambda x: PandasRollingOLS(window=min(T, x.shape[0]-1), y=x.return_1m, x=x.drop('return_1m', axis=1)).beta))
cmap = sns.diverging_palette(10, 220, as_cmap=True)
sns.clustermap(betas.corr(), annot=True, cmap=cmap, center=0);

24-window期間でローリングしてbetaの相関行列を見る。

画像4

次にbetaを特徴量に入れます。

data = (data
       .join(betas
             .groupby(level='ticker')
             .shift()))  #欠陥データを平均補完する
data.loc[:, factors] = data.groupby('ticker')[factors].apply(lambda x: x.fillna(x.mean()))

モメンタムファクター

for lag in [2,3,6,9,12]:
   data[f'momentum_{lag}'] = data[f'return_{lag}m'].sub(data.return_1m)
data[f'momentum_3_12'] = data[f'return_12m'].sub(data.return_3m)

時間インジケーター

dates = data.index.get_level_values('date')
data['year'] = dates.year
data['month'] = dates.month

ラグリターン

for t in range(1, 7):
   data[f'return_1m_t-{t}'] = data.groupby(level='ticker').return_1m.shift(t)
data.info()

保有期間リターン

for t in [1,2,3,6,12]:
   data[f'target_{t}m'] = data.groupby(level='ticker')[f'return_{t}m'].shift(-t)

上場年数

data = (data
       .join(pd.qcut(stocks.ipoyear, q=5, labels=list(range(1, 6)))
             .astype(float)
             .fillna(0)
             .astype(int)
             .to_frame('age')))
data.age = data.age.fillna(-1)

時価総額

NASDAQティッカー情報からの時価総額情報を使用して、企業の大きさを作成します。

size_factor = (monthly_prices
              .loc[data.index.get_level_values('date').unique(),
                   data.index.get_level_values('ticker').unique()]
              .sort_index(ascending=False)
              .pct_change()
              .fillna(0)
              .add(1)
              .cumprod())
msize = (size_factor
        .mul(stocks
             .loc[size_factor.columns, 'marketcap'])).dropna(axis=1, how='all')

サイズのインジケーターとしてそれぞれの期間の十分位を作成

data['msize'] = (msize
                .apply(lambda x: pd.qcut(x, q=10, labels=list(range(1, 11)))
                       .astype(int), axis=1)
                .stack()
                .swaplevel())
data.msize = data.msize.fillna(-1)

データの結合

data = data.join(stocks[['sector']])
data.sector = data.sector.fillna('Unknown')

データの保存

with pd.HDFStore(DATA_STORE) as store:
   store.put('engineered_features', data.sort_index().loc[idx[:, :datetime(2018, 3, 1)], :])
   print(store.info())

ダミー変数の作成

dummy_data = pd.get_dummies(data,
                           columns=['year','month', 'msize', 'age',  'sector'],
                           prefix=['year','month', 'msize', 'age', ''],
                           prefix_sep=['_', '_', '_', '_', ''])
dummy_data = dummy_data.rename(columns={c:c.replace('.0', '') for c in dummy_data.columns})
dummy_data.info()

まとめ

特徴量エンジニアリングは、オルタナティブデータやマーケットデータを色々と弄ります。集計スキルはとても大事です。集計が出来れば、特徴量エンジニアリングはグッと楽になると思います。特徴量データセットを作成したら、やっと本格的にモデリングの話に入ることが出来ます。

この記事が気に入ったらサポートをしてみませんか?