Python
analytics
Kaggle

Kaggle まとめ:Instacart Market Basket Analysis

More than 1 year has passed since last update.

1. はじめに

過去に参加したKaggleの情報をアップしていきます.
ここでは,Instacart Market Basket Analysisのデータ紹介とフォーラムでの目立った議論をピックアップします.
優勝者のアプローチなどの紹介は,別の記事で紹介します.
notebookのコードを紹介しているので、%matplotlib inline等は適宜変更してください。

2. 背景

スクリーンショット 2017-07-20 16.54.52.png

Instacartのデータサイエンスチームは様々な開発をしています。例えば、商品を再購入するユーザー、新規購入するユーザー、または次にショッピングカートへ入れる商品などの予測モデル構築です。さらに最近では、Instacartは詳細なトランザクション情報を一般公開してさえいます。
今回のコンペでは、このオープンデータを用いた予測モデルの精度を競います。予測の内容は、「ユーザーが次に注文する商品を、以前購入した商品から予測する」です。
また、コンペとは直接関係ありませんが、現在Instacartでは優れた機械学習エンジニアを募集中です。興味のある方はチェックしてみてください。

ユーザーが次に購入する商品を予測するため、InstacartではXGBoostword2vec、そしてAnnoyを使用しています。一度購入した商品の類似品を"buy again"でリコメンドし、

1-LNpbMMzWBsKqKyNvNH2APA.png

さらに作成したモデルで予測した結果をリコメンドしたりします。

1-tf40kqB8rRajbRn6A_0Jcw.png

この一般公開されているデータを用いて、Instacartは消費者へ、新しい食料品を発見する可能性を提供しています。

今回の特徴としては,以下が挙げられます.

  • 匿名化された属性データが複数ファイルに分散
  • 典型的な2値分類

3. 評価指標

今回の評価指標は、2値分類問題の指標として一般的なF1 scoreを使用します。
スクリーンショット 2017-07-20 17.23.05.png

4. データの紹介

こちらのリンクからデータをダウンロードしてください。

今回のデータは7種類あります。
Jordan Tremoureuxがリレーショナルデータの紐付けを可視化してくれています。

instacart_Files.png

AISLES.csvとDEPARTMENTS.csvはそれぞれ商品のカテゴリ情報で、野菜・肉・お菓子といった感じの情報です。
PRODUCTS.csvが、商品名とこれらカテゴリ情報の具体的な関連を示します。
ORDER_PRODUCTS_ {PRIOR, TRAIN}.csvは、メインとなるトレーニングデータです。PRIORは前回の注文情報、TRAINは今回の注文情報です。これらのデータにはreorderedという0-1フラグが含まれており、これが1であれば同じ商品を購入していることになります。この辺りの説明は少し複雑なので、次のEDAで詳しくみていきます。
SAMPLE_SUBMISSION.csvは、提出形式を示したデモ用ファイルです。
最後に、ORDERS.csvは、ORDER_PRODUCTS__{PRIOR, TRAIN}.csvと関連付いた時系列情報が含まれています。時系列といっても、タイムスタンプではなく「次の購入まで?日」といった時間差分情報です。

データ全体としては、ユーザーの購入履歴1ヶ月分、20万件以上がすっきりまとまった形で整理されています。
時間差分として時間を表現することで、他のコンペに見られるような煩雑な解析を省略できています。

5. データの解析情報

2つの解析情報
1) SRKのnotebook
2) Instacartがblog上で公開している解析情報
について紹介します。

ここでは紹介しませんが、Rで記述されたPhilipp Spachtholzの解析結果も素晴らしいです。
1)の解析とほとんど同じ内容ですが、treemapで情報が見やすく整理されているので、興味のある方はご確認ください。

unnamed-chunk-26-1.png

5.1. SRKのnotebook

5.1.1. データ整理

まずはライブラリのインポート

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()

%matplotlib inline

pd.options.mode.chained_assignment = None  # default='warn'

一番最後のoption設定でなんども出てくるワーニングをオフにしています。この辺は趣味で変えてください。
データの配置を確認しましょう。

from subprocess import check_output
print(check_output(["ls", "../input"]).decode("utf8"))

#aisles.csv
#departments.csv
#order_products__prior.csv
#order_products__train.csv
#orders.csv
#products.csv
#sample_submission.csv

