見出し画像

第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();

画像1

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);

画像2

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);

画像3

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();

画像4

データの保存

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)

画像5





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