【SIGNATE初心者】【練習問題】民泊サービスの宿泊価格予測を実践

#はじめに

私が機械学習を勉強するにあたり、SIGANATEの「【練習問題】民泊サービスの宿泊価格予測」を実践しました。SIGNATEとは、日本版Kaggleと言われるとおり、コンペティションに参加したり、練習用データセットが提供されています。
ここではその実践における分析過程と結果について共有することを目的としています。

誤りがあればご意見いただきたい。。。
※同じ初心者の方と勉強できればいいなと思っております。

またSIGNATEでは実行環境であるカーネルが存在しないため、
以下では実行環境としてKaggleのNotebookを使用している。
参考サイト：https://www.codexa.net/kaggle-notebook-for-beginners/


## 課題
民泊サービスの物件データを使って、宿泊価格を予測するモデルを構築する。
近年、個人物件を貸し出す民泊サービスが流行っている。<br>民泊では物件オーナーが、部屋の広さや立地をもとに宿泊価格を決めていますが、妥当な料金設定を行うのは容易ではない。
そこで今回は、民泊サービスであるAirbnbの掲載物件データを使って、宿泊価格を予測するモデルの構築を実施する。
【練習問題】民泊サービスの宿泊価格予測 | https://signate.jp/competitions/266

##データ概要
課題種別：回帰
データ種別：多変量
学習データサンプル数：55583
説明変数の数：16
欠損値：有り

| カラム | ヘッダ名称 | データ型 | 説明 |
|--:|:--|:--|:--|
| 0 | id | int | インデックスとして使用 |
| 1 | accommodates | int | 収容可能人数 |
| 2 | amenities | char | アメニティ |
| 3 | bathrooms | float | 風呂数 |
| 4 | bed_type | char | ベッドの種類 |
| 5 | bedrooms | float | ベッドルーム数 |
| 6 | beds | float | ベッド数 |
| 7 | cancellation_policy | char | キャンセルポリシー |
| 8 | city | char | 都市 |
| 9 | cleaning_fee | int | クリーニング料金を含むか |
| 10 | description | char | 説明 |
| 11 | first_review | char | 最初のレビュー日 |
| 12 | host_has_profile_pic | int | ホストの写真があるかどうか |
| 13 | host_identity_verified | int | ホストの身元確認が取れているか |
| 14 | host_response_rate | char | ホストの返信率 |
| 15 | host_since | char | ホストの登録日 |
| 16 | instant_bookable | char | 即時予約可能か |
| 17 | last_review | char | 最後のレビュー日 |
| 18 | latitudes | float | 緯度 |
| 19 | longitude | float | 経度 |
| 20 | name | char | 物件名 |
| 21 | neighbourhood | char | 近隣情報 |
| 22 | number_of_reviews | int | レビュー数 |
| 23 | property_type | char | 物件の種類 |
| 24 | review_scores_rating | float | レビュースコア |
| 25 | room_type | char | 部屋の種類 |
| 26 | thumbnail_url | char | サムネイル画像リンク |
| 27 | zipcode | int | 郵便番号 |
| 28 | y | float | 宿泊価格 |

##データの登録
1. Kaggle上にはすでにデータセットが登録されているため、本来はデータを自分で登録することは不要ですが必要に応じて学習用データ(train.csv)、評価用データ(test.csv)のダウンロード・登録を行った。<br>※ここでは公開されているものではなく自分で実施する手順を実施。
![データセット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/952898/acf0fe8d-c0c8-34c7-32aa-21d55d354bae.png)
2. Kaggleのノートブックにアップロードを実施する。
![データセット登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/952898/edf7226d-460a-9e1e-ecad-d83ede31ffc5.png)
※すでにPublicで公開されているデータと同じであるため、SKipDuplicate⇒IncludeDuplicatesにする必要あり。
![データセット登録2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/952898/81ef4723-03a6-5f77-95df-d96ca2344b08.png)

##データの読込
以下で訓練用データとテストデータの読み込みを実施する。

```python:
# データの読み込み
df = pd.read_csv("/kaggle/input/houseprice/train.csv")
# ターゲットデータの読み込み
df_target = pd.read_csv("/kaggle/input/houseprice/test.csv")
```

##データの確認
以下で訓練用データとテストデータの読み込みを実施する。

