MLB Player Digital Engagement Forecasting -Day1-


#　はじめに
　今KaggleのMLB Player Digital Engagement Forecastingというコンペに参加してて、ベースとなるモデルを見つけたのでその知見について共有していこうと思っています。具体的な内容は試合の詳細やプロ野球選手の特性を持つデータセットから将来のファンエンゲージメントを予測するものです。エンゲージメントって言葉がマーケティングでは「企業/商品/サービス」と「ユーザー」とのつながりの深さを表す用語なので、このコンペにおけるファンエンゲージメントはプロ野球選手に対する愛着度や信頼度、人気度といった感じでとらえてます。具体的には、最近だと現メジャーリーグで活躍している大谷翔平選手が連続でHR？を打って話題になってた気がしますけど、そういうHRの数といった特徴量から、人気度とか話題度、注目度(やや広く意味をとっています)はどのくらい広がった(大きくなった)のか予測する感じです。もちろん、デッドボールやバッドプレイ等を行うとファンエンゲージメントは下がってしまいます。

今回参考したコンペのcodeは以下のものになります
  [LightGBM + ANN weight with LOVE](https://www.kaggle.com/batprem/lightgbm-ann-weight-with-love)
直近ではこのコードを用いて各自が性能を上げるスタンスがみられているので取り扱うことにしました。ローカル環境でデータを確認しています。(Kaggleでノートブックを提出しないといけないけどハードル高いです...)

データセットの規模がこのコンペでは莫大なため、すべてを可視化等することはしていないので、興味がある人は本コンペに参加してみるといいです。

# コード
## モジュールのインポート

```python 
import numpy as np
import pandas as pd
from pathlib import Path
from sklearn.metrics import mean_absolute_error
from datetime import timedelta
from functools import reduce
from tqdm import tqdm
import lightgbm as lgbm
```

## データの読み込みと、簡単なデータの可視化

``` python
BASE_DIR = Path('mlb-player-digital-engagement-forecasting')
TRAIN_DIR = Path('mlb-pdef-train-dataset')

players = pd.read_csv('players.csv')

rosters = pd.read_pickle('rosters_train.pkl')
targets = pd.read_pickle('nextDayPlayerEngagement_train.pkl')
scores = pd.read_pickle('playerBoxScores_train.pkl')
scores = scores.groupby(['playerId', 'date']).sum().reset_index()
```

Pathを使うと可読性がよくなるとかなんとか...(あまり気にしなくていいです。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/d9d3161b-9946-0db0-3a91-8a29d34dc9fa.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/435310ac-5200-f30e-9bce-914ec74207f0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/a8a2151f-0c2b-419c-7caf-67ab6312630e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/5588b5b8-b9c6-7353-e97f-b5de51f28a32.png)

このコンペの本来のコードはjson形式でコードが提供されていたり、特徴量(カラム数)が200近く？あったので、自分で初めからスタートするのではなく、他の参加者のコードを最初から活用することにしました。またデータの提出形式が初心者には複雑だったため、頼ってしまいました。

## 特徴量の説明と簡単な考察
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/e6e70725-93b4-8bb0-5406-004191cf463f.png)
・plaerID プレーヤーの一意の識別子
・gamePK　ゲームの一意の識別子
・teamId　プレーヤーがその日にいるteamId
・home　ホームチーム→1 離れている→0、  ホームだと影響力大きかったりしそう
・gamePK　ゲームの一意の識別子
・teamId チームの一意の識別子
・battingOrder　打順スポット。あまり解説読んでてもよくわかってない(3, 4桁ある)
・gamesPlayedBatting　ゲームに参加した場合1
・flyOuts　合計フライアウト、負の相関がありそう
・groundOuts　内野ゴロによるアウト、負の相関がありそう
・runsScored　合計ラン
・doubles　ゲームの合計の2倍?? 0, 1, 2, 3. 4の計5つの種類あるそうだけどよくわかってない
・triples　ゲームの合計の3倍
・homeRuns　ゲームの総本塁打。正の因果が強そう
・strikeOuts　ゲームの合計三振。正の因果が強そう
・baseOnBalls　ゲームの歩いた数？走塁数のことぽいけど、二桁以上のものが多く存在しているためよくわからず
・intentionalWalks　敬遠。負の因果がありそう
・hits　総ヒット数
・hitByPitch　ピッチによるゲームの合計ヒット
・atBats　打席数
・caughtStealing　走塁を防いだ回数。正の因果がありそう
・stolenBases　盗塁総数
・groundIntoDoublePlay　ダブルプレイの合計
・groundIntoTriplePlay　トリプルプレイの合計
・plateAppearances　ゲームの総打席
・totalBases 塁に到達した数
・rbi　ゲームの合計打点
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/67f55635-b99e-3086-0955-9a02cc9ed30d.png)

