機械学習でAirbnbのデータを分析してみた

はじめに

今回はAirbnbのデータをPython・機械学習を用いて分析してみようと思います。元ネタはMediumにあった英語の記事となります。

データ取得先

Inside Airbnbというサイトにデータセットがありました。こちら、Airbnbが管理しているサイトではなく、ある個人の方が独自にAirbnbからデータを集めてアップしているとのことです。

本来なら一番イメージがつきやすい日本、東京のデータを使用すべきかと思います。しかし、このサイトに日本のデータはありません。なので、今回はベタにニューヨークのデータで分析してみることとします。ちなみに元ネタの方はカナダ・トロント在住のようなのでトロントの分析をしています。

使用するデータ

・listings.csv.gz(2018/12/6収集分)
物件や、その物件を保有しているホストの情報が記載されている 
・calendar.csv.gz(2018/12/6収集分)
登録されているリスティングが予約可能か否か、価格がいくらかの情報が記載されている。対象は1年分(2018/12/06〜2019/12/05)

学べること

・カレンダーからどの月の価格が高いか、またはどの週の価格が高いを知ることができる
・登録されている物件の詳細、特徴量の相関を知ることができる
・物件の価格にもっとも影響を与える特徴量を機械学習にて予測し、その特徴量が何かを知ることができる

Calender

では、まずはCalenderの方から見ていきましょう。

calendar = pd.read_csv('calendar.csv.gz')
print('We have', calendar.date.nunique(), 'days and', calendar.listing_id.nunique(), 'unique listings in the calendar data.')

1年分の365日、49056件のリスティングが登録されています。

2018年12/06現在の予約受付可否

calendar.available.value_counts()

fはFalse(不可)、tはTrue(可能)を表します。よって、約2/3はすでに予約ができない状態になっていることがわかります。

日毎平均予約受付率

calendar_new = calendar[['date', 'available']]
calendar_new['busy'] = calendar_new.available.map( lambda x: 0 if x == 't' else 1)
calendar_new = calendar_new.groupby('date')['busy'].mean().reset_index()
calendar_new['date'] = pd.to_datetime(calendar_new['date'])
plt.figure(figsize=(10, 5))
plt.plot(calendar_new['date'], calendar_new['busy'])
plt.title('Airbnb Toronto Calendar')
plt.ylabel('% busy')
plt.show();

・直近はやはり予約が埋まっている率が高くなっている
・3月頃が一番予約が可能。それでも半分以上は予約を受け付けていない
・6月以降は予約を受け付けていない率が高い。これはホストも先の予定が判明していないため、現状予約ができない状態になっているのではないかと推測される

月毎の平均価格

calendar['date'] = pd.to_datetime(calendar['date'])
calendar['price'] = calendar['price'].str.replace(',', '')
calendar['price'] = calendar['price'].str.replace('$', '')
calendar['price'] = calendar['price'].astype(float)
calendar['date'] = pd.to_datetime(calendar['date'])
mean_of_month = calendar.groupby(calendar['date'].dt.strftime('%B'),
                                 sort=False)['price'].mean()
mean_of_month.plot(kind = 'barh' , figsize = (12,7))
plt.xlabel('average monthly price');

・1月、2月の価格が安い。冬は安い傾向にあるよう。
・その反面、夏が高くなる

週毎の平均価格

calendar['dayofweek'] = calendar.date.dt.weekday_name
cats = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
price_week=calendar[['dayofweek','price']]
price_week = calendar.groupby(['dayofweek']).mean().reindex(cats)
price_week.drop('listing_id', axis=1, inplace=True)
price_week.plot()
ticks = list(range(0, 7, 1)) # points on the x axis where you want the label to appear
labels = "Mon Tues Weds Thurs Fri Sat Sun".split()
plt.xticks(ticks, labels);

・想像通り、金曜日と土曜日が高くなる
・他はそれほど金額に差がない

Listings

ここからは物件の情報を見ていきましょう。

