プロKagglerブログ

プロKagglerがKaggleに参加して得た独自の知見

【機械学習】PyCaretを用いたベースライン作成とモデル分析

はじめに

こんにちは.株式会社音圧爆上げくんにプロKagglerとして所属していますAshmeと申します.

業務の一環としてKaggleの様々なコンペティションに参加し,そこで得られた知見などを記事にして投稿しております.よろしくお願いいたします.また使用したソースコードこちらにあります.

今回は,ベースラインモデルの作成や,モデル改善のためにどのモデルを採用するかなどの分析を非常に簡単にできるPyCaretというライブラリを紹介します.私も知人に教えていただいたものなのですが,非常に高性能で通常では長いコードになってしまうものも,短いコードで様々なことができるので非常に驚きました.

多くのコンペティションが行われるKaggleでも一度はベースラインモデルを作成すると思います.そんなときに非常に簡単にベースラインを作成することができるのでぜひ一度使用してみていただきたいです.

PyCaretのインストール

PyCaretは実行するとわかるのですが,scikit-learnなどの他のライブラリが内部で動作しています.なのでライブラリの整合性が取れないとエラーが出てしまいます.現在では解消されている可能性もありますが,私が初めて使ったときにはそのような事態になり手作業でライブラリのバージョンを合わせていました.

公式ドキュメントを見る限りでは現在では依存関係もインストールしてくれるそうです.ただフルバージョンをインストールする際には末尾に[full]を追加します.

$ pip install pycaret

# フルバージョン
$ pip install pycaret[full]

また公式でも仮想環境や,conda環境を使用することが推奨されています.ローカルに直接インストールされているPythonで使用するのは控えたほうが良いでしょう.

モデルの比較

まずはさまざまなモデルによる評価の比較を行ってみます.その前に,PyCaretの方にデータを渡す必要があります.これはsetup関数を用いることで実行できます.このときにデータの前処理も含めてPyCaretの方で実行してくれます.

reg_model = setup(data=train_df, target="price", session_id=1)

引数のdataが元なるデータ(DataFrame),targetが目的変数の列名,session_idはこの後モデルを学習したりするときのシード値を表しています.これを実行すると途中に入力を促されるのですが,これは変数の扱い(数値やカテゴリ)について問題がないかを聞かれています.特に問題がなければそのままエンターを押せば処理してくれます.

データの前処理が終わるとどのような処理を施したかを表にまとめたものが表示されます.表の一部を抜粋したものを以下に示します .

Data Preprocessing

例えばOriginal Dataはのサイズは(16512, 9)であることや,数値変数の特徴量は8つであり,カテゴリ変数の特徴量は0個であることなどがわかります.

次に実際に様々なモデルによる評価を出してみます.これはcompare_models()関数を用いることで実行できます.このときに引数として"exclude"を指定できるのですが,これは予測に使用しない特徴量名をリストで渡すことができます.

Compare Models

これにより様々なモデルを学習させた結果が表示されます.今回は回帰モデルのため評価指標としてMAEやRMSEなどが計算されています.なのでコンペティションの評価関数で最も良い性能のモデルがどれなのかも一目瞭然です.

今回は最も性能が良かったモデルがcatboostだったのでこちらのモデルを用いてこの後の説明をしていきます.まずあるモデルについて詳しく結果を見る場合は,create_model関数の引数にモデル名を与えることで,交差検証のときの各フォールドでの評価結果とその平均,分散を表示してくれます.このとき出力として上図のような表が表示されることになります.

catboost = create_model("catboost")

Catboost

これにより各評価指標での汎化性能や,データによって評価指標にどれだけばらつきがあるのかを見ることができます.

ここまではすべて各モデルのデフォルトのパラメータを用いて学習させた結果になっています.PyCaretではここからパラメータの調整もすることができます.tune_model関数に先程のモデルを渡すことでパラメータを調整してくれます.

tuned_catboost = tune_model(catboost)

当然ですが調整後のパラメータを取得することも可能です.

tuned_catboost.get_params()

このように様々な学習,比較だけでなく前回の記事で行ったようなパラメータの調整までも行ってくれます.ただ例として実行したときは調整する前のほうが性能が良かったのが気になったので,調整したから大丈夫と過信せずに自身でも比較すべきでしょう.

モデルの分析

モデルの比較や調整を先程示したような非常に少ないコードで実行してくれるのも非常にありがたいですが,更にモデルの分析も行うことができます.今回は回帰問題のため,残差プロット,誤差プロット,特徴量重要度のプロットをしてみます.

これらはすべてplot_model関数で実行可能であり,第1引数に学習済みのモデル,引数"plot"に何を出力するのかを指定することができます.

# 残差プロット
plot_model(tuned_catboost)

# 誤差プロット
plot_model(tuned_catboost, plot="error")

# 特徴量重要度
plot_model(tuned_catboost, plot="feature")

それぞれの実行結果は以下のようになりました.

残差プロット

誤差プロット

特徴量重要度

更にevaluate_model関数を用いることで上記の可視化を含んだ様々なデータを表示することができます.下の例では特徴量重要度を表示しています.表示するものによっては処理に時間がかかるものもありますが,モデルの分析を行うには非常に有用だと思います.

evaluate model

PyCaretにデータを渡した際に内部的に訓練用データと検証用データに分割されます.その訓練用データのみを用いて更に交差検証が行われています.なので訓練に全く使用されていない検証用データに対する評価を見てみましょう.これはpredict_model関数を実行することで検証用データに対する評価を表にまとめてくれます.

predict_model(tuned_catboost)

また実際にテストデータに対する予測を作成するときには元の訓練データすべてを用いると思います.そこでPyCaretのfinalize_model関数を用いることで渡されたデータすべてを用いて学習してくれます.

final_catboost = finalize_model(tuned_catboost)

その後predict_model関数の"data"引数にテストデータを与えることで予測を作成してくれます.

test_pred = predict_model(final_catboost, data=val_df)

このとき予測したデータだけではなく,入力したテストデータのDataFrameに予測を"Label"という新しい列を加えたDataFrameが返されるので注意しましょう.

ここまでできればテーブルデータを扱うコンペティションであればベースラインの提出ファイルを作成できると思います.通常であればモデルの比較,残差プロットなどの分析,訓練データ全体を用いた再学習など長いコードになってしまいますが,PyCaretを用いることで短く,簡単なコード(おそらく10行もないのではないでしょうか)で実装することができます.

