[MLSS 2015 Predictive Modeling Challenge] オンラインマーケットでの購買予測 チュートリアル

はじめに

オンラインマーケットにおける購買予測は、機械学習の重要なアプリケーションのひとつです。このチュートリアルは、コンペティションで提供されているデータを使って最初の予測結果を提出する方法を解説します。

掲載コードの実行には下記の環境が必要です:

ログデータの前処理

まず、分類器に入力する特徴ベクトルを作成しますログデータから特徴を抽出する基本的なアプローチは「数え上げ」です。ユーザーごとの購入数、商品ごとの売上数を数え上げてみましょう。ここでログデータlog-0331-0406.tsvは現在のディレクトリに置かれているものとします。

from csv import DictReader
user = {}  # Dictionary to store user's feature
item = {}  # Dictionary to store item's feature
with open('log-0331-0406.tsv', 'r') as f:
    for i, row in enumerate(DictReader(f, delimiter='\t')):
        if i%10000 == 0: print('Finished {} rows'.format(i))  # Process indicator
        if row['layer'] != 'order': continue
        uid = row['user_id']
        iid = row['item_id']
        # Count user's event
        if uid not in user:
            user[uid] = 1
        else:
            user[uid] += 1
        # Count item's event
        for ordered_iid in row['order_item_ids'].split('/')[1:]:  # Exclude empty ID
            ordered_iid = '/' + ordered_iid
            if ordered_iid not in item:
                item[ordered_iid] = 1
            else:
                item[ordered_iid] += 1
print('Total {} rows'.format(i+1))

いま作成したユーザーとアイテムの購入数を使って、テストデータの予測対象に対応する特徴ベクトルを作成することができます。以下のコードは、予測のための特徴をtest.tsvに保存します。

of = open('test.tsv', 'w')
with open('target.tsv') as f:
    for i, line in enumerate(f):
        if i%10000 == 0: print('Finished {} rows'.format(i))  # Process indigator
        row = line.rstrip().split('\t')
        uid = row[0]
        iid = row[1]
        feature = []
        # Combine counters
        ## User feature
        if uid in user:
            feature.append(user[uid])
        else:
            feature.append(0)
        ## Item feature
        if iid in item:
            feature.append(item[iid])
        else:
            feature.append(0)
        of.write('\t'.join(map(str, feature)) + '\n')
print('Total {} rows'.format(i+1))
of.close()

次に、分類器を訓練するためのデータを準備しましょう。訓練で使うための予測対象と正解データは与えられていないので、最初に行うべきことはログデータからそれらを抽出することです。与えられた7日間のデータを、特徴抽出用と、予測対象抽出用の2つに分けることを考えます。ここでは例として最初の5日間を特徴抽出用に、最後の2日間を予測対象抽出用としましょう。ここで、予測対象は、期間中に少なくともひとつの商品を購入したユーザーと、少なくともひとりのユーザーに購入された商品のすべての組み合わせとします。次のコードは7日間のログから特徴と予測対、正解データ象を抽出します。

user = {}
item = {}
uids = set()
iids = set()
positive_example = set()
target_date = set(['2015-04-05', '2015-04-06'])
with open('log-0331-0406.tsv', 'r') as f:
    for i, row in enumerate(DictReader(f, delimiter='\t')):
        if i%10000 == 0: print('Finished {} rows'.format(i))  # Process indicator
        if row['layer'] != 'order': continue
        uid = row['user_id']
        uids.add(uid)
        iid = row['item_id']

        # Target extraction
        if row['date'].split()[0] in target_date:
            for ordered_iid in row['order_item_ids'].split('/')[1:]:
                ordered_iid = '/' + ordered_iid
                positive_example.add((uid, ordered_iid))
                iids.add(ordered_iid)
            continue

        # Feature Extraction
        ## Count user's event
        if uid not in user:
            user[uid] = 1
        else:
            user[uid] += 1
        # Count item's event
        for ordered_iid in row['order_item_ids'].split('/')[1:]:
            ordered_iid = '/' + ordered_iid
            if ordered_iid not in item:
                item[ordered_iid] = 1
            else:
                item[ordered_iid] += 1
            iids.add(ordered_iid)
print('Total {} rows'.format(i+1))

では最後に、抽出した予測対象に対応する特徴ベクトルを作成しましょう。特徴ベクトルと正解ラベル (与えられたユーザーが与えられた購入したか否かを示す二値) を一緒にtrain.tsvに保存します。

