SIGNATE BeginnerCompe 携帯の価格帯予測

# はじめに
2023年5月1~5月31日まで開催されていた、SIGNATEのBeginnerCompeに参加したので自分が行ったアプローチを記録として残します。
今回の内容は携帯の価格価格帯予測でした。

- 関連リンク

https://signate.jp/competitions/750

https://github.com/Nsho0724/signate_phone

- 筆者のTwitter
https://twitter.com/showsay0724

# 1.データの確認/ライブラリのインストール

提供されたデータは以下の３つのファイルです。
- train.csv:学習用データ
- test.csv :予測用データ 
- sample_submission.csv

まず、これらのデータを読み込み、必要なライブラリをインストールします。

``` python

from google.colab import drive
drive.mount('/content/drive')
import numpy as np
import pandas as pd
from pandas import DataFrame, Series
import matplotlib.pyplot as plt
import seaborn as sns
path = "/content/drive/My Drive/SIGNATE/compe01/"
train = pd.read_csv(path + "train.csv")
test = pd.read_csv(path + "test.csv")
sample_submission = pd.read_csv(path + "sample_submission.csv")

```
---
trainデータとtestデータを確認します。
データを大量に表示させたかったので以下処理を実行しました。
``` python
pd.set_option('display.max_columns', 100)
pd.set_option('display.max_row', 1000000)
```
`pd.set_option('display.max_columns',100)`でpandasの表示オプションを設定し、データフレームを表示する際に最大100列まで表示するようにします。
`pd.set_option('display.max_row',1000000)`で最大1,000,000まで表示するように設定しました。

データの形を確認します。
``` python
print(train.shape)
print(test.shape)
```
出力結果:
```
(1200, 22)
(800, 21)
```
trainデータ:1200行,22列
testデータ :800行,21列
であることがわかります。

---
データフレームをコピーしておきます。

```Python
train_df = train
test_df = test
```
---
カラムを確認します。

```python 
train_df.columns
```
出力結果
```
Index(['id', 'battery_power', 'blue', 'clock_speed', 'dual_sim', 'fc',
       'four_g', 'int_memory', 'm_dep', 'mobile_wt', 'n_cores', 'pc',
       'px_height', 'px_width', 'ram', 'sc_h', 'sc_w', 'talk_time', 'three_g',
       'touch_screen', 'wifi', 'price_range'],
      dtype='object')
```

```python
test_df.columns
```
出力結果
```
Index(['id', 'battery_power', 'blue', 'clock_speed', 'dual_sim', 'fc',
       'four_g', 'int_memory', 'm_dep', 'mobile_wt', 'n_cores', 'pc',
       'px_height', 'px_width', 'ram', 'sc_h', 'sc_w', 'talk_time', 'three_g',
       'touch_screen', 'wifi'],
      dtype='object')
```
---
特徴量をまとめる
| カラム | ヘッダ名称 | データ型 | 説明 |
| --- | --- | --- | --- |
| 0 | id | int | インデックスとして使用 |
| 1 | battery_power | int | バッテリーが一度に蓄えることができる総エネルギー（mAh） |
| 2 | blue | int | ブルートゥース有無（有：1） |
| 3 | clock_speed | float | クロックスピード |
| 4 | dual_sim | int | デュアルSIMサポート有無（有：1） |
| 5 | fc | int | フロントカメラのメガピクセル |
| 6 | four_g | int | 4G対応（対応有：1） |
| 7 | int_memory | int | 内部メモリ（GB） |
| 8 | m_dep | float | モバイルの深さ（cm） |
| 9 | mobile_wt | int | 重量 |
| 10 | n_cores | int | コア数 |
| 11 | pc | int | プライマリカメラのメガピクセル |
| 12 | px_height | int | ピクセル解像度の高さ |
| 13 | px_width | int | ピクセル解像度の幅 |
| 14 | ram | int | アクセスメモリ（MB） |
| 15 | sc_h | int | 携帯電話の画面の高さ（cm） |
| 16 | sc_w | int | 携帯電話の画面幅（cm） |
| 17 | talk_time | int | 連続通話時間 |
| 18 | three_g | int | 3G対応（対応有：1） |
| 19 | touch_screen | int | タッチスクリーン有無 |
| 20 | wifi | int | 無線LAN有無（有：1） |
| 21 | price_range | int | 価格帯 0（低コスト）、1（中コスト）、2（高コスト）、および3（非常に高コスト） |
---
欠損値の有無を確認します。
```python
#　欠損値の確認
train_df.isnull().sum()
```
```
id               0
battery_power    0
blue             0
clock_speed      0
dual_sim         0
fc               0
four_g           0
int_memory       0
m_dep            0
mobile_wt        0
n_cores          0
pc               0
px_height        0
px_width         0
ram              0
sc_h             0
sc_w             0
talk_time        0
three_g          0
touch_screen     0
wifi             0
price_range      0
dtype: int64
```
```python
test_df.isnull().sum()
```
```
id               0
battery_power    0
blue             0
clock_speed      0
dual_sim         0
fc               0
four_g           0
int_memory       0
m_dep            0
mobile_wt        0
n_cores          0
pc               0
px_height        0
px_width         0
ram              0
sc_h             0
sc_w             0
talk_time        0
three_g          0
touch_screen     0
wifi             0
dtype: int64
```
trainデータ、testデータ共に欠損値はありませんでした。

