[レコメンドシステム（ユーザベース協調フィルタリング）]をコーディングしてみる

# はじめに
レコメンドシステムについて調べてみました。
レコメンドシステムとは、ユーザの好みにあったコンテンツなどをおすすめすることです。
例えば、Amazonの例にしてみます。
<img width="896" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/b8d8b7ad-5120-2f28-390e-360a50269765.png">
私の場合、Kindleストアのおすすめ商品に並んでいるのは、だいたい「ヒナまつり」です。
私自身も、ヒナまつりを購入しているので、おすすめ商品として出てくるのはわかります。
しかし、「かぐや様に告らせたい」や「あそびあそばれ」は購入していないのに、おすすめ商品としてでてきます。
これは、ヒナまつりを購入している人は、上記の2冊も購入している傾向にあるよ！ということで、おすすめ商品にでてくるのだと思います。
これが、レコメンドシステムです。

このレコメンドシステムは、様々な種類があります。

| レコメンドの種類|詳細|例|
|:-----------------:|:------------------:|:------------------:|
|アイテムベースレコメンド（テキストマイニング）|商品名や商品説明などの情報を元に、今見ている商品と類似性の高い商品を抽出し紹介。お客様の行動履歴がいらないレコメンドです。|「関連アイテム」「こちらの商品もおすすめ」|
|協調フィルタリング（（購買履歴タイプ））|そのお客様の購買履歴を分析し、同じような購買をするお客様が買っている商品を紹介|「この商品を買った人はこんな商品も買っています」|
|パーソナライズドレコメンド|お客様の行動履歴を基にお客様の嗜好性を分析し、リアルタイムでそのお客様が好みそうな商品を紹介します。|「あなたへのおすすめ商品」|
|ルールベースレコメンド（キャンペーンレコメンド）|サイト運営者が紹介したい商品を、運営者が定めたルールに従って紹介します。|「ピックアップアイテム」|