おわりに

今回はベースラインモデルの作成やモデルの比較,分析を非常に短いコードで簡単に実装できるPyCaretについて紹介しました.Pythonのライブラリは非常に豊富で使い勝手も良いものばかりですが,ついにこんなことまでできるのかといった感じですね.

コンペティションごとにベースラインを作成する必要がありますが,PyCaretを用いることで非常に簡単にできることに加えて,各モデルでの評価比較もできるためモデル改善のためにどのモデルを採用するかを考えるのにも使用できるのではないでしょうか.機会があればぜひ使用してみてください.

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています. 少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください.
Wantedlyリンク

参考

PyCaret 公式サイト: https://pycaret.org/
PyCaret GitHubチュートリアルなどもまとめられています): https://github.com/pycaret/pycaret

【機械学習】Optunaによるパラメータチューニング

はじめに

こんにちは.株式会社音圧爆上げくんにプロKagglerとして所属していますAshmeと申します.

業務の一環としてKaggleの様々なコンペティションに参加し,そこで得られた知見などを記事にして投稿しております.よろしくお願いいたします.また使用したソースコードこちらにあります.

Kaggleでは性能を競っているため,少しでも性能の高いモデルを作ろうとしていると思います.その際に,モデルの性能を少しでも上げるためにハイパーパラメータの調整をしているのではないでしょうか.手法としてランダムサーチやグリッドサーチ,ベイズ最適化などが存在します.今回はその中でもベイズ最適化を用いてパラメータ調整を行うOptunaというライブラリについて紹介します.Optunaは関数内で調整するパラメータと,目的関数を設定することでどのようなモデルに対しても使用することができます.

XGBoostを最適化する

Optunaの使い方を紹介するために,例としてxgboostを最適化してみます.例として使用するデータは前回から引き続きscikit-learnで使用できるカリフォルニア州の住宅価格データです.

モデルにどのようなパラメータが存在するかわからないという場合はライブラリの公式ページを見ればわかりますのでそちらを逐次見たり,よく調整するパラメータなどはメモしておくと良いでしょう.今回はscikit-learn APIを用いるため,設定できるパラメータはこちらに記載があります.

まず最適化するためには,どのパラメータを調整するのか,どのような目的関数を最大化,もしくは最小化したいのかを設定する必要があります.Optunaではこれを関数として実装します.この関数を定義するときにtrialクラスのインスタンスとして作成します.関数の中で処理することは以下のとおりです.

  1. 最適化したいパラメータと,パラメータの取りうる値を設定します.パラメータの値はtrial.suggest_intやtrial.suggest_loguniformなどで指定することができます.
  2. 設定したパラメータをモデルに渡してモデルの学習と評価を行います.
  3. 最小化,もしくは最大化したい目的関数を関数の返り値として設定します.

関数名には特に決まりはないですが,よく使用されているものとしてobjective(目的関数)があるのでこの名前で実装してみます.

def objective(trial):
    # データを訓練用と検証用に分割する
    train_X, val_X, train_y, val_y = train_test_split(X, y, test_size=0.2, random_state=6174)

    # 1. パラメータと値の設定
    # 最適化したいパラメータと,パラメータがとる値の範囲を指定する
    params = {
        "n_estimators": trial.suggest_int("n_estimators", 100, 500),
        "max_depth": trial.suggest_int("max_depth", 3, 10),
        "max_leaves": trial.suggest_int("max_leaves", 1, 30),
        "learning_rate": trial.suggest_loguniform("learning_rate", 1e-4, 1e-1),
    }

    # 2. モデルの訓練と評価
    # モデルの訓練
    model = xgb.XGBRegressor(**params)
    model.fit(train_X, train_y)

    # モデルの評価
    pred = model.predict(val_X)
    score = mse(val_y, pred, squared=False)

    # 3. 目的関数の値を返す
    # 今回は回帰問題のためRMSEの値を最小化することを目的とする
    return score

今回は学習率,使用する木の数,木の深さの最大値,葉の最大値を設定しました.

パラメータの取りうる値の設定ですが,今回は整数と,連続値が対象となるためtrial.suggest_intで範囲内の整数を設定し,trial.suggest_loguniformで範囲内の連続値を設定しています.これらは第1引数にパラメータ名,第2,第3引数に下限と上限を与えます.もし質量変数を扱いたい場合はtrial.suggset_categoricalを用いて,第1引数にパラメータ名,第2引数に取りうるパターンをリストやタプルで渡します.

その後optuna.create_studyを用いることでstudyクラスのインスタンスを作成します.このときの引数"direction"で目的関数を最大化するのか,最小化するのかを指定します.今回はRMSEを目的関数としているため最小化問題にします.

study = optuna.create_study(direction="minimize")  # maximizeにすると最大化問題になる

後はstudy.optimizeメソッドを用いることでパラメータを最適化できます.このときの第1引数が先ほど作成した関数,第2に引数がイテレーションを何回行うかを表しており,"timeout"を指定することで一定時間以上経過したときに処理がとまるように設定することができます.

study.optimize(objective, 30, timeout=600)

これでパラメータの調整が終わりました.実際に調整後のパラメータや,関数での最も性能が良かったときの目的関数の値を取り出したい場合は,studyインスタンスのbest_params, best_valueにアクセスします.

print(study.best_params)

print(study.best_value)

最も性能が良かったパラメータを得ることができました.後はこのパラメータを用いて,学習データ全体を使いモデルを学習させれば性能が高いモデルができるはずです.

ただパラメータチューニングは効果としては感じやすいですが,大きく性能を高めるためには,それ以前の特徴量エンジニアリングなどが必要になります.なので効果があったからといってモデルを変えてパラメータチューニングを繰り返すのは性能向上にはそこまで大きい効果がないので注意しましょう.

PyTorchのモデルを最適化する

深層学習モデルも計算量はかかってしまいますが,最適化することが可能です.この場合のパラメータは隠れ層のニューロン数やドロップアウトの割合,最適化手法の学習率などになります.ここでは単純な3層ニューラルネットワークの隠れ層のニューロン数と,ドロップアウトの割合を調整してみます.

