kaggleの「Predict Calorie Expenditure」をやってみた。

## kaggleの初心者向けコンペに取り組む
kaggleに登録して、やりたいコンペを探した見たのですがどうも初心者である私にとっては難しいコンペが多く、どのコンペに取り組んだらよいのか悩んでおりました。しかしどうやらkaggleには <strong>「playground」</strong> という初心者向けコンペがあり、それに取り込んでみようということになりました。

## 「Predict Calorie Expenditure」コンペについて

「Predict Calorie Expenditure」コンペは「playground」シリーズのコンペで、年齢や体重、性別、心拍数などをもとに、<strong>トレーニング中の消費カロリー</strong>を予測するというコンペです。特徴量も少なく取り組みやすいコンペですので、こちらをやってみることにしました。

## データを読み込んでデータを確認
まずデータを読み込んで学習データとテストデータを確認してみます。
```
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from sklearn.linear_model import LinearRegression

%matplotlib inline

# 学習データとテストデータの読み込み
train = pd.read_csv("/kaggle/input/playground-series-s5e5/train.csv")
test = pd.read_csv("/kaggle/input/playground-series-s5e5/test.csv")
```
学習用データを確認
```
train.head()
```

![caloriedata.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4064049/78170c42-59f5-4cd0-ab41-a9fe1864b50d.png)

データは性別、年齢、体重、期間、心拍数、体温などの特徴量をもとに消費カロリーを予測します。<strong>「Calories」</strong>が目的変数です。特徴量が少なく取り組みやすいですね。

そして学習用データと評価用データのサイズを取得

```
train.shape
#(750000, 9)
```

```
test.shape
#(250000, 8)
```
データ数は学習用データが<strong>750000</strong>といつものコンペと比べてかなり膨大な数ですね。学習時に時間がかかりそう。


## 次に欠損値を確認
学習用データの欠損値を確認してみます。
```
train.isnull().sum()
```
![calorie欠損値.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4064049/da6de52a-a0d3-4273-bb8b-aea7aba83b9a.png)

このコンペも欠損値が無いようです。欠損値の補充をしなくて良いので取り組みやすいコンペになっています。ちなみにテストデータも欠損値が無いようです。

## カテゴリ変数の変換
性別を特徴量に使いたいので性別を単純に数値に変換します。

```
#男性を0、女性を1に変える
train=train.replace({'male': 0 ,'female': 1 })
test=test.replace({'male': 0 ,'female': 1 })
```

男性を0、女性を1に変換します。

## BMIを特徴量に加える

次に特徴量エンジニアリングとして、BMIを特徴量に加えます。BMIは体重(kg)を身長(m)の2乗で割ることで求められます。

```
train['BMI'] = train['Weight'] / ((train['Height'] / 100) ** 2)
test['BMI'] = test['Weight'] / ((test['Height'] / 100) ** 2)
```

## 特徴量を掛け合わせる
特徴量を増やすために特徴量を掛けたものをデータに加えていきます。
```
numerical_features = ["Age","Height","Weight","Duration","Heart_Rate","Body_Temp"]

def add_feature_cross_terms(df, numerical_features):
    df_new = df.copy()
    for i in range(len(numerical_features)):
        for j in range(i + 1, len(numerical_features)):  
            feature1 = numerical_features[i]
            feature2 = numerical_features[j]
            cross_term_name = f"{feature1}_x_{feature2}"
            df_new[cross_term_name] = df_new[feature1] * df_new[feature2]

    return df_new

train = add_feature_cross_terms(train, numerical_features)
test = add_feature_cross_terms(test, numerical_features)
```

## 目的変数を正規化
次に学習データの準備をしますが、その際に目的変数の分布が偏っているようなので、目的変数を対数変換して正規化します。

```
X = train.drop(columns=["id", "Calories"])
y = train["Calories"]
X_test = test.drop(columns=["id"])
```

```
#Caloriesの分布を確認
sns.displot(y)
plt.show()
```

![calorieグラフ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4064049/2a0e95ca-2af9-48cc-bb45-14fdd3c0f5bb.png)