（[引用元](https://it-koala.com/recommend-engine-138)）

Amazonの例では、実際に購入履歴を分析し（ひなまつりを購入していると分析し）同じような購買をするお客様が買っている商品（かぐや様に告らせたい・あそびあそばれ）を紹介しているため、協調フィルタリングな気がします。

# 協調フィルタリングについて
協調フィルタリングの作成手順は下記の通りです。
1. ユーザが購入した（閲覧した／評価した）ものテーブルを作成
2. ユーザ間の類似スコアを計算
3. ユーザに似ている別のユーザを見つける
4. そのユーザが購入した（閲覧した／評価した）もので、まだしていないものをおすすめする。

例えば、ユーザが購入したテーブルを下記のように定義します。

1. ユーザが購入した（閲覧した／評価した）ものテーブルを作成

|ユーザA|ユーザB|ユーザC|
|:--:|:--:|:--:|:--:|
|ひなまつり１|ひなまつり１|ドラえもん１|
|ひなまつり２|ひなまつり２|どらえもん２|
|ひなまつり３|あそびあそばれ１|クレヨンしんちゃん１|
|ひなまつり４|あそびあそばれ２|クレヨンしんちゃん２|

次に、ユーザ間の類似スコアを計算します。

計算のイメージは、下記の様な感じです。

<img width="887" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/efc0bcdf-0756-408d-aa38-72180cd55894.png">

上記のことを、ユーザBとユーザCの関係、ユーザCとユーザAの関係も繰り返し処理すると、
下記のようになると思います。

|ユーザAとユーザBの関係|ユーザBとユーザCの関係|ユーザCとユーザAの関係|
|:--:|:--:|:--:|:--:|
|1+1+0+0|0+0+0+0|0+0+0+0|
|1+1+0+0|0+0+0+0|0+0+0+0|
|1+1+0+0|0+0+0+0|0+0+0+0|
|1+1+0+0|0+0+0+0|0+0+0+0|

以上より、ユーザAとユーザBが似ていることを示せます。
ユーザAが購入していて、ユーザBが購入しているものは「あそびあそばれ１」と「あそびあそばれ２」になります。
そのため、ユーザAへのおすすめ商品としては、「あそびあそばれ１」「あそびあそばれ２」が表示され、ユーザBへのおすすめ商品としては、「ひなまつり３」「ひなまつり４」が出てくることになります。

# 協調フィルタリングの問題点
協調フィルタリングの問題点としては、下記があります。
1. ユーザの好みが変化する。
2. ユーザの数が多い
3. ユーザの行動理由が異なる場合がある。

1. ユーザの好みが変化する。
ユーザの好みは変化します。上の例でいうと、ユーザAとユーザBは似ているユーザと判定していますが、急遽ユーザBの好みが変わってしまう場合もあります。そうすると、ユーザAへのおすすめ商品は、「これは本当におすすめ商品なのか？」と疑えてしまうような商品がレコメンドされてしまう可能性があります。

2. ユーザの数が多い
ユーザが多ければ多いほど、計算量が多くなります。
そのため、ユーザ数が多くなればなるほど、処理に時間がかかってしまいます。

3. ユーザの行動理由が異なる場合がある。
何らか理由（例えば、商品の割引があったり）により、購入している場合もあります。


# 使用するデータ
使用するデータは、MovieLensを使います。
MovieLensは、レコメンドシステムの開発やベンチマークのために作成された、映画レビューのデータセットです。GroupLens Researchプロジェクトの１つで、研究目的・非商用で下記ウェブサイトが運用されております。
https://grouplens.org/datasets/movielens/

その中でも「ml-100k」というデータセットを使用していきます。

# コーディング
```python
import pandas as pd

colname1 = ["user_id","movie_id","ratings"]
rates = pd.read_csv(r"hoge\ml-100k\ml-100k\u.data",engine="python",sep='\t',names=colname1,usecols=range(3))

colname2 = ["movie_id","title"]
mov = pd.read_csv(r"hoge\ml-100k\ml-100k\u.item",engine="python",sep="|",names=colname2,usecols=range(2))
```

ratesの中を見てみましょう。

<img width="197" alt="3.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/d32a78fa-bdca-f2ad-4926-c3b5e51bebbe.png">

10000レコードのデータがあり、user_idとmovie_idとratingsがあります。
user_idは個人を一意に識別するためのコードであり、movie_idは映画を一意に識別するためのコードであることがわかります。また、ratingsは映画の評価（5段階評価）であることがわかります。

では、1人の人がどの程度映画のレビューをしているのでしょうか。

```python
rates["user_id"].value_counts().sort_values()
rates["user_id"].value_counts().mean()
```

<img width="247" alt="4.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/a7f93615-8dfa-ad38-f722-d29793dc6f6b.png">

user_idが895の人は、20本の映画をレビューしておりますが、405の人は、737本の映画をレビューしていることがわかりました。
また、平均をとってみたところ、101本、中央値をとってみたところ、1人あたり、65本でした。
とてもレビュー書いていますね。

次に、movをみてみます。
<img width="296" alt="5.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/c5c7bc6d-1a1c-1fa6-3425-5320b976d519.png">

映画自体は、1682本しかないみたいです。
つまり、user_idが405の人は、45%近く映画のレビューをしていることになります。すごいですね。

では、これらをマージしてみます。

```python
df = pd.merge(mov,rates)
```

`merge`した結果、下記のようになりました。

<img width="258" alt="6.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/91b794fc-18ed-d80f-02ba-88a35986fa80.png">

次に、`df`のデータの形を変換します。
titleの項目を縦持ちから横持ちにします。
indexはuser_idにし、columnsはtitleにし、表中の値をratingsにします。

```python
df_pivot = df.pivot_table(index={"user_id"},columns={"title"},values="ratings")
```
<img width="744" alt="7.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/4b61f070-fb08-d511-d6ce-06ca20ec0027.png">

では、スターウォーズを見た人は、他にどのような映画をみているのか見てみます。

まず、スターを評価したユーザを抽出します。

```python
SW_df = df_pivot["Star Wars (1977)"]
```
<img width="314" alt="8.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/6df63d96-c48a-8ca6-d443-b60c7e429d5b.png">

スターウォーズを見た人は、943人いるみたいですね。

次に、スターウォーズを見たユーザの評価と、他のユーザの評価のペアワイズ相関を計算します。

`pandas`にはペアワイズ相関を計算するために`pd.corrwith`関数があります。

- DataFrameとDataFrameの相関を取得
- SeriesとDataFrameの相関を取得

の二つができます。

```python:DataFrame同士のcorrwith
df1 = pd.read_csv("hoge1.csv")
df2 = pd.read_csv("hoge2.csv")
df_t = df1.corrwith(df2)
```

```python:SeriesとDataFrameのcorrwith
df1 = pd.read_csv("hoge1.csv")

#df1から特定カラムを抽出
sr = df1["piyo"]
df2 = pd.read_csv("hoge2.csv")
df_t = df1.corrwith(sr)
```

それぞれ結果が異なります。

DataFrame同士の場合、カラム名が一致知っている列の相関を取得します。
SeriesとDataFrameの場合、`corrwith`関数の引数がSeries型だった場合、
カラム名に関係なく全ての相関を取得します。

<img width="850" alt="9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/fc2abffd-fed4-c51d-a4b5-75f77ca25f81.png">

今回はスターウォーズを見たユーザの評価と、他のユーザの評価の相関を取りたいため、
Series型とDataFrame型で比較します。

```python
#SW_df = df_pivot["Star Wars (1977)"]をしているので
#SW_dfには、Series型です。
simiSW_df = df_pivot.corrwith(SW_df)
```


ここでやっていることは、

|       | スターウォーズ | 映画１ | 映画２ | 映画３ | 映画４ | 映画５ |
|:-----:|:--------------:|:------:|:------:|:------:|:------:|:------:|
| user1 |       94       |   72   |   94   |   55   |   73   |   41   |
| user2 |       92       |   74   |   96   |   52   |   92   |   56   |
| user3 |       91       |   77   |   92   |   57   |   84   |   53   |
| user4 |       97       |   74   |   95   |   60   |   93   |   43   |
| user5 |       98       |   75   |   90   |   57   |   81   |   55   |
| user6 |       95       |   77   |   93   |   60   |   79   |   66   |
| user7 |       98       |   74   |   100  |   57   |   88   |   50   |

という表があった時、
スターウォーズと映画1のスコア相関
スターウォーズと映画2のスコア相関
スターウォーズと映画3のスコア相関
スターウォーズと映画4のスコア相関
スターウォーズと映画5のスコア相関
を算出しています。

結果は下記のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/0c0a545e-2dec-1063-c1ed-7f9d8b80c42b.png)
という結果になりました。

