購入履歴データからレコメンドを作成する

購入履歴データを使って「この商品を購入した方はこんな商品も購入してます」的なレコメンドツールを作成してみます。

# 作成しようと思ったきっかけ
レコメンド関連で多いと感じたのは、データに評点が付いているものでした。しかし私が持っている購入履歴データには評点が付いていないため同様のロジックでは作成できませんでした。

よって、今回は単純に商品を購入したデータのみを使ってレコメンドを取得するロジックを作成していきたいと思います。

# レコメンドロジックについて
今回は色々検討した結果、以下のようなロジックにすることにしました。

```
1. 購入した情報を使って類似ユーザーを抽出
2. 類似ユーザーが選択した他の商品を抽出
3. 2で選択率の高いもの順に返す
```
くどいかもしれませんが、今回扱う購入履歴データには`評点`がありません。
よって今回は

```
同じ商品を選択している率が高い　= 類似度が高い
```
とすることにしました。

よって、ベクトルでの類似度を求める`ユークリッド距離`や`ピアソンの積率相関係数`よりも、集合を使った`Jaccard係数`を使うのが良さそうなのでそちらを採用しました。

https://en.wikipedia.org/wiki/Jaccard_index#Generalized_Jaccard_similarity_and_distance

ちなみに`類似度`について調べた際に、こちらのサイトの解説がとても分かりやすく参考になりました。
https://mieruca-ai.com/ai/jaccard_dice_simpson/

# 作成するプログラム
今回作成するファイルは2つです。

```
├── data.py          ← 購入履歴データ
└── get_recommend.py ← レコメンド取得プログラム
```
ある商品id（複数可）を指定すると、オススメな商品idを返してくれるイメージです。

購入履歴データは説明しやすいためにこのようにさせていただきましたが、履歴データをファイルからDBに変えるとより実用的かと思います。

# 履歴データの準備
購入履歴データから以下のような形式のデータをあらかじめ作成しておきます。

```python:data.py
data_set = {
	'user1': [111, 222, 333, 444, 555],
	'user2': [111, 222, 333, 444, 555, 666, 777],
	'user3': [222, 444, 666],
	'user4': [111, 333, 555, 777],
	...
}
```
各ユーザー毎に購入した商品idをまとめています。
`user1`の部分は購入したユーザーが特定できれば良いので、メールアドレスなどが良いかもしれません。

# 購入した情報を使って類似ユーザーを抽出

最初に商品idと履歴データから類似ユーザーを取得するメソッドを作成します。

```python
def get_similar_user(item_ids, sales_data):
	"""
	類似度が高いユーザーを、指定した商品idから抽出する

	Parameters
	----------
	item_ids : list
		指定した商品id
	sales_data : dict
		購入したユーザーデータ

	Returns
	-------
	target_users : list
		類似度が高い順にソートされたユーザー情報
	"""
	target_users = {};

	# 全ての購入履歴データから類似度を検証させる
	for user in sales_data:
		# 類似度を算出
		jaccard = jaccard_distance(set(item_ids), set(sales_data[user]))

		# 全く同じデータは省く
		if jaccard != 1.0:
			target_users[jaccard] = user

	# 類似度が高い順にソートして返す
	return sorted(target_users.items())
```

類似度の算出部分にはJaccard係数を算出してくれるライブラリがあったので使わせていただきました。

詳細はこちらをご覧ください。
http://www.nltk.org/_modules/nltk/metrics/distance.html

# 類似ユーザーが選択した他の商品を抽出

次に類似ユーザーが購入した商品idを取得するメソッドを作成します。

```python
def get_target_item_ids(item_ids, target_user, sales_data):
	"""
	類似度が高いユーザーを対象に、非選択の商品idを抽出する

	Parameters
	----------
	item_ids : list
		指定した商品id
	target_user : list
		類似度の高いユーザー
	sales_data : dict
		購入したユーザーデータ

	Returns
	-------
	target_item_ids : list
		非選択の商品id
	"""
	target_item_ids = []

	# 類似度が高いユーザー分処理
	for jaccard, user in target_user:
		# 指定した商品id以外の商品idを取得
		diff = list(set(sales_data[user]) - set(item_ids))

		# 取得した製品idをリストに追加
		target_item_ids.extend(diff)

	return target_item_ids
```


# 購入率の高いもの順に返す

類似度の高いユーザーが購入した商品情報から購入数が多いもの順に並べ替えたものを取得するメソッドを作成しようと思ったのですが...なんと1行で書けました（笑）

```python
counter = collections.Counter(target_product_ids).most_common()
```
これだからPythonは素敵ですよね。

使い方はこちらを参考にして下さい。
https://docs.python.jp/3/library/collections.html#collections.Counter

# 完成したもの
```python
import argparse
import collections
from nltk.metrics import jaccard_distance
from data import data_set

# コマンドライン引数の設定
parser = argparse.ArgumentParser(description='This is a script that returns product id with high similarity')
parser.add_argument('-d', '--data', help = 'Please specify product id with comma separator', required = True)

def get_similar_user(item_ids, sales_data):
	"""
	類似度が高いユーザーを、指定した商品idから抽出する

	Parameters
	----------
	item_ids : list
		指定した商品id
	sales_data : dict
		購入したユーザーデータ

	Returns
	-------
	target_users : list
		類似度が高い順にソートされたユーザー情報
	"""
	target_users = {};

	# 全ての購入履歴データから類似度を検証させる
	for user in sales_data:
		# 類似度を算出
		jaccard = jaccard_distance(set(item_ids), set(sales_data[user]))

		# 全く同じデータは省く
		if jaccard != 1.0:
			target_users[jaccard] = user

	# 類似度が高い順にソートして返す
	return sorted(target_users.items())


def get_target_item_ids(item_ids, target_user, sales_data):
	"""
	類似度が高いユーザーを対象に、非選択の商品idを抽出する

	Parameters
	----------
	item_ids : list
		指定した商品id
	target_user : list
		類似度の高いユーザー
	sales_data : dict
		購入したユーザーデータ

	Returns
	-------
	target_item_ids : list
		非選択の商品id
	"""
	target_item_ids = []

	# 類似度が高いユーザー分処理
	for jaccard, user in target_user:
		# 指定した商品id以外の商品idを取得
		diff = list(set(sales_data[user]) - set(item_ids))

		# 取得した製品idをリストに追加
		target_item_ids.extend(diff)

	return target_item_ids


if __name__== '__main__':
	# コマンドライン引数から指定したデータをセット
	args     = parser.parse_args()
	item_ids = [int(id.strip()) for id in args.data.split(',')]

	# 類似度が高いユーザーを、指定した商品idから抽出する
	target_users = get_similar_user(item_ids, data_set)

	# 類似度が高いユーザーを対象に、非選択の商品idを抽出する
	target_item_ids = get_target_item_ids(item_ids, target_users, data_set)

	# 選択数を算出
	counter = collections.Counter(target_item_ids).most_common()

	# 商品idのみをカンマ区切りの文字列にして表示
	print(','.join([str(id) for id, count in counter]))
```

# 実行結果
```sh
$ python get_recommend.py -d 777,222
555,333,111,444,666
```