# 隠れ層のユニット数,ドロップアウトの割合を最適化の対象にする
def objective(trial):
    # dr: ドロップアウトの割合.[0.1, 0.6]
    # hidden_dim: 隠れ層のニューロン数.[2^i i = 5, 6, ..., 9]
    dr = trial.suggest_float("dr", 0.1, 0.6)
    hidden_dim = trial.suggest_categorical("hidden_dim", [2**(i+5) for i in range(5)])

    # モデル,損失関数,最適化手法の定義
    model = TestModel(len(features), hidden_dim, 1, dr=dr).to("cuda")
    criterion = nn.MSELoss()
    optimizer = optimizers.Adam(model.parameters())

    epochs = 30
    for epoch in range(epochs):
        train_loss = 0.
        val_loss = 0.

        for batch_X, batch_y in train_dataloader:
            loss = train(batch_X, batch_y)
            train_loss += loss.item()

        train_loss /= len(train_dataloader)

        for batch_X, batch_y in val_dataloader:
            loss = validation(batch_X, batch_y)
            val_loss += loss.item()

        val_loss /= len(val_dataloader)

    val_loss = 0.
    for batch_X, batch_y in val_dataloader:
        loss = validation(batch_X, batch_y)
        val_loss += loss.item()

    val_loss /= len(val_dataloader)

    return val_loss

上記のようにして深層学習モデルの場合でもパラメータの調整を行うことができます.ただモデルが複雑だったり,データ数が大きい場合にはやはり計算量が非常に大きいのであまり使用することはにと思います.とにかくどのようなパラメータでも目的関数を指定することで調整できるということです.

LightGBMの最適化

ここまでは自分でモデルの学習,評価の一連の流れを関数で定義し,パラメータの調整を行っていました.しかし,LightGBMの場合はそのような関数を定義する必要なくパラメータを調整することが可能です.このときにはoptuna.integration.lightgbmを用います.

# lightgbmの最低限設定したいパラメータ
params = {
    "objective": "regression",
    "metric": "mean_squared_error",
}

# モデルの学習
model = lgbo.train(params, lgb_train, valid_sets=lgb_val,
                   verbose_eval=False)

今回は回帰問題なので"regression",評価関数にRMSEを指定するため"l2"のパラメータだけ事前に渡しています.それ以外のパラメータはtrainメソッドで勝手に調整してくれます.先ほどと比較して非常にスッキリしたコードになりました.LightGBMモデルを用いる場合はこれで十分最適化できるのではないでしょうか.

もしパラメータを取得したい場合はインスタンスのparamsにアクセスすれば取得できます.また,通常のLightGBMのようにpredictメソッドで予測もすることができます.

print(model.params)