データはnotebookを開いている一つ上の階層、そこにあるinputフォルダの中へ設置されている前提です。
#~のように表示されたら正常です。

次にデータをインポートします。

order_products_train_df = pd.read_csv("../input/order_products__train.csv")
order_products_prior_df = pd.read_csv("../input/order_products__prior.csv")
orders_df = pd.read_csv("../input/orders.csv")
products_df = pd.read_csv("../input/products.csv")
aisles_df = pd.read_csv("../input/aisles.csv")
departments_df = pd.read_csv("../input/departments.csv")

読み込んだ内容を確認して見ます。

orders_df.head()

スクリーンショット 2017-07-20 18.26.37.png

order_products_prior_df.head()

スクリーンショット 2017-07-20 18.26.43.png

order_products_train_df.head()

スクリーンショット 2017-07-20 18.26.49.png

orders_dfにほとんどすべての情報が含まれていそうです。
order_products_prior_dfとorder_products_train_dfはカラムが全く同じです。
2つのファイルの違いは何でしょうか?
今回は過去の購入履歴から次の購入品を予測します。つまり、priorファイルにはtrain, test両方の購入履歴が含まれており、order_products_train_dfは学習用の正解データということです。DBENがわかりやすく図で説明してくれています。

train_user.png
test_user.png

例えばorders_dfでは、user_id=1のデータはeval_set=priorに10個あり, eval_set=trainに1個、eval_set=testに0個出てきます。一方user_id=4のデータはeval_set=priorに5個あり、eval_set=trainに0個、eval_set=testに1個出てきます。
このeval_set=testの"reordered"が今回予測すべきターゲットであり、user_id=4がpriorで購入した商品を再購入したかどうかを推定します。

確認のため、orders_dfに含まれるprior、train、testの数を見ましょう。

cnt_srs = orders_df.eval_set.value_counts()

plt.figure(figsize=(12,8))
sns.barplot(cnt_srs.index, cnt_srs.values, alpha=0.8, color=color[1])
plt.ylabel('Number of Occurrences', fontsize=12)
plt.xlabel('Eval set type', fontsize=12)
plt.title('Count of rows in each dataset', fontsize=15)
plt.xticks(rotation='vertical')
plt.show()

__results___10_0.png

複数のpriorからtrain, testを予測するので、priorが圧倒的に多いことがわかります。

5.1.2. orders, order_products__{prior, train}の可視化

5.1.1.の内容から、eval_setとreorderedで可視化すれば何か分析できるとわかります。

まずはprior, train, testにそれぞれ何人userがいるのか見ましょう。
orders_dfをeval_setでグループ化し、各グループ内でuser_idを重複無しでcountします。

def get_unique_count(x):
    return len(np.unique(x))

cnt_srs = orders_df.groupby("eval_set")["user_id"].aggregate(get_unique_count)
cnt_srs

aggregateはgroupbyしたグループに様々な関数を適応するメソッドです。

eval_set
prior    206209
test      75000
train    131209
Name: user_id, dtype: int64

206,209人トータルでおり、そのうち131,209人がtrainに、75,000人がtestにいることがわかります。
userの登場回数は何回でしょうか。

cnt_srs = orders_df.groupby("user_id")["order_number"].aggregate(np.max).reset_index()
cnt_srs = cnt_srs.order_number.value_counts()

plt.figure(figsize=(12,8))
sns.barplot(cnt_srs.index, cnt_srs.values, alpha=0.8, color=color[2])
plt.ylabel('Number of Occurrences', fontsize=12)
plt.xlabel('Maximum order number', fontsize=12)
plt.xticks(rotation='vertical')
plt.show()

__results___13_0.png

最小で4回、最大で100回商品を購入しています。

おそらく週末の注文は多そうです。曜日情報で注文数を切り分けて見ましょう。曜日情報は'order_dow'です。

plt.figure(figsize=(12,8))
sns.countplot(x="order_dow", data=orders_df, color=color[0])
plt.ylabel('Count', fontsize=12)
plt.xlabel('Day of week', fontsize=12)
plt.xticks(rotation='vertical')
plt.title("Frequency of order by week day", fontsize=15)
plt.show()