・leftOnBase　総ランナーがベースに残った
・sacBunts　ゲームの合計ギセイバント
・sacFlies　総犠牲フライ
・catchersInterference キャッチャーの関与回数。ピッチャーの状況が悪いと、多くなりそう。他の変数と共変量になってそう。
・pickoffs　ゲームの合計回数がベースから外れる。
・gamesPlayedPitching　プレーヤーが投手としてゲームに参加した場合は 1
・gamesStartedPitching バイナリ、プレーヤーがゲームの先発投手だった場合は1
・completeGamesPitching  バイナリ、完投でクレジットされている場合は1、正の因果が強そう
・shutoutsPitching  バイナリ、完封(無失点)でクレジットされている場合は1
・winsPitching  バイナリ、勝利でクレジットされている場合は 1
・lossesPitching  バイナリ、損失がクレジットされている場合は1
・flyOutsPitching  許可されたフライアウトのゲーム合計
・airOutsPitching エアアウト（フライアウト+ポップアウト）のゲーム合計が許可
・groundOutsPitching　ゲームの合計グラウンドアウトが許可
・runsPitching　ゲームの合計実行が許可
・doublesPitching  ゲームの合計は2倍
・triplesPitching ゲームの合計トリプルが許可されます。
・homeRunsPitching ゲームの合計ホームランが許可されます。
・strikeOutsPitching ゲームの合計三振が許可されます。
・baseOnBallsPitching ゲームの合計歩行が許可されます。
・intentionalWalksPitching ゲームの故意四球の合計が許可されます。
・hitsPitching　許可されるゲームの合計ヒット数。
・hitByPitchPitching 許可されたピッチによるゲームの合計ヒット。
・atBatsPitching　打席数でのゲーム合計

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/78eb8468-92cf-ea97-d5db-bbaaa0009259.png)
・caughtStealingPitching 盗塁を防いだ回数？。
・stolenBasesPitching  : ゲームの盗塁の合計は許可されます。
・inningsPitched  : ゲームの総投球回。
・saveOpportunities  : バイナリ、保存の機会がある場合は1。
・earnedRuns ゲームの合計自責点は許可されています。
・battersFaced 直面したゲームの総打者。
・outsPitching ゲームの合計アウトが記録。
・pitchesThrown 投げられた投球のゲーム総数。
・balls 投げられたゲームの合計ボール数。
・strikes スローされたゲームの合計ストライク。
・hitBatsmen ゲームの総死球打者。負の因果が強そう
・balks ボーク合計、ピッチャーの投球や塁への送球における反則行為、
・wildPitches 投げられた暴投のゲーム総数。
・pickoffsPitching ゲームのピックオフの総数。
・rbiPitching 打点のゲーム総数は許可されています。
・gamesFinishedPitching
・inheritedRunners 継承されたランナーのゲーム合計を想定。
・inheritedRunnersScored 得点した継承されたランナーのゲーム合計。
・catchersInterferencePitching キャッチャーの干渉のゲーム合計はバッテリーによって発生しました。
・sacBuntsPitching ゲームの犠牲バントの合計。
・sacFliesPitching ゲームの犠牲フライ。
・saves バイナリ、保存でクレジットされている場合は1。
・holds バイナリ、保留がクレジットされている場合は1。
・blownSaves バイナリ、ブローセーブでクレジットされている場合は1。
・assists ゲームのアシスト総数。
・putOuts ゲームの刺殺の総数。
・errors ゲームのエラーの総数。
・chances ゲームのトータルフィールディングチャンス。

　sacBuntsとsacBuntsPitchingの違いがわからなかったりして途中から説明は放棄してます...ただ、実務の上ではしっかりどういう特徴量なのかはしっかり抑えときたいです。後日特徴量エンジニアリングに挑戦したいと思っています。

## 必要な特徴量の抽出