print(model.predict(val_X[0].reshape(1, -1))

パラメータの調整はモデル作成の本当に最後にさらなる性能向上を目的として行うことが多いと思います.そのときにsklearnのランダムサーチやグリッドサーチのような総当りではなく,Optunaを用いたベイズ最適化による効率的なチューニングをすることで試行回数も増やすことができるのではないでしょうか.

おわりに

今回はパラメータチューニングを行うライブラリとしてOptunaを紹介しました.モデルの学習方法や評価方法も柔軟に変更することができ,ベイズ最適化による効率的な調整が可能となりますので,ぜひ1度使用してみてはいかがでしょうか.

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています. 少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください.
Wantedlyリンク

参考

Optuna 公式サイト: https://optuna.org/

【機械学習】モデルの説明可能性

はじめに

こんにちは.株式会社音圧爆上げくんにプロKagglerとして所属していますAshmeと申します.

業務の一環としてKaggleの様々なコンペティションに参加し,そこで得られた知見などを記事にして投稿しております.よろしくお願いいたします.また使用したソースコードこちらにあります.

Kaggleではより性能が高いモデルを作成することが目的であり,特にテーブルデータでは特徴量エンジニアリングが重要になります.そのようなときに作成した新たな特徴量は予測にどれだけ寄与しているのかを見たいときもあるのではないでしょうか.特徴量重要度やモデルの重みを用いることも可能ですが,ここでは1つ踏み込んだものとしてモデルの説明可能性を見る手法としてLIMEとSHAPを紹介します.これらの手法ではデータから予測をするのに,どの特徴量が予測が大きく,もしくは小さくなるような要因となったかを知ることができます.またこれらはどちらも基本的にはどのモデルでも使用することができます.

前記事に続いてここではscikit-learnで利用できるカリフォルニア州の住宅価格データを例として用います.

LIME

まずはLIMEについて説明していきます.LIMEは局所的説明を得る手法の1つです.局所的説明では,ある1つの対象データを予測する際にどの特徴量がどの程度予測に寄与しているかを表すことができます.

LIMEでは複雑なモデルの局所的な部分を線形回帰モデルを用いて近似します.予測するある1つの対象データにノイズをかけることで,空間上の一部分のみに着目することができるのがイメージできるのではないでしょうか(わかりづらい場合は2次元平面で考えてみてください).その部分でのモデルを線形回帰モデルで近似し,線形回帰モデルの重みを各特徴量の寄与度として扱います.

線形回帰モデルの近似では,まず入力データを二値(1, 0)のデータにします.対象データにノイズを与え,元の対象データと値が同じ特徴量は1,そうでない特徴量は0としたデータにします.これを入力として線形回帰モデルを学習させます.この線形回帰モデルの出力と,ノイズを与えた対象データ(これは二値ではない)を学習済みモデルの入力した際の出力の差が最小となるように線形回帰モデルを学習させます.

LIMEはテーブルデータだけでなく,画像データやテキストデータに対しても使用することができるそうです.ここでは簡単な紹介のためにテーブルデータに対してのみ用います.

一度通常通りモデルを学習させた後に,lime.lim_etabular.LimeTabularExplainerクラスにデータ,特徴量名などを渡します.そしてexplain_instanceメソッドで対象となるデータ,学習済みモデルで予測するメソッド,特徴量の数を指定してから結果をプロットします.

# データの分割
train_X, val_X, train_y, val_y = train_test_split(X, y, test_size=0.2, random_state=6174)

# モデルの学習
model = lgb.LGBMRegressor(n_jobs=12, random_state=6174)
model.fit(train_X, train_y)
pred = model.predict(val_X)
score = mse(val_y, pred, squared=False)

print(f"RMSE score: {score:.4f}")

# LIMEのモデルの準備
explaner = lime.lime_tabular.LimeTabularExplainer(train_X, feature_names=data.feature_names,
                                                  class_names=["price"], verbose=True, mode="regression")
i = 25
exp = explaner.explain_instance(val_X[i], model.predict, num_features=8)

exp.show_in_notebook(show_table=True)

プロットした結果が以下になります.

LIME result

一番左の棒が予測値を表しています.また一番右の表は入力データの特徴量名とその値を表しています.重要なものは真ん中のグラフです.これを見るに,"MedInc"が4.74より大きいことが要因で予測値が1.25大きくなる,"AveOcup"が3.28より大きいことが要因で予測値が0.36小さくなるというように見ます.また基準となる値は近似した線形回帰モデルの切片になっています.

この結果は予測する対象のデータによって異なるため,あるデータの予測値がなぜそのようになったのかというものをデータごとに見ることができます.

LIMEの注意点としては近似モデルを作成する際にランダムなノイズを与えるため,結果が一定にならないことが挙げられます.またモデルの複雑性によってはうまくモデルを近似できない場合もありえます.

SHAP

SHAPは先程のLIMEとは異なり,大局的説明を得る手法の1つです.こちらはモデル全体としてどの特徴量が予測に関連するかを得ることができます.前記事での線形回帰の重みや決定木などの特徴量重要度は大局的説明に分類されると考えられます.

SHAPはゲーム理論のシャープレイ値の考え方が用いられています.そこで先にシャープレイ値について簡単に説明します.

シャープレイ値はプレイヤーがどれだけ貢献したかを表すような指標です.例としてプレイヤーと各プレイヤーがゲームに参加した際の報酬をまとめると以下のような結果が得られたとします.

プレイヤー 報酬
1 3
2 4
3 5
1, 2 10
1, 3 12
2, 3 14
1, 2, 3 20

このときプレイヤーの参加順は順連の考え方を用いて6パターン(3!)あることがわかります.このときに各パターンにおいてプレイヤーが参加する前の状態での報酬と,参加した後の状態での報酬の差が貢献度となります.

プレイヤー1のシャープレイ値を考えてみます.プレイヤーの参加順は以下の6パターンになります.

  1. プレイヤー1 → プレイヤー2 → プレイヤー3
  2. プレイヤー1 → プレイヤー3 → プレイヤー2
  3. プレイヤー2 → プレイヤー1 → プレイヤー3
  4. プレイヤー2 → プレイヤー3 → プレイヤー1
  5. プレイヤー3 → プレイヤー1 → プレイヤー2
  6. プレイヤー3 → プレイヤー2 → プレイヤー1

各パターンのプレイヤー1が参加する前と後の報酬の差は上から3,3,6,6,7,6になります.参加者が0のときは報酬が0としています.平均を計算すると \frac{3 + 3 + 6 + 6 + 7 + 6}{6}=6.33と計算でき,これがプレイヤー1のシャープレイ値となります.

SHAPではこのときのプレイヤーを特徴量,報酬をモデルの予測値と置き換えます.つまりシャープレイ値はある特徴量を加えたときに予測値が大きくなるのか,小さくなるのかを表すことになります.

またモデルの予測値は以下のように表します.

 \displaystyle
f(\boldsymbol{x}) = E[f(\boldsymbol{x})] + \sum_i^n \phi_i

式中の \phi_iがi番目の特徴量のシャープレイ値を表し,第1項はモデルの出力の期待値です.

SHAPでも一度モデル学習させた後に,shap.Explainerクラスに学習済みモデルを渡します.その後インスタンスにデータを入力することで計算することができます.

X, y = df.drop("price", axis=1), df["price"]
train_X, val_X, train_y, val_y = train_test_split(X, y, test_size=0.2, random_state=6174)

# モデルの学習
model = lgb.LGBMRegressor(n_jobs=12, random_state=6174)
model.fit(train_X, train_y)

# SHAPの準備
explainer = shap.Explainer(model)
shap_values = explainer(train_X)

可視化については複数ありますが,個人的には以下の2つが見やすいと思います.1つ目はあるデータの予測のときのSHAP値,2つ目は各入力に対するSHAP値を表したものです.

water fall

beeswarm

1つ目のwater fallは1つのデータに対してのものですが直感的にわかるのではないでしょうか.例えば"MedInc"によって予測値は期待値よりも0.23大きくなるという影響を与えていることがわかります.

2つ目のbee swarmではすべてのデータに対するSHAP値を表したものです.図中の赤点は入力データが大きいもの,青点は小さいものを表しており,右に行くほどSHAP値が大きくなることを示しています.つまり,"MedInc"は値が大きくなるほどSHAP値が大きくなるので,予測値が大きくなることがわかります.逆に"Latitude"は値が小さくなるほど予測値が大きくなることがわかります.

余談: Attention

説明可能性と少しずれるかもしれませんが,Attentionの説明可能性について紹介します. AttentionはTransformerに用いられる層の1つでTransformerが主流の現在では非常によく使用されています.

まずはAttentionが開発された経緯から考えていきます.AttentionはそれまでのRNNやLSTMを用いたSeq2Seqモデルの構造では,系列データの学習時に新しい情報を用いて隠れ状態を更新するため,データの初めの方の情報がどんどん消えていっているという考えがありました.そこで各時刻の隠れ状態を保持しておき,ある時刻を予測するときに入力データのどの時刻に着目するかを計算するためにAttentionが開発されました.

この考えに基づけば,ある時刻の予測に入力データのどの時刻が関連しているかを表していると言えるのではないでしょうか.現在ではAttentionの重みをヒートマップとして可視化することで予測について説明を加えたり,Attentionをもとに特徴量重要度を計算しようという論文もあります(Attention-like feature explanation for tabular data).

それに対し,Attentionでは説明可能性を持たないという主張も存在します(Attention is not Explanation).一番最初のAttentionではエンコーダ側の隠れ状態とデコーダ側の隠れ状態の内積であり,Self Attentionでは同じ入力を線形変換した後の内積が重みになっています.これが予測の寄与度を表しているという証明は厳密にはされておらず,イメージとしてもこれが寄与度を表すとは考えにくいのではないでしょうか.

Transformerとともに注目されているAttentionですが,説明可能性として用いる際には確実なことが言えないため注意が必要だと考えられます.

おわりに

今回はモデルが予測にどの特徴量に注目するかを計算するLIMEとSHAPについて紹介しました.Kaggleなどのコンペティションでの使用はもちろんのこと,実データに対しても有用なのではないでしょうか.またこれらの結果を用いて使用する特徴量を選択するのも1つの方法かもしれません.

特に深層学習では予測の要因が明確にならない複雑なモデルであることが問題とされているため,今後の発展にも注目していきたいところです.

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています. 少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください. Wantedlyリンク

参考

LIME GitHub: https://github.com/marcotcr/lime
SHAP GitHub: https://github.com/slundberg/shap

【機械学習】特徴量選択手法

はじめに

こんにちは.株式会社音圧爆上げくんにプロKagglerとして所属していますAshmeと申します.

業務の一環としてKaggleの様々なコンペティションに参加し,そこで得られた知見などを記事にして投稿しております.よろしくお願いいたします.また使用したソースコードこちらにあります.

Kaggleでも実データでも入力データの特徴量数が非常に多い場合があります.その中には予測に不要なものや,ときには悪影響を与えるようなものが存在します.今回は使用する特徴量を絞るための,様々な特徴量選択の手法について説明していきます.

本記事では説明のために特に特徴量が多いわけではないですが,scikit-learnで利用できるカリフォルニア州の住宅価格データを用います.

フィルター法

これはデータを統計的に分析したときの結果などを用いて使用する特徴量を選択する手法の総称です.事前に行うEDAの際に分析をしておけばその時の仮定から利用する特徴量を選択することができます.

ここでは最も簡単と思われる相関係数を用いた方法を実行してみます.このとき仮定として目的変数と強い相関がある特徴量は,予測に大きく寄与する良い特徴量という仮定に基づきます.

まず相関係数を計算し,その結果をヒートマップで可視化します.

# 相関係数の算出,プロット
corr = df.corr()
plt.figure(figsize=(16, 16))
sns.heatmap(corr, annot=True, square=True)
plt.tight_layout()

plt.show()

heatmap

目的変数は"price"という名前にしています.結果から"price"とMedInc"に正の相関があることがわかります.その他の特徴量についてはほとんど相関がないことがわかります.

単一の特徴量になってしまいますが,"MedInc"のみを入力データとして線形回帰(Ridge)モデルと,LightGBMを学習してみます.

# 訓練用データとテストデータに分ける
train_X, test_X, train_y, test_y = train_test_split(X[:, 0], y, test_size=0.1, random_state=6174)

# 訓練商データを更に訓練用と検証用に分ける
train_X, val_X, train_y, val_y = train_test_split(train_X, train_y, test_size=0.1, random_state=6174)

train_X, val_X, test_X = train_X.reshape(-1, 1), val_X.reshape(-1, 1), test_X.reshape(-1, 1)

# 線形回帰ではスケーリングが必要
# GBDTでは大小関係のみ必要なため特に影響はない
sc = StandardScaler()
train_X = sc.fit_transform(train_X)
val_X, test_X = sc.transform(val_X), sc.transform(test_X)

linear = Ridge()
lgbmodel = lgb.LGBMRegressor(n_jobs=12, random_state=6174)

linear.fit(train_X, train_y)
lgbmodel.fit(train_X, train_y, eval_set=(val_X, val_y), verbose=-1)

linear_val_pred = linear.predict(val_X)
linear_test_pred = linear.predict(test_X)
linear_val_rmse = mse(val_y, linear_val_pred, squared=False)
linear_test_rmse = mse(test_y, linear_test_pred, squared=False)

lgb_val_pred = lgbmodel.predict(val_X)
lgb_test_pred = lgbmodel.predict(test_X)
lgb_val_rmse = mse(val_y, lgb_val_pred, squared=False)
lgb_test_rmse = mse(test_y, lgb_test_pred, squared=False)

print(f"Linear Model(Ridge) val score: {linear_val_rmse:.4f} test_score: {linear_test_rmse:.4f}")
print(f"LGB Model val score: {lgb_val_rmse:.4f} test score: {lgb_test_rmse:.4f}")

結果としてすべての特徴量を用いたときよりも性能が低くなっています.単一の特徴量しか用いていないため当然ではありますが,他の単一の特徴量を用いるとこの場合よりも性能が低くなることが確認できます.

ラッパー法

これは使用する特徴量の組み合わせを変えてモデルを学習させていき使用する特徴量の組み合わせを決定する手法です.これはどのようなモデルに対しても使用することができますが,特徴量の組み合わせごとにモデルの訓練,評価を行う必要があるため計算量が非常に大きくなることが難点です.

ここでは使用する特徴量の数を指定して,その数に達するまで最も性能が向上する特徴量を追加していく,もしくは最も性能が低下しない特徴量を削除していくSequential Feature Selectionを用います. これはscikit-learnのfeature_extractionモジュールのSequentialFeatureSelectorクラスを用いることで実装できます.

# Sequential Feature Selection
forward_lgb = lgb.LGBMRegressor(random_state=6174)
backward_lgb = lgb.LGBMRegressor(random_state=6174)

# forward selection
sfs = SequentialFeatureSelector(forward_lgb, n_features_to_select=6,
                                scoring="neg_mean_squared_error",
                                direction="forward", n_jobs=12)
# backward selection
sbs = SequentialFeatureSelector(forward_lgb, n_features_to_select=6,
                                scoring="neg_mean_squared_error",
                                direction="backward", n_jobs=12)

sfs.fit(X, y)
sbs.fit(X, y)

# 選択した特徴量を用いてモデルを学習する
forward_idx = sfs.get_support()
backward_idx = sbs.get_support()

for idx in [forward_idx, backward_idx]:
    # データ全体を用いた場合
    # 訓練用データとテストデータに分ける
    train_X, test_X, train_y, test_y = train_test_split(X[:, idx], y, test_size=0.1, random_state=6174)

    # 訓練商データを更に訓練用と検証用に分ける
    train_X, val_X, train_y, val_y = train_test_split(train_X, train_y, test_size=0.1, random_state=6174)

    lgbmodel = lgb.LGBMRegressor(n_jobs=12, random_state=6174)

    linear.fit(train_X, train_y)
    lgbmodel.fit(train_X, train_y, eval_set=(val_X, val_y), verbose=-1)

    val_pred = lgbmodel.predict(val_X)
    test_pred = lgbmodel.predict(test_X)
    val_rmse = mse(val_y, val_pred, squared=False)
    test_rmse = mse(test_y, test_pred, squared=False)

    print(f"LGB Model val score: {val_rmse:.4f} test score: {test_rmse:.4f}")

時間をかければ指定した数での最適な特徴量の組み合わせを得ることができますが,モデルが複雑だとその分計算量が大きくなることや,最適な特徴量の数が事前に知ることができないことが難点です.

埋め込み法

これは学習済みモデルから特徴量重要度や,重みを用いて使用する特徴量を決定する手法で,最も使用しやすいのではないかと思います.これらを計算できるモデルは限られており,LightGBMなどの決定木ベースのモデルや,線形回帰モデルが使用できます.これらモデルで特徴量重要度を計算した後に,使用する特徴量を決めほかのモデルで学習するということも可能です.

LightGBMとRidge回帰モデルを学習し,特徴量重要度と重みを可視化してみます.

# データ全体を用いて学習する
# 訓練用データとテストデータに分ける
train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.1, random_state=6174)