__results___15_0.png

見た感じ、day of week = 0, 1がそれぞれ土曜、日曜に当たるようです。
平日での注文数はわずかな違いがあります。
同様に、時間で区切って注文数を見ていきましょう。

plt.figure(figsize=(12,8))
sns.countplot(x="order_hour_of_day", data=orders_df, color=color[1])
plt.ylabel('Count', fontsize=12)
plt.xlabel('Hour of day', fontsize=12)
plt.xticks(rotation='vertical')
plt.title("Frequency of order by hour of day", fontsize=15)
plt.show()

__results___17_0.png

土日込みでチェックしているせいか、最も注文が多い時間は昼間(午前10時と午後15時)となっています。朝の注文は少なく、夜は時間が遅くなるごとに注文数が減少していきます。

曜日と時間の注文数への関係性は深そうです。それらをヒートマップで可視化して見ます。

grouped_df = orders_df.groupby(["order_dow", "order_hour_of_day"])["order_number"].aggregate("count").reset_index()
grouped_df = grouped_df.pivot('order_dow', 'order_hour_of_day', 'order_number')

plt.figure(figsize=(12,6))
sns.heatmap(grouped_df)
plt.title("Frequency of Day of week Vs Hour of day")
plt.show()

__results___19_0.png

pivotはDataFrameの2軸だけに着目し、新しいフレームを作成します。

平日と休日では明らかな違いがあります。
平日は曜日ごとの違いがあまりなく、午前9時から午後16時までが注文数が多くなります。
休日は土日で顕著に異なります。土曜日は昼過ぎの注文が最も多く、遅い時間まで注文が続きます。日曜は午前中の注文が最も多いです。

次に注文の時間間隔を見ていきます。

plt.figure(figsize=(12,8))
sns.countplot(x="days_since_prior_order", data=orders_df, color=color[3])
plt.ylabel('Count', fontsize=12)
plt.xlabel('Days since prior order', fontsize=12)
plt.xticks(rotation='vertical')
plt.title("Frequency distribution by days since prior order", fontsize=15)
plt.show()

__results___21_0.png

前の注文から次の注文までの日数は'days_since_prior_order'で示されます。
7日と30日がピークで、7日を過ぎたあたりから14日、21日、28日と1週間ごとに周期性が存在しています。

時間と注文数の関係性が明らかかになったところで、今回のコンペで予測対象となる再購入について見ましょう。

print(order_products_prior_df.reordered.sum() / order_products_prior_df.shape[0])
print(order_products_train_df.reordered.sum() / order_products_train_df.shape[0])
0.589697466792
0.598594412751

prior, train共に、再購入した割合は59%前後であることがわかります。
残りの40%近くは再購入ではない注文だったということなので、おそらく再購入が含まれていない注文(order_id)が含まれているはずです。

grouped_df = order_products_prior_df.groupby("order_id")["reordered"].aggregate("sum").reset_index()
grouped_df["reordered"].ix[grouped_df["reordered"]>1] = 1
grouped_df.reordered.value_counts() / grouped_df.shape[0]
1    0.879151
0    0.120849
Name: reordered, dtype: float64

priorでは再購入していないオーダーは12%です。

grouped_df = order_products_train_df.groupby("order_id")["reordered"].aggregate("sum").reset_index()
grouped_df["reordered"].ix[grouped_df["reordered"]>1] = 1
grouped_df.reordered.value_counts() / grouped_df.shape[0]
1    0.93444
0    0.06556
Name: reordered, dtype: float64

trainでは再購入していないオーダーは6%と、priorの半分の割合となっています。

一回のオーダーでどれだけ注文するのでしょうか?ヒストグラムを作って見ましょう。

grouped_df = order_products_train_df.groupby("order_id")["add_to_cart_order"].aggregate("max").reset_index()
cnt_srs = grouped_df.add_to_cart_order.value_counts()

plt.figure(figsize=(12,8))
sns.barplot(cnt_srs.index, cnt_srs.values, alpha=0.8)
plt.ylabel('Number of Occurrences', fontsize=12)
plt.xlabel('Number of products in the given order', fontsize=12)
plt.xticks(rotation='vertical')
plt.show()

__results___29_0.png

