【Kaggle】House PricesをLightGBMで分析

# はじめに
初めてKaggleに取り組んだのでその内容をメモとして残します。

初心者向けの[House Prices - Advanced Regression Techniques](https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques/overview)に取り組みました。

初めてということもあり、一から自分で前処理をしてモデル構築するのは難しかったので以下のサイトを参考しながら見よう見まねでモデルを構築して提出まで行いました。

**参考にさせていただいた記事**
[【Kaggle】「House Prices」をパクりながらやってみた。](https://qiita.com/muscle_nishimi/items/901ed94f3cdf1c8d893a)

結果はスコアが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](https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques/data)にまとめられています。

### データの読み込み
学習データとテストデータをデータフレームに格納します。
```python
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)の分布を確認してみます。
```python
import seaborn as sns

sns.distplot(train_df['Saleprice'])
```
[出力結果]
![スクリーンショット 2023-08-12 11.27.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/9e9cb276-ba24-3b2c-0dfd-5c8ae6b4d532.png)
住宅価格の平均は180000ドルぐらいでしょうか？
正確なデータを見るには`describe()`を用います。

```python
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同様に欠損値を対処するアルゴリズムが組み込まれているので欠損値があるデータもそのままモデルに学習させることができます。

効率よく前処理を行うために学習データとテストデータを結合させて同時に前処理ができるようにします。

```python
all_df = pd.concat([train_df.drop(columns='SalePrice'),test_df])
```
テストデータには'SalePrice'カラムは含まれていないのでうまく結合させるためにtrain_dfから'SalePrice'のカラムを取り除いています。

### 数値のカテゴリ変換
データに数値が入っているものの、その数値の大小が予測に影響を与えないものが存在します。
![スクリーンショット 2023-08-12 11.58.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/91ea9393-8eb3-bc95-2f56-525e15c2b134.png)
例えばMSSubClassは売却対象となる住居のタイプを数値で示しています。数値の大小はここでは関係なく、タイプ別に分けるために数値を使用しています。

このように数値として意味をなさないデータは文字列に変換しておく必要があります。

```python
num2str_list = ['MSSubClass','YrSold','MoSold']
for column in num2str_list:
    all_df[column] = all_df[column].astype(str)
```

### 特徴量の生成
特徴量エンジニアリングとも呼ばれます。新たな特徴量を生成することでデータの質を向上させ、モデルの予測精度の向上に繋げることができます。

ただ、どのように特徴量の生成をすればいいかわからなかったので、一旦[参考にさせていただいた記事](https://qiita.com/muscle_nishimi/items/901ed94f3cdf1c8d893a)のアイデアをそのまま採用しています。

```python
# 特徴量エンジニアリングによりカラムを追加する関数
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
![スクリーンショット 2023-08-12 11.58.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/91ea9393-8eb3-bc95-2f56-525e15c2b134.png)
例えばStreetカラムは道路の種類を表しています。このようなカテゴリーで表す変数のことをカテゴリ変数や質的変数と言います。質的変数は数値ではないのでそのまま機械学習のモデルに特徴量として使うことができません。

そこで質的変数をうまく数値で扱えるようにするための方法がOne-Hot-Encodingになります。

やり方は簡単で、`pd.get_dummies(df)`を実行することで完了します。

```python
all_df = pd.get_dummies(all_df)
all_df.head()
```
[実行結果]
![スクリーンショット 2023-08-12 12.49.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/cdcd429e-2f8d-af0b-0b9a-ed4958f82244.png)
**数値のカテゴリ変換**で質的変数に変換したところもうまく変換できています。

### 外れ値の除去
学習データにある飛び抜けたデータを除去します。
テストデータは削除できないので学習データとテストデータを結合したデータフレームを元のデータフレームに戻します。

```python
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]:]
```

特に予測に大きな影響を与えそうな物件価格、築年数、敷地面積に絞ってデータの散らばりをみてみます。

```python
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')
```
[出力結果]
![スクリーンショット 2023-08-12 12.57.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/cb20df41-f02e-8164-2f24-66ced62d8e62.png)
![スクリーンショット 2023-08-12 13.00.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/3c074a6e-46b7-ae85-72c3-966a0809aff3.png)
![スクリーンショット 2023-08-12 13.01.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/cb5034df-1d52-8ca7-8a4e-8ebcc457d4d4.png)

以上の結果から物件価格が400000ドル以上、築年数が1920年以前、敷地面積が20000平方メートル以上のデータを外れ値として削除することにします。
```python
train_df = train_df[(train_df['SalePrice']<400000) & (train_df['YearBuilt']>1920) & (train_df['LotArea']<20000)]
```
### 対数変換
多くの機械学習モデルは目的変数が正規分布に従っている場合に効果を発揮することが多いため、SalePriceのデータを対数変換します。

対数変換前のヒストグラムを一度確認しておきます。
```python
sns.distplot(train_df['SalePrice'])
```
[出力結果]
![スクリーンショット 2023-08-12 14.23.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/90401d42-e82f-26ed-171a-f4eae048d120.png)

```python
import numpy as np

train_df['SalePriceLog'] = np.log(train_df['SalePrice'])
sns.distplot(train_df['SalePriceLog'])
```
[出力結果]
![スクリーンショット 2023-08-12 14.25.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/700f31ee-1776-6450-271e-1137c068acc6.png)

変換前と比べて少しですが正規分布の形に近づいていることがわかります。
## 4. モデル構築
学習データとテストデータを整理しておきます。
```python
train_X = train_df.drop(columns = ['SalePrice','SalePriceLog'])
train_y = train_df['SalePriceLog']

test_X = test_df
```

ようやくここから学習データを用いてモデルを作成していきます。
参考にした記事ではLasso回帰を使っていましたが、今回はLightGBMを用いてみます。

### ハイパーパラメータチューニング
決定木の深さのようにそれぞれの機械学習アルゴリズムにはハイパーパラメータがあります。高精度のモデルを作成するには最適なハイパーパラメータを見つける必要があります。そこで、ハイパーパラメータのチューニングを行なっていきます。

ここではGrid Searchを用います。Grid Searchはあらかじめ決めた値を総当たりで組み合わせて最も精度が高い組み合わせを探すやり方です。

記事を参考にしながらLightGBMのパラメータを見つける関数を作成しました。

```python
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の結果から最適なパラメータを設定して学習を行います。
```python
train_data = lgb.Dataest(train_X, train_y)
model = lgb.train(best_alpha, train_data, num_boost_round=100)
```
これで学習は完了です。

## 5. データの予測
先ほど作成したモデルで住宅価格を予測していきます。
```python
pred = best_model.predict(test_X)
```
**3. 前処理**の対数変換のところで、対数変換した値を目的変数としてモデルを作成したので、予測結果も対数変換されています。

そこで、予測結果を指数変換して元に戻す必要があるので戻します。

```python
pred_exp = np.exp(pred)
```
これでほとんどの作業は完了しました。あとは提出するだけです。

```python
# 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の特性やチューニングのやり方などを主体的に学ぶことができました。

他にもさまざまなコンペに参加して、機械学習の実践に慣れていきたいと思います。