LoginSignup
4
3
お題は不問!Qiita Engineer Festa 2023で記事投稿!

SIGNATE BeginnerCompe 携帯の価格帯予測

Last updated at Posted at 2023-06-26

はじめに

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

  • 関連リンク

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

提供されたデータは以下の3つのファイルです。

  • train.csv:学習用データ
  • test.csv :予測用データ
  • sample_submission.csv

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


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データを確認します。
データを大量に表示させたかったので以下処理を実行しました。

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まで表示するように設定しました。

データの形を確認します。

print(train.shape)
print(test.shape)

出力結果:

(1200, 22)
(800, 21)

trainデータ:1200行,22列
testデータ :800行,21列
であることがわかります。


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

train_df = train
test_df = test

カラムを確認します。

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')
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(非常に高コスト)

欠損値の有無を確認します。

# 欠損値の確認
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
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データ共に欠損値はありませんでした。


#モデルの構築

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を使用します。


重要な特徴量

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


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がモデルの予測結果に大きく影響を与えていることがわかります。

#グラフ表示
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を行いデータセットの特徴やパターンを観察し理解します。

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
を参照ください。


# ヒストグラムの作成
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以下を異常値とするかは検討の余地があると思います。

#データの結合
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に追加しています。

# 分割
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)

 かなり多くの特徴量が生成されたため閾値を定めて表示する。

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
スクリーンの高さと幅の特徴量を乗算し、面積の情報を持った特徴量

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を基準としました。実際には削除して検証しています。

#相関行列を確認
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
を参照してください。

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

モデルの検証

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を使用しました。

#ランダムサーチ
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)
# グリッドサーチ
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}
lg_scores = cross_validate(tuned_model,X,y,scoring ="accuracy",cv =5)
lg_scores["test_score"].mean()
0.5041666666666667

モデルの精度が上がりました。このモデルを使ってテストデータのprice_rangeに対して予測を行います。

lg_pred =lg.predict(test.drop(["id"],axis = 1))

提出

予測結果をsample_submisson.csvの形式に合わせてダウンロードして提出しました。

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位でした。

4
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3