こんにちは。システムトレーダーの卵ことKenKenです。前回は”Jane Street Market Prediction”のデータに全体の特徴について簡単に見ました。そこでは、リターンと特徴量では相関が低いものの、各特徴量では相関が高いものがあることがわかりました。しかし、特徴量の数が130個あり、リターン(resp)との関係を把握するには苦しい状況でした。そこで今回は、特徴量の次元を圧縮し、各クラスタごとにrespとの関係を調べていきたいと思います。次元圧縮には、ひとまず主成分分析とクラスタリング(K-means)を用いてみます。
※本記事は、公開しているNotebookをまとめたものとなっております。
データの標準化
まずは、データを標準化しておきます。前回の分析では、標準化しておりませんが結果は大差なかったです。標準化には、scikit-learnのpreprocessing.StandartScaler()を利用します。尚、テストデータも同じパラメータで標準化を行うので、学習データ使用したインスタンスも保存しておきます。また、今回の学習データとテストデータはデータの形状が異なるので、学習データのデータフレームをテストデータと同じ形状に統一しておきます。合わせて、respに関する情報を1オブジェクトにまとめ保存しておきます。
import numpy as np import pandas as pd import pickle # read data train = pd.read_csv('/kaggle/input/jane-street-market-prediction/train.csv') test = pd.read_csv('/kaggle/input/jane-street-market-prediction/example_test.csv') # 欠損値の補完(前の値で補完する) train.fillna(method = 'ffill', inplace=True) train.dropna(inplace=True) # respについて resp_params = (train['resp'].mean(), train['resp'].std()) resp_standardized = ((train['resp'] - resp_params[0])/resp_params[1]).values resp_info = (resp_params, resp_standardized) # 基準化 from sklearn.preprocessing import StandardScaler sc = StandardScaler() sc.fit(train[test.columns]) Z = sc.transform(train[test.columns]) train = pd.DataFrame(Z, columns=test.columns) # dfの整形 train = reduce_mem_usage(train) # メモリ対策(関数は前回の記事参照) # 保存 import pickle train.to_pickle('/kaggle/working/train_standardized_without_null.pickle') with open('/kaggle/working/SC.pickle', 'wb') as f: pickle.dump(sc, f) with open('/kaggle/working/resp_info.pickle', 'wb') as f: pickle.dump(resp_info, f)
主成分分析(PCA)
特徴量(feature_X)について主成分分析を行い、累積寄与率をプロットしてみる。
from sklearn.decomposition import PCA import matplotlib.pyplot as plt # 特徴量のみにする X = train.drop(['weight', 'date', 'ts_id'],axis=1).values # 主成分分析 pca = PCA() pca.fit(X) # データを主成分空間に写像(主成分スコア) score = pca.transform(X) # 累積寄与率を図示する plt.plot([0] + list( np.cumsum(pca.explained_variance_ratio_)), "-o") plt.xlabel("Number of principal components") plt.ylabel("Cumulative contribution rate") plt.grid() plt.show()

上図から概ね第30主成分程度で特徴量全体の90%を表現できていることがわかる。調べてみたら第16主成分で80%、第20主成分で85%、第28主成分目で90%を上回っていた。
リターンと各主成分の関係
第16主成分までを取り出し、resp(リターン)や各主成分間の関係を見てみる。
import seaborn as sns # respと主成分スコアの関係 # df作成 target = pd.DataFrame(np.concatenate([resp_info[1][:, np.newaxis], score[:, :16]], axis=1)) target.columns = pd.Index(['resp'] + ['PC{}'.format(i+1) for i in range(16)]) # ヒートマップ作成 sns.heatmap(target.corr())

