はじめに
初めてKaggleに取り組んだのでその内容をメモとして残します。
初心者向けのHouse Prices - Advanced Regression Techniquesに取り組みました。
初めてということもあり、一から自分で前処理をしてモデル構築するのは難しかったので以下のサイトを参考しながら見よう見まねでモデルを構築して提出まで行いました。
参考にさせていただいた記事
【Kaggle】「House Prices」をパクりながらやってみた。
結果はスコアが0.15202で順位は4205人中2489位(上位59%)でした。
参考記事ではモデルとしてLasso回帰を用いていました。
ただ真似るだけでは身につかないので、LightGBMで再度分析を行ってスコアを出してみました。その過程を書いていきます。
本記事の概要
House PricesのコンペでLightGBMを使ってモデル構築を行います。目次は以下のとおりです。
1. House Pricesの概要
2. データの理解
3. 前処理
4. モデル構築
5. データの予測
6. 結果
1. House Pricesの概要
House Pricesは初心者向けのコンペとして有名で、他の方が詳しく解説してくれているのでここでは詳細な説明は省きます。
一言で言うと、築年数や敷地面積など住宅のすべての特徴を表す79種類のデータを用いて各住宅の価格を予測するコンペです。
価格を予測するのでジャンルとしては回帰になります。
2. データの理解
データの詳細についてはKaggleのHouse PricesのDataにまとめられています。
データの読み込み
学習データとテストデータをデータフレームに格納します。
import pandas as pd
train_df = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/train.csv')
test_df = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/test.csv')
目的変数である住宅価格(SalePrice)の分布を確認してみます。
import seaborn as sns
sns.distplot(train_df['Saleprice'])
[出力結果]
住宅価格の平均は180000ドルぐらいでしょうか?
正確なデータを見るにはdescribe()
を用います。
print(train_df['SalePrice'].describe())
[出力結果]
count 1460.000000
mean 180921.195890
std 79442.502883
min 34900.000000
25% 129975.000000
50% 163000.000000
75% 214000.000000
max 755000.000000
平均値と最大値にかなり差があることがわかります。このようにかけ離れているデータもあるようなのであとで外れ値として処理する必要がありそうです。
3. 前処理
ここでは数値のカテゴリ変換、特徴量の生成、One-Hot-Encoding、外れ値の除去、対数変換を行います。
※モデルはLightGBMを使うため欠損値の処理は行いません。XGBoost同様に欠損値を対処するアルゴリズムが組み込まれているので欠損値があるデータもそのままモデルに学習させることができます。
効率よく前処理を行うために学習データとテストデータを結合させて同時に前処理ができるようにします。
all_df = pd.concat([train_df.drop(columns='SalePrice'),test_df])
テストデータには'SalePrice'カラムは含まれていないのでうまく結合させるためにtrain_dfから'SalePrice'のカラムを取り除いています。
数値のカテゴリ変換
データに数値が入っているものの、その数値の大小が予測に影響を与えないものが存在します。
例えばMSSubClassは売却対象となる住居のタイプを数値で示しています。数値の大小はここでは関係なく、タイプ別に分けるために数値を使用しています。
このように数値として意味をなさないデータは文字列に変換しておく必要があります。
num2str_list = ['MSSubClass','YrSold','MoSold']
for column in num2str_list:
all_df[column] = all_df[column].astype(str)
特徴量の生成
特徴量エンジニアリングとも呼ばれます。新たな特徴量を生成することでデータの質を向上させ、モデルの予測精度の向上に繋げることができます。
ただ、どのように特徴量の生成をすればいいかわからなかったので、一旦参考にさせていただいた記事のアイデアをそのまま採用しています。
# 特徴量エンジニアリングによりカラムを追加する関数
def add_new_columns(df):
# 建物内の総面積 = 1階の面積 + 2階の面積 + 地下の面積
df["TotalSF"] = df["1stFlrSF"] + df["2ndFlrSF"] + df["TotalBsmtSF"]
# 一部屋あたりの平均面積 = 建物の総面積 / 部屋数
df['AreaPerRoom'] = df['TotalSF']/df['TotRmsAbvGrd']
# 築年数 + 最新リフォーム年 : この値が大きいほど値段が高くなりそう
df['YearBuiltPlusRemod']=df['YearBuilt']+df['YearRemodAdd']
# お風呂の総面積
# Full bath : 浴槽、シャワー、洗面台、便器全てが備わったバスルーム
# Half bath : 洗面台、便器が備わった部屋)(シャワールームがある場合もある)
# シャワーがない場合を想定してHalf Bathには0.5の係数をつける
df['TotalBathrooms'] = (df['FullBath'] + (0.5 * df['HalfBath']) + df['BsmtFullBath'] + (0.5 * df['BsmtHalfBath']))
# 合計の屋根付きの玄関の総面積
# Porch : 屋根付きの玄関 日本風にいうと縁側
df['TotalPorchSF'] = (df['OpenPorchSF'] + df['3SsnPorch'] + df['EnclosedPorch'] + df['ScreenPorch'] + df['WoodDeckSF'])
# プールの有無
df['HasPool'] = df['PoolArea'].apply(lambda x: 1 if x > 0 else 0)
# 2階の有無
df['Has2ndFloor'] = df['2ndFlrSF'].apply(lambda x: 1 if x > 0 else 0)
# ガレージの有無
df['HasGarage'] = df['GarageArea'].apply(lambda x: 1 if x > 0 else 0)
# 地下室の有無
df['HasBsmt'] = df['TotalBsmtSF'].apply(lambda x: 1 if x > 0 else 0)
# 暖炉の有無
df['HasFireplace'] = df['Fireplaces'].apply(lambda x: 1 if x > 0 else 0)
# カラムを追加
add_new_columns(all_df)
カラム同士を足し合わせたり、割合を出したりして住宅の価格に影響がありそうな特徴量を作っていくというイメージを掴むことができました。
One-Hot-Encoding
例えばStreetカラムは道路の種類を表しています。このようなカテゴリーで表す変数のことをカテゴリ変数や質的変数と言います。質的変数は数値ではないのでそのまま機械学習のモデルに特徴量として使うことができません。
そこで質的変数をうまく数値で扱えるようにするための方法がOne-Hot-Encodingになります。
やり方は簡単で、pd.get_dummies(df)
を実行することで完了します。
all_df = pd.get_dummies(all_df)
all_df.head()
[実行結果]
数値のカテゴリ変換で質的変数に変換したところもうまく変換できています。
外れ値の除去
学習データにある飛び抜けたデータを除去します。
テストデータは削除できないので学習データとテストデータを結合したデータフレームを元のデータフレームに戻します。
train_df = pd.merge(all_df.iloc[train_df.index[0]:train_df.index[-1]],train_df['SalePrice'],left_index=True,right_index=True)
test_df = all_df.iloc[train_df.index[-1]:]
特に予測に大きな影響を与えそうな物件価格、築年数、敷地面積に絞ってデータの散らばりをみてみます。
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
sns.boxplot(train_df['SalePrice'])
plt.title('SalePrice')
plt.figure(figsize=(12, 6))
sns.boxplot(train_df['YearBuilt'])
plt.title('YearBuilt')
plt.figure(figsize=(12, 6))
sns.boxplot(train_df['LotArea'])
plt.title('LotArea')
以上の結果から物件価格が400000ドル以上、築年数が1920年以前、敷地面積が20000平方メートル以上のデータを外れ値として削除することにします。
train_df = train_df[(train_df['SalePrice']<400000) & (train_df['YearBuilt']>1920) & (train_df['LotArea']<20000)]
対数変換
多くの機械学習モデルは目的変数が正規分布に従っている場合に効果を発揮することが多いため、SalePriceのデータを対数変換します。
対数変換前のヒストグラムを一度確認しておきます。
sns.distplot(train_df['SalePrice'])
import numpy as np
train_df['SalePriceLog'] = np.log(train_df['SalePrice'])
sns.distplot(train_df['SalePriceLog'])
変換前と比べて少しですが正規分布の形に近づいていることがわかります。
4. モデル構築
学習データとテストデータを整理しておきます。
train_X = train_df.drop(columns = ['SalePrice','SalePriceLog'])
train_y = train_df['SalePriceLog']
test_X = test_df
ようやくここから学習データを用いてモデルを作成していきます。
参考にした記事ではLasso回帰を使っていましたが、今回はLightGBMを用いてみます。
ハイパーパラメータチューニング
決定木の深さのようにそれぞれの機械学習アルゴリズムにはハイパーパラメータがあります。高精度のモデルを作成するには最適なハイパーパラメータを見つける必要があります。そこで、ハイパーパラメータのチューニングを行なっていきます。
ここではGrid Searchを用います。Grid Searchはあらかじめ決めた値を総当たりで組み合わせて最も精度が高い組み合わせを探すやり方です。
記事を参考にしながらLightGBMのパラメータを見つける関数を作成しました。
import lightgbm as lgb
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import itertools
def lightgbm_tuning(train_x, train_y):
# パラメータの探索空間
param_grid = {
'learning_rate': [0.01, 0.05, 0.1],
'num_leaves': [31, 50, 100],
'boosting_type': ['gbdt'],
'objective': ['regression'],
'metric': ['rmse'],
'max_depth': [3, 4, 5]
}
# 全ての組み合わせのリストを作成
all_params = [dict(zip(param_grid.keys(), values)) for values in itertools.product(*param_grid.values())]
best_score = float('inf')
best_params = None
# 各パラメータセットでのモデルの訓練
for params in all_params:
# データの分割
X_train, X_test, y_train, y_test = train_test_split(train_x, train_y, test_size=0.3, random_state=0)
# モデルの訓練
train_data = lgb.Dataset(X_train, y_train)
model = lgb.train(params, train_data, num_boost_round=100)
# RMSEの計算
test_preds = model.predict(X_test)
test_rmse = np.sqrt(mean_squared_error(y_test, test_preds))
# ベストスコアの更新
if test_rmse < best_score:
best_score = test_rmse
best_params = params
print(f"Best parameters: {best_params}")
print(f"Best RMSE: {best_score:.4f}")
return best_params
best_alpha = lightgbm_tuning(train_X, train_y)
[出力結果]
Best parameters: {'learning_rate': 0.1, 'num_leaves': 31, 'boosting_type': 'gbdt', 'objective': 'regression', 'metric': 'rmse', 'max_depth': 4}
Best RMSE: 0.1031
モデル構築
Grid Searchの結果から最適なパラメータを設定して学習を行います。
train_data = lgb.Dataest(train_X, train_y)
model = lgb.train(best_alpha, train_data, num_boost_round=100)
これで学習は完了です。
5. データの予測
先ほど作成したモデルで住宅価格を予測していきます。
pred = best_model.predict(test_X)
3. 前処理の対数変換のところで、対数変換した値を目的変数としてモデルを作成したので、予測結果も対数変換されています。
そこで、予測結果を指数変換して元に戻す必要があるので戻します。
pred_exp = np.exp(pred)
これでほとんどの作業は完了しました。あとは提出するだけです。
# sample_submission.csvの読み込み
submission_df = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/sample_submission.csv')
submission_df['SalePrice'] = pred_exp
# submission.csvを出力
submission_df.to_csv('submission.csv',index=False)
出力されたcsvファイルを提出します。
結果
スコアは0.13748で順位は4216人中1319位(上位31%)でした。
Lassoモデルの時と比べて順位を上げることができました(スコアが0.15202で順位は4205人中2489位(上位59%))。
LightGBMは他のモデルと比べて高精度だということを何度か目にしていて気になって使ってみたのですが、本当に精度が上がってびっくりしました。
Lassoモデルは線形モデルであるのに対し、LightGBMは勾配ブースティングをベースとしたモデルで複雑なパターンにも対応できることや前処理の仕方が異なっているなど、さまざまな要因が考えられます。
まとめ
特徴量の生成や対数変換、Grid Searchなど難しい部分がたくさんありましたが、実際に取り組んでみることで機械学習の回帰分析の基本的な流れを掴むことができました。
また、自分でモデルを変えてみることでLightGBMの特性やチューニングのやり方などを主体的に学ぶことができました。
他にもさまざまなコンペに参加して、機械学習の実践に慣れていきたいと思います。