``` python 
targets_cols = ['playerId', 'target1', 'target2', 'target3', 'target4', 'date']
players_cols = ['playerId', 'primaryPositionName']
rosters_cols = ['playerId', 'teamId', 'status', 'date']
scores_cols = ['playerId', 'battingOrder', 'gamesPlayedBatting', 'flyOuts',
       'groundOuts', 'runsScored', 'doubles', 'triples', 'homeRuns',
       'strikeOuts', 'baseOnBalls', 'intentionalWalks', 'hits', 'hitByPitch',
       'atBats', 'caughtStealing', 'stolenBases', 'groundIntoDoublePlay',
       'groundIntoTriplePlay', 'plateAppearances', 'totalBases', 'rbi',
       'leftOnBase', 'sacBunts', 'sacFlies', 'catchersInterference',
       'pickoffs', 'gamesPlayedPitching', 'gamesStartedPitching',
       'completeGamesPitching', 'shutoutsPitching', 'winsPitching',
       'lossesPitching', 'flyOutsPitching', 'airOutsPitching',
       'groundOutsPitching', 'runsPitching', 'doublesPitching',
       'triplesPitching', 'homeRunsPitching', 'strikeOutsPitching',
       'baseOnBallsPitching', 'intentionalWalksPitching', 'hitsPitching',
       'hitByPitchPitching', 'atBatsPitching', 'caughtStealingPitching',
       'stolenBasesPitching', 'inningsPitched', 'saveOpportunities',
       'earnedRuns', 'battersFaced', 'outsPitching', 'pitchesThrown', 'balls',
       'strikes', 'hitBatsmen', 'balks', 'wildPitches', 'pickoffsPitching',
       'rbiPitching', 'gamesFinishedPitching', 'inheritedRunners',
       'inheritedRunnersScored', 'catchersInterferencePitching',
       'sacBuntsPitching', 'sacFliesPitching', 'saves', 'holds', 'blownSaves',
       'assists', 'putOuts', 'errors', 'chances', 'date']

feature_cols = ['label_playerId', 'label_primaryPositionName', 'label_teamId',
       'label_status', 'battingOrder', 'gamesPlayedBatting', 'flyOuts',
       'groundOuts', 'runsScored', 'doubles', 'triples', 'homeRuns',
       'strikeOuts', 'baseOnBalls', 'intentionalWalks', 'hits', 'hitByPitch',
       'atBats', 'caughtStealing', 'stolenBases', 'groundIntoDoublePlay',
       'groundIntoTriplePlay', 'plateAppearances', 'totalBases', 'rbi',
       'leftOnBase', 'sacBunts', 'sacFlies', 'catchersInterference',
       'pickoffs', 'gamesPlayedPitching', 'gamesStartedPitching',
       'completeGamesPitching', 'shutoutsPitching', 'winsPitching',
       'lossesPitching', 'flyOutsPitching', 'airOutsPitching',
       'groundOutsPitching', 'runsPitching', 'doublesPitching',
       'triplesPitching', 'homeRunsPitching', 'strikeOutsPitching',
       'baseOnBallsPitching', 'intentionalWalksPitching', 'hitsPitching',
       'hitByPitchPitching', 'atBatsPitching', 'caughtStealingPitching',
       'stolenBasesPitching', 'inningsPitched', 'saveOpportunities',
       'earnedRuns', 'battersFaced', 'outsPitching', 'pitchesThrown', 'balls',
       'strikes', 'hitBatsmen', 'balks', 'wildPitches', 'pickoffsPitching',
       'rbiPitching', 'gamesFinishedPitching', 'inheritedRunners',
       'inheritedRunnersScored', 'catchersInterferencePitching',
       'sacBuntsPitching', 'sacFliesPitching', 'saves', 'holds', 'blownSaves',
       'assists', 'putOuts', 'errors', 'chances','target1_mean',
 'target1_median', 'target1_std', 'target1_min', 'target1_max', 'target1_prob',
 'target2_mean', 'target2_median', 'target2_std', 'target2_min', 'target2_max',
 'target2_prob', 'target3_mean', 'target3_median', 'target3_std', 'target3_min',
 'target3_max', 'target3_prob', 'target4_mean', 'target4_median', 'target4_std',
 'target4_min', 'target4_max', 'target4_prob']
```

## 主に用いるデータセット

