はじめに
2023年5月1~5月31日まで開催されていた、SIGNATEのBeginnerCompeに参加したので自分が行ったアプローチを記録として残します。
今回の内容は携帯の価格価格帯予測でした。
- 関連リンク
- 筆者のTwitter
https://twitter.com/showsay0724
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()
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位でした。