```python:
# データの表示
df.head()
# ターゲットデータの表示
df_target.head()
# データの中身の確認
df.info()
# ターゲットデータの中身の確認
df_target.info()
```
##分析に使用するデータの選定
表示されたデータから以下のように宿泊価格との関係性を予測した。
分析対象として影響が軽微のものや、言語データであるものはなしとした。
下記予測から分析対象の列は１５となった。

| ヘッダ名称 | 説明 |宿泊価格との関係性予測|備考|
|--:|:--|:--|:--|
|id|インデックスとして使用|なし|
|accommodates|収容可能人数|あり|
|amenities|アメニティ|あり|処理が複雑化するため対象外|
|bathrooms|風呂数|あり|
|bed_type|ベッドの種類|あり|数にする必要あり|
|bedrooms|ベッドルーム数|あり|
|beds|ベッド数|あり|
|cancellation_policy|キャンセルポリシー|あり|数にする必要あり|
|city|都市|あり|数にする必要あり|
|cleaning_fee|クリーニング料金を含むか|あり|数にする必要あり|
|description|説明|なし|言語データのため分析対象外|
|first_review|最初のレビュー日|なし|
|host_has_profile_pic|ホストの写真があるかどうか|なし|
|host_identity_verified|ホストの身元確認が取れているか|あり|
|host_response_rate|ホストの返信率|なし|
|host_since|ホストの登録日|なし|
|instant_bookable|即時予約可能か|あり|数にする必要あり|
|last_review|最後のレビュー日|なし|
|latitude| 緯度 |なし|
|longitude|経度|なし|
|name|物件名|なし|
|neighbourhood|近隣情報|あり|数にする必要あり|
|number_of_reviews|レビュー数|あり|
|property_type|物件の種類|あり|数にする必要あり|
|review_scores_rating|レビュースコア|あり|
|room_type|部屋の種類|あり|
|thumbnail_url|サムネイル画像リンク|あり|
|zipcode|郵便番号|なし|

##分析対象外列データの削除
上記で分析対象外と予測したデータをデータから削除する。

```python:
#不要なデータをドロップ
drop_id = ["id","amenities","description","first_review","host_has_profile_pic","host_response_rate","host_since","last_review","latitude","longitude","name","zipcode"]
df = df.drop(drop_id, axis=1)
df.info()
```

##分析対象列の加工(欠損値補完)
行数が55583であるのに対し、欠損が存在するカラムは以下である。
- bathrooms(欠損数:147)
- bedrooms(欠損数:71)
- beds(欠損数:96)
- host_identity_verified(欠損数:1)
- neighbourhood(欠損数:5,160)
- review_scores_rating(欠損数:12,556)
- thumbnail_url(欠損数:6,145)

今回は以下の対策を実施した。
①欠損数が少ない場合は行を削除する

- bathrooms
- bedrooms
- beds
- host_identity_verified

```python:
# 欠損値を含むデータを削除
df = df.dropna(subset=['bathrooms',"bedrooms","beds","host_identity_verified"])
```

②欠損数が多く補完が難しいため分析対象外とする。

- neighbourhood

```python:
# 不要なデータをドロップ
df = df.drop("neighbourhood", axis=1)
```
③別の値で補完する。

- review_scores_rating

```python:
# レビュースコアの確認
print(df["review_scores_rating"].describe())
print(df["review_scores_rating"].unique())
print(df["review_scores_rating"].value_counts())

# 中央値による補完
df["review_scores_rating"] = df["review_scores_rating"].fillna(df["review_scores_rating"].median())
```

④分析対象の加工にて変換処理の実施対象とする。(ここでは対象外)

- thumbnail_url

##分析対象列の加工(変換)
①True,Falseのデータを1,0に変更することで数値データとする。
対象列は「cleaning_fee」、「instant_bookable」、「host_identity_verified」の3列である。
以下の関数を適用することでデータの加工処理を実施する。

```python:
# 変換関数の定義
def checkTF(x):
    if x == "t":
        return 1
    elif x == "f":
        return 0

# 対象カラムに関数を適応して変換実施
df["cleaning_fee"] = df["cleaning_fee"].apply(checkTF)
df["host_identity_verified"] = df["host_identity_verified"].apply(checkTF)
df["instant_bookable"] = df["instant_bookable"].apply(checkTF)
```

②サムネイル画像リンク(thumbnail_url)の有無を0と1に変換する。

