Twitterのいいね数推定の精度向上を図ってみる

# 続き・・・
以下前回の記事
[Twitterのいいね数を推定してみる](https://qiita.com/obr_bt/items/6c2294a16f38c67bfc94)

ツイートの内容からいいね数を推定するというものです。

今回は精度を求めていきます。

# データセットの見直し

データがあまりにも少なすぎるのでもっと関係がありそうな項目を取得していきます。

とは言ってもそんなに伸びツイートか否かに使えそうだなぁと思えたのは以下の2つでした。

- リプライか否か
- 引用リツイートか否か

この2つを追加してみます。

```get_twitter.py
    # ユーザーを指定して取得 （screen_name）
    getter = TweetsGetter.byUser('hana_oba')
    df = pd.DataFrame(columns = ['week_day','have_photo','have_video','tweet_time','text_len','favorite_count','retweet_count','quoted_status','reply','year_2018','year_2019','year_2020'])
 
    cnt = 0
    for tweet in getter.collect(total = 10000):
        cnt += 1

        week_day = tweet['created_at'].split()[0]
        tweet_time = tweet['created_at'].split()[3][:2]
        year = tweet['created_at'].split()[5]

        # Eoncodeしたい列をリストで指定。もちろん複数指定可能。
        list_cols = ['week_day']
        # OneHotEncodeしたい列を指定。Nullや不明の場合の補完方法も指定。
        ce_ohe = ce.OneHotEncoder(cols=list_cols,handle_unknown='impute')

        photo = 0
        video = 0
        quoted_status = 0
        reply = 0
        yar_2018 = 0
        yar_2019 = 0
        yar_2020 = 0

        if 'media' in tweet['entities']:

            if 'photo' in tweet['entities']['media'][0]['expanded_url']:
                photo = 1
            else:
                video = 1

        if 'quoted_status_id' in tweet:
            quoted_status = 1
        else:
            quoted_status = 0
        
        if tweet['in_reply_to_user_id_str'] is None:
            reply = 0
        else:
            reply = 1
        if year == '2018':
            yar_2018 = 1
            yar_2019 = 0
            yar_2020 = 0
        if year == '2019':
            yar_2018 = 0
            yar_2019 = 1
            yar_2020 = 0
        if year == '2020':
            yar_2018 = 0
            yar_2019 = 0
            yar_2020 = 1


        df = df.append(pd.Series([week_day, photo, video, int(tweet_time), len(tweet['text']),tweet['favorite_count'],tweet['retweet_count'],quoted_status,reply,yar_2018,yar_2019,yar_2020], index=df.columns),ignore_index=True)
        df_session_ce_onehot = ce_ohe.fit_transform(df)

    df_session_ce_onehot.to_csv('oba_hana_data.csv',index=False)
```

これでスコアを出してみます。

```IhaveOBAHANAfullyunderstood.ipynb
datapath = '/content/drive/My Drive/data_science/'
df = pd.read_csv(datapath + 'oba_hana_data.csv')

train_count = int(df.shape[0]*0.7)
df_train = df.sample(n=train_count)
df_test = df.drop(df_train.index)

have_photo = 'have_photo'
have_video = 'have_video'
tweet_time = 'tweet_time'
text_len = 'text_len'
favorite_count = 'favorite_count'
retweet_count = 'retweet_count'
quoted_status = 'quoted_status'
reply = 'reply'
year_2018 = 'year_2018'
year_2019 = 'year_2019'
year_2020 = 'year_2020'


# モデルの宣言
from sklearn.ensemble import RandomForestRegressor

# 外れ値除去
df_train = df_train[df_train['favorite_count'] < 4500]
df_train.shape

x_train = df_train.loc[:,[have_photo,have_video,tweet_time,text_len,quoted_status,reply,year_2018,year_2019,year_2020]]
t_train = df_train['favorite_count']
x_test = df_test.loc[:,[have_photo,have_video,tweet_time,text_len,quoted_status,reply,year_2018,year_2019,year_2020]]
t_test = df_test['favorite_count']

# モデルの宣言
model = RandomForestRegressor(n_estimators=2000, max_depth=10,
                                min_samples_leaf=4, max_features=0.2, random_state=0)

# モデルの学習
model.fit(x_train, t_train)
# モデルの検証
print(model.score(x_train, t_train))
print(model.score(x_test, t_test))
```

```
0.7189988420451674
0.6471214647821018
```
![ダウンロード (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569713/438dfbe9-ac81-4d6d-082c-aface4d635f3.png)

飛躍的に精度が上昇しました！
引用リツイートか否かはそこまで寄与度は高くありませんが曜日ほど無関係ではなさそうです。

ただ、もう一つ気になっていたのがツイート時間。現在ただの数値として見ていますのでいくつかの帯域に分けてみた方がよさそうです。

```
time_mean = pd.DataFrame(columns = ['time','favorite_mean'])
time_array = range(23)
for i in range(23):
  time_mean = time_mean.append(pd.Series([i,df_train[df_train['tweet_time'] == time_array[i]].favorite_count.mean()], index=time_mean.columns),ignore_index=True)

time_mean['time'] = time_mean['time'].astype(int)

sns.set_style('darkgrid')
plt.figure(figsize=(12, 8))
sns.catplot(x="time", y="favorite_mean", data=time_mean,
                height=6, kind="bar", palette="muted")
plt.show()
```

![ダウンロード (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569713/089a7904-c289-7770-ecf0-e1827a04714c.png)

※時間は標準時です

イコラブのメンバーはSNSは24時までと決まりがあるので(少しはみ出てもセーフ)0の部分もありますがそれ以外だと日本時間の24時台(おそらくお誕生日おめでとうなツイート)は平均値が高いことがわかります。
↑ではいくつかの帯域別に分けた方がいいといいましたがおそらくこれは帯域に分けるのではなく` TargetEncoding `の方がいいでしょう(ざっくりと時間で分けるのが難しそうなため)

```
pip install category_encoders

from category_encoders.target_encoder import TargetEncoder
df_train["tweet_time"] = df_train["tweet_time"].astype(str)

TE = TargetEncoder(smoothing=0.1)

df_train["target_enc_tweet_time"] = TE.fit_transform(df_train["tweet_time"],df_train["favorite_count"])
df_test["target_enc_tweet_time"] = TE.transform(df_test["tweet_time"])
```

` tweet_time `の代わりに` target_enc_tweet_time `を用いて学習し、そのスコアを見てみます

```
0.6999237089367164
0.6574824327192588
```

訓練データの方は下がりましたが検証データの方は上がりました。
ちなみに` tweet_time `、` target_enc_tweet_time `の両者を採用すると以下のようになりました。

```
0.7210047209796951
0.6457969793382683
```

訓練データでのスコアは一番良いですが検証データでは一番よくありません。
どれも甲乙つけがたいですが一旦どの可能性も残しておいて次に行きましょう。

# モデルの変更

今はランダムフォレストで設定も何もいじっていません。そこでモデルを` XGBoost `にし、` optuna ` を使って最適なパラメータを見つけていきたいと思います。

` optuna `をインストールします

```
!pip install optuna
```
続いて` XGboost `を` optuna `で回せるように関数化していきます。
ここで、それぞれのハイパーパラメータの値に適当な数値が並んでいますがこれは何回も繰り返していくうえで微調整していきます。

```
# XGboostのライブラリをインポート
import xgboost as xgb
# モデルのインスタンス作成
#mod = xgb.XGBRegressor()
import optuna

def objective(trial):
    # ハイパーパラメータの候補設定
    min_child_samples = trial.suggest_int('min_child_samples', 60, 75)
    max_depth  = trial.suggest_int('max_depth', -60, -40)
    learning_rate   = trial.suggest_uniform('suggest_uniform ', 0.075, 0.076)
    min_child_weight = trial.suggest_uniform('min_child_weight', 0.1, 0.8)
    num_leaves = trial.suggest_int('num_leaves', 2, 3)
    n_estimators = trial.suggest_int('n_estimators', 100, 180)
    subsample_for_bin = trial.suggest_int('subsample_for_bin', 450000, 600000)

    model = xgb.XGBRegressor(min_child_samples = min_child_samples,min_child_weight = min_child_weight,
                          num_leaves = num_leaves,subsample_for_bin = subsample_for_bin,learning_rate = learning_rate,
                          n_estimators = n_estimators)


    # 学習
    model.fit(x_train, t_train)

    # 評価を返す
    return (1 - model.score(x_test, t_test))
```

まずは100回回してみましょう。

```
# 試行回数を指定
study = optuna.create_study()
study.optimize(objective, n_trials=100)

print('ハイパーパラメータ：', study.best_params)
print('精度：', 1 - study.best_value)
```
それぞれでスコアを見てみます

## ①tweet_timeのみ採用

```
0.690093409305073
0.663908038217022
```

## ②target_enc_tweet_time のみ採用

```
0.6966901697205284
0.667797061960107
```

## ③tweet_timeとtarget_enc_tweet_time 両採用

```
0.6972461315076879
0.6669948080176482
```

わずかではありますが②が一番精度が良いようです。

ありとあらゆる可能性を試した方がいいに決まっているのですが、もしここから1つに絞るならばどれが一番よいのでしょうか？
②と③はほとんど変わりませんが②が検証データでスコアが良く、③は訓練でスコアが良い。
ここから` optuna `で精度を高めるべく微調整をする場合
- 訓練のスコアが高い方が伸びしろが高いと取るか
- 検証のスコアが高い方が過学習がひどくないのでもっと精度を上げていく
のどちらを考えるべきなんでしょう？
どちらかと言い切れる問題ではないかもしれませんが一般的にどちらが正しい道筋なのか知ってる方いらしたら教えていただけると幸いです。

今回は②を極める方で進めていきます。

ハイパーパラメータの範囲を一旦広く持ち、試行回数を1000回に増やします。
それで得られた最良の結果のときのハイパーパラメータ付近に範囲を狭め、再度1000回試行。
これで得られた結果が以下です。

```
0.6962221939011508
0.6685252235753019
```

これまでで一番よい検証結果が得られました。
僕が知っている精度向上の方法はすべて試したので今回はここまでにします。

最後に推論結果と実際の値のヒストグラムを見てみましょう。

![ダウンロード (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569713/b4545db9-3337-0b31-76a2-6dd1e5f8321f.png)

本当は1600当たりにある山が何に引っ張られて作られているのかがわかればよかったですね。
最初に出した精度の低い時と何が変わったか、パッとわからないあたりプロットするグラフの種類を間違えたかな…

見にくいですがオレンジ:本当のいいね数、緑:推論したいいね数、青:誤差
でプロットしてみました。

![ダウンロード (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569713/e2cd8b59-a641-211a-1927-4b3b16f54f69.png)


基本的に低めに推論していますね。やはり大場花菜はAIの予測を上回るツイートをしているわけか・・・(お前のモデルをはAI呼べる代物じゃない)
Fin