各主成分やrespの間に相関は低いことがわかる。
主成分回帰モデル(PCRモデル)
ひとまず、この16主成分でrespの線形モデルを作成し、テストデータのresp推定してみる。
import statsmodels.api as sm # 'weight'を追加 target = pd.concat([target, train['weight']], axis=1).copy() target.head() # PCR x = target.drop(['resp'], axis=1) x = sm.add_constant(x) model = sm.OLS(target['resp'], x) result = model.fit() print(result.summary()) # グラフを書く plt.plot(target['resp'], label='resp', linestyle='--') result.fittedvalues.plot(label='fitted', style=':') plt.legend()


全期間で線形回帰を行った結果、決定係数は0.002と酷い。全期間のデータを使うのは賢明ではなさそう。周期性などを考慮したほうが良いのかもしれない。次に、最初の100個を対象に最適化を行った結果は以下の通りである。


そこそこフィットしている様子が見られる。データを少なくすればフィットするようである。期間を複数に分け、各期間ごとで最適なモデルを採用するようなものにすると良いのかもしれない。分析に入る前に、リターンの特徴(周期性など)を最初に見るべきだった。
クラスタリング(K-means)
主成分スコア(第16主成分まで)をクラスタリングし、各クラスタごとのリターンに特徴があるか見てみる。
from sklearn.cluster import KMeans # K-means kmeans_model = KMeans(n_clusters=5, random_state=0).fit(target.iloc[:, 1:]) # resp以外 km_result = pd.concat([target, pd.DataFrame(kmeans_model.labels_, columns=['cluster'])],axis=1) km_result.groupby('cluster').describe()['resp']

次にrespを正負に変換し、クラスタごとにカウントしてみる。
km_result['resp_pn'] = km_result['resp'].apply(lambda x:'p' if x>0 else 'n') km_result.groupby(['cluster', 'resp_pn']).count()['resp'].plot.bar(color=['blue', 'red'])

カウントベースでみると、クラスター1と3ではマイナスの方が数が多いことがわかる。クラスター1については、平均もマイナスであり、マイナスの特性を持ちそうだ。
クラスター数を3にしてみると以下の結果が得られた。


次にクラスター数を10にしてみた場合。


クラスターで分けることでマイナスの特徴を捉えることができている可能性がある。また、クラスター数は、3程度でも十分と思われる。ひとまず、今回の結果を踏まえると、モデル開発においてクラスター番号を入れてみる価値はありそうだ。ただ、ポジティブの特徴について見ると、クラスター数5の(2,p)が捉えることができている可能性があるのを踏まえると、クラスター数は5としてみようと思う。
主成分分析+クラスタリング
上記で算出したクラスター番号を説明変数に追加し、線形回帰を行ってみる。
# PCR + Clustering x = km_result.drop(['resp', 'resp_pn'], axis=1) x = sm.add_constant(x) model = sm.OLS(km_result['resp'][:100], x[:100]) result = model.fit() print(result.summary())

あまり改善はされていない。変数が一つ増えたことで、補正R2は低下している。
そもそも主成分分析を行い、次元数を縮小させることに価値があるのか確認のため、元の特徴量(feature_X)で全期間に対して線形回帰を行ってみた。

決定係数だけについて見てるとこちらのほうがフィットしている(大差はない)。ここまでの結果を踏まえると主成分分析による次元圧縮では厳しい。ただ、使い方次第では、意味があるかもしれないのでもう少し吟味していきたい。
まとめ
今回は、特徴量について主成分分析により次元圧縮を行い、各主成分とresp(リターン)との関係について簡単に見てみました。resp(リターン)の特徴(周期性はないか、定常過程か、等)を見ていかないとモデル構築へは進めないと思うので、次回はrespに的を絞って特徴を見ていきたいと思います。ここまでの結果から、提供されたデータだけを使ってモデルを作るだけでは、良いモデルは作れなそうな印象を抱いております。提供されたデータの特徴を見て、そこから新たな変数を作るなどしていくらかのひねり(工夫)を加えていかないと駄目な気がしてます。
良いモデルができるイメージが湧かない。。。金融データはやっぱ難しいですね。。。
以上