# 訓練商データを更に訓練用と検証用に分ける
train_X, val_X, train_y, val_y = train_test_split(train_X, train_y, test_size=0.1, random_state=6174)

# 線形回帰ではスケーリングが必要
# GBDTでは大小関係のみ必要なため特に影響はない
sc = StandardScaler()
train_X = sc.fit_transform(train_X)
val_X, test_X = sc.transform(val_X), sc.transform(test_X)

linear = Ridge()
lgbmodel = lgb.LGBMRegressor(n_jobs=12, random_state=6174)

linear.fit(train_X, train_y)
lgbmodel.fit(train_X, train_y, eval_set=(val_X, val_y), verbose=-1)

linear_val_pred = linear.predict(val_X)
linear_test_pred = linear.predict(test_X)
linear_val_rmse = mse(val_y, linear_val_pred, squared=False)
linear_test_rmse = mse(test_y, linear_test_pred, squared=False)

lgb_val_pred = lgbmodel.predict(val_X)
lgb_test_pred = lgbmodel.predict(test_X)
lgb_val_rmse = mse(val_y, lgb_val_pred, squared=False)
lgb_test_rmse = mse(test_y, lgb_test_pred, squared=False)

print(f"Linear Model(Ridge) val score: {linear_val_rmse:.4f} test_score: {linear_test_rmse:.4f}")
print(f"LGB Model val score: {lgb_val_rmse:.4f} test score: {lgb_test_rmse:.4f}")

