H＆Mにおけるレコメンデーション

みなさんこんにちは、現在Aidemyを受講しており、データ分析を勉強しております。

学習したことを活かし、この記事では
カグルのＨ＆Ｍのレコメンデーションモデルをしていこうと思います。
今回のコンペティションはこのような内容となっています。
https://www.kaggle.com/competitions/h-and-m-personalized-fashion-recommendations

# 目次
[1.コンペに参加した理由](#1コンペに参加した理由)
[2.ライブラリのインストール](#2ライブラリのインストール)
[3.必要なデータの読み込み](#3必要なデータの読み込み)
[4.データクレンジング](#4データクレンジング)
[5.マトリクスの理解](#5マトリクスの理解)
[6.自作関数の作成](#6自作関数の作成)
[7.DataFrameを分割する](#7DataFrameを分割する)
[8.グリッドサーチ](#8グリッドサーチ)
[9.モデルをフィットさせていく](#9モデルをフィットさせていく)
[10.提出用のデータを作成](#10提出用のデータを作成)
[11.考察](#11考察)
[12.参考文献](#12参考文献)
[13.実行環境](#13実行環境)

# 1.コンペに参加した理由
なぜ、私がこのコンペティションに参加したのかを簡単に言いますと、
データサイエンスの知見を使って、レコメンドシステムを使い、様々な企業の社会的価値を創出していきたいと考えているため、今回参加させて頂きました。


前段ですが、このコンペディションのEDAも書いたので、これも添えて頂きたいと思います。
H&M Personalized Fashion Recommendations:
https://www.kaggle.com/code/uchiiyusaku/h-m-eda

# 2.ライブラリのインストール
```
!pip install --upgrade implicit 
```
```
#初期の必要なライブラリをインポート
import os; os.environ['OPENBLAS_NUM_THREADS']='1'
import numpy as np
import pandas as pd
import implicit
from scipy.sparse import coo_matrix
from implicit.evaluation import mean_average_precision_at_k

```
今回のデータセットは合計で４つあり、
transactions_train.csv  誰がいつ何を買ったかが履歴として残っている
sample_submission.csv  最終的なデータセットの形
customers.csv  顧客情報
articles.csv　 商品情報
これらをpandasを用いてそれぞれ読み込んでいきます。
また、`!pip install --upgrade implicit`で`implicit`ライブラリをアップグレードしないとグリッドサーチの部分でエラーが生じてしまうので、必ず実行しましょう。
# 3.必要なデータの読み込み
```

base_path = '../input/h-and-m-personalized-fashion-recommendations/'
csv_train = f'{base_path}transactions_train.csv'
csv_sub = f'{base_path}sample_submission.csv'
csv_users = f'{base_path}customers.csv'
csv_items = f'{base_path}articles.csv'

df = pd.read_csv(csv_train, dtype={'article_id': str}, parse_dates=['t_dat'])
df_sub = pd.read_csv(csv_sub)
dfu = pd.read_csv(csv_users)
dfi = pd.read_csv(csv_items, dtype={'article_id': str})
```
# 4.データクレンジング

今回使用するtransactionデータというのは、
2018/09/20から2020/09/22のデータとなります
```

df['t_dat']
0          2018-09-20
1          2018-09-20
2          2018-09-20
3          2018-09-20
4          2018-09-20
              ...    
31788319   2020-09-22
31788320   2020-09-22
31788321   2020-09-22
31788322   2020-09-22
31788323   2020-09-22
```
二年間のデータを使用するとなると、持ち前のパソコン上負荷が大変重くなってしまう為、
１カ月のデータを用いていきたいと思います
```
"""
Trying with less data(少ないデータで試す)
"""

df = df[df['t_dat'] > '2020-08-21']
df.shape
"""
(1190911, 5)
"""
```
データを一か月にしたところで、学習用データセットと検証用データセットにわけていこうと思います。
３週間分を学習用とし、１週間分を検証用データにします
```
validation_cut = df['t_dat'].max() - pd.Timedelta(7,"d")

"""
dfの最新の日付から、７日分を差し引き、
validation_cutより大きい日付は検証用（１週間）
　　　　　　　　　 小さい場合は学習用（３週間）とします。
"""
```

## 4.1 アイテム・ユーザー名を扱いやすくする

一旦、データを見てみましょう
```
df.head()
"""
　　　　　　　　t_dat	customer_id	　　　　　　　　　　　　　　　　　　　　　　article_id	price	sales_channel_id
30597413	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0913688003	0.033881	2
30597414	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0913688003	0.033881	2
30597415	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0923460001	0.042356	2
30597416	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0934380001	0.050831	2
30597417	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0913688001	0.033881	2
"""
```

cutomer_idは顧客を識別する番号で、article_idは商品画像を識別する番号です。
ただし、このcutomer_idやarticle_idだと、なかなか扱うのが面倒です。
その為
to_listとenumerateの組み合わせで、データを扱いやすくします。

customer_idのみを取り出すと、seriesになります。
そのseries型を```tolist```を用いることで、リスト形式にします。
リスト型にしたあとは、そのリストの要素にdict型で番号を振ります。それが、
```enumerate```という関数になります。そうすれば、複雑なcustomer_idも数字で処理することが出来るのです。
実際にコードを書いてみます。
```
ALL_USERS = dfu['customer_id'].unique().tolist()
ALL_ITEMS = dfi['article_id'].unique().tolist()

#上では、それぞれのカスタマー名と商品名の固有の値を抽出させ、リストに変換させている

user_ids = dict(list(enumerate(ALL_USERS)))
item_ids = dict(list(enumerate(ALL_ITEMS)))
"""
enumerate()を用いることにより、インデックス番号, 要素と取得することが出来る。
商品、アイテム名を番号処理することが可能。
"""

user_map = {u: uidx for uidx, u in user_ids.items()}
item_map = {i: iidx for iidx, i in item_ids.items()}

df['user_id'] = df['customer_id'].map(user_map)
df['item_id'] = df['article_id'].map(item_map)

del dfu, dfi
```
それではデータの処理が出来たところでデータフレームを確認していきましょう。
```
df.head()
"""

           t_dat	    customer_id	                                 article_id	        price	sales_channel_id	user_id	item_id
30597413	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0913688003	0.033881	   2	            38	103595
30597414	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0913688003	0.033881	   2	            38	103595
30597415	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0923460001	0.042356	   2	            38	104483
30597416	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0934380001	0.050831	   2	            38	105214
30597417	2020-08-22	0001d44dbe7f6c4b35200abdb052c77a87596fe1bdcc37...	0913688001	0.033881	   2	            38	103593
"""
```

これで、ユーザーIDとアイテムIDを扱いやすくすることができました。


# 5.マトリクスの理解
行列分解は、`AlternatingLeastSquares`のモデルを用いるには必須になります。
まずは、簡単に行列分解について触れていきます。

`no.ones`を使うことにより、1が並ぶ配列を作ることが出来ます。
レコメンドにはとても大切なもので、
[1,2,3,4,5] ユーザーの配列
[a,b,c,d,e] アイテムの配列
[1,1,1,1,1] np.onesを使った配列
として、例えば
１の人がaを買って、３の人がcを買ったとします。
そうすると
```
   a b c d e 
1  1 0 0 0 0
2  0 0 0 0 0
3  0 0 1 0 0
4  0 0 0 0 0
5  0 0 0 0 0
```
このように表示することができ、誰がなにを買ったかわかるようになります。

それで、この0に注目して頂きたいと思います。
仮にアイテムの数が１億個あったとして、ユーザーがaというアイテムしか買っていないとします。
そうすると、0が大量に出てくることになります。これは分析する上で非常にメモリが消費してしまいます。
そのような行列を疎行列と呼びます。

それで、coo_matrixを使うことによってどのようなことが出来るのかというと
買った１というデータに着目してデータを加工してくれます。

例えば先ほどのデータをcoo_matrixを使うとどうなるのかというと
```
(0, 0)	1 # 0行目0列目に１がある
(2, 2)	1 # 2行目2列目に１がある
```
ということになり、先ほどの行列よりもはるかに削減されたかと思います。
それが```coo_matrix```という行列分解になります。

# 6.自作関数の作成

## 6.1 データを行列分解させる関数
次に、数値化したユーザーとアイテム情報を使って、coo_matrixを作っていきます
```
def to_user_item_coo(df):
    """
    Turn a dataframe with transactions into a COO sparse items x users matrix
    トランザクションを含むデータフレームをCOOスパースアイテム×ユーザー行列に変換する
    """
    row = df['user_id'].values
    col = df['item_id'].values
    data = np.ones(df.shape[0])
    coo = coo_matrix((data, (row, col)), shape=(len(ALL_USERS), len(ALL_ITEMS)))
    return coo
```
## 6.2 学習用と検証用データに分けていく
```
def split_data(df, validation_days=7):
    validation_cut = df['t_dat'].max() - pd.Timedelta(validation_days,"d")
    df_train = df[df['t_dat'] < validation_cut]
    df_val = df[df['t_dat'] >= validation_cut]
    return df_train, df_val
```
## 6.3 `coo_matrix`と`csr_matrix`を作成する関数

```
"""
Split into training and validation and create various matrices
    トレーニング用と検証用に分割し、各種マトリクスを作成する
    
        
         Returns a dictionary with the following keys:
            coo_train: training data in COO sparse format and as (users x items)
             csr_train: training data in CSR sparse format and as (users x items)
             csr_val:  validation data in CSR sparse format and as (users x items)
作成した学習用データと検証用データに分け、それぞれをCOOスパースアイテム×ユーザー行列に変換する
その後、作成したスパース行列を```tocsr```を使って、データをcsrに変換している。
"""
def get_val_matrices(df, validation_days=7):
    df_train, df_val = split_data(df, validation_days=validation_days)
    coo_train = to_user_item_coo(df_train)
    coo_val = to_user_item_coo(df_val)

    csr_train = coo_train.tocsr()
    csr_val = coo_val.tocsr()
    
    return {'coo_train': coo_train,
            'csr_train': csr_train,
            'csr_val': csr_val
          }
```
## 6.4 モデルの関数を作成

モデルの学習に使うデータは３週間分のcoo_trainデータです
```show_progress```を用いることにより、データの進捗を確認することができます。
作成したモデルの正確性を確かめるために```mean_average_precision_at_k```
を使い、検証していきます
```
def validate(matrices, factors=200, iterations=20, regularization=0.01, 
show_progress=True):
    coo_train, csr_train, csr_val = matrices['coo_train'], matrices['csr_train'], matrices['csr_val']
    
    model = implicit.als.AlternatingLeastSquares(factors=factors, 
                                                 iterations=iterations, 
                                                regularization=regularization, 
                                                 random_state=42)
    # factors:    計算する潜在的な要因の数
    # iterations: データをフィットする際に使用する ALS の反復回数
    # regularization: 使用する正則化係数(L1正則化)
    
    model.fit(coo_train, show_progress=show_progress)
    #show_progress 学習の進捗を確認
    
    # The MAPK by implicit doesn't allow to calculate allowing repeated items, which is the case.
    # TODO: change MAP@12 to a library that allows repeated items in prediction
    map12 = mean_average_precision_at_k(model, csr_train, csr_val, K=12, show_progress=show_progress, num_threads=4)
    print(f"Factors: {factors:>3} - Iterations: {iterations:>2} - Regularization: {regularization:4.3f} ==> MAP@12: {map12:6.5f}")
    return map12
```
# 7.DataFrameを分割する
さきほど6で作成した関数を用いて、`matrices`に代入していく
```
matrices = get_val_matrices(df)
```
# 8.グリッドサーチ
6.4で作成したモデルと、7で分割したデータを用いてグリッドサーチをしていきます。
```
%%time
best_map12 = 0
for factors in [10, 25, 50, 75, 100]:
    for iterations in [3, 12, 14, 15, 20]:
        for regularization in [0.01]:
            map12 = validate(matrices, factors, iterations, regularization, show_progress=False)
            if map12 > best_map12: 
                best_map12 = map12
                best_params = {'factors': factors, 'iterations': iterations, 'regularization': regularization}
                print(f"Best MAP@12 found. Updating: {best_params}")
```
得られた結果がこちら
かなり時間がかかっていると思います。参考にさせて頂いたノートブックよりもかなり
```factors```や```iterations```を狭めているので、もっと幅広くすることにより、より最適なパラメーターを得られることが出来ると思います。

```
Factors:  75 - Iterations: 20 - Regularization: 0.010 ==> MAP@12: 0.00472
Factors: 100 - Iterations:  3 - Regularization: 0.010 ==> MAP@12: 0.00396
Factors: 100 - Iterations: 12 - Regularization: 0.010 ==> MAP@12: 0.00484
Best MAP@12 found. Updating: {'factors': 100, 'iterations': 12, 'regularization': 0.01}
Factors: 100 - Iterations: 14 - Regularization: 0.010 ==> MAP@12: 0.00484
Factors: 100 - Iterations: 15 - Regularization: 0.010 ==> MAP@12: 0.00482
Factors: 100 - Iterations: 20 - Regularization: 0.010 ==> MAP@12: 0.00478
CPU times: user 46min 8s, sys: 29min 29s, total: 1h 15min 38s
Wall time: 19min 58s
```
# 9.モデルをフィットさせていく
次に、最適なパラメータを得られたところで実際に当てはめていきたいと思います
```
coo_train = to_user_item_coo(df)
csr_train = coo_train.tocsr()


def train(coo_train, factors=200, iterations=15, regularization=0.01, show_progress=True):
    model = implicit.als.AlternatingLeastSquares(factors=factors, 
                                                 iterations=iterations, 
                                                 regularization=regularization, 
                                                 random_state=42)
    model.fit(coo_train, show_progress=show_progress)

    return model
   
```
再度最適なパラメーターを確認していきましょう
```
best_params

{'factors': 100, 'iterations': 12, 'regularization': 0.01}
```
```
model = train(coo_train, **best_params)
```
最適なモデルを作成できたので、実際に誰にレコメンドをするのかをデータフレームにして格納していきます。
```
def submit(model, csr_train, submission_name="submissions.csv"):
    preds = []
    batch_size = 2000
    to_generate = np.arange(len(ALL_USERS))
    for startidx in range(0, len(to_generate), batch_size):
        batch = to_generate[startidx : startidx + batch_size]
        ids, scores = model.recommend(batch, csr_train[batch], N=12, filter_already_liked_items=False)
        for i, userid in enumerate(batch):
            customer_id = user_ids[userid]
            user_items = ids[i]
            article_ids = [item_ids[item_id] for item_id in user_items]
            preds.append((customer_id, ' '.join(article_ids)))

    df_preds = pd.DataFrame(preds, columns=['customer_id', 'prediction'])
    df_preds.to_csv(submission_name, index=False)
    
    display(df_preds.head())
    print(df_preds.shape)
    
    return df_preds
```
# 10.提出用のデータを作成
```
%%time
df_preds = submit(model, csr_train);


"""
	customer_id	　　　　　　　　　　　　　　　　　　　　　　prediction
0	00000dbacae5abe5e23885899a1fa44253a17956c6d1c3...	0568601006 0762846031 0568601044 0568597006 05...
1	0000423b00ade91418cceaf3b26c6af3dd342b51fd051e...	0112679048 0111609001 0111593001 0111586001 01...
2	000058a12d5b43e67d225668fa1f8d618c13dc232df0ca...	0805000001 0804992014 0804992017 0794321011 07...
3	00005ca1c9ed5f5146b52ac8639a40ca9d57aeff4d1bd2...	0112679048 0111609001 0111593001 0111586001 01...
4	00006413d8573cd20ed7128e53b7b13819fe5cfc2d801f...	0112679048 0111609001 0111593001 0111586001 01...
"""
```
無事にレコメンデーションを行うことが出来ました。

# 11.考察

得られたデータから考察をしていきたいと思います。
```
df_preds.iloc[: , 1].value_counts()
0112679048 0111609001 0111593001 0111586001 0111565003 0111565001 0110065011 0110065002 0110065001 0108775051 0108775044 0108775015    1115625
0909370001 0909371001 0908799002 0923128001 0923028002 0881942001 0923388001 0918525001 0881942002 0921906002 0860285001 0923028001        391
0915526001 0751592001 0811525001 0923758001 0924243002 0920610005 0871527003 0909093003 0859125001 0906094004 0806388002 0817354001        374
0918522001 0877273001 0923758001 0751592001 0906352001 0909093003 0896161001 0929275001 0892455001 0859125001 0892455003 0929744001        248
0714790020 0714790028 0714790017 0714790021 0714790024 0905365001 0714790008 
Name: prediction, Length: 177644, dtype: int64

```
```df.value_counts()```で実際にレコメンドされた商品の数を見ていくと、該当のアイテムが1115625回レコメンドされています。
実際にどのようなアイテムなのかを見ていきます。

```
import cv2
import matplotlib.pyplot as plt

path = "../input/h-and-m-personalized-fashion-recommendations/images/011/0112679048.jpg"
im1 = plt.imread(path)
plt.figure(figsize=(15, 6))
plt.imshow(im1)

path = "../input/h-and-m-personalized-fashion-recommendations/images/011/0111609001.jpg"
im2 = plt.imread(path)
plt.figure(figsize=(15, 6))
plt.imshow(im2)

path = "../input/h-and-m-personalized-fashion-recommendations/images/011/0111593001.jpg"
im3 = plt.imread(path)
plt.figure(figsize=(15, 6))
plt.imshow(im3)

path = "../input/h-and-m-personalized-fashion-recommendations/images/011/0110065002.jpg"
im4 = plt.imread(path)
plt.figure(figsize=(15, 6))
plt.imshow(im4)
```

本来は```subplot```とfor文を用いればまとめて情報を見られますが、割愛します。
既に得られている商品の写真データから、```matplotlib```を用いて写真で表示してみました。
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2526914/d4790d88-e411-2cfc-1d95-ac1cc1bf3792.png)
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2526914/a729a216-3838-8c04-1e79-456e4a106dc6.png)
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2526914/78c0e606-3d70-4cf9-91a3-54bec4d5c0d1.png)
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2526914/b987f1ba-86d2-c425-353a-67fd16a83aae.png)

データを見ていくと、ストッキングや肌着が多い印象でした。
自身で作成したEDAにて、売れ筋ランキングを見てみると、ボトムスがダントツで一番人気でした。（上記のURL参照）
その傾向が強いのか、ボトムスの下に履くストッキングなどのインナーウェアがレコメンドされるのでは考察します。
また、女性のアイテムがほぼレコメンドされていることから、女性ユーザーが多い印象でした。
今回のコンペディションのデータには、女性、男性のユーザー情報がなかったので、レコメンドの数で新たな顧客層がみられることができました。

余談にはなりますが、私自身Ｈ＆Ｍで商品を買うことがあまりなく、男性のユーザーが少ないのは納得でした（笑）
これからも様々なデータに触れ、データサイエンスの知見を増やしていきたいと考えています。

# 12.参考文献
ALSについて:https://www.kaggle.com/code/julian3833/h-m-implicit-als-model-0-014
EDAについて:https://www.kaggle.com/code/lunapandachan/h-m-eda
ALSの理解: https://recruit.gmo.jp/engineer/jisedai/blog/sketchfab_implicit_feedback/

# 13.実行環境

OS:Windows 10(RAM8GB)
使用言語：Python
使用したエディター：kaggle notebook