``` python
player_target_stats = pd.read_csv("player_target_stats.csv")
data_names=player_target_stats.columns.values.tolist()
data_names
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/22d4b97b-f7bf-9dbf-7ce1-ddd7e1c54c93.png)

ここの特徴量が今回の分析コンペの中枢になります。(結果への影響力が高い)


## 訓練用のデータセットの作成

``` python 
# creat dataset
train = targets[targets_cols].merge(players[players_cols], on=['playerId'], how='left')
train = train.merge(rosters[rosters_cols], on=['playerId', 'date'], how='left')
train = train.merge(scores[scores_cols], on=['playerId', 'date'], how='left')
train = train.merge(player_target_stats, how='inner', left_on=["playerId"],right_on=["playerId"])


# label encoding
# 辞書形式でplayerIdとprimaryPositionNameとteamIdとstatusのそれぞれの特徴量を保存
player2num = {c: i for i, c in enumerate(train['playerId'].unique())}
position2num = {c: i for i, c in enumerate(train['primaryPositionName'].unique())}
teamid2num = {c: i for i, c in enumerate(train['teamId'].unique())}
status2num = {c: i for i, c in enumerate(train['status'].unique())}
train['label_playerId'] = train['playerId'].map(player2num)
train['label_primaryPositionName'] = train['primaryPositionName'].map(position2num)
train['label_teamId'] = train['teamId'].map(teamid2num)
train['label_status'] = train['status'].map(status2num)
```

イメージとして、前半の#create datasetではplayerIdとdateをキーとして特徴量を結合し、後半の# label encodingでは適当になっているIDに対して新しくインデックスを割り振っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/e2a0a13f-b249-ee1a-9db6-950c6bd60ee2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/1e52f6c0-461a-4010-5193-32495baa2922.png)

## 目標変数の確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/1538a990-cbb0-f862-2811-4fa92711cdb3.png)
先ほどの各特徴量から目標変数target1～4を予測する感じです。

## 訓練データと検証用データに分割

``` python
train_X = train[feature_cols]
train_y = train[['target1', 'target2', 'target3', 'target4']]

_index = (train['date'] < 20210401) # 条件式に合致しているものにはTrue、そうでないものにはFalseが付与されます。
x_train = train_X.loc[_index].reset_index(drop=True)
y_train = train_y.loc[_index].reset_index(drop=True)
x_valid = train_X.loc[~_index].reset_index(drop=True)
y_valid = train_y.loc[~_index].reset_index(drop=True)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/f5b89ccf-e975-ff93-c277-3e69c9d8c9b7.png)
```

こんな感じになります。indexを新しく振り直していることだけ注意です。

## LightGBMのモデル構築

``` python 
def fit_lgbm(x_train, y_train, x_valid, y_valid, params: dict=None, verbose=100):
    # 長さy_validの[0, 0, ....を生成]
    # 各target1～4に対して予測を行い、最終的にそれらのtargetを結合する
    oof_pred = np.zeros(len(y_valid), dtype=np.float32)
    model = lgbm.LGBMRegressor(**params)
    model.fit(x_train, y_train, 
        eval_set=[(x_valid, y_valid)],  
        early_stopping_rounds=verbose, 
        verbose=verbose)
    # 先ほど作った[0,0, ...]に値を格納していく
    oof_pred = model.predict(x_valid)
    score = mean_absolute_error(oof_pred, y_valid)
    print('mae:', score)
    return oof_pred, model, score


# training lightgbm

params1 = {'objective':'mae',
           'reg_alpha': 0.14947461820098767, 
           'reg_lambda': 0.10185644384043743, 
           'n_estimators': 3633, 
           'learning_rate': 0.08046301304430488, 
           'num_leaves': 674, 
           'feature_fraction': 0.9101240539122566, 
           'bagging_fraction': 0.9884451442950513, 
           'bagging_freq': 8, 
           'min_child_samples': 51}
oof1, model1, score1 = fit_lgbm(
    x_train, y_train['target1'],
    x_valid, y_valid['target1'],
    params1
)

params2 = {'objective':'mae',
           'reg_alpha': 0.1,
           'reg_lambda': 0.1, 
           'n_estimators': 80,
           'learning_rate': 0.1,
           'random_state': 42,
           "num_leaves": 22}
oof2, model2, score2 = fit_lgbm(
    x_train, y_train['target2'],
    x_valid, y_valid['target2'],
    params2
)


