こんにちは。私は経営工学を専攻している学部3年生で、shu421という名前で活動しております。この記事では、2021/8/9に締め切られたAI Quest2021アセスメントの解法について解説します。これはSIGNATEの過去コンペと同様のものでした。私は最終的に
- cv: 103.5750774
- lb: 144.0033488
- 964人中23位
という結果でした。拙い部分もあるとは思いますが、この記事が皆さんのお役に立てればなと思います。
Twitterもやっているので、ぜひフォローお願いします。また、全コードをGitHubに公開したので、そちらもご覧になってください!
Twitter: (https://twitter.com/shu421_)
GitHub: (https://github.com/shu421/AIQuest_2021) : main/nb001.ipynbが今回の最終サブコードです。
この記事について
この記事では、GitHubにあげたアセスメントの解法を簡単に解説します。特に、amenitiesというカラムの前処理についてわからない方が多いように感じたので、少し丁寧に書いたつもりです。
目次
- タスク
- データ
- 前処理
- モデリング
- 最後に
タスク
アメニティの種類やベッドの数、レビュー日から宿泊施設の価格(y)を予測するコンペでした。全てのカラムは以下の通りです。
train_df.columns
Index(['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', 'y'],
dtype='object')
前処理
ライブラリインポート
import pandas as pd
import numpy as np
import gc
import matplotlib.pyplot as plt
import seaborn as sns
pd.options.display.max_columns = 100
%matplotlib inline
from sklearn.preprocessing import LabelEncoder
from category_encoders.target_encoder import TargetEncoder
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
基本的に、trainとtestには同様の前処理をするので、先にこれら二つをまとめてから処理して行きます。
train_df = pd.read_csv('../input/train.csv')
test_df = pd.read_csv('../input/test.csv')
train = train_df.copy() # 念の為、オリジナルのtrain_dfは残しておく
test = test_df.copy()
target = train.pop('y') # yを抽出し、trainから削除
all_df = pd.concat([train, test], axis=0) # 結合
all_df = all_df.drop(['id'], axis=1) # id列は使わない。これがあるとうまく学習できない
all_df = all_df.reset_index(drop=True)# indexを揃える
カテゴリ型
all_df.select_dtypes(include=object).columns
Index(['amenities', 'bed_type', 'cancellation_policy', 'city', 'cleaning_fee',
'description', 'first_review', 'host_has_profile_pic',
'host_identity_verified', 'host_response_rate', 'host_since',
'instant_bookable', 'last_review', 'name', 'neighbourhood',
'property_type', 'room_type', 'thumbnail_url', 'zipcode'],
dtype='object')
この中でも、amenities, description, first_review, host_since, last_reviewが少し癖があるので、解説します。
amenities
all_df['amenities'].head() # 0~5行目を抽出
0 {TV,"Wireless Internet",Kitchen,"Free parking ...
1 {TV,"Cable TV",Internet,"Wireless Internet","A...
2 {TV,Internet,"Wireless Internet",Kitchen,"Indo...
3 {TV,"Cable TV",Internet,"Wireless Internet","A...
4 {TV,Internet,"Wireless Internet","Air conditio...
Name: amenities, dtype: object`
上の結果より、amenitiesの一つのセルには、「{}」で囲まれており、「"」がついているデータとついていないデータが混在していることがわかります。また、「,」によって単語が区切られています。そこで、「{}」と「"」を削除し、「,」によって単語をわけます。
# amenities
# dict → one-hot
def processing_amenities(x):
# all_df['amenities']の各要素をapplyによって順に入力していく
# 入力xの例: {TV,"Wireless Internet",Kitchen}
x = x.replace('{','') # 「{」を空白''で置き換えることで削除
x = x.replace('}','')
x = x.replace('"','')
x = x.split(',') # ,で分ける
return x # [TV, Wireless Internet, Kitchen]のような形で出力される
all_df['amenities'] = all_df['amenities'].apply(processing_amenities)
以上により、汚かったデータがリスト形式になりました。これでall_df['amenities']の各要素には、[TV, Wireless Internet, Kitchen]
という形でデータが保存されています。次に、これらのリストをone-hot化していきます。
mlb = MultiLabelBinarizer(sparse_output=True)
all_df = all_df.join(
pd.DataFrame.sparse.from_spmatrix(
mlb.fit_transform(all_df.pop('amenities')) # popにすることで、amenities列を同時に削除
index=all_df.index,
columns=mlb.classes_))
以上のコードを書くことで、TV
やWireless
列が作成され、それらを含む行には1が代入され、含まない場合には0が代入されました。
これでamenitiesの処理は終了です。
description
desriptionには、宿泊施設の説明が記載されていました。例えば、「豪華」や、「高価」などの単語が含まれていれば重要な特徴量になると考えたので、特徴的な単語を見つける、TF-IDFという手法を試しました。
しかし、精度は改善されなかったので、純粋に文章の長さだけを特徴とし、元の列は落としました。
# description
# length of sentence
all_df['n_description'] = all_df['description'].apply(lambda x: len(x))
all_df = all_df.drop('description', axis=1)
first_review, host_since, last_review
これらは、年-月-日にちという形のデータです。
train_df['first_review']
0 2016-07-27
1 2016-09-12
2 2016-06-15
3 2014-03-15
4 2015-08-05
...
55578 2013-02-27
55579 2015-11-29
55580 2016-03-02
55581 2016-10-18
55582 NaN
Name: first_review, Length: 55583, dtype: object
こららは、それぞれに関して年月日のカラムを作成しました。
# date to one-hot
def convert_date(df_, col):
df = df_.copy()
df[col] = pd.to_datetime(df[col]) # datetime型に変換
df[f'{col}_year'] = df[col].map(lambda x: x.year)
df[f'{col}_month'] = df[col].map(lambda x: x.month)
df[f'{col}_day'] = df[col].map(lambda x: x.day)
df = df.drop(col, axis=1)
return df
all_df = convert_date(all_df, 'first_review')
all_df = convert_date(all_df, 'host_since')
all_df = convert_date(all_df, 'last_review')
Encoding
純粋なカテゴリ型のカラムは、label encodingとtarget encodingしました。target encodingによって、若干スコアが改善された記憶があります(どのくらい上がったかは忘れました)。
# categorical columns
# Label Encoding
cat_col = ['bed_type', 'cancellation_policy', 'city',
'cleaning_fee', 'host_has_profile_pic',
'host_identity_verified', 'instant_bookable',
'name', 'neighbourhood', 'property_type', 'room_type',
'thumbnail_url']
le = LabelEncoder()
for col in cat_col:
all_df[col] = le.fit_transform(all_df[col])
for col in cat_col:
train[col] = le.fit_transform(train[col])
for col in cat_col:
test[col] = le.fit_transform(test[col])
# Target Encoding
te = TargetEncoder(smoothing=0.1)
for col in cat_col:
train[f'te_{col}'] = te.fit_transform(train[col], target)
for col in cat_col:
test[f'te_{col}'] = te.transform(test[col])
数値型
欠損値
LightGBMは賢いので、基本的に数値型カラムはそのままです。欠損値は埋めなくても大丈夫ですが、-9999で埋めることで若干精度が改善されました。
集約特徴量
kaggleやSIGNATEなどのコンペでは、ある列をキーにした集約特徴量がよく採用されているので今回も作ってみました。価格帯によって特徴が異なると予想できるので、それをうまく表現するキーを見つけるの重要です。私は、
- knnによる価格帯の予測値
- kmeansによるクラスタリングされたクラス
をキーにして集約特徴量を作ってみましたが、精度は改善されませんでした。そこで都市ごとに宿泊施設の値段は変わると予想し、city
をキーにするとスコアが改善されました。集約統計量としては、z得点(zscore)、偏差(deviation)、標準偏差(std)を採用しました。mean
、sum
、median
、count
ではスコアが改善されませんでした。後から考えると、deviationやstdによって、価格帯の散らばりをうまく表現できていたのかなと思います。
集約特徴量の作成方法については、過去コンペのディスカッションを参考にしました
# aggregated features
group_key = 'city'
group_values = ['accommodates',
'bathrooms',
'bed_type',
'bedrooms',
'beds',
'cancellation_policy',
# 'city',
'cleaning_fee',
'host_has_profile_pic',
'host_identity_verified',
'host_response_rate',
'instant_bookable',
'latitude',
'longitude',
'name',
'neighbourhood',
'number_of_reviews',
'property_type',
'review_scores_rating',
'room_type',
'thumbnail_url',
'zipcode']
agg_methods = [
'deviation',
'zscore',
'std',
# 'mean',
# 'count',
# 'sum',
# 'median'
]
# GroupFeatureExtractorについては、GitHubのmain/nb001.ipynbに記載しています。
gfe = GroupFeatureExtractor(
group_key=group_key,
group_values=group_values,
agg_methods=agg_methods)
df_ = gfe.fit_transform(all_df)
all_df = pd.concat([all_df, df_], axis=1)
その他
結果論ですが、以下の特徴量重要度は0でしたので、計算スピードを上げるために落としています。落としても精度は変わらないです。
# drop features: importance=0
drop_features =['Handheld shower head', 'Table corner guards', 'Host greets you',
'Bath towel', 'Cleaning before checkout', 'Accessible-height bed',
'Beachfront', 'Waterfront', 'Hot water kettle', 'Body soap',
'Air purifier', 'Changing table', 'Accessible-height toilet',
'Bathtub with shower chair', 'Washer / Dryer', 'Lake access',
'Toilet paper', 'Hand or paper towel',
'agg_zscore_host_identity_verified_grpby_city',
'Wide clearance to shower & toilet', 'Disabled parking spot',
'agg_std_host_has_profile_pic_grpby_city', 'Ground floor access',
'agg_std_beds_grpby_city', 'Grab-rails for shower and toilet',
'agg_std_bed_type_grpby_city', 'Pocket wifi', 'Hand soap',
'Private bathroom', 'Ski in/Ski out', 'Refrigerator',
'Fixed grab bars for shower & toilet', 'Flat',
'Roll-in shower with chair', 'Flat smooth pathway to front door',
'Single level home', 'agg_std_host_identity_verified_grpby_city',
'agg_std_host_response_rate_grpby_city',
'Path to entrance lit at night', 'agg_zscore_cleaning_fee_grpby_city',
'Other', 'Dishes and silverware', 'Wide clearance to shower and toilet',
'agg_zscore_instant_bookable_grpby_city', 'Free parking on street',
'Wide entryway', 'Wide hallway clearance',
'agg_std_longitude_grpby_city', 'Paid parking off premises',
'agg_std_room_type_grpby_city',
'agg_std_review_scores_rating_grpby_city', 'Step-free access',
'agg_std_neighbourhood_grpby_city', 'agg_std_name_grpby_city',
'Firm matress']
モデリング
5 KFoldのLightGBMを使用しています。よく見かけるコードかもしれませんが、一応載せておきます。
n_splits = 5
kf = KFold(n_splits=n_splits, shuffle=True, random_state=71)
X = train
y = target
# 各Foldの結果を格納する
models = []
scores = []
preds = np.zeros((len(test),n_splits))
for i, (train_index, valid_index) in enumerate(kf.split(X, y)): # enumerateと書くことで、iにFoldの番号を代入できる(0-4)
X_train = X.iloc[train_index]
y_train = y.iloc[train_index]
X_valid = X.iloc[valid_index]
y_valid = y.iloc[valid_index]
lgb_train = lgb.Dataset(X_train.values, y_train)
lgb_valid = lgb.Dataset(X_valid.values, y_valid)
params ={'objective':'regression',
'seed':71,
'verbose':-1,
'metrics':'rmse',
'learning_rate':0.1}
model = lgb.train(params, lgb_train, verbose_eval=False, early_stopping_rounds=100, num_boost_round=9999,
valid_names=['train','test'], valid_sets=[lgb_train, lgb_valid])
# 学習の評価
valid_pred = model.predict(X_valid)
score = mean_squared_error(y_valid, valid_pred, squared=False)
# テストデータの予測
test_pred = model.predict(test)
# Foldの結果を格納
models.append(model)
scores.append(score)
preds[:,i] = test_pred
print(f'Fold{i}:', score)
print('cv:', np.mean(scores))
まとめ
精度向上に貢献した要因を一部記載します。
効いたもの
- target encoding
- 各種集約特徴量(zscore, deviation, std)
- descriptionの長さ
効かなかったもの
- knn, kmeansの結果をキーにした集約特徴量
- knn score
- 各カラムのbinning
- optuna hyperparameter tuning(やり方が間違ってたかも)
- low learning rate(例: 0.01)
- count encoding
- Fold数を増やす(10, 15...)
- cvは改善しましたが、過学習していたようでlbは悪化しました。
- bedroomあたりのbed数
最後に
特にamenitiesの処理方法について知りたい方が多い気がしたので、今回の記事を作成しました。全コードの中で説明していない部分も多くありますので、わからない点や解説してほしい箇所がありましたら、TwitterのDMやslackで遠慮なく聞いてください。また再喝にはなりますが、全コードはGitHubにあげてあります。そちらもぜひご覧になってください。半年間、一緒に切磋琢磨して行けたら嬉しいです!!