# プロット用に特徴量名格納
feature = np.array(data.feature_names)

# 線形回帰モデルの重み
linear_coef = linear.coef_

# lightgbmの特徴量重要度
lgb_importances = lgbmodel.feature_importances_

# それぞれをプロットする
# 重み/特徴量重要度をソート
linear_idx = np.argsort(linear_coef)
lgb_idx = np.argsort(lgb_importances)

f, ax = plt.subplots(nrows=2, ncols=1, figsize=(16, 20))
ax = ax.flatten()

ax[0].barh(feature[linear_idx], linear_coef[linear_idx])
ax[1].barh(feature[lgb_idx], lgb_importances[lgb_idx])

plt.tight_layout()
plt.show()

それぞれプロットした結果が以下のようになります.上がRidge回帰の重み,下がLightGBMの特徴量重要度をプロットしたものです.Ridge回帰では重みが負になることもあります.この場合は変数が大きくなるほど予測値は小さくなるということが考えられます.

Feature Importance

Lasso回帰

スパースモデリングの1つであるLasso回帰はRidge回帰と同様に線形回帰モデルですが,正則化項にL1ノルムを用いています.Lasso回帰を用いて学習すると予測に不要な特徴量に対する重みは0になります.これによって特徴量を選択することも可能です.

おわりに

今回は特徴量選択手法についてまとめました.実際はドメイン知識をつけて,どの特徴量が関連するかを自分であたりをつけられれば良いのですが,Kaggleなどのコンペティションで匿名の特徴量が存在する場合にはこのような手法を用いるのが有効なのではないでしょうか.

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています. 少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください. Wantedlyリンク

EDA ターゲット分析

こんにちは、tera と申します。

私は現在、株式会社音圧爆上げくんという会社に所属しており、プロKagglerとして活動しています。 私は現在プロKagglerとして、業務の一環で、現在Kaggleで行われているコンペの一つである JPX Tokyo Stock Exchange Prediction に参加しています。

また、今回の記事は、Kaggle上にNotebookとして同じ内容の記事を英語で投稿しています。 リンクは こちら です。

csvロード & スコアの計算

df_prices = pd.read_csv(os.path.join(TRAIN_DIR, 'stock_prices.csv'))
df_stock_list = pd.read_csv("../input/jpx-tokyo-stock-exchange-prediction/stock_list.csv")

_df_stock_list = df_stock_list[['Name', 'SecuritiesCode', 'Section/Products','NewMarketSegment','33SectorName', 'NewIndexSeriesSize']]
target_std = df_prices.groupby('SecuritiesCode')['Target'].std().rename('TargetStd')
df_stocks = pd.merge(_df_stock_list, target_std, on='SecuritiesCode')
target_mean = df_prices.groupby('SecuritiesCode')['Target'].mean().rename('TargetMean')
df_stocks = pd.merge(df_stocks, target_mean, on='SecuritiesCode')
target_score = (target_mean / target_std).rename('TargetScore')
df_stocks = pd.merge(df_stocks, target_score, on='SecuritiesCode')
df_stocks['TargetStdRank'] = df_stocks['TargetStd'].rank(axis=0, ascending=True).astype(int)
df_stocks['TargetMeanRank'] = df_stocks['TargetMean'].rank(axis=0, ascending=False).astype(int)
df_stocks['TargetScoreRank'] = df_stocks['TargetScore'].rank(axis=0, ascending=False).astype(int)

stock_priceとstock_listを使い、銘柄別にtargetの平均/標準偏差/スコアとそのランクを求めます。

テーブルを見る

print('long')
display(df_stocks.sort_values('TargetScore', ascending=False).head(10))
print('short')
display(df_stocks.sort_values('TargetScore', ascending=True).head(10))

TargetStd TargetMean TargetScore このカラムを中心に考えます。 stdが重要だと思っていましたが、銘柄別スコアではmeanのほうが支配的であることがわかります。 shortでは比較的stdが重要そうなので、longとshortの銘柄選びの基準は分けたほうが良さそうです。

"銘柄別スコア "の33Sectorの割合

