こんにちは。システムトレーダーの卵ことKenKenです。現在、開催中のKagle”のコンペ”Jane Street Market Prediction”に挑戦中です。前回は、主成分分析やクラスタリングを用いて特徴量の次元圧縮をしてみました。そして、クラスタ別でリターンに差があるように見えました。そこで、線形回帰を行いましたが、散々たる結果でした。
ただ、せっかく主成分分析やクラスタリングの結果があるので、それらを利用してコンペにエントリーだけはしてみたいと思います。先に結果を記載すると、スコアは1,927と残念な結果なりました。ただ、今回のエントリーを通して、コンペに参加する上で基本的なお作法について学ぶことができてよかったと思っております。エントリーに際して、「Not Found」や「Notebook Timeout」のエラーで苦しんだので、同じようなことで悩んでいる人には多少は有益になるかもしれません。また、これらのエラーハンドリングを通して、Pythonにおける計算速度の改善やメモリ対策について学ぶことができたのはよかったと思っております。
では早速、学習データから算出した主成分スコアやクラスター番号を用いてモデルを構築し、テストデータの予測行っていきたいと思います。
※本記事は、公開しているNotebookをまとめたものとなっております。
前処理
import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv) import matplotlib.pyplot as plt %matplotlib inline import gc, pickle, os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) import janestreet train = pd.read_csv('/kaggle/input/jane-street-market-prediction/train.csv') # 欠損値の補完(前の値で補完する) train.fillna(method = 'ffill', inplace=True) train.dropna(inplace=True)
欠損値は前の値で埋めることにしている。理由については、過去の記事に記載している。ただし、この処理を行うテストデータで再現するときに処理が複雑になってしまった。
基準化
# 目的変数の逆変換時に使用 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) # 列を取得 columns = train.columns.drop(['date', 'resp_1', 'resp_2', 'resp_3', 'resp_4', 'resp', 'ts_id']) # 基準化 from sklearn.preprocessing import StandardScaler sc = StandardScaler() sc.fit(train[columns]) Z = sc.transform(train[columns]) train = pd.DataFrame(Z, columns=columns) # メモリ対策 train = reduce_mem_usage(train)
学習データについては、StandardScaler()モジュールを用いて基準化を行っている。テストデータの推定時にはrespが含まれていないため、目的変数(resp)については、別で平均と標準偏差を取得し、基準化を行っている。
主成分分析
import sklearn from sklearn.decomposition import PCA # 主成分分析 pca = PCA() pca.fit(train.drop(['weight'],axis=1).values) # データを主成分空間に写像 score = pca.transform(train.drop(['weight'],axis=1).values) # respと主成分スコアを1つの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)]) # 'weight'を追加 target = pd.concat([target, train['weight']], axis=1).copy() # メモリ対策 del score del train gc.collect()
K-Means
from sklearn.cluster import KMeans # K-means kmeans_model = KMeans(n_clusters=5, random_state=0).fit(target.iloc[:, 1:]) # resp以外でクラスタリング # 結果をdfにまとめる km_result = pd.concat([target, pd.DataFrame(kmeans_model.labels_, columns=['cluster'])],axis=1) km_result['resp_pn'] = km_result['resp'].apply(lambda x:'p' if x>0 else 'n') km_result.head()
モデル構築
主成分スコアとクラスト番号からrespを予測するモデルを構築してみる。今回は、ひとまずLightGBMを用いている(ハイパーパラメータは適当に設定)。
# 学習デートと検証データに分ける(時系列データのため、直近2割を検証用) from sklearn.model_selection import train_test_split train_data, valid_data = train_test_split(km_result, shuffle=False, test_size=0.2) import lightgbm as lgb lgb_train = lgb.Dataset(train_data.drop(['resp', 'resp_pn'], axis=1), train_data['resp']) lgb_eval = lgb.Dataset(valid_data.drop(['resp', 'resp_pn'], axis=1), valid_data['resp']) # LightGBM parameters params = { 'task': 'train', 'boosting_type': 'gbdt', 'objective': 'regression', # 目的 : 回帰 'metric': {'rmse'}, # 評価指標 : rsme(平均二乗誤差の平方根) 'num_iteration': 10000, #10000回学習 'verbose': 0 } # モデルの学習 model = lgb.train(params, # パラメータ train_set=lgb_train, # トレーニングデータの指定 valid_sets=lgb_eval, # 検証データの指定 early_stopping_rounds=100 # 100回ごとに検証精度の改善を検討 → 精度が改善しないなら学習を終了(過学習に陥るのを防ぐ) )
116epoch目で学習がEarly Stoppingにより止まった。最適なepochは16となった。今回は、このモデルを使用するが、あまりにも早い為、学習が適切にできていない可能性が高い。
検証データについて
まずは推定値の分布について見てみる。
mu, sigma = resp_info[0] valid_predict = model.predict(valid_data.drop(['resp', 'resp_pn'], axis=1)) * sigma + mu # ヒストグラムを書いてみる plt.figure() plt.subplot(1, 2, 1) plt.hist(valid_predict, bins=50, label='valid_predict', color='blue') plt.title('predict') plt.subplot(1, 2, 2) valid_data['resp'].hist(bins=50, histtype='step', label='valid_resp', color='red') plt.title('true resp')