params3 = { 'objective':'mae',
           'reg_alpha': 0.1,
           'reg_lambda': 0.1, 
           'n_estimators': 10000,
           'learning_rate': 0.1,
           'random_state': 42,
           "num_leaves": 100}
oof3, model3, score3 = fit_lgbm(
    x_train, y_train['target3'],
    x_valid, y_valid['target3'],
    params3
)


params4 = {'objective':'mae',
           'reg_alpha': 0.016468100279441976, 
           'reg_lambda': 0.09128335764019105, 
           'n_estimators': 9868, 
           'learning_rate': 0.10528150510326864, 
           'num_leaves': 157, 
           'feature_fraction': 0.5419185713426886, 
           'bagging_fraction': 0.2637405128936662, 
           'bagging_freq': 19, 
           'min_child_samples': 71}
oof4, model4, score4 = fit_lgbm(
    x_train, y_train['target4'],
    x_valid, y_valid['target4'],
    params4
)

score = (score1+score2+score3+score4) / 4
print(f'score: {score}')

```
元々optunaでモデルを作って多っぽくて、それを適応したためにパラメータの値が複雑になっています。スタッキングによる手法を用いているため、最終的にはscoreがそれぞれの予測値を4で割っています。(勉強になります。)

## 実際に予測
tensorflowを用いて予測していますが、冒頭部分のコードが難しくて調べているのが大変でした。

``` python 
import pandas as pd
import numpy as np
from datetime import timedelta
from tqdm import tqdm
import gc
from functools import reduce
from sklearn.model_selection import StratifiedKFold

ROOT_DIR = "mlb-player-digital-engagement-forecasting"

#=======================#
# データ整形(データを並び替える)
# add_prefexはカラム名に追加で名前を付けた感じです。
def flatten(df, col):
    du = (df.pivot(index="playerId", columns="EvalDate", 
               values=col).add_prefix(f"{col}_").
      rename_axis(None, axis=1).reset_index())
    return du
#============================#
# playerIdに基づいてleftにrightを結合
def reducer(left, right):
    return left.merge(right, on="playerId")
#========================

TGTCOLS = ["target1","target2","target3","target4"]
def train_lag(df, lag=1):
    # .copy()で値渡し
    # ["playerId", "EvalDate"], TGTCOLSのカラムを指定
    dp = df[["playerId","EvalDate"]+TGTCOLS].copy()
    dp["EvalDate"] = dp["EvalDate"] + timedelta(days=lag) 
    # suffixes ... カラムに接尾辞を指定してデータフレームを連結
    df = df.merge(dp, on=["playerId", "EvalDate"], suffixes=["",f"_{lag}"], how="left")
    return df
#=================================
def test_lag(sub):
    sub["playerId"] = sub["date_playerId"].apply(lambda s: int(  s.split("_")[1]  ) )
    assert sub.date.nunique() == 1
    dte = sub["date"].unique()[0]
    
    eval_dt = pd.to_datetime(dte, format="%Y%m%d")
    dtes = [eval_dt + timedelta(days=-k) for k in LAGS]
    mp_dtes = {eval_dt + timedelta(days=-k):k for k in LAGS}
    
    sl = LAST.loc[LAST.EvalDate.between(dtes[-1], dtes[0]), ["EvalDate","playerId"]+TGTCOLS].copy()
    sl["EvalDate"] = sl["EvalDate"].map(mp_dtes)
    du = [flatten(sl, col) for col in TGTCOLS]
    du = reduce(reducer, du)
    return du, eval_dt
    #
#===============

tr = pd.read_csv("target.csv")
print(tr.shape)
gc.collect()

tr["EvalDate"] = pd.to_datetime(tr["EvalDate"])
# dateを一つ前にずらす
tr["EvalDate"] = tr["EvalDate"] + timedelta(days=-1)
# 2018-...の2018の部分だけを取り出す
tr["EvalYear"] = tr["EvalDate"].dt.year

MED_DF = tr.groupby(["playerId","EvalYear"])[TGTCOLS].median().reset_index()
MEDCOLS = ["tgt1_med","tgt2_med", "tgt3_med", "tgt4_med"]
# 中央値をとったのでカラム名を変える
MED_DF.columns = ["playerId","EvalYear"] + MEDCOLS

# FECOLSは4×20 = 80個のリスト
LAGS = list(range(1,21))
# 'target4_19', reversedはリストを逆順にする
FECOLS = [f"{col}_{lag}" for lag in reversed(LAGS) for col in TGTCOLS]