ロングでは、情報・通信、サービス、電機が強く、 ショートでは、銀行、小売業、情報・通信業が強いです。 100位以内と500~600位以内の差は歴然としています。

TargetScoreソートで各データを見る

ロングとショートで明確な差が出ていることがわかります。 また、それぞれ最上位銘柄群は異質な感じも受けます。

ロング:

ショート:

上位銘柄を年別で見る

最上位銘柄はある程度上位には残り続けてはいますが、トップ言えるほどではありません。 全体として最上位まで引き上がった理由は上場1年目のスコアが原因のようです。 LBでは、新しい銘柄の追加はないため、上場1年目の成績が反映されることがありません。 そのため、銘柄選びには1年目のスコアを外して計算したほうがよさそうです。

お知らせ

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています。
少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください。
Wantedlyリンク

提出用APIの理解

こんにちは、tera と申します。

私は現在、株式会社音圧爆上げくんという会社に所属しており、プロKagglerとして活動しています。 私は現在プロKagglerとして、業務の一環で、現在Kaggleで行われているコンペの一つである JPX Tokyo Stock Exchange Prediction に参加しています。

また、今回の記事は、Kaggle上にNotebookとして同じ内容の記事を英語で投稿しています。 リンクは こちら です。

コンペ概要の説明

You must submit to this competition using the provided python time-series API, which ensures that models do not peek forward in time.

とある通り、時系列でデータが渡されていることが予想できます。 ラグ特徴量を使う場合、コードレベルで時系列順に渡されていることの確認が重要です。

時系列順に渡されていること確認

import numpy as np
import time
import jpx_tokyo_market_prediction
env = jpx_tokyo_market_prediction.make_env()
iter_test = env.iter_test()

prev_date = None
for (prices, options, financials, trades, secondary_prices, sample_prediction) in iter_test:
    current_date = prices["Date"].iloc[0]
    print(f"prev: {prev_date} current: {current_date}")
    if prev_date != None:
        if current_date <= prev_date:
            raise ValueError("MyError!!")
    prev_date = current_date
    sample_prediction['Rank'] = np.arange(len(sample_prediction))
    env.predict(sample_prediction)

このコードでエラーがないため、時系列で並んでいると言えます。

Reference Links

https://www.kaggle.com/competitions/g-research-crypto-forecasting/discussion/289000#1587704
https://www.kaggle.com/competitions/jane-street-market-prediction/discussion/199203#1089839

お知らせ

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています。
少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください。
Wantedlyリンク

LGBM baseline (with テクニカル指標)

こんにちは、tera と申します。

私は現在、株式会社音圧爆上げくんという会社に所属しており、プロKagglerとして活動しています。 私は現在プロKagglerとして、業務の一環で、現在Kaggleで行われているコンペの一つである JPX Tokyo Stock Exchange Prediction に参加しています。

また、今回の記事は、Kaggle上にNotebookとして同じ内容の記事を英語で投稿しています。 リンクは こちら です。

はじめに

LGBM baselineは数多くのコードベースが公開されていますが、ラグ特徴量(テクニカル指標など)を使ったbaselineはまだ数が少ない状況です。 これには理由があり、kaggleの時系列APIの仕様で1銘柄1レコードずつしか一度に取得&評価できないため、testデータとtrainデータを上手くマージしながら管理しつつ毎度特徴量を計算し直す必要があり、調整に手間がかかるためです。

今回はその面倒なラグ特徴量(テクニカル指標)を使ったLBスコアを出す手順をまとめました。

talibのインストール

!pip install ../input/talib-source/talib_binary-0.4.19-cp37-cp37m-manylinux1_x86_64.whl
import talib as ta

talibはテクニカル指標を計算するpythonライブラリとして有名なものですが、インストールに工夫が必要です。 talib-sourceとして、whlファイルをデータセットに準備しているのでそちらを使ってインストールを行います。

テクニカル指標計算 関数