listings = pd.read_csv('/content/drive/Airbnb/listings.csv.gz')
print('We have', listings.id.nunique(), 'listings in the listing data.')

ニューヨークには49056件のリスティングが登録されているようです。

地区毎の総物件数

listings.groupby(by='neighbourhood_cleansed').count()[['id']].sort_values(by='id', ascending=False).head(10)

ニューヨークの物件はそれほど地区によって差がないようです。元ネタのトロントと比べてもらうと差が明らかです。

外れ値を除外し、価格の分布を表示

価格が"0"と"600"より大きいレコードを外れ値とし除外します。除外済みのデータを分布を表示します。

listings.loc[(listings.price <= 600) & (listings.price > 0)].price.hist(bins=200)
plt.ylabel('Count')
plt.xlabel('Listing price in $')
plt.title('Histogram of listing prices');

リスティングが100件以上ある地区を対象として、箱ひげ図を表示します。

select_neighbourhood_over_100 = listings.loc[(listings.price <= 600) & (listings.price > 0)]\
                    .groupby('neighbourhood_cleansed')\
                    .filter(lambda x: len(x)>=100)\
                    ["neighbourhood_cleansed"].values

listings_neighbourhood_over_100 = listings.loc[listings["neighbourhood_cleansed"].map(lambda x: x in select_neighbourhood_over_100)]

sort_price = listings_neighbourhood_over_100.loc[(listings_neighbourhood_over_100.price <= 600) & (listings_neighbourhood_over_100.price > 0)]\
                    .groupby('neighbourhood_cleansed')['price']\
                    .median()\
                    .sort_values(ascending=False)\
                    .index

sns.boxplot(y='price', x='neighbourhood_cleansed', data=listings_neighbourhood_over_100.loc[(listings_neighbourhood_over_100.price <= 600) & (listings_neighbourhood_over_100.price > 0)], 
            order=sort_price)
ax = plt.gca()
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right')
plt.show();

Tribecaという地区の中央値が一番高くなっています。ちなみにWikipediaによると"今日、トライベッカはアメリカで最もファッション性が高く、憧れられる地区の1つであり、セレブが住んでいることで知られる。トライベッカはニューヨーク市内の最も安全な地区になっている"とのことです。おしゃれで安全な場所には高い値がつくということでしょうか。

物件のタイプと価格との関係

select_property_over_100 = listings.loc[(listings.price <= 600) & (listings.price > 0)]\
                    .groupby('property_type')\
                    .filter(lambda x: len(x)>=20)\
                    ["property_type"].values

listings_property_over_100 = listings.loc[listings["property_type"].map(lambda x: x in select_property_over_100)]

sort_price = listings_property_over_100.loc[(listings_property_over_100.price <= 600) & (listings_property_over_100.price > 0)]\
                    .groupby('property_type')['price']\
                    .median()\
                    .sort_values(ascending=False)\
                    .index
sns.boxplot(y='price', x='property_type', data=listings_property_over_100.loc[(listings_property_over_100.price <= 600) & (listings_property_over_100.price > 0)], order=sort_price)
ax = plt.gca()
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right')
plt.show();

・リゾート、サービスアパートメント、ホテルの中央値が高い
・villaは別荘のことらしいですが、中央値が低い。私達が連想する別荘とは違うのかも
・ホステルはやはり中央値が低くなっている

部屋のタイプと価格の関係

listings.loc[(listings.price <= 600) & (listings.price > 0)].pivot(columns = 'room_type', values = 'price').plot.hist(stacked = True, bins=100)
plt.xlabel('Listing price in $');

一軒家、アパートすべてを対象とした物件の数が一番多いようです。

アメニティ(部屋の設備)

pd.Series(np.concatenate(listings['amenities'].map(lambda amns: amns.split(","))))\
    .value_counts().head(20)\
    .plot(kind='bar')
ax = plt.gca()
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right', fontsize=12)
plt.show();