スターウォーズのSeriesを使って`corrwith`をした結果、下記のような結果になりました。
<img width="297" alt="10.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/22b9163d-accd-f57e-e5fc-bf4ee3b8fdac.png">

この中には、NaNもあるため、下記で削除します。

```python
#dropna()で削除。ついでにDataFrameにキャスト
simiSW_df = pd.DataFrame(simiSW_df.dropna())
```

すると、下記のようになりました。
<img width="259" alt="11.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/2a820dca-75ab-19ad-677b-e6758bc37d16.png">

NaNを消さないと、1664。NaNを消すと1410になりました。

次に、見やすくするためにカラム名の変更を行い、ソートします。

```python
#indexをつけ直す。
simiSW_df = simiSW_df.reset_index()
simiSW_df.columns = ["title","Score"]
simiSW_df.sort_values("Score",ascending=False)
```
結果は下記の通りです。
<img width="274" alt="12.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/e62e615a-5b5f-387e-68ef-5a92e3c63d03.png">

Scoreが1の部分が多いような気もします。1ということは類似度が100という意味なので、ちょっと考えにくいです。

もう少し拡大してみます。

```python
simiSW_dfs[simiSW_dfs["Score"]>=0.9]
```

結果は下記のようになります。
<img width="302" alt="13.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/6db7ac5c-a506-521d-853f-286f3b5f78ca.png">
やはり、相関が1のところが多いような気がします。

