Pythonデータ分析(勾配ブースティング②ランキング法)
今回チャレンジしたのはSIGNATE練習問題:ECサイトにおける購買率の最適化です。(Competitions名:オプト レコメンドエンジン作成)
前回のSIGNATEの練習問題:債務不履行リスクの低減でLightGBMを初めて使用しましたが、理解が今一つだったので再度勾配ブースティング(GBDT)問題に挑みます。
データ前処理
学習用データが四種類あり、train_A.tsv, train_B.tsv, train_C.tsv, train_D.tsvでそれぞれ人材、旅行、不動産、アパレルのデータです。
今回は該当ユーザーにおける該当商品の関連度ランクを予測して提出する、ランキング法を使用します。
user_id:ユーザーID
product_id:商品ID
event_type:行動種別(コンバージョン:3, クリック:2, 閲覧:1, カート:0)
ad:(cv)コンバージョンが広告経由か否か。コンバージョンではない場合は-1
time_stamp:タイムスタンプ
さらに評価関数の説明でevent_type:3の場合はad:1の場合のみ評価対象とするとあるので、留意します。
df_A = pd.read_csv('train_A.tsv', sep='\t')
#日付データ変換
df_A['time_stamp'] = pd.to_datetime(df_A['time_stamp'])
データ分割
3分割して学習データ2/3,検証データ1/3の割合に分けていきます。
さらに上記を説明変数期間と目的変数期間に分けます。
今回行いたいのは、将来ユーザーが購入しそうな商品を推薦するアルゴリズムを作成することです。過去の行動履歴データをもとにしてそれぞれのユーザーに対して、将来購入する、または購入に結び付くような商品を予測するということです。つまり、説明変数データは過去のデータとして作成し、目的変数データは未来のデータとして作成することが必要となりそうです。
# 最も古い年月日時を取得
start = min(df_A['time_stamp'])
# 最も新しい年月日時を取得
end = max(df_A['time_stamp'])
# 学習期間の長さを取得
interval = (end-start)/3
# 学習用期間を抽出
df_A_t = df_A[df_A['time_stamp']<=start+interval*2]
# 検証用期間を抽出
df_A_v = df_A[df_A['time_stamp']>start+interval]
#学習データ二分割
# 最も古い年月日時を取得
start_train = min(df_A_t['time_stamp'])
# 最も新しい年月日時を取得
end_train = max(df_A_t['time_stamp'])
# 学習期間の半分の長さを取得
interval_train = (end_train- start_train) / 2
# 説明変数期間を抽出
df_A_tX = df_A_t[df_A_t['time_stamp'] <= start_train + interval_train]
# 目的変数期間を抽出
df_A_ty = df_A_t[df_A_t['time_stamp'] >= start_train + interval_train]
#検証データ二分割
# 最も古い年月日時を取得
start_v = min(df_A_v['time_stamp'])
# 最も新しい年月日時を取得
end_v = max(df_A_v['time_stamp'])
# 学習期間の半分の長さを取得
interval_v = (end_v- start_v) / 2
# 説明変数期間を抽出
df_A_vX = df_A_v[df_A_v['time_stamp'] <= start_v + interval_v]
# 目的変数期間を抽出
df_A_vy = df_A_v[df_A_v['time_stamp'] >= start_v + interval_v]
print(max(df_A_tX['time_stamp']),min(df_A_tX['time_stamp']))
print(max(df_A_ty['time_stamp']),min(df_A_ty['time_stamp']))
print(max(df_A_vX['time_stamp']),min(df_A_vX['time_stamp']))
print(max(df_A_vy['time_stamp']),min(df_A_vy['time_stamp']))
2017-04-10 14:59:58.316000 2017-03-31 15:00:00.179000
2017-04-20 14:59:57.406000 2017-04-10 14:59:59.577000
2017-04-20 14:59:57.406000 2017-04-10 15:00:00.093000
2017-04-30 14:59:59.107000 2017-04-20 15:00:00.410000
特徴量生成
ユーザーが見た商品や買われた商品等、特徴量として使えるようにデータを加工します。具体的には以下の通りです。
・各ユーザーが関わったユニーク商品数
・各ユーザーが何らかの行動をとった日数
・各ユーザーが商品の詳細ページを閲覧した回数
・各商品に対して何らかの行動をとったユニークユーザー数
・各商品に対して閲覧された回数
・各商品に対してカートに入れられた回数
・各商品に対してクリックされた回数
・各商品に対して購入された回数
・各ユーザー×商品に対して閲覧した回数
・各ユーザー×商品に対してカートに入れた回数
・各ユーザー×商品に対してクリックした回数
・各ユーザー×商品に対して購入した回数
#ユーザーに対する特徴量
# ユニーク商品数
u_p = df_A_tX.groupby('user_id').apply(lambda x: len(x['product_id'].unique()))
# 何らかの行動をとった日数
u_d = df_A_tX.groupby('user_id').apply(lambda x: len(x['time_stamp'].apply(lambda x: x.date()).unique()))
# 商品の詳細ページを閲覧した回数
u_pv = df_A_tX.groupby('user_id').apply(lambda x: (x['event_type']==1).sum())
# 3つのデータを行方向に連結
u = pd.concat([u_p, u_d, u_pv], axis=1)
u.columns = ['u_p', 'u_d', 'u_pv']
#商品に対する特徴量
# ユニークユーザー数
p_u = df_A_tX.groupby('product_id').apply(lambda x: len(x['user_id'].unique()))
# カートに入れられた回数
p_ca = df_A_tX.groupby('product_id').apply(lambda x: (x['event_type'] == 0).sum())
# 閲覧された回数
p_pv = df_A_tX.groupby('product_id').apply(lambda x: (x['event_type']==1).sum())
# クリックされた回数
p_cl = df_A_tX.groupby('product_id').apply(lambda x:(x['event_type']==2).sum())
# 購入された回数
p_cv = df_A_tX.groupby('product_id').apply(lambda x: ((x['event_type']==3) & (x['ad']==1)).sum())
# 5つのデータを連結
p = pd.concat([p_u, p_pv, p_ca, p_cl, p_cv], axis=1)
p.columns = ['p_u', 'p_pv', 'p_ca', 'p_cl', 'p_cv']
#ユーザー×商品に関する特徴量を作成する
# カートに入れられた回数
u_p_ca = df_A_tX.groupby(['user_id','product_id']).apply(lambda x: (x['event_type']==0).sum())
# 閲覧された回数
u_p_pv = df_A_tX.groupby(['user_id','product_id']).apply(lambda x: (x['event_type']==1).sum())
# クリックされた回数
u_p_cl = df_A_tX.groupby(['user_id','product_id']).apply(lambda x: (x['event_type']==2).sum())
# 購入された回数
u_p_cv = df_A_tX.groupby(['user_id','product_id']).apply(lambda x: ((x['event_type']==3) & (x['ad']==1)).sum())
# 5つのデータを連結
u_p = pd.concat([u_p_pv, u_p_ca, u_p_cl, u_p_cv], axis=1)
# カラム名を変更
u_p.columns = ['u_p_pv', 'u_p_ca', 'u_p_cl', 'u_p_cv']
# データuのuser_idをカラムにする
u = u.reset_index()
# データpのproduct_idをカラムにする
p = p.reset_index()
# データu_pのuser_idとproduct_idをカラムにする
u_p = u_p.reset_index()
# データu_pとデータuを結合
merged = pd.merge(u_p, u, on = 'user_id', how = 'inner')
# データmergedとデータpを結合
merged = pd.merge(merged, p, on = 'product_id', how = 'inner')
目的変数の作成
ユーザーと商品の各ペアについて関連度を付与して目的変数を作成します。0は関連度なしで1以上は何らかの関連があるということで、event_typeカラムの値に1を足したものを作ります。
#目的変数データを作成する
# ユーザー×商品に関連度を付与
rel = df_A_ty.groupby(['user_id','product_id']).apply(lambda x: x['event_type'].max()+1)
# user_idとproduct_idをカラムに変更
rel = rel.reset_index()
# 関連度のカラムを'y'と命名
rel = rel.rename(columns={0:'y'})
特徴量データに存在するユーザーを抽出
特徴量データには存在するが目的変数データには存在しないユーザーもいる為、両方に存在するユーザーのみ抽出します。
# 特徴量データのユーザー
user_x = set(merged['user_id'])
# 目的変数データのユーザー
user_y = set(rel['user_id'])
# 共通のユーザー
user_xy = user_x.intersection(user_y)
# mergedから’user_id’列がuser_xy(特徴量・目的変数共通のユーザー)に含まれる行だけを抽出
merged = merged[merged['user_id'].isin(user_xy)]
# mergedとrelという2つのデータフレームを’user_id’と’product_id’の列で結合し、relという変数に代入
rel = pd.merge(merged[['user_id', 'product_id']], rel)
# 特徴量データと目的変数データをouter join
train_all = pd.merge(merged, rel, on=['user_id', 'product_id'], how='outer')
# 欠損を0で埋める
train_all = train_all.fillna(0)
# 関連度を整数型にする
train_all['y'] = train_all['y'].astype(int)
クエリリスト作成
LightGBMのランキング学習ではクエリリストが必要なので作成します。
中身はユーザー毎に関連のある商品の数です。
(Bing回答)
このクエリデータとは何行が同じクエリかということを表しており、例えば下のような配列になります。最初の3行が同じクエリに属する、次の13行が同じクエリに属する、...といったことを表しています。ここで、また注意しなければならないのはDatasetクラスにわたすデータはクエリごとに連続して配置されていなければなりません。これは、ランキング学習が、同じクエリ内での順位付けを行うためです。
# クエリリスト作成
query_list = train_all['user_id'].value_counts()
# user_id, product_idをインデックス化
train_all = train_all.set_index(['user_id', 'product_id'])
# クエリリストをインデックスでソート
query_list = query_list.sort_index()
# 特徴量と目的変数データをインデックスでソート
train_all = train_all.sort_index()
# 特徴量(['p_u', 'p_pv', 'p_ca', 'p_cl', 'p_cv', 'u_p', 'u_d', 'u_pv', 'u_p_pv', 'u_p_ca', 'u_p_cl', 'u_p_cv'])のみ抽出
X_train = train_all[['p_u', 'p_pv', 'p_ca', 'p_cl', 'p_cv', 'u_p', 'u_d', 'u_pv', 'u_p_pv', 'u_p_ca', 'u_p_cl', 'u_p_cv']]
# 目的変数('y')を抽出
y_train = train_all['y']
ここまでの処理を検証データに対しても行います。
モデル学習
LightGBMに必要なデータを渡して学習します。
過学習対策にearly_stopping_rounds=1を追加します。
# lightgbmをlgbとしてインポート
import lightgbm as lgb
# LGBMRankerのモデルを作成
model = lgb.LGBMRanker()
# LGBMRankerを作成して学習
model.fit(X_train, y_train, group=query_list,eval_set=[(X_v, y_v)], eval_group=[list(query_list_v)],early_stopping_rounds=1)
出力結果がこんな感じで出ます。精度がnDCG0.5~0.6ぐらいのモデルが出来上がりました。(0~1の値を取り、大きいほど精度が良い)
[1] valid_0's ndcg@1: 0.513019 valid_0's ndcg@2: 0.541743 valid_0's ndcg@3: 0.567757 valid_0's ndcg@4: 0.590041 valid_0's ndcg@5: 0.608749
[2] valid_0's ndcg@1: 0.513712 valid_0's ndcg@2: 0.54232 valid_0's ndcg@3: 0.56849 valid_0's ndcg@4: 0.590892 valid_0's ndcg@5: 0.609606
[3] valid_0's ndcg@1: 0.513724 valid_0's ndcg@2: 0.542307 valid_0's ndcg@3: 0.568496 valid_0's ndcg@4: 0.590877 valid_0's ndcg@5: 0.60959
提出データ作成
traindデータがA~Dまであり、各カテゴリごとにモデルを作成してみたかったのでtrain_Aの対象ユーザー(ユーザーIDに'_A'を含む)を抽出して予測値を出しました。
※提出データのユーザーはtrain_Aに必ず存在するユーザーです。なのでtrain_Aデータ全てのユーザーで特徴量を生成し直してX_tを作成した後model.predict()で予測値を算出しています。
# ユーザー一覧取得
users = pd.read_csv('test.tsv', sep='\t', header=None)
# '_A' を含むデータの抽出
users = users[users[0].str.contains('_A')]
# ユーザー一覧に対する予測結果
user_ids = []
product_ids = []
ranks = []
for user in users[0]:
pred = model.predict(X_t.loc[user]) # 機械学習モデルの出力
pred_products = pd.Series(pred, index=X_t.loc[user].index) # 出力に対して商品IDを割り当てる
products = pred_products.sort_values(ascending=False).index # 関連度が高い順に並び変える
output = list(products)[:22] # 上位22件以内に絞る
user_ids += [user]*len(output) # ユーザーIDを予測数の分だけ列挙
product_ids += output # products_idsに予測結果を追加
ranks += list(range(0, len(output))) # 各product_idに対してランキングを付与して追加
# 結果を連結
results = pd.DataFrame({'user_id': user_ids, 'product_id': product_ids, 'rank': ranks})
# 結果の確認
print(results.head())
# 'results.tsv'として保存
results.to_csv('resultsA.tsv', sep='\t', header=False, index=None)
上記処理をtrain_B、train_C、train_Dにも行い、全てのresultsファイルを結合します。
#提出データ読込
a_r = pd.read_csv('resultsA.tsv',sep='\t',header=None)
b_r = pd.read_csv('resultsB.tsv',sep='\t',header=None)
c_r = pd.read_csv('resultsC.tsv',sep='\t',header=None)
d_r = pd.read_csv('resultsD.tsv',sep='\t',header=None)
#提出データ縦方向に結合
r = pd.concat([a_r,b_r,c_r,d_r],axis=0)
# csvファイルとして保存
# 'results.tsv'として保存
r.to_csv('./submit.tsv', sep='\t', header=False,index=False)
提出データの結果は0.2そこそこでした。
トップ成績の方が0.28程なので、この問題の精度を上げるのは難易度が高いように思います。何より私のやり方は処理時間が結構かかるので大変でした…(特にtrain_Dは35万行ありresultsデータを作成するのに2時間かかった)
そしてランキング学習が今までの回帰や分類と勝手が違って理解が進まず、Quest2周しました。我ながらよく折れずにやっていると思います!
ディープラーニングを使いこなせるようになれば大容量データでも効率よく学習できるようになるのかなぁと思いつつ、PytorchでGPUを使えるようにはしてみました。
次回はそろそろディープラーニングに手をつける予定です。
体調は相変わらず波があるので、焦らずぼちぼち勉強します。