これも上位は順当な結果になってますね。Heating(暖房)が多い結果を見ると、ニューヨークはやはり寒いんだなという印象です。

アメニティと価格の関連

amenities = np.unique(np.concatenate(listings['amenities'].map(lambda amns: amns.split(","))))
amenity_prices = [(amn, listings[listings['amenities'].map(lambda amns: amn in amns)]['price'].mean()) for amn in amenities if amn != ""]
amenity_srs = pd.Series(data=[a[1] for a in amenity_prices], index=[a[0] for a in amenity_prices])
amenity_srs.sort_values(ascending=False)[:20].plot(kind='bar')
ax = plt.gca()
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right', fontsize=12)
plt.show();

でました、金持ちの象徴のプールは価格との関連が一番高いようです。続いて、Crib(ベビーベッド)。うーん、これについてはよくわかりませんね。理由が気になるところ。

ベッドの数と価格の関連

listings.loc[(listings.price <= 600) & (listings.price > 0)].pivot(columns = 'beds',values = 'price').plot.hist(stacked = True,bins=100)
plt.xlabel('Listing price in $');

・ベッドが一つの物件は安いものから高いものまで多岐に渡る
・ベッド0が意外と目立つ
・最大値はベッド21台。どんな家なんだろうか・・・
select_beds_over_5 = listings.loc[(listings.price <= 600) & (listings.price > 0)]\
                    .groupby('beds')\
                    .filter(lambda x: len(x)>=5)\
                    ["beds"].values

listings_beds_over_5 = listings.loc[listings["beds"].map(lambda x: x in select_beds_over_5)]

sns.boxplot(y='price', x='beds', data = listings_beds_over_5.loc[(listings_beds_over_5.price <= 600) & (listings_beds_over_5.price > 0)])
plt.show();

・ベッドの数が増える度、価格の中央値も上昇
・ベッド9~11台ある物件の価格が高い
・12台、16台は11台より価格が低くなっている。大人数が安く泊まるための物件?

数値データをヒートマップで表示

#accommodatesは収容人数
corr = listings.loc[(listings.price <= 600) & (listings.price > 0)][col].dropna().corr()
plt.figure(figsize = (6,6))
sns.set(font_scale=1)
sns.heatmap(corr, cbar = True, annot=True, square = True, fmt = '.2f', xticklabels=col, yticklabels=col)
plt.show();

・価格は収容人数との相関が一番高い
・収容人数とベッドルームの数、ベッドの数に相関がある

物件の価格予測モデル作成

前処理と特徴エンジニアリング

listings['price'] = listings['price'].str.replace(',', '')
listings['price'] = listings['price'].str.replace('$', '')
listings['price'] = listings['price'].astype(float)
listings = listings.loc[(listings.price <= 600) & (listings.price > 0)]

アメニティを特徴エンジニアリングを行い数値データに変換

from sklearn.feature_extraction.text import CountVectorizer

listings.amenities = listings.amenities.str.replace("[{}]", "").str.replace('"', "")
## カラム内の単語の出現頻度を数えて、結果を素性ベクトル化する(Bag of words)
count_vectorizer =  CountVectorizer(tokenizer=lambda x: x.split(','))
amenities = count_vectorizer.fit_transform(listings['amenities'])
df_amenities = pd.DataFrame(amenities.toarray(), columns=count_vectorizer.get_feature_names())
df_amenities = df_amenities.drop('',1)

f(False), t(True)の値を0,1に置換

columns =  ['host_is_superhost', 'host_identity_verified', 'host_has_profile_pic',
                   'is_location_exact', 'requires_license', 'instant_bookable',
                   'require_guest_profile_picture', 'require_guest_phone_verification']
for c in columns:
    listings[c] = listings[c].replace('f',0,regex=True)
    listings[c] = listings[c].replace('t',1,regex=True)

価格系のカラムをすべてドルマークを消し、nanは0で置換