---
#モデルの構築
```python
from sklearn.model_selection import cross_validate, train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from lightgbm import LGBMClassifier
from sklearn.model_selection import  GridSearchCV 
from sklearn.svm import SVC
from xgboost import XGBClassifier

# 説明変数と目的変数に分割
X = train.drop(["id","price_range"], axis = 1)
y = train["price_range"]
# 標準化
scaler = StandardScaler()
X_sc = scaler.fit_transform(X)
test_sc = scaler.fit_transform(test)

# 予測
lg = LGBMClassifier()
lg.fit(X, y)
lg_scores = cross_validate(lg, X, y, scoring ="accuracy", cv =5)

rf = RandomForestClassifier()
rf.fit(X, y)
rf_scores = cross_validate(rf, X, y, scoring ="accuracy", cv =5)

svc = SVC()
svc.fit(X,y )
svc_scores = cross_validate(lg, X, y, scoring ="accuracy", cv =5)

xg = XGBClassifier()
xg.fit(X,y)
xg_scores = cross_validate(xg,X,y,scoring ="accuracy",cv =5)


def model_scores(model_scores):
    for key in model_scores.keys():
        print(key + ' test scores: ', model_scores[key])
    print("Average test score: ", model_scores["test_score"].mean())

scores =[lg_scores,rf_scores,svc_scores,xg_scores]
scores_ =["LGBMClassifier","RandomForestClassifier","SVC","XGBClassifier"]

for i in range(len(scores)):
  print(scores_[i])
  model_scores(scores[i])
  print("\n")
```
出力結果
```
LGBMClassifier
fit_time test scores:  [0.35226512 0.33734202 0.4041152  0.350945   0.33679342]
score_time test scores:  [0.00878692 0.00893569 0.00949788 0.0101068  0.01563263]
test_score test scores:  [0.44166667 0.46666667 0.475      0.48333333 0.47916667]
Average test score:  0.4691666666666666


RandomForestClassifier
fit_time test scores:  [0.31668067 0.32651067 0.31903839 0.31738639 0.31901526]
score_time test scores:  [0.01617026 0.01701117 0.01774025 0.01801443 0.01871347]
test_score test scores:  [0.48333333 0.4875     0.46666667 0.45833333 0.49583333]
Average test score:  0.47833333333333333


SVC
fit_time test scores:  [0.39972949 0.3379643  0.38660169 0.33896661 0.34569311]
score_time test scores:  [0.00876737 0.01443887 0.0090251  0.00851536 0.00867605]
test_score test scores:  [0.44166667 0.46666667 0.475      0.48333333 0.47916667]
Average test score:  0.4691666666666666


XGBClassifier
fit_time test scores:  [0.71933222 0.68610764 0.71109962 3.48943901 0.6938858 ]
score_time test scores:  [0.00646996 0.0076642  0.00648594 0.0064168  0.00651884]
test_score test scores:  [0.48333333 0.48333333 0.4875     0.4875     0.48333333]
Average test score:  0.485
```
XGBClassifierの精度がいいですが、検証時は学習時間を踏まえてLightGBMを使用します。

