はじめに
近年、個人物件を貸し出す民泊サービスが流行している。民泊サービスであるAirbnbの掲載物件データを使用して、宿泊価格を予測するモデルを構築した。
実行環境
パソコン:Windows 10 Home
開発環境:Google Coraboratory
言語:Python
ライブラリ:Matplotlib、pandas、numpy、seaborn
分析の流れ
1.ライブラリのインポート
2.データの確認
3.データの前処理
4.モデル構築と学習
5.モデル評価
6.評価用データで予測、評価
7.ハイパーパラメータチューニング
8.課題
9.まとめ
1.ライブラリのインポート
# ライブラリをインポート
import numpy as np
import pandas as pd
import seaborn as sns
# matplotlibを使ってグラフを描く際に、フォントを日本語に対応させるためにjapanize-matplotlibというライブラリをインストール
!pip install japanize_matplotlib
# ライブラリをインポート
import japanize_matplotlib
2.データの確認
# pandasライブラリを使ってCSVファイルを読み込む
df = pd.read_csv('/content/train.csv')
# 先頭5行を表示
df.head()
# 日本語の列名にする
# 最初の5行を表示
feature_names_JPN = ['id','収容可能人数','アメニティ','風呂数','ベッドの種類','ベッドルーム数','ベッド数','キャンセルポリシー','都市','クリーニング料金を含むか','説明','最初のレビュー日','ホストの写真があるかどうか','ホストの身元確認が取れているか','ホストの返信率','ホストの登録日','即時予約可能か','最後のレビュー日','緯度','経度','物件名','近隣情報','レビュー数','物件の種類','レビュースコア','部屋の種類','サムネイル画像リンク','郵便番号','宿泊価格']
df.columns = feature_names_JPN
df.head()
# データフレームのサイズ(配列の各次元の大きさ)をタプルで取得
df.shape
55583行 29列 のデータフレームであることが確認できた。
# 欠損値やデータタイプの確認
df.info()
- 欠損値のあるデータフレームであることを確認
- データ型はint(整数)、float(小数)、object(文字列)があることを確認
# 各要素が欠損値かどうかを確認
df.isnull()
# 列ごとの欠損値の数を確認
df.isnull().sum()
# 欠損値がある列数の確認
print(f'欠損値がある列の数: {(df.isnull().sum() > 0).sum()}')
- 欠損値がある列が、13列あると確認
- 最初のレビュー日、ホストの返信率、最後のレビュー日、レビュースコアは全体の約5分の1の欠損値があることを確認
- 近隣情報、サムネイル画像リンクは全体の約10分の1の欠損値があることを確認
# 数値データの基本統計量を確認
df.describe()
-
収容可能人数
収容可能人数が2〜4人の物件が全体の50%を占めている。
最大収容人数が16人は、外れ値である可能性がある。
-
風呂数
ほとんどの物件の風呂数は1であることが分かる。
最大値は8であり、外れ値である可能性がある。
-
ベッドルーム数
ほとんどの物件のベッドルーム数は1であることが分かる。
最大値は10で、外れ値である可能性がある。
-
ベッド数
ベッド数が1〜2の物件が全体の50%を占めていることが分かる。
最大値は18で、外れ値である可能性がある。
-
緯度
データはやや高緯度側に偏っている可能性がある。
-
経度
データセットに含まれる宿泊施設は、分布に大きなばらつきがあることがわかる。
-
レビュー数
レビュー数が1〜23件の物件が全体の50%を占めていることが分かる。
最大値は605件で、外れ値である可能性がある。
-
レビュースコア
平均が約94.08点、中央値が96点であることから、全体的にレビュースコアは高い傾向にあることがわかる。
-
宿泊価格
標準偏差が168.09ドルと非常に大きいことから、宿泊価格の分布には非常に大きなばらつきがあることがわかる。
可視化したデータ
-
宿泊価格の分布は正規分布ではない。宿泊価格のヒストグラムは、低価格帯に偏っている。一部の宿泊施設は非常に高価格である。
-
宿泊価格に関して、ヒストグラムでは大きい値の分布を把握しづらいため、箱ひげ図を作成する。
# '宿泊価格' 列の 箱ひげ図 を作成
import matplotlib.pyplot as plt
plt.boxplot(df['宿泊価格'])
箱ひげ図より、外れ値があることが確認できたが、 数が多いことが分かる。とりあえず、このまま分析を進める。
3. データの前処理
目的変数の対数変換
回帰の場合、目的変数の値が正規分布に従っていることが重要だが、現状の目的変数の値(宿泊価格)は正規分布からは程遠いため対数変換をする。
# 宿泊価格Log列をdfに追加
df['宿泊価格Log'] = np.log(df['宿泊価格'])
# dfを確認。
df.head()
# '宿泊価格Log' 列の分布を可視化
sns.distplot(df['宿泊価格Log'])
宿泊価格を対数変換した結果、正規分布に近づいたことがわかる。
目的変数は宿泊価格Logとする。
数値データに焦点を当ててモデル構築を進めるため、文字列のデータを削除
# 文字列objectを削除し df_num という変数にいれる
df_num = df.drop(columns=['アメニティ', 'ベッドの種類', 'キャンセルポリシー', '都市', 'クリーニング料金を含むか','説明', '最初のレビュー日', 'ホストの写真があるかどうか', 'ホストの身元確認が取れているか', 'ホストの返信率', 'ホストの登録日','即時予約可能か','最後のレビュー日','物件名','近隣情報','物件の種類','部屋の種類','サムネイル画像リンク','郵便番号'])
# df_numを表示
df_num
# df_num データを確認し、すべての列が数値になっていることを確認
df_num.dtypes
前処理済データの保存
前処理済のデータをCSVファイルとして保管。
# データフレーム df_num の内容を 'train_md.csv' という名前のCSVファイルに保存。
df_num.to_csv('train_md.csv')
4. モデル構築と学習
説明変数Xと目的変数yに分割
# dfコピー作成
df = df_num.copy()
X = df.drop(columns=['宿泊価格','宿泊価格Log']).to_numpy()
y = df['宿泊価格Log'].to_numpy()
Xとyを訓練用データとテスト用データに分割
# 機械学習ライブラリscikit-learn(sklearn)からtrain_test_splitをインポート
from sklearn.model_selection import train_test_split
# X(特徴量データ)とy(ラベルデータ)を訓練用データ(X_train, y_train)とテスト用データ(X_test, y_test)に分割する。
# train.csv から 訓練用データ(X_train, y_train)とテスト用データ(X_test, y_test)を作成する。
# 30%をテストデータとする。同じシードを使用する。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
# 訓練用データおよびテスト用データの形状(shape)を表示。
print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)
LinearRegression (線形回帰モデル) 作成と学習
# 機械学習ライブラリscikit-learn(sklearn)からLinearRegressionクラスをインポート
# from sklearn.linear_model import LinearRegression
# インポートしたLinearRegressionクラスをインスタンス化して、modelという変数に代入
# model = LinearRegression()
# 線形回帰modelを訓練する処理を行う。訓練データX_trainとその対応するラベルy_trainを使って、モデルを学習させる。
# model.fit(X_train, y_train)
上述のコードを実行したが、学習させるデータに欠損値(NaN)が含まれており、線形回帰モデルは欠損値を含むデータを扱えないためエラーが発生した。
解決策としては、以下の2つが考えられる。
1.欠損値をそのまま扱える別のモデル(HistGradientBoostingClassifier、HistGradientBoostingRegressorなど)を使用する。
2.データを前処理する。例えば、欠損値を補完する(imputerを使用する)か、欠損値を含むサンプルを削除する。
今回は1の欠損値をそのまま扱える別のモデル(HistGradientBoostingRegressor)を使用することにする。
HistGradientBoostingRegressor(勾配ブースティングという手法を用いた回帰モデル)作成と学習
# 機械学習ライブラリであるscikit-learnから、HistGradientBoostingRegressor クラスをインポート
from sklearn.ensemble import HistGradientBoostingRegressor
# モデルの学習
model_HGBR = HistGradientBoostingRegressor(random_state=42)
model_HGBR.fit(X_train, y_train)
# 予測
y_pred_HGBR = model_HGBR.predict(X_test)
5.モデル評価
# インストール済みの scikit-learn ライブラリを最新バージョンにアップグレード
!pip install --upgrade scikit-learn
# sklearn.metricsモジュールから特定のmean_squared_error関数をインポート。
from sklearn.metrics import mean_squared_error,root_mean_squared_error
# y_test を元のスケールに戻す
y_test_original = np.exp(y_test)
# 予測値を元のスケールに戻す
y_pred_original = np.exp(y_pred_HGBR)
# RMSE を計算
rmse_original = np.sqrt(mean_squared_error(y_test_original, y_pred_original))
# RMSE を出力
print(f'Original scale RMSE: {rmse_original}')
# 学習データの予測値を計算
y_train_pred_HGBR = model_HGBR.predict(X_train)
# y_train を元のスケールに戻す
y_train_original = np.exp(y_train)
# 予測値を元のスケールに戻す
y_train_pred_original = np.exp(y_train_pred_HGBR)
# RMSE を計算
rmse_train_original = np.sqrt(mean_squared_error(y_train_original, y_train_pred_original))
# RMSE を出力
print(f'Original scale RMSE (train): {rmse_train_original}')
6.評価用データで予測、評価
# SIGNATEの評価用データを読み込む
df_test = pd.read_csv('/content/test.csv')
# デフォルトで先頭5行を表示
df_test.head()
# 日本語の列名にする
# 最初の5行を表示
feature_names_JPN_test = ['id','収容可能人数','アメニティ','風呂数','ベッドの種類','ベッドルーム数','ベッド数','キャンセルポリシー','都市','クリーニング料金を含むか','説明','最初のレビュー日','ホストの写真があるかどうか','ホストの身元確認が取れているか','ホストの返信率','ホストの登録日','即時予約可能か','最後のレビュー日','緯度','経度','物件名','近隣情報','レビュー数','物件の種類','レビュースコア','部屋の種類','サムネイル画像リンク','郵便番号']
df_test.columns = feature_names_JPN_test
df_test.head()
# 文字列objectを削除し df_num という変数にいれる
df_test_num = df_test.drop(columns=['アメニティ', 'ベッドの種類', 'キャンセルポリシー', '都市', 'クリーニング料金を含むか','説明', '最初のレビュー日', 'ホストの写真があるかどうか', 'ホストの身元確認が取れているか', 'ホストの返信率', 'ホストの登録日','即時予約可能か','最後のレビュー日','物件名','近隣情報','物件の種類','部屋の種類','サムネイル画像リンク','郵便番号'])
# df_numを表示
df_test_num
# df_test_num を NumPy 配列に変換し、X_test_test という変数に格納
X_test_test = df_test_num.to_numpy()
# 評価データで予測
pred = model_HGBR.predict(X_test_test)
# 予測値
pred
#予測値を元のスケールに戻す
pred_original = np.exp(pred)
pred_original
# 提出用ファイルを読み込み、sample という変数に格納
sample = pd.read_csv("sample_submit.csv", header=None)
# sampleの2列目に予測値predを代入
# 先頭 5 行を表示
sample[1] = pred_original
sample.head()
# 結果をcsvデータに保存
sample.to_csv("submit.csv",index=None,header=None)
SIGNATEにてスコアが2500人中600位となる。
7.ハイパーパラメータをチューニング
精度を上げるために、モデルのハイパーパラメーターのチューニングを試みた。
#モデルのハイパーパラメーターをチューニングするために、
#グリッドサーチ(GridSearchCV)をインポートする。
#(ここでCVは、Cross Validation(交差検証)を意味)
from sklearn.model_selection import GridSearchCV
model_HGBR2 = HistGradientBoostingRegressor(random_state=42)
#max_depthを以下の3つで試す
max_depth = [2, 8, 32]
params = {'max_depth':max_depth}
GridSearchCV()は、回帰ではデフォルトのスコアが決定係数Rなので、引数にscoring='neg_mean_squared_error'を追加してRMSEをスコアに指定する。
#グリッドサーチで,上記ハイパーパラメータ組み合わせの5通りを試す。
#入力したデータを更に5分割(cv=5)し,その内4つを訓練用データに,1つを評価用データにして、
#モデルを構築し,精度を出力することを5回繰り返す。
#5回のスコアの平均値を,モデルのスコアとする(層化5分割交差検証)。
grid_cv = GridSearchCV(estimator=model_HGBR2, param_grid=params, cv=5, scoring='neg_mean_squared_error')
grid_cv.fit(X_train, y_train)
#GridSearchCV によるハイパーパラメータチューニングの結果、最も性能が良かったモデルのインスタンスを返す。
grid_cv.best_estimator_
#GridSearchCV によるハイパーパラメータチューニングの結果、
#最も性能が良かったハイパーパラメータの組み合わせを辞書形式で返す。
grid_cv.best_params_
GridSearchの結果、ベストなmax_depth=8であることが分かった。
RMSEの計算
# grid_cv はすでに学習済みの GridSearchCV オブジェクトと仮定
best_model_2 = grid_cv.best_estimator_
# X_train で予測
y_pred_log_2 = best_model_2.predict(X_train)
# 予測値を元のスケールに戻す
y_pred = np.exp(y_pred_log_2)
# 元のスケールの y_train でRMSEを計算
rmse_2 = np.sqrt(mean_squared_error(np.exp(y_train), y_pred))
print(f'元のスケールでの train best RMSE スコア : {rmse_2}')
# 予測
# グリッドサーチの結果のモデルgrid_cvを使用する。
model_HGBR=grid_cv
y_pred_HGBR = model_HGBR.predict(X_test)
# テストデータの予測精度
from sklearn.metrics import mean_squared_error,root_mean_squared_error
rmse_hgbr = root_mean_squared_error(np.exp(y_test), np.exp(y_pred_HGBR))
print(f"RMSE: {rmse_hgbr}")
テストデータの予測精度はRMSEで116.85
# 学習データの予測精度
y_train_pred_HGBR = model_HGBR.predict(X_train)
rmse_hgbr_train = root_mean_squared_error(np.exp(y_train), np.exp(y_train_pred_HGBR))
print(f"RMSE: {rmse_hgbr_train}")
グリッドサーチの結果、ハイパーパラメータ―max_depth=8に調整した。
8.課題
(1)現状では数値データのみを使用しているため、物件名、説明、アメニティなどのテキストデータに含まれる情報を活用することで、予測精度を向上させる可能性がある。自然言語処理技術を用いた特徴量エンジニアリングが有効と考えられる。
(2)より広範なハイパーパラメータの探索や、より適切な評価指標を用いたチューニングによって、モデルの性能をさらに最適化できる可能性がある。
(3)宿泊価格など、一部の特徴量に外れ値が存在することが確認されている。外れ値の影響を軽減するための処理を検討することで、モデルの安定性と精度を向上させることが期待される。
9.まとめ
Airbnbのリスティングデータから宿泊価格を予測するために、HistGradientBoostingRegressorモデルを用いた回帰分析を行った。宿泊価格を対数変換することで正規分布に近づけ、モデルの精度向上に寄与した。数値データのみを用いたシンプルなモデルでも、一定の予測精度(テストデータのRMSE:約116)を達成できた。さらに、上述の課題について取り組むことが、モデルの予測性能をさらに向上させる上で重要であると言える。