SRKはorder_idをカウントするのではなく、add_to_cart_orderの最大値から一度の注文数を求めています。
最も多いのが5個の同時注文で、そこからポアソン分布のようなright tailedの減少を見せています。

5.1.3. 詳細な分析

商品に関するデータは、products, aisles, departmentsに分散しています。
まずはこれらの情報を結合していきます。

order_products_prior_df = pd.merge(order_products_prior_df, products_df, on='product_id', how='left')
order_products_prior_df = pd.merge(order_products_prior_df, aisles_df, on='aisle_id', how='left')
order_products_prior_df = pd.merge(order_products_prior_df, departments_df, on='department_id', how='left')
order_products_prior_df.head()

スクリーンショット 2017-07-21 12.16.03.png

product_id, aisle_id, department_idをキーとして、これらのデータを結合しました。

商品名別分析
最も注文されている商品名は何でしょうか。

cnt_srs = order_products_prior_df['product_name'].value_counts().reset_index().head(20)
cnt_srs.columns = ['product_name', 'frequency_count']
cnt_srs

スクリーンショット 2017-07-21 12.18.15.png

トップはバナナ、それ以降はフルーツを中心として様々な食料品が並びます。また、ほとんどすべてオーガニック商品となっています。

商品カテゴリ(aisle)別分析
どのような商品が並んでいるのでしょうか。商品の種類情報はaisleにあります。

cnt_srs = order_products_prior_df['aisle'].value_counts().head(20)
plt.figure(figsize=(12,8))
sns.barplot(cnt_srs.index, cnt_srs.values, alpha=0.8, color=color[5])
plt.ylabel('Number of Occurrences', fontsize=12)
plt.xlabel('Aisle', fontsize=12)
plt.xticks(rotation='vertical')
plt.show()

__results___38_0.png

最も多い2つは、生鮮果物と生鮮野菜です。

部門別分析

商品の部門(department)について見ていきましょう。

plt.figure(figsize=(10,10))
temp_series = order_products_prior_df['department'].value_counts()
labels = (np.array(temp_series.index))
sizes = (np.array((temp_series / temp_series.sum())*100))
plt.pie(sizes, labels=labels, 
        autopct='%1.1f%%', startangle=200)
plt.title("Departments distribution", fontsize=15)
plt.show()

__results___40_0.png

Produceが最も多い部門になります。

部門 - 再購入比率

再購入の対象となりやすい部門はどれでしょうか。

grouped_df = order_products_prior_df.groupby(["department"])["reordered"].aggregate("mean").reset_index()

plt.figure(figsize=(12,8))
sns.pointplot(grouped_df['department'].values, grouped_df['reordered'].values, alpha=0.8, color=color[2])
plt.ylabel('Reorder ratio', fontsize=12)
plt.xlabel('Department', fontsize=12)
plt.title("Department wise reorder ratio", fontsize=15)
plt.xticks(rotation='vertical')
plt.show()

__results___42_0.png

personal careが最も再購入されにくく、dairy eggsが最も再購入されています。

商品カテゴリ - 再購入比率
部門(department)と商品カテゴリ(aisle)を再購入比率で見ていきましょう。

grouped_df = order_products_prior_df.groupby(["department_id", "aisle"])["reordered"].aggregate("mean").reset_index()

fig, ax = plt.subplots(figsize=(12,20))
ax.scatter(grouped_df.reordered.values, grouped_df.department_id.values)
for i, txt in enumerate(grouped_df.aisle.values):
    ax.annotate(txt, (grouped_df.reordered.values[i], grouped_df.department_id.values[i]), rotation=45, ha='center', va='center', color='green')
plt.xlabel('Reorder Ratio')
plt.ylabel('department_id')
plt.title("Reorder ratio of different aisles", fontsize=15)
plt.show()

__results___44_0.png

縦軸が部門、横軸は再購入比率です。同じ高さにある点は、同部門の別カテゴリ商品です。

カートに入れる順番 - 再購入率

order_products_prior_df["add_to_cart_order_mod"] = order_products_prior_df["add_to_cart_order"].copy()
order_products_prior_df["add_to_cart_order_mod"].ix[order_products_prior_df["add_to_cart_order_mod"]>70] = 70
grouped_df = order_products_prior_df.groupby(["add_to_cart_order_mod"])["reordered"].aggregate("mean").reset_index()