---

# 重要な特徴量
木系モデルはモデルに影響を与えている特徴量の重要度を確かめることができます。

```python

feature_importances = pd.Series(lg.feature_importances_,index=train.columns).sort_values(ascending=False)
feature_importances
```

出力結果
```
clock_speed      1207
m_dep            1137
mobile_wt        1025
int_memory        982
px_width          982
px_height         932
talk_time         788
ram               702
pc                684
sc_h              597
battery_power     582
sc_w              562
n_cores           517
fc                361
dual_sim          217
blue              192
touch_screen      180
four_g            148
wifi              141
three_g            64
dtype: int32
```
clock_speedがモデルの予測結果に大きく影響を与えていることがわかります。
```python
#グラフ表示
plt.figure(figsize=(10, 8))
sns.barplot(x=feature_importances.values, y=feature_importances.index, orient='h')
plt.xlabel('Feature Importance')
plt.ylabel('Features')
plt.title("Feature Importance Plot")
plt.show()
```
結果は
https://github.com/Nsho0724/signate_phone/blob/main/price_range_EDA_%E5%A4%96%E3%82%8C%E5%80%A4_ipynb_%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC.ipynb
を参照してください。

---
     

# EDA(Exploratory Data Analysis)
EDAを行いデータセットの特徴やパターンを観察し理解します。
```python
features = ['id', 'battery_power', 'blue', 'clock_speed', 'dual_sim', 'fc',
       'four_g', 'int_memory', 'm_dep', 'mobile_wt', 'n_cores', 'pc',
       'px_height', 'px_width', 'ram', 'sc_h', 'sc_w', 'talk_time', 'three_g',
       'touch_screen', 'wifi', 'price_range']

# 外れ値の確認
for feature in features:
  sns.histplot(data = train, x = feature, kde = True)
  plt.show()
  sns.boxplot(data = train, x = feature)
  plt.show()
```

各特徴量に対してヒストグラムと箱ひげ図が表示し、外れ値がないか確認します。一見、目立った外れ値は見当たりませんでした。
グラフを確認したい場合は
https://github.com/Nsho0724/signate_phone/blob/main/price_range.ipynb
を参照ください。

---
```python
# ヒストグラムの作成
train_df.hist(figsize=(12, 10))
plt.tight_layout()
plt.show()

# 相関行列の作成とヒートマップの表示
corr_matrix = train_df.corr()
plt.figure(figsize=(12, 10))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm')
plt.title('Correlation Matrix')
plt.show()

# カテゴリごとの価格帯の分布を可視化
categorical_columns = ['blue', 'dual_sim', 'four_g', 'three_g', 'touch_screen', 'wifi']
for column in categorical_columns:
    plt.figure(figsize=(8, 6))
    sns.countplot(data=train_df, x=column, hue='price_range')
    plt.title(f'Price Range Distribution by {column}')
    plt.show()

# 数値データと価格帯の関係を可視化
numerical_columns = ['battery_power', 'clock_speed', 'fc', 'int_memory', 'm_dep', 'mobile_wt',
                     'n_cores', 'pc', 'px_height', 'px_width', 'ram', 'sc_h', 'sc_w', 'talk_time']
for column in numerical_columns:
    plt.figure(figsize=(8, 6))
    sns.boxplot(data=train_df, x='price_range', y=column)
    plt.title(f'{column} vs Price Range')
    plt.show()
```
ヒストグラム、相関行列のヒートマップ、カテゴリごとの価格帯の分布、数値データと価格帯の関係を可視化しました。
グラフを確認したい場合は
https://github.com/Nsho0724/signate_phone/blob/main/price_range.ipynb
を参照してください。