```python:
# 変換関数の定義
def checkNull(x):
    if x != "":
        return 1
    else:
        return 0

# 対象カラムに関数を適応して変換実施
# 対象カラムに関数を適応して変換実施
df["thumbnail_url"] = df["thumbnail_url"].apply(checkNull)
```

③質的データをダミー変数に変換
以下の項目は質的データであるため変換処理を実施
'bed_type','cancellation_policy','city','property_type','room_type'

| ヘッダ名称 | 説明 |
|--:|:--|:--|
|bed_type|ベッドの種類|
|cancellation_policy|キャンセルポリシー|
|city|都市|
|property_type|物件の種類|
|room_type|部屋の種類|

```python:
# 質的データをダミー変数化
select_columns = ['bed_type','cancellation_policy','city','property_type','room_type']
dummy_df = pd.get_dummies(df[select_columns],drop_first=True)

# 元データとダミー変数化したものを結合
df =  pd.concat([df, dummy_df], axis=1)

#不要なデータをドロップ
drop_id2 = ['bed_type','cancellation_policy','city','property_type','room_type']
df = df.drop(drop_id2, axis=1)
```

##モデルと評価
今回は線形回帰モデルを使用して分析を実施した。
参考：https://qiita.com/0NE_shoT_/items/08376b08783cd554b02e

また評価については、RMSEを使用した。
参考:https://mathwords.net/rmsemae

①分析データの分割と学習を実施。

```python:
# データを訓練用とテスト用に分割するモジュールのインポート
from sklearn.model_selection import train_test_split
# LinearRegression(線形回帰)モデルモジュールのインポート
from sklearn.linear_model import LinearRegression

#目的変数の退避
y = df["y"]
df = df.drop("y", axis=1)
# データの分割を実施
X_train,X_test,y_train,y_test = train_test_split(df, y, test_size=0.3,random_state = 1000)
# モデルインスタンス準備
lr = LinearRegression()
# モデルを学習
lr.fit(X_train, y_train)
# X_trainに対して予測を実施
y_pred_train = lr.predict(X_train)

```
<br>



②テストデータに対する予測と評価を実施

```python:
# X_testに対する予測値を算出
y_pred_test = lr.predict(X_test)

# RMSEを求め、rmse_trainに代入してください。
mse_test = MSE(y_pred_test,y_test)
rmse_test = np.sqrt(mse_test)

# RMSEの表示
print(rmse_test)

```

```python:
125.88739952642386
```

##予測データに対する前処理(欠損値の補完)
予測データに対しても欠損値に対して、前処理を実施する必要がある。
以下の手順で前処理を実施した。

①予測不要データの削除と、欠損値の確認

```python:
#不要なデータをドロップ
drop_id = ["id","amenities","description","first_review","host_has_profile_pic","host_response_rate","host_since","last_review","latitude","longitude","name","zipcode","neighbourhood"]
df_target = df_target.drop(drop_id, axis=1)
df_target.info()
```



実行結果

```python:
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   accommodates            18528 non-null  int64  
 1   bathrooms               18475 non-null  float64
 2   bed_type                18528 non-null  object 
 3   bedrooms                18508 non-null  float64
 4   beds                    18493 non-null  float64
 5   cancellation_policy     18528 non-null  object 
 6   city                    18528 non-null  object 
 7   cleaning_fee            18528 non-null  object 
 8   host_identity_verified  18488 non-null  object 
 9   instant_bookable        18528 non-null  object 
 10  number_of_reviews       18528 non-null  int64  
 11  property_type           18528 non-null  object 
 12  review_scores_rating    14362 non-null  float64
 13  room_type               18528 non-null  object 
 14  thumbnail_url           16457 non-null  object 
```

上記の結果から以下カラムに対して欠損値の確認ができる。
それぞれの欠損値に対して、対策を実施した。

- bathrooms(欠損数：53)
- bedrooms(欠損数：20)
- beds(欠損数：35)
- host_identity_verified(欠損数：40)
- review_scores_rating(欠損数：4166)
- thumbnail_url(欠損数：2071)

①欠損数が少ないものに対しては最頻値で保管する処理を実施

- bathrooms(欠損数：53)
- bedrooms(欠損数：20)
- beds(欠損数：35)