plt.figure(figsize=(12,8))
sns.pointplot(grouped_df['add_to_cart_order_mod'].values, grouped_df['reordered'].values, alpha=0.8, color=color[2])
plt.ylabel('Reorder ratio', fontsize=12)
plt.xlabel('Add to cart order', fontsize=12)
plt.title("Add to cart order - Reorder ratio", fontsize=15)
plt.xticks(rotation='vertical')
plt.show()

__results___46_0.png

当たり前と言えば当たり前ですが、最初の方にカートへ入れた商品ほど再購入しています。恒常的に購入する商品は先にカートへ入れているということがわかります。

時間 - 再購入率
曜日、1日の時間について、再購入率で見ていきましょう。
5.1.2.で作成した3つのグラフを、reorderedの平均値を指標として作成します。

order_products_train_df = pd.merge(order_products_train_df, orders_df, on='order_id', how='left')
grouped_df = order_products_train_df.groupby(["order_dow"])["reordered"].aggregate("mean").reset_index()

plt.figure(figsize=(12,8))
sns.barplot(grouped_df['order_dow'].values, grouped_df['reordered'].values, alpha=0.8, color=color[3])
plt.ylabel('Reorder ratio', fontsize=12)
plt.xlabel('Day of week', fontsize=12)
plt.title("Reorder ratio across day of week", fontsize=15)
plt.xticks(rotation='vertical')
plt.ylim(0.5, 0.7)
plt.show()

__results___48_0.png

特定の曜日で再購入率が増減するわけではないようです。

grouped_df = order_products_train_df.groupby(["order_hour_of_day"])["reordered"].aggregate("mean").reset_index()

plt.figure(figsize=(12,8))
sns.barplot(grouped_df['order_hour_of_day'].values, grouped_df['reordered'].values, alpha=0.8, color=color[4])
plt.ylabel('Reorder ratio', fontsize=12)
plt.xlabel('Hour of day', fontsize=12)
plt.title("Reorder ratio across hour of day", fontsize=15)
plt.xticks(rotation='vertical')
plt.ylim(0.5, 0.7)
plt.show()

__results___49_0.png

朝に注文した商品は、再購入である可能性が高いです。

grouped_df = order_products_train_df.groupby(["order_dow", "order_hour_of_day"])["reordered"].aggregate("mean").reset_index()
grouped_df = grouped_df.pivot('order_dow', 'order_hour_of_day', 'reordered')

plt.figure(figsize=(12,6))
sns.heatmap(grouped_df)
plt.title("Reorder ratio of Day of week Vs Hour of day")
plt.show()

__results___50_0.png

全体的に朝の再購入率が高いです。週末の6-8時、火・水の5-6時が特に高くなっています。
5.1.2.では、土日の午前午後に注文が多かったです。
しかし再購入という面で見ると、平日休日関係なく早朝に多いとわかりました。

5.2. Instacartがblog上で公開している解析情報

Mediumの記事では、Instacartがオープンデータの紹介をしています。記事内で使用しているデータは、今回のコンペで取り扱うデータと同様のようです。また、データの紹介だけでなく、いくつかの解析結果も掲載しています。
ここでは記事で紹介されている、2つの図について説明します。

1-jwDcKJTXV8D1DK0KOlUJAQ.png

この図は購入数と再購入率について、aisle別でプロットした散布図です。
生鮮果物は生鮮野菜より再購入率が高いです。野菜は果物よりもレシピ用で途切れ途切れに購入している結果かもしれません。
また、スープやパン生地の材料といった食料品は再購入率が最も低いです。そもそも需要頻度が少ないからでしょう。

1-wKfV6OV-_1Ipwrl7AjjSuw (1).png

健康系のスナックや主食品は午前中の購入が多く、アイスクリーム(特にHalf Baked, The Tonight Dough)は夕方の購入が多いです。
最も最近(夕方)に注文を受けた商品トップ25のうち、24がアイスクリーム、そして25番目が冷凍ピザでした。

後半の図を再現したのが、Chippyです。
この図はRで書かれており、300行程度のボリュームです。興味のある方はチェックして見てください。

5.3. light GBMを使用したベンチマーク

(あとで追記します。)