ヒートマップより相関がある特徴良が少なく、機械学習モデルで精度の高い予測をするの難しい予感が(笑)

m_dep（端末の分厚さ）とsc_w(スクリーンの横幅)が実際にはありえない値のデータを見つけました。

# 特徴量を補完
EDAで異常値が見つかった特徴量を補完します。
具体的には、sc_wが1cm以下のデータを目的変数として、その他を説明変数ととしてLightGBMを使って予測しました。
sc_wが何cm以下を異常値とするかは検討の余地があると思います。

```python
#データの結合
df = pd.concat([train, test], axis=0)
import lightgbm as lgb
# トレーニングデータの準備
train_data = df[df['sc_w'] >= 1]  # sc_wが0でないデータをトレーニングデータとする

# 特徴量の選択
features = ['battery_power', 'blue', 'clock_speed', 'dual_sim', 'fc', 'four_g', 'int_memory', 'm_dep', 'mobile_wt', 'n_cores', 'pc', 'px_height', 'px_width', 'ram', 'sc_h', 'talk_time', 'three_g', 'touch_screen', 'wifi']

# データセットの分割
X_train = train_data[features]
y_train = train_data['sc_w']

# LightGBMモデルの定義とトレーニング
model = lgb.LGBMRegressor()
model.fit(X_train, y_train)

# テストデータの準備
test_data = df[df['sc_w'] == 0]  # sc_wが0のデータをテストデータとする
X_test = test_data[features]

# テストデータの予測
predicted_sc_w = model.predict(X_test)

# sc_wの更新
df.loc[df['sc_w'] == 0, 'sc_w'] = np.round(predicted_sc_w)  # 予測した値を元のデータに更新する

#データをもどず
train = df.iloc[:len(train), :]
test = df.iloc[len(train):, :]
```
無事sw_wの値を補完できました。

---

# 特徴量を作成
モデルの精度を上げるために新しい特徴量を作成します。特徴量同士を掛け合わせた特徴量と特徴量同士を足し合わせた特徴量を全ての通り作成し、その都度dfに追加してLightGBMで検証します。デフォルトのsocreを超えていた場合、そのscoreと特徴量をdictに追加しています。

```python
# 分割
X_train = train.drop(["price_range","id"], axis=1)
y_train = train["price_range"]

# デフォルト
lgb = LGBMClassifier()
lgb.fit(X_train,y_train)
lgb_scores = cross_validate(lgb,X_train,y_train,scoring ="accuracy",cv =5)

print("Average test score: ", lgb_scores["test_score"].mean())

# デフォルトのスコア
default_score = lgb_scores["test_score"].mean()

# # 掛け合わせた特徴量を探索
multiply_features = {}
for feat1, feat2 in combinations(X_train.columns, 2):
    multiply_feature = feat1 + "_" + feat2
    X_train[multiply_feature] = X_train[feat1]*X_train[feat2]

    lgb = LGBMClassifier()
    lgb.fit(X_train,y_train)
    lgb_scores = cross_validate(lgb,X_train,y_train,scoring ="accuracy",cv =5)
    score = lgb_scores["test_score"].mean()

    if score > default_score:
        multiply_features[multiply_feature] = score
    X_train = X_train.drop(multiply_feature, axis=1)

# 足し合わせた特徴量を探索
add_features = {}
for feat1, feat2 in combinations(X_train.columns, 2):
    add_feature = feat1 + "_" + feat2
    X_train[add_feature] = X_train[feat1] + X_train[feat2]

    lgb = LGBMClassifier()
    lgb.fit(X_train,y_train)
    lgb_scores = cross_validate(lgb,X_train,y_train,scoring ="accuracy",cv =5)
    score = lgb_scores["test_score"].mean()

    if score > default_score:
      add_features[add_feature] = score
    X_train = X_train.drop(add_feature, axis=1)

# print("Multiply Features:", multiply_features)
print("Add Features:", add_features)
```
　かなり多くの特徴量が生成されたため閾値を定めて表示する。
```python
for feat, score in multiply_features.items():
    if score >= 0.49:
        print(feat, "Score:", score)
for feat, score in add_features.items():
    if score >= 0.49:
        print(feat, "Score:", score)
```
表示された中から理屈的におかしくない、最もらしい特徴量を採用しました。
例：sc_h × sc_w
スクリーンの高さと幅の特徴量を乗算し、面積の情報を持った特徴量

