第11章:ランダムフォレスト - ロングショート戦略 第0節: 本章の方針とデータの準備
この章で学ぶこと
この章では、決定木とランダム木を利用した予測モデルについて紹介する。
モデルがどのように説明変数と目的変数の非線形な関係を解釈するのかについて見ていきたいと思います。
また、アンサンブルモデルという複数の個別のモデルを組み合わせたものについても紹介します。
この章を読み終わると習得できること
・回帰と分類のための決定木を使うことが出来る
・決定木への理解を深め、モデルが学んだ規則を視覚化出来る
・なぜアンサンブルモデルがより良い結果をもたらす傾向があるのかを理解出来る
・決定木のオーバーフィッティング課題をバギングで解決する
・ランダム木の訓練、改善、解釈が出来る
・ランダムフォレストを用いて利益的な投資戦略を設計、評価できる
データの準備(データをファクターへ変換する)
主要なファクターのカテゴリー、その根拠、および有名な指標を概念的に理解するためには、リターンのドリフトによって体現されるリスクをより適切に捉えるかもしれない新しい要因を特定するか、新しいものを見つけることです。
どちらの場合でも、革新的な要因のパフォーマンスを既知の要因のパフォーマンスと比較して、何がプラスαになっているのかを特定することが重要です。
ここでデータセットを作成し、それをデータフォルダーに保存して、後の章での再利用を容易にします。
インポートと設定
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
import numpy as np
import pandas as pd
import pandas_datareader.data as web
from pyfinance.ols import PandasRollingOLS
from talib import RSI, BBANDS, MACD, NATR, ATR
from sklearn.feature_selection import mutual_info_classif, mutual_info_regression
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')
idx = pd.IndexSlice
米株市場のOHLCVデータを読み込む
DATA_STORE = '../data/assets.h5'
YEAR = 12
START = 1995
END = 2017
with pd.HDFStore(DATA_STORE) as store:
prices = (store['quandl/wiki/prices']
.loc[idx[str(START):str(END), :], :]
.filter(like='adj_')
.dropna()
.swaplevel()
.rename(columns=lambda x: x.replace('adj_', ''))
.join(store['us_equities/stocks']
.loc[:, ['sector']])
.dropna())
prices.info(null_counts=True)
'''
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 10241831 entries, ('AAN', Timestamp('1995-01-03 00:00:00')) to ('ZUMZ', Timestamp('2017-12-29 00:00:00'))
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 open 10241831 non-null float64
1 high 10241831 non-null float64
2 low 10241831 non-null float64
3 close 10241831 non-null float64
4 volume 10241831 non-null float64
5 sector 10241831 non-null object
dtypes: float64(5), object(1)
memory usage: 508.0+ MB
'''
len(prices.index.unique('ticker'))
'''
2369
'''
一応セクター情報も入っており、銘柄数は2369です。
上場10年未満の銘柄を取り除く
min_obs = 10 * 252
nobs = prices.groupby(level='ticker').size()
to_drop = nobs[nobs < min_obs].index
prices = prices.drop(to_drop, level='ticker')
prices.info(null_counts=True)
'''
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 9532628 entries, ('AAN', Timestamp('1995-01-03 00:00:00')) to ('ZUMZ', Timestamp('2017-12-29 00:00:00'))
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 open 9532628 non-null float64
1 high 9532628 non-null float64
2 low 9532628 non-null float64
3 close 9532628 non-null float64
4 volume 9532628 non-null float64
5 sector 9532628 non-null object
dtypes: float64(5), object(1)
memory usage: 472.9+ MB
'''
len(prices.index.unique('ticker'))
'''
1883
'''
1883銘柄残りました。
いくつかの基本ファクターを入れる
RSI
prices['rsi'] = prices.groupby(level='ticker').close.apply(RSI)
ボリンジャーバンド
def compute_bb(close):
high, mid, low = BBANDS(np.log1p(close), timeperiod=20)
return pd.DataFrame({'bb_high': high,
'bb_mid': mid,
'bb_low': low}, index=close.index)
prices = (prices.join(prices
.groupby(level='ticker')
.close
.apply(compute_bb)))
prices['bb_up'] = prices.bb_high.sub(np.log1p(prices.close))
prices['bb_down'] = np.log1p(prices.close).sub(prices.bb_low)
fig, axes = plt.subplots(ncols=2, figsize=(10,4))
for i, col in enumerate(['bb_down', 'bb_up']):
sns.boxenplot(prices[col], ax=axes[i])
axes[i].set_title(col);
fig.tight_layout();
ATR
by_ticker = prices.groupby('ticker', group_keys=False)
def compute_atr(stock_data):
atr = ATR(stock_data.high,
stock_data.low,
stock_data.close,
timeperiod=14)
return atr.sub(atr.mean()).div(atr.std())
prices['atr'] = by_ticker.apply(compute_atr)
prices['natr'] = by_ticker.apply(lambda x: NATR(high=x.high, low=x.low, close=x.close))
MACD
def compute_macd(close):
macd = MACD(close)[0]
return macd.sub(macd.mean()).div(macd.std())
prices['macd'] = prices.groupby(level='ticker').close.apply(compute_macd)
ドル出来高
prices['dollar_volume'] = (prices.loc[:, 'close']
.mul(prices.loc[:, 'volume'], axis=0))
prices.dollar_volume /= 1e6
prices.to_hdf('data.h5', 'us/equities/prices')
prices = pd.read_hdf('data.h5', 'us/equities/prices')
月次リターンへリサンプリング
last_cols = [c for c in prices.columns.unique(0) if c not in ['dollar_volume', 'volume',
'open', 'high', 'low']]
prices = prices.unstack('ticker')
data = (pd.concat([prices.dollar_volume.resample('M').mean().stack('ticker').to_frame('dollar_volume'),
prices[last_cols].resample('M').last().stack('ticker')],
axis=1)
.swaplevel()
.dropna())
上位500出来高銘柄を選ぶ
data['dollar_volume'] = (data
.groupby('ticker',
group_keys=False,
as_index=False)
.dollar_volume
.rolling(window=5*12)
.mean()
.fillna(0)
.reset_index(level=0, drop=True))
data['dollar_vol_rank'] = (data
.groupby('date')
.dollar_volume
.rank(ascending=False))
data = data[data.dollar_vol_rank < 500].drop(['dollar_volume', 'dollar_vol_rank'], axis=1)
月次リターン系列の作成
主にトレンド系の指標です。
outlier_cutoff = 0.01
lags = [1, 3, 6, 12]
returns = []
for lag in lags:
returns.append(data
.close
.unstack('ticker')
.sort_index()
.pct_change(lag)
.stack('ticker')
.pipe(lambda x: x.clip(lower=x.quantile(outlier_cutoff),
upper=x.quantile(1-outlier_cutoff)))
.add(1)
.pow(1/lag)
.sub(1)
.to_frame(f'return_{lag}m')
)
returns = pd.concat(returns, axis=1).swaplevel()
cmap = sns.diverging_palette(10, 220, as_cmap=True)
sns.clustermap(returns.corr('spearman'), annot=True, center=0, cmap=cmap);
data = data.join(returns).drop('close', axis=1).dropna()
min_obs = 5*12
nobs = data.groupby(level='ticker').size()
to_drop = nobs[nobs < min_obs].index
data = data.drop(to_drop, level='ticker')
len(data.index.unique('ticker'))
'''
578
'''
578銘柄残りました。
ローリングファクターベータ
factors = ['Mkt-RF', 'SMB', 'HML', 'RMW', 'CMA']
factor_data = web.DataReader('F-F_Research_Data_5_Factors_2x3',
'famafrench',
start=START)[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']).dropna().sort_index()
factor_data['return_1m'] -= factor_data['Mkt-RF']
T = 60
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)
.rename(columns={'Mkt-RF': 'beta'}))
cmap = sns.diverging_palette(10, 220, as_cmap=True)
sns.clustermap(betas.corr(), annot=True, cmap=cmap, center=0);
data = (data
.join(betas
.groupby(level='ticker')
.shift())
.dropna()
.sort_index())
モメンタムファクター
for lag in [3, 6, 12]:
data[f'momentum_{lag}'] = data[f'return_{lag}m'].sub(data.return_1m)
if lag > 3:
data[f'momentum_3_{lag}'] = data[f'return_{lag}m'].sub(data.return_3m)
時間指標
dates = data.index.get_level_values('date')
data['year'] = dates.year
data['month'] = dates.month
目的変数:フォワードリターン
data['target'] = data.groupby(level='ticker')[f'return_1m'].shift(-1)
data = data.dropna()
セクターブレイクダウン
ax = data.reset_index().groupby('sector').ticker.nunique().sort_values().plot.barh(title='Sector Breakdown')
ax.set_ylabel('')
ax.set_xlabel('# Tickers')
sns.despine()
plt.tight_layout();
データの保存
with pd.HDFStore('data.h5') as store:
store.put('us/equities/monthly', data)
相互情報量評価
X = data.drop('target', axis=1)
X.sector = pd.factorize(X.sector)[0]
mi = mutual_info_regression(X=X, y=data.target)
mi_reg = pd.Series(mi, index=X.columns)
mi_reg.nlargest(10)
'''
natr 0.109561
return_12m 0.062000
return_6m 0.053269
year 0.052604
return_3m 0.050100
momentum_3_6 0.041006
momentum_12 0.040702
bb_low 0.040701
bb_up 0.039735
momentum_3_12 0.039040
dtype: float64
'''
mi = mutual_info_classif(X=X, y=(data.target>0).astype(int))
mi_class = pd.Series(mi, index=X.columns)
mi_class.nlargest(10)
'''
month 0.010603
year 0.008486
return_6m 0.004189
beta 0.003494
natr 0.003493
bb_low 0.002777
return_12m 0.002652
HML 0.002575
macd 0.001823
sector 0.001667
dtype: float64
'''
mi = mi_reg.to_frame('Regression').join(mi_class.to_frame('Classification'))
mi.index = [' '.join(c.upper().split('_')) for c in mi.index]
fig, axes = plt.subplots(ncols=2, figsize=(12, 4))
for i, t in enumerate(['Regression', 'Classification']):
mi[t].nlargest(20).sort_values().plot.barh(title=t, ax=axes[i])
axes[i].set_xlabel('Mutual Information')
fig.suptitle('Mutual Information', fontsize=14)
sns.despine()
fig.tight_layout()
fig.subplots_adjust(top=.9)
この記事が気に入ったらサポートをしてみませんか?