#この記事について
Aidemyの卒業ブログとして記載しています。
覚えたての手法を実践することが目的のため、分析手法の妥当性、精度についてはいったん保留しています。
記事はSIGNATEの情報公開ポリシーに沿って公開しているつもりですが、問題があれば削除します。
#データについて
SIGNATEの【練習問題】民泊サービスの宿泊価格予測 のデータを利用しています。特徴量のデータ詳細は上記ページを参照ください。
欠損値あり、半分以上カテゴリデータとなってます。
どの特徴量を使うか、欠損値の埋め方、カテゴリデータの変換方法を考えないとですね。。
<class 'pandas.core.frame.DataFrame'>
Int64Index: 55583 entries, 0 to 55582
Data columns (total 28 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 accommodates 55583 non-null int64
1 amenities 55583 non-null object
2 bathrooms 55436 non-null float64
3 bed_type 55583 non-null object
4 bedrooms 55512 non-null float64
5 beds 55487 non-null float64
6 cancellation_policy 55583 non-null object
7 city 55583 non-null object
8 cleaning_fee 55583 non-null object
9 description 55583 non-null object
10 first_review 43675 non-null object
11 host_has_profile_pic 55435 non-null object
12 host_identity_verified 55435 non-null object
13 host_response_rate 41879 non-null object
14 host_since 55435 non-null object
15 instant_bookable 55583 non-null object
16 last_review 43703 non-null object
17 latitude 55583 non-null float64
18 longitude 55583 non-null float64
19 name 55583 non-null object
20 neighbourhood 50423 non-null object
21 number_of_reviews 55583 non-null int64
22 property_type 55583 non-null object
23 review_scores_rating 43027 non-null float64
24 room_type 55583 non-null object
25 thumbnail_url 49438 non-null object
26 zipcode 54867 non-null object
27 y 55583 non-null float64
dtypes: float64(7), int64(2), object(19)
memory usage: 12.3+ MB
#利用する特徴量の検討
数値データの傾向確認
まずは簡単に確認できる数値データの傾向を確認。
多重共線性が発生しそうな説明変数同士の強い相関や、問題になりそうな外れ値はありませんでした。(多分)
なお、正解ラベル("y")と相関の低い特徴量については、以下の対応方針を立てた。
・緯度/経度('latitude', 'longitude')については、2つを合成し、新たな特徴量を作成
・レビュー情報('number_of_reviews', 'review_scores_rating')についても、上記同様、合成により新たな特徴量を作成
temp = ['accommodates', 'bathrooms', 'bedrooms', 'beds', 'latitude',
'longitude', 'number_of_reviews', 'review_scores_rating', 'y']
# 統計量
df.describe()
# 相関散布図
sns.pairplot(df[temp])
# 相関行列
df[temp].corr()
# 外れ値確認
df[temp].boxplot()
カテゴリデータの傾向確認
カテゴリーデータの統計量より以下の対応方針を検討。
・ユニークデータ数の少ない 'host_identity_verified', 'room_type', 'cancellation_policy' をワンホットラベリング
・"amenities"には{}内に様々な情報が入っているため、分解して、意味ありげな要素で特徴量作成にチャレンジ
# カテゴリデータの統計量把握
df.describe(include="O")
# amenitiesデータのサンプル
{TV,"Wireless Internet",Kitchen,"Free parking on premises",Washer,Dryer,"Smoke detector"}
新たな特徴量の作成
緯度/経度('latitude', 'longitude')の合成
実のところ、ロケーションはカテゴリデータに"city"があり、6クラスに分けられるが、もっと細かく分けた方が精度が上がるのでは?と言う想定から、'latitude', 'longitude'を20分割するとどうなるかを確認。
20分割してもそれほど細かく分かれないようなので、このまま'latitude', 'longitude'を合成してみる。
# ヒストグラムで20分割時の傾向把握
df[["latitude","longitude"]].hist(bins=20)
# 20分割したデータでカラム作成
# 両データを合成しても判別できるよう、"latitude"はラベルを1~20とし、"longitude"は100~2000の100単位でラベルを作成
df["_lati"] = pd.cut(df["latitude"],20,labels=range(1,21))
df["_long"] = pd.cut(df["longitude"],20,labels=range(100,2001,100))
# クロス集計
pd.crosstab(df["_lati"], df["_long"])
特徴量の合成にはラベルをintに変換して加算。
後ほどワンホットラベリングのためにdtypeを"object"に変換しておく。
df["location"] = np.array([int(i) for i in df["_lati"].values]) + np.array([int(j) for j in df["_long"].values])
df = df.astype({'location': object})
レビュースコアの合成
レビュー数("number_of_reviews")はレビュースコアの信頼度と考え対数化。
対数化したレビュー数とレビュースコア("review_scores_rating")を乗算し特徴量を作成。
欠損値は0で埋める。
df["_rev_score"] = df["number_of_reviews"].map(lambda x: np.log(x)) * df["review_scores_rating"]
df["_rev_score"] = df["_rev_score"].fillna(0)
アメニティから特徴量抽出
"amenities"から要素1つづつ抜き出し、文字列となっている{}内からさらに要素を抽出し辞書化。
130個の要素を確認。
その中から一先ずは感覚で"Wireless Internet","Air conditioning","24-hour check-in","Pool"を選択。
import re
from collections import defaultdict
dic = defaultdict(int)
for _ in range(df["amenities"].shape[0]):
keys = re.findall(r'\{(.*)\}', df.loc[_,"amenities"])[0].split(",")
for key in keys:
dic[key] += 1
上記で選択した"Wireless Internet","Air conditioning","24-hour check-in","Pool"でカラムを作成し、ワンホットエンコーディング。
sele = ["\"Wireless Internet\"","\"Air conditioning\"","\"24-hour check-in\"","Pool"]
df["Wireless Internet"] = 0
df["Air conditioning"] = 0
df["24-hour check-in"] = 0
df["Pool"] = 0
row_num = 0
for _ in range(df["amenities"].shape[0]):
keys = re.findall(r'\{(.*)\}', df.loc[_,"amenities"])[0].split(",")
if "\"Wireless Internet\"" in keys:
df.loc[row_num, "Wireless Internet"] = 1
elif "\"Air conditioning\"" in keys:
df.loc[row_num, "Air conditioning"] = 1
elif "\"24-hour check-in\"" in keys:
df.loc[row_num, "24-hour check-in"] = 1
elif "Pool" in keys:
df.loc[row_num, "Pool"] = 1
row_num += 1
#欠損値の補完
特徴量として選択した数値データにいくつか欠損値があるため、補完方法を検討。
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 accommodates 55583 non-null int64
1 bathrooms 55436 non-null float64
2 bedrooms 55512 non-null float64
3 beds 55487 non-null float64
冒頭で確認した統計量、相関係数より、欠損値のある特徴量は"accommodates"と相関が強く出ているため、"accommodates"でグループ化し傾向確認。
temp = ['accommodates', 'bathrooms', 'bedrooms', 'beds']
# 平均値確認
df[temp].groupby("accommodates").mean()
# 中央値確認
df[temp].groupby("accommodates").median()
基本的には中央値で補完すれば問題なさそうだが、中央値が取れない要素が数個あるため、当該要素は"accommodates"の半分の値で補完。
pd.options.display.max_rows = 300
temp = ['accommodates', 'bathrooms', 'bedrooms', 'beds']
# 欠損値を抽出したデータフレームを作成
test = df.loc[df[temp].isnull().any(axis=1),temp]
# bathroomsの欠損箇所を補完
test['_bathrooms'] = test.groupby(["accommodates"])["bathrooms"].transform(lambda x: x.fillna(x.median()))
test.loc[test["_bathrooms"].isna(),"_bathrooms"] = test.loc[test["_bathrooms"].isna(), "accommodates"] / 2
# bedroomsの欠損箇所を補完
test['_bedrooms'] = test.groupby(["accommodates"])["bedrooms"].transform(lambda x: x.fillna(x.median()))
test.loc[test["_bedrooms"].isna(),"_bedrooms"] = test.loc[test["_bedrooms"].isna(), "accommodates"] / 2
# bedsの欠損箇所を補完
test['_beds'] = test.groupby(["accommodates"])["beds"].transform(lambda x: x.fillna(x.median()))
test.loc[test["_beds"].isna(), "_beds"] = test.loc[test["_beds"].isna(), "accommodates"]
# 上記で作成した補完値で元データを補完
df.loc[df["bathrooms"].isna() ,"bathrooms"] = test['_bathrooms']
df.loc[df["bedrooms"].isna() ,"bedrooms"] = test['_bedrooms']
df.loc[df["beds"].isna() ,"beds"] = test['_beds']
#回帰分析
分析用データ作成
利用する特徴量でトレーニングデータを作成し、カテゴリーデータをワンホットエンコーディング。
temp = ['accommodates', 'bathrooms', 'bedrooms', 'beds', '_rev_score',
'location', 'host_identity_verified', 'room_type', 'cancellation_policy','y',
"Wireless Internet", "Air conditioning", "24-hour check-in", "Pool"]
df_test = pd.get_dummies(df[temp])
分析
トレーニングデータをKFoldで分割し分析。
LinearRegressionのハイパーパラメーター調整はいったんなしで実行。
スコアはそれほど悪くなさそうです。
from sklearn.model_selection import KFold, train_test_split
from sklearn.linear_model import LinearRegression
r = list(df_test.columns)
r.remove("y")
X_train, X_test, y_train, ytest = train_test_split(df_test[r], df_test["y"], random_state=7)
cv = KFold(n_splits=10, random_state=42, shuffle=True)
acc_results = []
for trn_index, val_index in cv.split(X_train):
X_trn, X_val = X_train.iloc[trn_index], X_train.iloc[val_index]
y_trn, y_val = y_train.iloc[trn_index], y_train.iloc[val_index]
model = LinearRegression()
model.fit(X_trn, y_trn)
pred = model.predict(X_val)
acc = np.sqrt(sum((y_val - pred)**2)/len(y_val))
acc_results.append(acc)
np.mean(acc_results)
---結果---
129.30577249199695
傾きと切片の確認
頑張って作った特徴量は、レビュースコア("_rev_score")以外はそれなりに計算に影響があったようです。
print(pd.DataFrame(model.coef_, index=r))
print(model.intercept_)
---結果---
accommodates 19.018487
bathrooms 68.747517
bedrooms 36.672900
beds -11.329242
_rev_score -0.115965
Wireless Internet -15.173891
Air conditioning -2.918114
24-hour check-in -11.302865
Pool -52.899458
location_110 76.621340
location_201 -14.307127
location_202 -4.965153
location_203 -30.172658
location_204 -3.022386
location_1419 -27.533552
location_1420 -30.521696
location_1813 48.595823
location_1916 -39.833978
location_1917 14.953999
location_2020 10.185389
host_identity_verified_f -2.579610
host_identity_verified_t -8.606028
room_type_Entire home/apt 60.211179
room_type_Private room -14.672023
room_type_Shared room -45.539156
cancellation_policy_flexible -91.104716
cancellation_policy_moderate -102.609632
cancellation_policy_strict -93.470902
cancellation_policy_super_strict_30 -76.781232
cancellation_policy_super_strict_60 363.966482
84.5255962833438
推測
別に配布されてるtest.csvをpredictして提出してます。
結果は 170.8292272 でした。
トレーニングデータの分析結果とかなり乖離があるので、、もっと追い込めそうですね。。