```python
train["sc_hw"] = train["sc_h"] * train["sc_w"]
train["clock_speed_n_cores"] = train["clock_speed"] * train["n_cores"]
train["px_height_px_width"] = train["px_height"] * train["px_width"]
# メモリの容量比率（memory_ratio）
train['memory_ratio'] = train['int_memory'] / train['battery_power']

test["sc_hw"] = test["sc_h"] * test["sc_w"]
test["clock_speed_n_cores"] = test["clock_speed"] * test["n_cores"]
test["px_height_px_width"] = test["px_height"] * test["px_width"]
# メモリの容量比率（memory_ratio）
test['memory_ratio'] = test['int_memory'] / test['battery_power']
```
特徴量を組み合わせて新しい特徴量を作成したため、特徴量同士の相関が高くなりやすいです。多重共線性に配慮して、相関が高すぎる特量を削除します。今回は0.85を基準としました。実際には削除して検証しています。
```python
#相関行列を確認
correlation_matrix = train.corr()
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap="coolwarm")
plt.title("Correlation Matrix Heatmap")
plt.show()
```
出力結果は
https://github.com/Nsho0724/signate_phone/blob/main/price_range.ipynb
を参照してください。


```python
#多重共線性の可能性があるため、相関が高い特徴量を削除
train = train.drop(['sc_h', 'sc_w'], axis=1)
test = test.drop(['sc_h', 'sc_w'], axis=1)
```
# モデルの検証

```python
test = test.drop(["price_range"],axis = 1)
# 説明変数と目的変数に分割
X = train.drop(["id","price_range"],axis = 1)
y = train["price_range"]
# 標準化
scaler = StandardScaler()
X_sc = scaler.fit_transform(X)
test_sc = scaler.fit_transform(test)

lg = LGBMClassifier()
lg.fit(X,y)
lg_scores = cross_validate(lg,X,y,scoring ="accuracy",cv =5)


rf = RandomForestClassifier()
rf.fit(X,y)
rf_scores = cross_validate(rf,X,y,scoring ="accuracy",cv =5)

svc = SVC()
svc.fit(X,y)
svc_scores = cross_validate(lg,X,y,scoring ="accuracy",cv =5)


xg = XGBClassifier()
xg.fit(X,y)
xg_scores = cross_validate(xg,X,y,scoring ="accuracy",cv =5)

def model_scores(model_scores):
    for key in model_scores.keys():
        print(key + ' test scores: ', model_scores[key])
    print("Average test score: ", model_scores["test_score"].mean())


scores =[lg_scores,rf_scores,svc_scores,xg_scores]
scores_ =["LGBMClassifier","RandomForestClassifier","SVC","XGBClassifier"]


for i in range(len(scores)):
  print(scores_[i])
  model_scores(scores[i])
  print("\n")
```
出力結果
```
LGBMClassifier
fit_time test scores:  [2.06192613 0.4070487  0.40785122 0.41463399 0.40792966]
score_time test scores:  [0.00882125 0.00872922 0.01366091 0.00868773 0.00868535]
test_score test scores:  [0.4125     0.5        0.49166667 0.50833333 0.5125    ]
Average test score:  0.485


RandomForestClassifier
fit_time test scores:  [0.32291794 0.32343102 0.33063865 0.31998205 0.39031243]
score_time test scores:  [0.02374387 0.01734591 0.01885915 0.01794052 0.02371716]
test_score test scores:  [0.43333333 0.47916667 0.475      0.48333333 0.4875    ]
Average test score:  0.4716666666666667


SVC
fit_time test scores:  [0.42754698 0.43627    0.46224523 0.43469763 0.39923334]
score_time test scores:  [0.00889158 0.0114255  0.00884199 0.00959539 0.00874639]
test_score test scores:  [0.4125     0.5        0.49166667 0.50833333 0.5125    ]
Average test score:  0.485


XGBClassifier
fit_time test scores:  [0.86342263 3.61981678 0.81816506 0.83832884 0.83990264]
score_time test scores:  [0.00654149 0.00848222 0.00641561 0.00725198 0.00644708]
test_score test scores:  [0.44583333 0.49166667 0.47916667 0.4875     0.51666667]
Average test score:  0.4841666666666667
```
ベースラインモデルよりも精度が上がっています。