listings['security_deposit'] = listings['security_deposit'].fillna(value=0)
listings['security_deposit'] = listings['security_deposit'].replace( '[\$,)]','', regex=True ).astype(float)
listings['cleaning_fee'] = listings['cleaning_fee'].fillna(value=0)
listings['cleaning_fee'] = listings['cleaning_fee'].replace( '[\$,)]','', regex=True ).astype(float)

次の数値特性を使用します

listings_new = listings[['host_is_superhost', 'host_identity_verified', 'host_has_profile_pic','is_location_exact', 
                         'requires_license', 'instant_bookable', 'require_guest_profile_picture', 
                         'require_guest_phone_verification', 'security_deposit', 'cleaning_fee', 
                         'host_listings_count', 'host_total_listings_count', 'minimum_nights',
                     'bathrooms', 'bedrooms', 'guests_included', 'number_of_reviews','review_scores_rating', 'price']]

nanを中央値で埋めます

for col in listings_new.columns[listings_new.isnull().any()]:
    listings_new[col] = listings_new[col].fillna(listings_new[col].median())

カテゴリ変数をダミー変数に変換します。

for cat_feature in ['zipcode', 'property_type', 'room_type', 'cancellation_policy', 'neighbourhood_cleansed', 'bed_type']:
    listings_new = pd.concat([listings_new, pd.get_dummies(listings[cat_feature])], axis=1)

最後にアメニティと新しく作成したリスティングのデータフレームを結合します。

listings_new = pd.concat([listings_new, df_amenities], axis=1, join='inner')

LightGBM

元ネタはランダムフォレストとこのLightGBMで価格決定に重要な特徴量を確認しています。結果がLightGBMの方が良かったため、こちらのみ載せることとします。正直チューニングについてはわかっていないため、これは今後の課題としたいと思います。

from lightgbm import LGBMRegressor

y = listings_new['price']
x = listings_new.drop('price', axis =1)
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size = 0.25, random_state=1)
fit_params={
    "early_stopping_rounds":20, 
            "eval_metric" : 'rmse', 
            "eval_set" : [(X_test,y_test)],
            'eval_names': ['valid'],
            'verbose': 100,
            'feature_name': 'auto', 
            'categorical_feature': 'auto'
           }
        
class LGBMRegressor_GainFE(LGBMRegressor):
    @property
    def feature_importances_(self):
        if self._n_features is None:
            raise LGBMNotFittedError('No feature_importances found. Need to call fit beforehand.')
        return self.booster_.feature_importance(importance_type='gain')
        
clf = LGBMRegressor_GainFE(num_leaves= 25, max_depth=20, 
                         random_state=0, 
                         silent=True, 
                         metric='rmse', 
                         n_jobs=4, 
                         n_estimators=1000,
                         colsample_bytree=0.9,
                         subsample=0.9,
                         learning_rate=0.01)
clf.fit(X_train.values, y_train.values, **fit_params)
y_pred = clf.predict(X_test.values)
print('R^2 test: %.3f' % (r2_score(y_test, y_pred)))

feat_imp = pd.Series(clf.feature_importances_, index=x.columns)
feat_imp.nlargest(20).plot(kind='barh', figsize=(10,6))

・一軒家もしくはアパート全て貸し切りがもっとも重要度が高い
・RMSE誤差が約54.8
・テストデータに対するr2が0.657

まとめ

・コードを写経することは作成者の意図がわかるため、今後も続けていく価値がある
・価格は収容人数との相関が一番高い
・一軒家もしくはアパート全て貸し切りがもっとも物件の価格に対して重要度が高い



この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

note.user.nickname || note.user.urlname

R, Pythonで分析した結果のアウトプットをする場として利用しています。現時点の目標は"就職"することです。興味あること:R、 Python、 英語、そして筋トレです。

28

Ryo

note編集部のお気に入り記事

様々なジャンルでnote編集部がお気に入りの記事をまとめていきます。
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。