for lag in tqdm(LAGS):
    tr = train_lag(tr, lag=lag)
    gc.collect()
#===========
tr = tr.sort_values(by=["playerId", "EvalDate"])
print(tr.shape)
tr = tr.dropna()
print(tr.shape)
tr = tr.merge(MED_DF, on=["playerId","EvalYear"])
gc.collect()

X = tr[FECOLS+MEDCOLS].values
y = tr[TGTCOLS].values
cl = tr["playerId"].values

NFOLDS = 6
skf = StratifiedKFold(n_splits=NFOLDS)
folds = skf.split(X, cl) # 訓練データと検証データのインデックスが入ったラベル
folds = list(folds)

import tensorflow as tf
import tensorflow.keras.layers as L
import tensorflow.keras.models as M
from sklearn.metrics import mean_absolute_error, mean_squared_error
from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping

tf.random.set_seed(777)

# 今回はラグを20としたモデルを作ってます。
def make_model(n_in):
    inp = L.Input(name="inputs", shape=(n_in,))
    x = L.Dense(50, activation="relu", name="d1")(inp)
    x = L.Dense(50, activation="relu", name="d2")(x)
    preds = L.Dense(4, activation="linear", name="preds")(x)
    
    model = M.Model(inp, preds, name="ANN")
    model.compile(loss="mean_absolute_error", optimizer="adam")
    return model

net = make_model(X.shape[1])
print(net.summary())

# callbacks 訓練中にモデル内部の状態と統計量を可視化
oof = np.zeros(y.shape)
nets = []
for idx in range(NFOLDS):
    print("FOLD:", idx)
    tr_idx, val_idx = folds[idx]
    ckpt = ModelCheckpoint(f"w{idx}.h5", monitor='val_loss', verbose=1, save_best_only=True,mode='min')
    """
    ModelCheckpoint 引数
    ・filepath : 重みファイル名そのものを指示
    ・moitor : 監視する値(acc, loss, val_acc, val_loss)
    ・verbose : 0 = 結果非表示, 1 = 結果表示
    ・save_best_only True...判定結果から保存を決定。False...periodの間隔で保存
    ・model...判定条件 max...「acc, val_acc」, min... [loss, val_loss]
    ・save_weigth_only... True= モデルの重みが保存。False= モデル全体を保存
    ・period... 何エポックごとに保存するか
    
    """
    reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.2,patience=3, min_lr=0.0005)
    """
    ReduceLROnPlateau
    https://keras.io/api/callbacks/reduce_lr_on_plateau/
    監視する評価値、何エポック改善しなかったら学習率を落とすか、その変化の割合、最小値などを指定すると、
    学習の進みに応じて調整
    ・factor : 学習率が減少する割合 new_factor = lr * factor
    ・patience : 学習率が向上しない回数
    ・min_lr : 学習率の最低ライン
    """
    es = EarlyStopping(monitor='val_loss', patience=6)
    reg = make_model(X.shape[1])
    reg.fit(X[tr_idx], y[tr_idx], epochs=10, batch_size=35_000, validation_data=(X[val_idx], y[val_idx]),
            verbose=1, callbacks=[ckpt, reduce_lr, es])
    reg.load_weights(f"w{idx}.h5")
    oof[val_idx] = reg.predict(X[val_idx], batch_size=50_000, verbose=1)
    nets.append(reg)
    gc.collect()
    #
#

mae = mean_absolute_error(y, oof)
mse = mean_squared_error(y, oof, squared=False)
print("mae:", mae)
print("mse:", mse)

# Historical information to use in prediction time
bound_dt = pd.to_datetime("2021-01-01")
LAST = tr.loc[tr.EvalDate>bound_dt].copy()

LAST_MED_DF = MED_DF.loc[MED_DF.EvalYear==2021].copy()
LAST_MED_DF.drop("EvalYear", axis=1, inplace=True)
del tr

#"""
#import mlb
FE = []; SUB = [];
```
gc.collect()はガベージコレクションという不要になったメモリ領域を自動的に開放する操作を行ってるらしいです。
trは以下のようになっています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/8fae09c1-c3c9-b025-db50-edbdf0b0c666.png)


## データの提出

``` python
import copy

env = mlb.make_env() # initialize the environment
iter_test = env.iter_test() # iterator which loops over each date in test set