def add_technical(df):
    op = df['Open']
    hi = df['High']
    lo = df['Low']
    cl = df['Close']
    volume = df['Volume']
    hilo = (hi + lo) / 2

    # print('calc ta overlap')
    df['BBANDS_upperband'], df['BBANDS_middleband'], df['BBANDS_lowerband'] = ta.BBANDS(cl, timeperiod=5, nbdevup=2, nbdevdn=2, matype=0)
    df['BBANDS_upperband'] -= hilo
    df['BBANDS_middleband'] -= hilo
    df['BBANDS_lowerband'] -= hilo
    df['DEMA'] = ta.DEMA(cl, timeperiod=30) - hilo
    df['EMA'] = ta.EMA(cl, timeperiod=30) - hilo
    df['HT_TRENDLINE'] = ta.HT_TRENDLINE(cl) - hilo
    df['KAMA'] = ta.KAMA(cl, timeperiod=30) - hilo
    df['MA'] = ta.MA(cl, timeperiod=30, matype=0) - hilo
    df['MIDPOINT'] = ta.MIDPOINT(cl, timeperiod=14) - hilo
    df['SMA'] = ta.SMA(cl, timeperiod=30) - hilo
    df['T3'] = ta.T3(cl, timeperiod=5, vfactor=0) - hilo
    # df['TEMA'] = ta.TEMA(cl, timeperiod=30) - hilo
    df['TRIMA'] = ta.TRIMA(cl, timeperiod=30) - hilo
    df['WMA'] = ta.WMA(cl, timeperiod=30) - hilo

    # print('calc ta momentum')
    df['ADX'] = ta.ADX(hi, lo, cl, timeperiod=14)
    df['ADXR'] = ta.ADXR(hi, lo, cl, timeperiod=14)
    df['APO'] = ta.APO(cl, fastperiod=12, slowperiod=26, matype=0)
    df['AROON_aroondown'], df['AROON_aroonup'] = ta.AROON(hi, lo, timeperiod=14)
    df['AROONOSC'] = ta.AROONOSC(hi, lo, timeperiod=14)
    df['BOP'] = ta.BOP(op, hi, lo, cl)
    df['CCI'] = ta.CCI(hi, lo, cl, timeperiod=14)
    df['DX'] = ta.DX(hi, lo, cl, timeperiod=14)
    df['MACD_macd'], df['MACD_macdsignal'], df['MACD_macdhist'] = ta.MACD(cl, fastperiod=12, slowperiod=26, signalperiod=9)
    # skip MACDEXT MACDFIX
    df['MFI'] = ta.MFI(hi, lo, cl, volume, timeperiod=14)
    df['MINUS_DI'] = ta.MINUS_DI(hi, lo, cl, timeperiod=14)
    df['MINUS_DM'] = ta.MINUS_DM(hi, lo, timeperiod=14)
    df['MOM'] = ta.MOM(cl, timeperiod=10)
    df['PLUS_DI'] = ta.PLUS_DI(hi, lo, cl, timeperiod=14)
    df['PLUS_DM'] = ta.PLUS_DM(hi, lo, timeperiod=14)
    df['RSI'] = ta.RSI(cl, timeperiod=14)
    df['STOCH_slowk'], df['STOCH_slowd'] = ta.STOCH(hi, lo, cl, fastk_period=5, slowk_period=3, slowk_matype=0, slowd_period=3, slowd_matype=0)
    df['STOCHF_fastk'], df['STOCHF_fastd'] = ta.STOCHF(hi, lo, cl, fastk_period=5, fastd_period=3, fastd_matype=0)
    df['STOCHRSI_fastk'], df['STOCHRSI_fastd'] = ta.STOCHRSI(cl, timeperiod=14, fastk_period=5, fastd_period=3, fastd_matype=0)
    # df['TRIX'] = ta.TRIX(cl, timeperiod=30)
    df['ULTOSC'] = ta.ULTOSC(hi, lo, cl, timeperiod1=7, timeperiod2=14, timeperiod3=28)
    df['WILLR'] = ta.WILLR(hi, lo, cl, timeperiod=14)

    # print('calc ta volume')
    df['AD'] = ta.AD(hi, lo, cl, volume)
    df['ADOSC'] = ta.ADOSC(hi, lo, cl, volume, fastperiod=3, slowperiod=10)
    df['OBV'] = ta.OBV(cl, volume)

    # print('calc ta vola')
    df['ATR'] = ta.ATR(hi, lo, cl, timeperiod=14)
    df['NATR'] = ta.NATR(hi, lo, cl, timeperiod=14)
    df['TRANGE'] = ta.TRANGE(hi, lo, cl)

    # print('calc ta cycle')
    df['HT_DCPERIOD'] = ta.HT_DCPERIOD(cl)
    df['HT_DCPHASE'] = ta.HT_DCPHASE(cl)
    df['HT_PHASOR_inphase'], df['HT_PHASOR_quadrature'] = ta.HT_PHASOR(cl)
    df['HT_SINE_sine'], df['HT_SINE_leadsine'] = ta.HT_SINE(cl)
    df['HT_TRENDMODE'] = ta.HT_TRENDMODE(cl)

    # print('calc ta stats')
    df['BETA'] = ta.BETA(hi, lo, timeperiod=5)
    df['CORREL'] = ta.CORREL(hi, lo, timeperiod=30)
    df['LINEARREG'] = ta.LINEARREG(cl, timeperiod=14) - cl
    df['LINEARREG_ANGLE'] = ta.LINEARREG_ANGLE(cl, timeperiod=14)
    df['LINEARREG_INTERCEPT'] = ta.LINEARREG_INTERCEPT(cl, timeperiod=14) - cl
    df['LINEARREG_SLOPE'] = ta.LINEARREG_SLOPE(cl, timeperiod=14)
    df['STDDEV'] = ta.STDDEV(cl, timeperiod=5, nbdev=1)

    return df

talibで使える指標は概ね載せています。 LBGMの場合、不要な特徴量を追加してもスコアが劣化しにくいので、すべて載せてもある程度スコアには影響するはずです。

CSVロード

prices = pd.read_csv("../input/jpx-tokyo-stock-exchange-prediction/train_files/stock_prices.csv")

一旦trainの全データを使っていますが、スコアを伸ばす場合、supplemental_filesとマージして期間を限定すると良いと思います。

特徴量追加

prices.groupby('SecuritiesCode').apply(add_technical).dropna(axis=0)

groupbyを使ってSecuritiesCodeごとにテクニカル指標を計算します。

学習

model_o = LGBMRegressor(learning_rate=0.6818202991034834, max_bin=95, n_estimators=655, num_leaves=1263, random_seed=0)

ハイパーパラメタは他のnotebookのものを流用しています。 チューニングの余地はあります。

予測

# trainを初期状態としてセット
past_df = prices.copy()

# sample_prediction: 各銘柄1行ずつ
for i, (_prices, options, financials, trades, secondary_prices, sample_prediction) in enumerate(iter_test):
    current_date = _prices["Date"].iloc[0]
    print(f"current_date: {current_date}")

    # リークを防止するため、時系列APIから受け取ったデータより未来のデータを削除
    if i == 0:
        past_df = past_df.loc[past_df["Date"] < current_date]
    
    # リソース確保のため古い履歴を削除
    threshold = (pd.Timestamp(current_date) - pd.offsets.BDay(80)).strftime("%Y-%m-%d")
    print(f"threshold: {threshold}")
    past_df = past_df.loc[past_df["Date"] >= threshold]
    
    # _pricesの列調整
    _prices['Date'] = pd.to_datetime(_prices['Date'])
    _prices['DateInt'] = _prices['Date'].dt.strftime("%Y%m%d").astype(int)

    # 最新レコードと履歴をマージ
    past_df = pd.concat([past_df, _prices]).reset_index()

    # 履歴に対して特徴量再計算
    past_df = past_df.groupby('SecuritiesCode').apply(add_technical)

    # 最新レコード取り出し
    df = past_df.query(f'Date == "{current_date}"')

    # predict
    sample_prediction["Prediction"] = model_o.predict(df[fit_columns])

    # pred(リターン)を降順ソートしてランク付け
    sample_prediction = sample_prediction.sort_values(by="Prediction", ascending=False).drop_duplicates(subset=['SecuritiesCode'])
    sample_prediction.Rank = np.arange(0,2000)

    # コードで昇順ソート
    sample_prediction = sample_prediction.sort_values(by="SecuritiesCode", ascending=True)
    sample_prediction.drop(["Prediction"], axis=1)

    # 送信
    submission = sample_prediction[["Date","SecuritiesCode","Rank"]]
    env.predict(submission)

キモの部分ですが、past_dfの管理がややこしく、追加/削除/型変換/特徴量再計算/最新レコード分離 あたりがラグ特徴量を使うための処理になります。

お知らせ

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています。
少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください。
Wantedlyリンク