```python:
# 各値の確認
print(df_target["bathrooms"].value_counts())
print(df_target["bedrooms"].value_counts())
print(df_target["beds"].value_counts())

#中央値による補完
df_target["bathrooms"] = df_target["bathrooms"].fillna(df_target["bathrooms"].mode())
df_target["bedrooms"] = df_target["bedrooms"].fillna(df_target["bedrooms"].mode())
df_target["beds"] = df_target["beds"].fillna(df_target["beds"].mode())
```

②host_identity_verifiedはTrue,Falseがランダムである可能性が高いため、直前の値で埋める方式を採用

```python:
# 直前の値を使って埋めていく
print(df_target["host_identity_verified"].value_counts())
df_target["host_identity_verified"] = df_target["host_identity_verified"].fillna(method='ffill') 
print(df_target["host_identity_verified"].value_counts())
```

実行結果からおおむね均等に欠損値を埋めることができたことを確認した。

```python:
t    12484
f     6004
Name: host_identity_verified, dtype: int64
t    12509
f     6019
Name: host_identity_verified, dtype: int64
```

③review_scores_ratingは訓練データと同様に中央値で埋める処理を実施

```python:
#中央値による補完
df_target["review_scores_rating"] = df_target["review_scores_rating"].fillna(df_target["review_scores_rating"].median())
```

④分析対象の加工にて変換処理の実施対象とする。(ここでは対象外)

##予測データに対する前処理(変換)
①True,Falseのデータを1,0に変更することで数値データとする。
対象列は「cleaning_fee」、「instant_bookable」、「host_identity_verified」の3列である。

```python:
# 対象カラムに関数を適応して変換実施
df_target["cleaning_fee"] = df_target["cleaning_fee"].apply(checkTF)
df_target["host_identity_verified"] = df_target["host_identity_verified"].apply(checkTF)
df_target["instant_bookable"] = df_target["instant_bookable"].apply(checkTF)
```

②サムネイル画像リンク(thumbnail_url)の有無を0と1に変換する。

```python:
# 対象カラムに関数を適応して変換実施
df_target["thumbnail_url"] = df_target["thumbnail_url"].apply(checkNull)
```

③質的データをダミー変数に変換
以下の項目は質的データであるため変換処理を実施
'bed_type','cancellation_policy','city','property_type','room_type'

| ヘッダ名称 | 説明 |
|--:|:--|:--|
|bed_type|ベッドの種類|
|cancellation_policy|キャンセルポリシー|
|city|都市|
|property_type|物件の種類|
|room_type|部屋の種類|

```python:
# 質的データをダミー変数化
dummy_df_target = pd.get_dummies(df_target[select_columns],drop_first=True)

# 元データとダミー変数化したものを結合
df_target =  pd.concat([df_target, dummy_df_target], axis=1)

#不要なデータをドロップ
df_target = df_target.drop(drop_id2, axis=1)
```

##最終データの確認実施
以下を実施して訓練データとの齟齬がないかを確認した。

```python:
print(df.info())
print(df_target.info())
```

実施結果から大きく差分が発生したため、この差分を埋めるために以下の処理を実施した。
①df_targetのみに存在する情報の削除

```python:
#不要なデータをドロップ
df_target = df_target.drop("property_type_Lighthouse", axis=1)
df_target.info()
```

①dfのみに存在する情報の追加

```python:
df_target.insert(29, "property_type_Casa particular",0)
df_target.insert(35, "property_type_Earth House",0)
df_target.insert(42, "property_type_Island",0)
df_target.insert(48, "property_type_Tipi",0)
df_target.insert(50, "property_type_Train",0)
df_target.insert(51, "property_type_Treehouse",0)
df_target.insert(52, "property_type_Vacation home",0)
```
##予測とファイルの出力

```python:
# モデルインスタンス準備
lr_result = LinearRegression()
# モデルを学習
lr_result.fit(df, y)
# 目的変数yの予測を実施
y_predict = lr_result.predict(df_target)
y_predict = pd.DataFrame(y_predict)
# ターゲットデータの読み込み
df_tmp = pd.read_csv("/kaggle/input/houseprice/test.csv")
df_id = df_tmp["id"]
# 特徴量を抽出(idのみ)
df_final = pd.concat([df_id,y_predict],axis=1)
# 並び替えを行う
df_final.to_csv('/kaggle/working/submit.csv', header=False, index=False)
```

##提出ファイルの評価
上記で出力したファイルをSIGNATEに提出した。
170.4177160で133位であった。
この結果に対し他の手法で改善が見込めるかを検討することを今後の課題とする。