# パラメータチューニング
今回はランダムサーチとグリッドサーチを行いました。計算時間の問題から、最適なパラメータを見つけることを目標とせず、モデルの精度の少しでも改善することを目的としました。また、計算時間が比較的に早く精度のいいLightGBMを使用しました。

```python
#ランダムサーチ
import lightgbm as lgb
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint as sp_randint

# パラメータ探索範囲を設定
param_dist = {
    "num_leaves": sp_randint(20, 50),
    "n_estimators": sp_randint(100, 1000),
    "min_child_samples":sp_randint(10, 50)
}

# LightGBMの分類器モデルを作成
model = lgb.LGBMClassifier()

# ランダムサーチのインスタンスを作成
random_search = RandomizedSearchCV(
    estimator=model,
    param_distributions=param_dist,
    n_iter=20,
    scoring="accuracy",
    cv=5,
    random_state=42
)

# ランダムサーチを実行して最適なパラメータを探索
random_search.fit(X, y)

# 最適なパラメータを表示
print("Best parameters found: ", random_search.best_params_)

# 最適なパラメータでモデルを再構築
best_model = lgb.LGBMClassifier(**random_search.best_params_)

# トレーニングデータで学習させる
best_model.fit(X, y)
```
```python
# グリッドサーチ
import lightgbm as lgb
from sklearn.model_selection import GridSearchCV

# ベースラインモデルの作成
baseline_model = lgb.LGBMClassifier()

# チューニングするパラメータと範囲の設定
param_grid = {
    # 'learning_rate': [0.1, 0.05, 0.01],
    # 'n_estimators': [100, 200, 300],
    # 'max_depth': [3, 5, 7],
    # 'num_leaves': [20, 30, 40],
    # 'feature_fraction': [0.8, 0.9],
    # 'bagging_fraction': [0.8, 0.9],
    "num_leaves": [28,29,30,31,32,33,34,35],
    "min_child_samples":[15,16,17,18,19,20,21,22,23,24,25]
}


# グリッドサーチによるパラメーターチューニング
grid_search = GridSearchCV(estimator=baseline_model, param_grid=param_grid, scoring='accuracy', cv=5)
grid_search.fit(X, y)

# チューニング後のモデルの取得
tuned_model = grid_search.best_estimator_

# チューニング後のパラメータの表示
print("Best parameters found: ", grid_search.best_params_)

# テストデータの予測
# y_pred = tuned_model.predict(test)
```
出力結果
```
Best parameters found:  {'min_child_samples': 25, 'num_leaves': 28}
```

```python
lg_scores = cross_validate(tuned_model,X,y,scoring ="accuracy",cv =5)
lg_scores["test_score"].mean()
```
```
0.5041666666666667
```
モデルの精度が上がりました。このモデルを使ってテストデータのprice_rangeに対して予測を行います。
```python
lg_pred =lg.predict(test.drop(["id"],axis = 1))
```

# 提出
予測結果を`sample_submisson.csv`の形式に合わせてダウンロードして提出しました。
```python
submission = pd.DataFrame({
    "id":test["id"],
    "price_range":lg_pred
})
submission["price_range"] = submission["price_range"].astype(int)

submission.to_csv('submission.csv',header=False,index=False)

from google.colab import files
files.download('submission.csv')
```

提出結果は`0.4708876`で399人中79位でした。