Kaggleに挑戦!【Jane Street Market Prediction】⑤エントリーしてみた(散々たる結果)

前回の記事では、主成分分析やクラスタリングを用いて特徴量の次元圧縮をしてみました。そして、クラスタ別でリターンに差があるように見えました。そこで、線形回帰を行いましたが、散々たる結果でした。
ただ、せっかく主成分分析やクラスタリングの結果があるので、それらを利用してコンペにエントリーだけはしてみます。

今回のエントリーを通して、コンペに参加する上で基本的なお作法について理解しました。
エントリーに際して、「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で最適な点となりました。
テストデータの推定の際は、この閾値をを使用します。

テストデータの推定

 処理の流れは以下の通りです。

  1. テストデータの欠損値処理
  2. テストデータを学習データの標準化の際に利用したパラメータで標準化
  3. 標準化したテストデータに対して主成分スコアを算出
  4. テストデータに対してクラスター番号を算出
  5. モデル構築
  6. モデルにより算出した標準化済みrespを元に戻す
  7. 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とスコアはあまりよくありませんでしたが、これをベースラインとして今後はモデルの改良をしていけたらと思います。

コメント

タイトルとURLをコピーしました