下記の仮説を考えてみます。
`人気具合が異なる映画を同列に考えてはいけない`
例えば、スター・ウォーズは一般的に考えて、有名な映画で、人気のある映画です。とても人気のある映画と、あまり人気がなく、視聴者数が少ない映画を一緒くたにレコメンド分析すると、おかしくなってしまうのではないかと仮説をしました。つまり、何らかの特性で映画を区分わけして分析する必要があると仮説をたてます。

<img width="831" alt="14.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/a375d7ae-27a8-25ba-0a67-60dbeefd60ac.png">

現在のデータを考えると、ratingsから考えるか、映画のレビュー数（映画の視聴者人数）のどちらかから、映画の特性を分割できそうです。

一旦、映画のレビュー数（映画の視聴者人数）から

1. スターウォーズのような多くの人が視聴している映画
2. 少数人数が視聴している映画

の二つに分類してみます。

```
#titleでグルーピングして、ratingsの数と平均値を集計する。
df_stat = df.groupby("title").agg({"ratings":[np.size,np.mean]})
df_stat.head(10)
```
結果は下記のようにでてきます。
<img width="306" alt="15.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/11444e9b-6b84-e3c2-1994-50bdcca1d552.png">


ついでにヒストグラムも見てみます。


```python
hist(df_stat["ratings"]["size"])
```

結果は下記のような感じです。
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/a839cecc-b935-b70a-ea91-5f57de3f26c8.png)

上記のヒストグラムをみると、基本的に0~100が一番多く、どんどん小さくなっていることがわかります。

```python
df_stat2 = df_stat.reset_index()
df_stat2[df_stat2["title"]=="Star Wars (1977)"]

#わざわざreset_index()を使わなくても、下記でも大丈夫
df_stat[df_stat.index=="Star Wars (1977)"]
```
としたところ、
<img width="199" alt="17.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/8d46fadb-5cde-cae5-92d9-2e451fb3420e.png">

という結果になりました。
まさかのsizeが583という、かなり大きな値がわかりました。
583人の人たちによってratingsがつけられている映画と、100人ぐらいの人たちによってratingsがつけられている映画を同列に扱って相関係数を算出しても、あまり意味がないものになってしまうと考えられるため、sizeが少ないものは切り離して分析したほうがよいということがわかりました。

```python
#sizeが200以下の映画を対象に分析します。
popdf_stat = df_stat["ratings"]["size"]>=200

#前段で作成したpopdf_statを元に、ratingsのsizeでソーティングする
df_stat[popdf_stat].sort_values([("ratings","size")],ascending=False)[:15]
```
結果は下記の通りです。
<img width="238" alt="18.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/2d5f761f-95fa-ab6f-ae6d-bc82d60a933d.png">

スターウォーズが最も評価されており、次点でコンタクト、ファーゴ、ジェダイの帰還ということがわかりました。

では、最後にsizeが200以下で絞った映画と、スター・ウォーズとペアワイズ相関をとったデータフレームを`pd.merge`関数を使ってマージします。

```python
df2 = df_stat[popdf_stat]

#Scoreでソーティング
df_v1.sort_values(["Score"],ascending=False)[:15]
```

結果は下記の通りです。

<img width="410" alt="19.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284911/6adb8bc4-adb1-194d-20cc-c1fa197c8716.png">

スターウォーズを元にペアワイズ相関をとっているので、一番相関が高いのは自身との相関をとっているものになります。
次点で、帝国の逆襲、ジェダイの帰還、レイダース／失われたアークということがわかりました。

以上より、スターウォーズを見ている人は、帝国の逆襲か、ジェダイの帰還か、レイダース／失われたアークがレコメンドされることになります。

#おわりに
分析の切り口として、`人気具合が異なる映画を同列に考えてレコメンドしないほうがよい`ということをしました。結果としてまぁそれっぽい結果が出たと思います。
今回、sizeを200と、適当に決めてしまっていますが、ここをどうにか自動で決めれるようにしたいです。
平均＋標準偏差とかでしょうか。こういう場合、どのように決めるのが正解なのだろうか。。。