見てみるとCaloriesが少ない方に偏っていますね。そこで<strong>対数変換</strong>を行って正規化します。

```
y = np.log1p(y)
sns.displot(y)
plt.show()
```

![calorie正規化.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4064049/67a0e8a8-2a3a-43cf-bcb0-2d852c527494.png)

これで正規化できましたね。

## モデルの学習

次にモデルの学習をします。モデルは勾配ブースティングのlighigbmを選択し、5分割のクロスバリデーションでモデルを評価します。パラメーターは事前にoptunaで探った数値を採用します。

```
import lightgbm as lgb
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_log_error

y_preds = []
models = []
oof_train = np.zeros((len(X)))
cv = KFold(n_splits = 5, shuffle = True, random_state = 0)

categorical_features = ['Sex']

params = {
        'objective': 'regression',
        'max_bin': 495,
        'learning_rate': 0.05,
        'num_leaves': 109,
        }

for fold_id, (train_index, valid_index) in enumerate(cv.split(X)):
    X_tr = X.loc[train_index, :]
    X_val = X.loc[valid_index, :]
    y_tr = y.loc[train_index]
    y_val = y.loc[valid_index]

    lgb_train = lgb.Dataset(X_tr, y_tr, categorical_feature = categorical_features)
    lgb_eval = lgb.Dataset(X_val, y_val,categorical_feature = categorical_features, reference = lgb_train,)

    model = lgb.train(params, lgb_train,
                  valid_sets=[lgb_train, lgb_eval],
                  callbacks=[lgb.log_evaluation(period=10),lgb.early_stopping(10)],
                  num_boost_round = 1000,
                  )
    oof_train[valid_index] = \
        model.predict(X_val)
    y_val_pred = model.predict(X_val)
    
    y_pred = model.predict(test_features)

    y_preds.append(y_pred)
    models.append(model)
```

## モデルの評価
変数oof_trainにはバリデーションデータで予測した数値が入っています。
```
oof_train
# array([4.99761586, 3.60250051, 3.40595078, ..., 5.44440419, 4.68565496,
       4.6243114 ])
```

この数値は対数変換した数値ですので、これを元の数値に戻します。
```
oof_train2 = np.expm1(oof_train)
oof_train2
# array([147.05974314,  35.68986331,  29.14294156, ..., 230.45933212,
       107.38123451, 100.93255792])
```

目的変数も元に戻します。
```
y1 = np.expm1(y)
```
そしてモデルを評価します。評価指標は<strong>RMSLE</strong>です。
```
from sklearn.metrics import mean_squared_log_error

rmsle_val = np.sqrt(mean_squared_log_error(y1, oof_train2))
rmsle_val
#0.06027480563955721
```
<strong>0.06027</strong>という数値が出ました。

## 提出してみる
変数y_predsにはテストデータで予測した数値が入っていますので、処理をして提出します。

```
#予測値の平均をとる
y_sub = sum(y_preds) / len(y_preds)
#数値を元に戻す
y_sub2 = np.expm1(y_sub)
submission = pd.DataFrame({'id': test['id'], 'Calories': y_sub2})
submission.to_csv('submission_calorie_12.csv', index = False )
```
## 提出した結果
提出した結果はpublic scoreで<strong>0.05787</strong>でした。バリデーションデータで予測した数値よりも良くなっていますが、private scoreでは<strong>0.05929</strong>でした。public scoreよりも少し悪化してますね。

## このコンペの感想
後でモデルにXGBoostを使ったもの提出しました。結果はpublic scoreは<strong>0.05690</strong>でpublic scoreで300位台になりましたが、private scoreは<strong>0.05921</strong>で順位も1000位台と落ちてしまいました。このコンペは0.01変わるだけで順位が大幅に動きますね。0.01を改善するのは非常に大変なコンペでした。

特徴量をさらに加えてみましたが、スコアは改善せず、特徴量を加えるのを止めました。やはり限界があるのでしょうか？

private scoreでトップ10の人のDiscussionを見ると、モデルを多数使ってアンサンブルをやっている人が多いですね。非常に勉強になりました。また別の「playground」のコンペもやってみたいと思います。