LightGBMによるサザエさんのジャンケン予測

#はじめに
時系列データの機械学習予測の練習としてLightGBMでサザエさんのジャンケン予測をしてみました。

#LightGBMとは
機械学習アルゴリズムのひとつで決定木に基づいた勾配ブースティングを採用しています。最近のKaggleコンペの上位陣の多くが使用しており注目度の高い機械学習手法です。

LightGBM 徹底入門
https://www.codexa.net/lightgbm-beginner/

#データセット
1991年のジャンケンの開始から今日までの全てのジャンケンが記録されている「サザエさんじゃんけん研究所」よりデータを拝借しました。インターネット以前のパソコン通信時代から記録し続けているというから驚きです。また記録だけでなく次にどの手が出るのかを様々角度から分析されています。

サザエさんじゃんけん研究所公式ウェブサイト
http://park11.wakwak.com/~hkn/

#特徴量の選択
「サザエさんじゃんけん研究所」の分析によると過去2回の手から次の手を予測する方法と、最も長期間出ていない手が次の手であると予測する方法がありました。またシーズン（1,4,7,10月）の最初の放送とスペシャル回の放送時はチョキがでやすいとのこと。

まずはこれらを特徴量としてベースとなるモデルを作成し、特徴量を増減しながらどの特徴量が有効か検証しました。

その結果、最終的に選択した特徴量は以下の通り。
・過去3回のジャンケン結果（過去4回～まで含めると精度は下がる）
・シーズン最初かどうか
・スペシャル回かどうか
・前回がシーズン最初かどうか（こちらは過去１回分のみ。2回～まで含めると精度は下がる）
・前回がスペシャル回かどうか（こちらは過去１回分のみ。2回～まで含めると精度は下がる）

「サザエさんじゃんけん研究所」によると、手は制作会社の担当者が気分で決めているとのこと。その際に過去数回分のジャンケンを参考にかぶらないように次の手を出す、というのは大いにありそう。なので特徴量としてもせいぜい3回分の結果をラグ特徴とするのは理にかなっているかなと思います。
また研究所では最も長期間出ていない手が次の手であると予測する方法があったのですが、これを特徴量としてみたところあまり精度は変わらなかったので除外しています。

#学習とテスト
これらの特徴量としてLightGBMで最終的な学習とテストを行いました。
・学習データ：1991年12月1日～2015年8月9日までの1196回
・検証データ：2015年8月16日～2019年7月14日までの194回
・テストデータ：2019年7月21日～2020年7月5日までの50回


結果はテストデータでの**正解率（Accuracy）は65.3%**となりました。
また予測のうち一番確率が高い手に勝つ手を出す、という戦略をとってサイトと同じ指標で勝率を計算（勝ち／（勝ち＋負け））したところ、**勝率は84.2%**となりました。

#コード
最後にコードを載せておきます。
※読み込んでいるJanken2.csvは[こちら](https://github.com/kshimiz/sazae/blob/master/Janken2.csv)

##学習・テスト
```python
import pandas as pd
import lightgbm as lgb

import numpy as np

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

#Janken2.csvの時点である程度の加工は済ませています。
sazae = pd.read_csv('Janken2.csv')

sazae.drop(labels=['cnt','date'], axis=1, inplace=True)
sazae.year = sazae.year - 1990
sazae.season = sazae.season.fillna(0)
sazae.special = sazae.special.fillna(0)
sazae['janken_lag_1'] = sazae['janken'].shift(1)
sazae['janken_lag_2'] = sazae['janken'].shift(2)
sazae['janken_lag_3'] = sazae['janken'].shift(3)
sazae['season_lag'] = sazae['season'].shift(1)
sazae['special_lag'] = sazae['special'].shift(1)

sazae = sazae[3:]
sazae = sazae.astype({'special_lag':int,'season_lag':int,'janken': int, 'special': int, 'season': int, 'janken_lag_1':int, 'janken_lag_2':int, 'janken_lag_3':int})
sazae[(sazae['janken_lag_1']==0) & (sazae['janken_lag_2']==1)& (sazae['janken_lag_3']==0)]

X, y = sazae[['special','season','janken_lag_1','janken_lag_2','janken_lag_3','season_lag','special_lag']], sazae[['janken']]
cat = ['season','janken_lag_1','janken_lag_2','janken_lag_3','season_lag','special_lag', 'special']

X_train, y_train = X[(X.index >= 0) & (X.index < 1200)], y[(y.index >= 0) & (y.index < 1200)]
X_valid, y_valid = X[(X.index >= 1200) & (X.index < 1394)], y[(y.index >= 1200) & (y.index < 1394)]
X_test, y_test = X[(X.index >= 1394)], y[(y.index >= 1394)]

# 学習に使用するデータを設定
lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=cat)
lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train, categorical_feature=cat)

# LightGBM parameters
params = {
        'task': 'train',
        'boosting_type': 'gbdt',
        'objective': 'multiclass', # 目的 : 多クラス分類 
        'num_class': 3, # クラス数 : 3
        'metric': {'multi_error'}, # 評価指標 : 誤り率(= 1-正答率) 
}

# モデルの学習
model = lgb.train(params,
train_set=lgb_train, # トレーニングデータの指定
valid_sets=lgb_eval, # 検証データの指定
# 最大で 1000 ラウンドまで学習する
num_boost_round=2000,
# 10 ラウンド経過しても性能が向上しないときは学習を打ち切る
early_stopping_rounds=100              
)

# テストデータの予測 ((各クラスの予測確率 [グー(0)予測確率,チョキ(1)の予測確率,パー(2)の予測確率] を返す))
y_pred_prob = model.predict(X_test)
y_pred = np.argmax(y_pred_prob, axis=1) 

accuracy = accuracy_score(y_test, y_pred)
print(accuracy)
```

##勝率計算
```python
win = 0
lose = 0
draw = 0
janken = (y_pred + 2) % 3
print(janken)
for (test, j) in zip(y_test.values, janken):
    if test == j:
        draw += 1
    elif (test+2)%3 == j:
        win += 1
    elif (test+1)%3 == j:
        lose += 1
print(win,draw,lose)
print(win/(win+lose))
```

##次回放送の予測
```python
test2 = pd.DataFrame(index=[], columns=X_test.columns)
print(test2)

#[スペシャル回, シーズン最初の回, 前回の手, 前々回の手, 三回前の手, 前回がシーズン最初の回かどうか,前回がスペシャル回かどうか]を指定
test2.loc[0] = [0,0,1,0,2,1,0] #2020/7/12

test2 = test2.astype({'special_lag':int,'season_lag':int, 'special': int, 'season': int, 'janken_lag_1':int, 'janken_lag_2':int, 'janken_lag_3':int})

result = model.predict(test2)

#[グーの確率、チョキの確率、パーの確率]
print(result)
```

#今後やりたいこと
・コードがちょっときたないので整理します。
・CV（Cross Validation）
・パラメータチューニング

#参考サイト
[機械学習でサザエさんとじゃんけん勝負（ニューラルネットワーク編）](https://qiita.com/everylittle/items/59da0702260b0e7729dd)