for (test_df, sample_prediction_df) in iter_test: # make predictions here
    
    sub = copy.deepcopy(sample_prediction_df.reset_index())
    sample_prediction_df = copy.deepcopy(sample_prediction_df.reset_index(drop=True))
    
    # LGBM summit
    # creat dataset
    sample_prediction_df['playerId'] = sample_prediction_df['date_playerId']\
                                        .map(lambda x: int(x.split('_')[1]))
    # Dealing with missing values
    if test_df['rosters'].iloc[0] == test_df['rosters'].iloc[0]:
        test_rosters = pd.DataFrame(eval(test_df['rosters'].iloc[0]))
    else:
        test_rosters = pd.DataFrame({'playerId': sample_prediction_df['playerId']})
        for col in rosters.columns:
            if col == 'playerId': continue
            test_rosters[col] = np.nan
            
    if test_df['playerBoxScores'].iloc[0] == test_df['playerBoxScores'].iloc[0]:
        test_scores = pd.DataFrame(eval(test_df['playerBoxScores'].iloc[0]))
    else:
        test_scores = pd.DataFrame({'playerId': sample_prediction_df['playerId']})
        for col in scores.columns:
            if col == 'playerId': continue
            test_scores[col] = np.nan
    test_scores = test_scores.groupby('playerId').sum().reset_index()
    test = sample_prediction_df[['playerId']].copy()
    test = test.merge(players[players_cols], on='playerId', how='left')
    test = test.merge(test_rosters[rosters_cols], on='playerId', how='left')
    test = test.merge(test_scores[scores_cols], on='playerId', how='left')
    test = test.merge(player_target_stats, how='inner', left_on=["playerId"],right_on=["playerId"])
    

    test['label_playerId'] = test['playerId'].map(player2num)
    test['label_primaryPositionName'] = test['primaryPositionName'].map(position2num)
    test['label_teamId'] = test['teamId'].map(teamid2num)
    test['label_status'] = test['status'].map(status2num)
    
    test_X = test[feature_cols]
    
    # predict
    pred1 = model1.predict(test_X)
    pred2 = model2.predict(test_X)
    pred3 = model3.predict(test_X)
    pred4 = model4.predict(test_X)
    
    # merge submission
    sample_prediction_df['target1'] = np.clip(pred1, 0, 100)
    sample_prediction_df['target2'] = np.clip(pred2, 0, 100)
    sample_prediction_df['target3'] = np.clip(pred3, 0, 100)
    sample_prediction_df['target4'] = np.clip(pred4, 0, 100)
    sample_prediction_df = sample_prediction_df.fillna(0.)
    del sample_prediction_df['playerId']
    # TF summit
    # Features computation at Evaluation Date
    sub_fe, eval_dt = test_lag(sub)
    sub_fe = sub_fe.merge(LAST_MED_DF, on="playerId", how="left")
    sub_fe = sub_fe.fillna(0.)
    
    _preds = 0.
    for reg in nets:
        _preds += reg.predict(sub_fe[FECOLS + MEDCOLS]) / NFOLDS
    sub_fe[TGTCOLS] = np.clip(_preds, 0, 100)
    sub.drop(["date"]+TGTCOLS, axis=1, inplace=True)
    sub = sub.merge(sub_fe[["playerId"]+TGTCOLS], on="playerId", how="left")
    sub.drop("playerId", axis=1, inplace=True)
    sub = sub.fillna(0.)
    # Blending
    blend = pd.concat(
        [sub[['date_playerId']],
        (0.1*sub.drop('date_playerId', axis=1) + 0.9*sample_prediction_df.drop('date_playerId', axis=1))],
        axis=1
    )
    env.predict(blend)
    # Update Available information
    sub_fe["EvalDate"] = eval_dt
    #sub_fe.drop(MEDCOLS, axis=1, inplace=True)
    LAST = LAST.append(sub_fe)
    LAST = LAST.drop_duplicates(subset=["EvalDate","playerId"], keep="last")
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546672/62cfc7b7-f4f2-fbe2-7a73-d5e20a9ab681.png)

Kaggle上ではmlbモジュールについての説明が簡潔になされています。


#おわりに
　コンペやりながら書いてたのですが、Your Notebook tried to allocate more memory than available. It has been restarted.というエラーを解決するのが難しくてしばらく席を離れようと思います....