形状はかなり似通っている。次に、actionを決める際に、推定値(回帰)を{0, 1}の2値に変換する必要がある。通常であれば、推定値がプラス(0以上)の場合にaction=1とするが、よりスコアが高くなる閾値はないかを簡単に算出してみる。今回は、0以上の推定値について0~100%の分位点に分け、各点におけるスコアを算出し、最も高い点を最適な閾値としてみた。スコアの算出は、respの合計としている。(本来なら、コンペのスコア算出にすべき?)
valid_data['predict'] = valid_predict * sigma + mu # actionを決める為、thresholdを設定する(予測が正だったものから1%刻みのパーセンタイルとする) score_data = {} for i in range(100): threshold = np.percentile(valid_data.loc[valid_data['predict'] > 0]['predict'], i) score = valid_data['resp'].loc[valid_data['predict'] > threshold].sum() # save score_data[i] = [threshold, score] # total scoreのthreshold毎の推移 plt.plot([score for _, score in score_data.values()], label='total score') plt.title('total score in each thresholds') plt.xlabel('threshold') plt.ylabel('total score') best_score_point = np.argmax([score for _, score in score_data.values()]) best_score = score_data[best_score_point][1] best_score_threshold = score_data[best_score_point][0] print('best score: {}'.format(best_score)) print('best score point: {}'.format(best_score_point)) print('best threshold: {}'.format(best_score_threshold))


60%点がスコア5334で最適な点となった。テストデータの推定の際は、この閾値をを使用する。
テストデータの推定
処理の流れは以下の通り。
- テストデータの欠損値処理
- テストデータを学習データの標準化の際に利用したパラメータで標準化
- 標準化したテストデータに対して主成分スコアを算出
- テストデータに対してクラスター番号を算出
- モデル構築
- モデルにより算出した標準化済みrespを元に戻す
- respの正負に応じて売買執行を決定
env = janestreet.make_env() iter_test = env.iter_test() first_step = True second_step = False for (test_df, sample_prediction_df) in iter_test: null_pos = test_df.isnull().values # 欠損値の位置(True or Flaseの配列) with_null = null_pos.any() # 欠損値の判定 # 最初の欠損値の処理:actionをしないでスキップ if first_step: if with_null: sample_prediction_df["action"] = 0 env.predict(sample_prediction_df) else: first_step = False second_step = True # 欠損値が無いデータ以降の処理(※途中、欠損値を含む) if second_step: if with_null: # 欠損値を前のレコードの値で埋める null_columns = np.where(null_pos)[1] test_df.iloc[:, null_columns] = test_df_prv.iloc[:, null_columns].values # 前レコードを保存 test_df_prv = test_df.copy() if test_df['weight'].items() == 0: sample_prediction_df["action"] = 0 env.predict(sample_prediction_df) continue # 正規化 Z = sc.transform(test_df[columns]) # 主成分分析:データを主成分空間に写像 score_test = pca.transform(Z[:, 1:]) # weightを追加する score_test = np.append(score_test[:, :16], Z[:, 0].item()) # クラスター番号(予測値)を追加する cluster_num = kmeans_model.predict(score_test[np.newaxis, :]) # respの推定 y_pred = np.dot(model.predict(np.append(score_test, cluster_num)[np.newaxis, :]), sigma) + mu # action{0, 1}に変換 action = 1 if y_pred > best_score_threshold else 0 # 結果を格納 sample_prediction_df["action"] = action env.predict(sample_prediction_df)
Submit時のエラーについて
Submission CSV Not Found
ノートブック内でテストデータを推定し、csvで保存をして提出していた時に以下のエラーが出ていた。コンペのルール(こちらのSubmission Fileをご参照)をよく確認したところ、API経由での提出が必須であった。ルールにのっとり提出したところ、以下のエラーは解消された。

Notebook Timeout
提出したファイルがルールに定めれた時間内に終わらない場合、以下のエラーが発生。

対策としては、テストデータの推定時の処理を工夫して計算時間を短縮させた。その結果、エラーは解消し、問題なく提出できた。pandasの使用を極力避けたり、numpyで計算を完結できるように心がけた。
まとめ
これまで5回の記事で【Jane Street Market Prediction】についてエントリーから探索的データ解析、予測モデルの提出まで行ってきました。今回は、ディープラーニングを用いずにオーソドックスな流れでモデルを構築してみました。今回作成したモデルは、1,927とスコアはあまりよくありませんでしたが、これをベースラインとして今後はモデルの改良をしていけたらと思います。
以上