of = open('train.tsv', 'w')
i = 0
for uid in uids:
    for iid in iids:
        if i%10000 == 0: print('Finished {} rows'.format(i))  # Process indigator
        feature = []
        # Combine counters
        ## User feature
        if uid in user:
            feature.append(user[uid])
        else:
            feature.append(0)
        ## Item feature
        if iid in item:
            feature.append(item[iid])
        else:
            feature.append(0)
        label = 0
        if (uid, iid) in positive_example: label = 1
        of.write('\t'.join(map(str, feature + [label,])) + '\n')
        i += 1
print('Total {} rows'.format(i+1))
of.close()

訓練データの読み込み

ここまでで分類器の訓練のための特徴ベクトルと正解ラベルが作成できました。作成した訓練データはtrain.csvに、テストデータはtest.csvに保存されているとします。次のようにしてデータを読み込みます:

import numpy as np
DATA_TRAIN_PATH = 'train.tsv'
train = np.genfromtxt(DATA_TRAIN_PATH)

次に読み込んだデータを、分類器学習に用いるサンプル(X_train, y_train)と、分類器の評価に用いるサンプル(X_val, y_val)に分割します。

from sklearn.cross_validation import train_test_split
X = train[:, :-1]
y = train[:, -1]
TEST_SIZE = 0.2
RANDOM_STATE = 0
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=TEST_SIZE, random_state=RANDOM_STATE)

ここでは、全体の20%を評価用としています。以下のコードにより、647,264件のサンプルが517,811件(学習用)と129,453件(評価用)に分割されていることが確認できます。

print(X.shape, y.shape)
print(X_train.shape, y_train.shape)
print(X_val.shape, y_val.shape)
Out: 
((647264, 2), (647264,))
((517811, 2), (517811,))
((129453, 2), (129453,))

分類器の訓練と評価

本問題では購買確率を予測する必要があるので、確率を出力できるロジスティック回帰の学習手順を紹介します。ここではsklearn.linear_model.LogisticRegressionを用います。

以下のコードで、分類器の学習を実行できます。

from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(random_state=RANDOM_STATE)

ここでは、分類器のパラメータはscikit learnのデフォルト値に設定しています。

次に、評価用データを用いて、分類器の性能を確認します。まず、評価用データに対する予測結果を出力します。

clf.fit(X_train, y_train)
y_pred = clf.predict_proba(X_val)[:, 1]

ここでは、コンペティションで指定されている通り、「ラベルが1である確率」を出力しています。

コンペティションで採用されている評価指標 (Area under the ROC curve, AUC)により、性能を評価します。roc_auc_scoreを用います。

from sklearn.metrics import roc_auc_score
print(roc_auc_score(y_val, y_pred))
Out: 0.922303182959

評価用データに対するAUCスコアを確認できました。次に、パラメータの値を変えて新しい分類器を学習してみましょう。

clf_new = LogisticRegression(fit_intercept=False,random_state=RANDOM_STATE)
clf_new.fit(X_train, y_train)
y_pred_new = clf_new.predict_proba(X_val)[:, 1]
print(roc_auc_score(y_val, y_pred_new))
Out: 0.92388724538

fit_intercept=FalseにするとAUCスコアが向上することを確認できました。このパラメータはバイアス項をモデルに含めるかどうかを指定します。

予測結果の提出

fit_intercept=Falseを採用し、予測結果を提出しましょう。これまでは評価用のデータは学習に用いませんでしたが、提出時には全てのデータ(X, y)を用いて分類器を再度学習します。

clf_submit = LogisticRegression(fit_intercept=False,random_state=RANDOM_STATE)
clf_submit.fit(X, y)

次に、学習した分類器を用いてテストデータに対する予測結果を出力します。まずは、テストデータを読み込みます。

DATA_TEST_PATH = 'test.tsv'
X_test = np.genfromtxt(DATA_TEST_PATH)

テストデータに対する予測を行い、結果を出力します。

SUBMIT_PATH = 'sample-submission-basic.tsv'
y_submit = clf_submit.predict_proba(X_test)[:, 1]
np.savetxt(SUBMIT_PATH, y_submit, fmt='%.5f')

おめでとうございます!これで分類器の学習と予測結果の提出が完了しました。コンペティションページにて、sample-submission-basic.tsvを提出しましょう。このチュートリアルで紹介した方法は、ログデータのほんの一部の情報しか使っていません。より多くの特徴を活用することにより、予測精度を上げることができるでしょう。