機械学習初学者がアニメレコメンドを作ってみた【2020新版】

<div align="center">
<img width="100%" alt="2-010.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/22cffa28-e680-92eb-74e9-4835b1374dd1.jpeg">
</div>

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# はじめに
はじめまして。SIerのおっさんです。
SIerで働いておりましたが今後に漠然とした不安を感じ、この春から給付金制度を利用してAidemyさんのデータ分析講座を受講しています。最終成果物のテーマを決めるのにかなり悩みましたが、子供といっしょに楽しめそうなアニメの「レコメンド」にしました。本格的に機械学習の勉強をすることは初めてでまとまっていない部分も多々あるかと思いますが、温かい目で読んでいただけますと幸いです。

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# 目次
１．レコメンドとは
２．データ項目説明
３．候補生成（コンテンツ特徴量の作成）
４．アニメEmbeddingベクトルの可視化
５．リランク
　　・ユーザー特徴量の作成
　　・学習データセットの作成
　　・モデルの定義と学習
　　・推論
６．分析
７．リランクその２（ユーザー特徴量次元削減による精度改善）
８．まとめ



<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# １．レコメンドとは

![1-010.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/8f882503-c060-334b-ca55-976477aa52c3.png)

「レコメンド」とは簡単に言うと「おすすめシステム」のことです。

普段インターネットをやっているとAmazon、Netflix、Spotifyなどで
「この商品を購入したお客様はこちらも一緒にお買い上げいただいてます」
「あなたにおすすめの動画一覧」
など見かけることも多いかと思います。



これは「レコメンド機能（レコメンドエンジン）」と呼ばれるもので、近年ではインターネット上の様々なサービスで使われています。


今回はこのレコメンド機能を作ってみたいと思います。
尚、プログラミングはすべてGoogle colaboratory上で行っております。

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# ２．データ項目説明

レコメンドを実装する上で、元となるデータは欠かせません。

企業であれば自社のWEBサイトで蓄積されていく顧客データを元にレコメンドをつくることができますが、残念ながら私はそのようなデータは持ち合わせておりません。

そこで今回はKaggleで公開されている「Anime Recommendation Database 2020」というデータセットを利用していきたいと思います。


#### ・「Anime Recommendation Database 2020」 

https://www.kaggle.com/datasets/hernan4444/anime-recommendation-database-2020

myanimelist.net で蓄積された 325,772 人のユーザーと 17,562 のアニメからなるデータセットです。下記の5つのファイルで構成されています。


| ファイル名 |ファイルの概要説明| データ件数| 
|:--|:--|:-:|
| Anime.csv                | アニメのマスタ情報（タイトル、ジャンル、話数、放送日など） |17,562|
| Anime_with_<br>synopsis.csv  |アニメのあらすじ。|16,214|
| Animelist.csv            |ユーザーが視聴しているアニメとその評価点数のリスト。（視聴途中のものや評価をつけていないアニメも含まれる。）|　109,224,747　|
| rating_complete.csv      |ユーザーが完全に視聴したアニメとその評価点数のリスト。（視聴途中のものや評価をつけなかったアニメは含まれない）|57,633,278|
| watch_status.csv         | 視聴状況のマスタ（現在視聴中、視聴完了、視聴保留中、視聴中断、視聴予定） |5|



#### ・データ読込

まずは必要なデータを読み込んでいきます。
表に記載の通り非常に件数の多いデータなので、処理に必要となるanime.csv、rating_complete.csvの2つだけを読み込みます。

~~~py
import pandas as pd

df_anime = pd.read_csv('/content/drive/MyDrive/anime/anime.csv')
df_rating = pd.read_csv('/content/drive/MyDrive/anime/rating_complete.csv')

df_anime.head(5)
~~~



試しにanime.csvの最初の5行を表示してみます。

![2-020.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/f71725f5-2a2d-92eb-ac9e-4a53c550ae29.png)


カウボーイビバップがIDの1番目、2番目に来ているのが興味深いですね。
海外でも人気があるようなので、データの作者の並々ならぬこだわりがあるのかもしれません。

ファイルレイアウトは下記のようになっています。

~~~
■ Anime.csv
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17562 entries, 0 to 17561
Data columns (total 35 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   MAL_ID         17562 non-null  int64   アニメのMyAnimelistID
 1   Name           17562 non-null  object  アニメのフルネーム
 2   Score          17562 non-null  object  アニメの平均スコア
 3   Genres         17562 non-null  object  このアニメのジャンルのコンマ区切りリスト
 4   English name   17562 non-null  object  アニメの英語でのフルネーム
 5   Japanese name  17562 non-null  object  アニメの日本でのフルネーム
 6   Type           17562 non-null  object  テレビ、映画、OVAなど
 7   Episodes       17562 non-null  object  章の数、話数
 8   Aired          17562 non-null  object  放送日
 9   Premiered      17562 non-null  object  初演、シーズンプレミア
 10  Producers      17562 non-null  object  製作者のコンマ区切りリスト
 11  Licensors      17562 non-null  object  ライセンサーのコンマ区切りリスト
 12  Studios        17562 non-null  object  制作スタジオのコンマ区切りリスト
 13  Source         17562 non-null  object  原作（マンガ、ライトノベル、本など）
 14  Duration       17562 non-null  object  エピソードごとのアニメの長さ
 15  Rating         17562 non-null  object  評価（R指定など）
 16  Ranked         17562 non-null  object  ランク付け
 17  Popularity     17562 non-null  int64   リストに追加したユーザーの数に基づく人気度数
 18  Members        17562 non-null  int64   「グループ」に所属しているユーザー数
 19  Favorites      17562 non-null  int64   「お気に入り」に登録しているユーザー数
 20  Watching       17562 non-null  int64   アニメを見ているユーザーの数
 21  Completed      17562 non-null  int64   視聴完了したユーザーの数
 22  On-Hold        17562 non-null  int64   視聴保留にしているユーザーの数
 23  Dropped        17562 non-null  int64   アニメをドロップしたユーザーの数
 24  Plan to Watch  17562 non-null  int64   アニメを観る予定のユーザー数
 25  Score-10       17562 non-null  object  10点を獲得したユーザーの数
 26  Score-9        17562 non-null  object  9点を獲得したユーザーの数
 27  Score-8        17562 non-null  object  8点を獲得したユーザーの数
 28  Score-7        17562 non-null  object  7点を獲得したユーザーの数
 29  Score-6        17562 non-null  object  6点を獲得したユーザーの数
 30  Score-5        17562 non-null  object  5点を獲得したユーザーの数
 31  Score-4        17562 non-null  object  4点を獲得したユーザーの数
 32  Score-3        17562 non-null  object  3点を獲得したユーザーの数
 33  Score-2        17562 non-null  object  2点を獲得したユーザーの数
 34  Score-1        17562 non-null  object  1点を獲得したユーザーの数
dtypes: int64(9), object(26)
memory usage: 4.7+ MB


■ rating_complete.csv
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 57633278 entries, 0 to 57633277
Data columns (total 3 columns):
 #   Column    Dtype
---  ------    -----
 0   user_id   int64    ユーザーID
 1   anime_id  int64    このユーザーが評価したアニメのID。
 2   rating    int64    このユーザーが割り当てた評価。
dtypes: int64(3)
memory usage: 1.3 GB

~~~

これらのデータを使って進めていきたいと思います。

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# ３．候補生成（コンテンツ特徴量の作成）

「カウボーイビバップをご覧になったあなたへ」を表示したいとき、どのようにすればおすすめアニメの候補が得られるでしょうか？

カウボーイビバップを過去に見た人たちが高く評価している他のアニメを集めてくれば、なんとなく候補が得られそうです。

そこで、まずは読み込んだデータの中にあるユーザーの評価（rating）を活用して、各アニメの特徴量を作成していきます。

いろいろな手法がありますが、ここではNMF(Nonnegative Matrix Factorization: 非負値行列因子分解)という手法を用いたいと思います。

<br>

#### ・NMF(Nonnegative Matrix Factorization)

NMFとは「負の値が含まれていない行列の掛け算形式で、元の行列を分解する」手法です。
数式で表すと、分解前の行列をXとしたときには、

~~~math
X≅WH
~~~

となるような非負値行列WとHを発見することに相当します。
（≅は「およそ等しい」という意味の記号です。）

ソース：https://qiita.com/nozma/items/d8dafe4e938c43fb7ad1
![3-010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/e074b9de-cf25-37f6-a729-08b4d97c11d2.png)


今回の要素を適用すると以下のようなイメージになります。

X・・・Nがuser_id、pがanime_idの行列。中身はユーザーがつけたアニメのrating（評価）
W・・・各ユーザーに対応した長さrのベクトル
H・・・各コンテンツに対応した長さrのベクトル


NMFを用いることで、設定したrの長さのベクトル（つまり特徴量）が取得できます。
そして、これらのベクトルの類似度を計算することで、どれだけ似ているかを算出することができます。

「カウボーイビバップをご覧になったあなたへ」を表示したいときは、カウボーイビバップと類似度が高いものを候補として選べばよいということになります。


~~~py
import numpy as np
from scipy.sparse import csr_matrix
from pandas.api.types import CategoricalDtype
from sklearn.decomposition import NMF

# user_id, anime_idのユニークかつ昇順にソートされた配列を作成
user_id_unique = list(sorted(df_rating['user_id'].unique()))
anime_id_unique = list(sorted(df_rating['anime_id'].unique()))

# ratingの配列を作成
score = df_rating['rating'].tolist()

# user_id列をuser_id_uniqueをラベルとしたカテゴリ変数にキャスト
row = df_rating['user_id'].astype(
    CategoricalDtype(categories=user_id_unique)).cat.codes 

# anime_id列をanime_id_uniqueをラベルとしたカテゴリ変数にキャスト
col = df_rating['anime_id'].astype(
    CategoricalDtype(categories=anime_id_unique)).cat.codes

# 行（user_id）と列（anime_id）の疎行列X を作成
X = csr_matrix((score, (row, col)), shape=(len(user_id_unique), len(anime_id_unique)))

# NMFのモデルを作成しデータを学習させる（n_componentsは何次元に次元削減するのかの指定）
nmf = NMF(n_components=10, init='random', max_iter=30)

W = nmf.fit_transform(X)
H = nmf.components_

~~~


#### ・コサイン類似度

各アニメのベクトルからコサイン類似度を算出します。
２つのベクトルの類似度を求める際によく使われる手法です。


~~~math
{\cos( \vec{q}, \vec{d} ) = \frac{ \vec{q} \cdot \vec{d}}{  |\vec{q}| |\vec{d}|} =  \frac{ \vec{q} }{  |\vec{q}| }\cdot\frac{  \vec{d}}{  |\vec{d}| } = \frac{\sum_{i=1}^{|V|} q_i d_i}{\sqrt{\sum_{i=1}^{|V|} q_i^2} \cdot \sqrt{\sum_{i=1}^{|V|} d_i^2}}
}
~~~
参考：
http://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html
https://qiita.com/Qiitaman/items/fa393d93ce8e61a857b1


~~~py
# アニメごとにループしながら特徴量を取得、他のアニメとのコサイン類似度を求めて配列に格納する
target_anime_id_list = [1]
def get_mf_candidate(target_anime_id_list, limit=20):
  df_result_list = []
  for anime_id_sub in target_anime_id_list:

    # Hを転置。アニメごとの10個のベクトル（特徴量）を取得
    anime_vec_sub = H.T[anime_id_unique.index(anime_id_sub),:]
    
    # 類似度を格納する配列を準備
    similar_temp = []

    for anime_num_sim in range(len(anime_id_unique)):

      # コサイン類似度の算出
      cos_sim = np.dot(anime_vec_sub, H.T[anime_num_sim, :]) / (np.linalg.norm(anime_vec_sub) * np.linalg.norm(H.T[anime_num_sim, :]))
      similar_temp.append(cos_sim)

    df_result = pd.DataFrame({'score':similar_temp}, index=anime_id_unique).sort_values('score', ascending=False)

    df_result_list.append(pd.merge(df_result, df_anime[['MAL_ID', 'Japanese name']],
                                  left_index=True, right_on='MAL_ID').set_index('MAL_ID').drop(index=anime_id_sub).iloc[:limit,:])    
  return df_result_list
~~~


試しにカウボーイビバップについて各アニメとのコサイン類似度を算出し、類似度の高い方から20件をランキングすると下記のようになりました。

~~~
get_mf_candidate([1],20)

[           score                                  Japanese name
 MAL_ID                                                         
 1       1.000000                                      カウボーイビバップ
 33      0.947848                                      剣風伝奇ベルセルク
 205     0.917112                                     サムライチャンプルー
 1292    0.872859                                        アフロサムライ
 227     0.866054                                           フリクリ
 30      0.863220                                    新世紀エヴァンゲリオン
 160     0.860621                                       今、そこにいる僕
 467     0.855296                      攻殻機動隊 STAND ALONE COMPLEX
 387     0.855074                                           灰羽連盟
 1566    0.854283  攻殻機動隊 STAND ALONE COMPLEX Solid State Society
 114     0.851687                                    魁!! クロマティ高校
 770     0.851668                                       ペイル・コクーン
 801     0.851325                           攻殻機動隊 S.A.C. 2nd GIG
 3089    0.849332                                        スカイ・クロラ
 2164    0.843086                                          電脳コイル
 81      0.841772                               機動戦士ガンダム 第08MS小隊
 4672    0.840325                                       攻殻機動隊2.0
 2001    0.837236                                     天元突破グレンラガン
 4106    0.837072                                          トライガン
 47      0.836964                                     AKIRA（アキラ）]
~~~
まぁまぁそれっぽい結果になってますね。（分かりやすいようにカウボーイビバップ自身も入れています。自分自身は類似度1.00です。）
「カウボーイビバップをご覧になったあなたへ」には、このアニメたちが左が順番に表示されます。

# ４．アニメEmbeddingベクトルの可視化

ここで一旦、これまで作成してきたアニメのベクトルを可視化してみます。
アニメのベクトルは先ほどのNMFで得た結果のHの方に入っています。
まずこれをKMeansを用いて10のグループにクラスタリングします。
その後、PCAで2次元に削減し、可視化していきます。


~~~py
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

# データを標準化
H_T_norm = (H.T - H.T.mean(axis=0)) / H.T.std(axis=0)

# KMeansでクラスタリング
km = KMeans(n_clusters=10)
H_km = km.fit_predict(H_T_norm)

df_analysis = pd.DataFrame(H_km, index=anime_id_unique, columns=['cluster'])
df_analysis = df_analysis.astype({'cluster':'object'})

# 主成分分析のインスタンスを生成。
pca = PCA(n_components=2)

# データを学習し、変換する。
H_pca = pca.fit_transform(H_T_norm)

df_analysis = df_analysis.assign(pca_0=H_pca[:,0], pca_1=H_pca[:,1])

df_analysis = pd.merge(df_analysis, df_anime[['MAL_ID', 'Japanese name']],
                       left_index=True, right_on='MAL_ID').set_index('MAL_ID')
~~~

可視化のライブラリはplotlyを使います。

~~~py
# plotlyで可視化する
import plotly.express as px

fig = px.scatter(df_analysis, x='pca_0', y='pca_1',
                 hover_name="Japanese name", log_x=False, color='cluster')

fig.update_traces(textposition='top center')

fig.update_layout(
    title_text='Anime Recommendation Database 2020'
)

fig.show()
~~~



<iframe class="html" src="https://plotly-graph-test.glitch.me/" width="560" height="315"  frameborder="0"></iframe>

<div align="center">
<img width="100%" alt="4-010.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/26aa3244-6bb1-03c4-bb33-e33d0cce993b.png">
</div>

この点のひとつひとつがアニメで、クラスタリングされ色分けされています。
カウボーイビバップは中央の下の方にいますね。
カウボーイビバップと同じ3番のクラスタにいるアニメが類似度が高いものといえます。

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# ５．リランク

これまではアニメをベースとした特徴量から候補を生成してきました。

しかし、現状ではアニメの特徴量だけですので「カウボーイビバップをご覧になったあなたへ」にはどのユーザーにも同じ並び順で候補が並んでしまうことになります。

ここでパーソナライズという考え方が出てきます。
ユーザーの特徴量を用いて個人に最適な順に並べ替え、ランキングの精度を高めていく方法です。

「年齢」「性別」などパーソナライズにも様々なやり方がありますが、今回はユーザーそのものの情報をcsvの項目に持っていません。（ユーザーの情報はuser_idのみです。）

そこでアニメのマスタ情報に持っている「ジャンル」を用いてパーソナライズします。
「ユーザーがこれまでにどのジャンルをよく見ているか」という情報からユーザーの特徴量を生成し、先ほどの候補一覧を再ランキング（リランク）していきます。

### ・ジャンルリストの取得

ジャンルについて集計していきたいので、まずはユニークなジャンル一覧が欲しいです。ジャンルはマスタやコードでもっていないので、データの中から集めていきます。
anime.csvのカラム「Genres」にはジャンル名がカンマ区切りで格納されています。
（例）カウボーイビバップの場合　
　　　Action,Adventure,Comedy,Dorama,SciFi,Space

<div align="center">
<img width="400" alt="5-010.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/30b9e048-1504-1c72-7ca1-83d3708060bc.png">
</div>

全データのGenresをすべて集めて配列に格納し、重複を排除して、ジャンルのユニークなリストを取得します。

~~~py
df_anime['Genres'].str.split(', ')

genre_list = []
for list_temp in df_anime['Genres'].str.split(', '):
  genre_list = genre_list + list_temp

genre_set = set(genre_list)
genre_set
~~~

{'Action', 'Adventure', 'Cars', 'Comedy', 'Dementia', 'Demons', 'Drama', 'Ecchi', 'Fantasy', 'Game', 'Harem', 'Hentai', 'Historical', 'Horror', 'Josei', 'Kids', 'Magic', 'Martial Arts', 'Mecha', 'Military', 'Music', 'Mystery', 'Parody', 'Police', 'Psychological', 'Romance', 'Samurai', 'School', 'Sci-Fi', 'Seinen', 'Shoujo', 'Shoujo Ai', 'Shounen', 'Shounen Ai', 'Slice of Life', 'Space', 'Sports', 'Super Power', 'Supernatural', 'Thriller', 'Unknown', 'Vampire', 'Yaoi', 'Yuri'}

こんなにたくさんジャンルがあるんですね・・・。
ジャンルに日本語が多く含まれていることが、アニメ文化における日本の影響力の大きさを物語っています。ジャンルの中で分かりにくそうなものを以下にピックアップして概説してみました。
|ジャンル名|説明|数|例|
|:-:|:--|:-:|:--|
|Dementia|直訳は「認知症」だが、病的な意味合いではなく「見た人の脳を混乱させるような不思議な世界観」といった意味のようだ。|512|新世紀エヴァンゲリオン、フリクリ、Serial Experiments lain、ブギーポップは笑わない、パーフェクトブルー、パプリカ、妄想代理人|
|Demons|デーモン（悪魔）が指すように、何かしら人外のものが出現するアニメのようだ|501|剣風伝奇ベルセルク、幻想魔伝 最遊記、犬夜叉、金色のガッシュベル!!、3×3EYES、幽☆遊☆白書、美少女戦士セーラームーン、スレイヤーズ|
|Ecchi|「エッチ」という響きからも推察されるように、わりと軽めのエッチな場面の登場するアニメに振られているジャンルのよう。|767|電影少女 、ちょびっツ、魔法先生 ネギま！、ラブひな、いちご100%、一騎当千、まほろまてぃっく|
|Hentai|18禁アニメや18禁ゲームのアニメ化など、Ecchiよりは重めの変態系アニメとお見受けします。|1348|Piaキャロットへようこそ!!、同窓会、下級生、その他多数|
|Harem|男性主人公1人を大勢の女性キャラが取り囲むという形態のアニメ|399|藍より青し、魔法先生 ネギま！、ラブひな、いちご100%|
|Josei|少女まんがを原作としているアニメ（と思われる）|97|ハチミツとクローバー、まっすぐにいこう、のだめカンタービレ、ちはやふる|
|Seinen|青年誌のまんがを原作としているアニメ（に見える）|830|モンスター、剣風伝奇ベルセルク、ああっ女神さまっ、ローゼンメイデン、ギャラリーフェイク|
|Shoujo|少女まんがを原作としているアニメ？「Josei」とかぶってそうだが、「Josei」よりは若めの年齢層（小中学生？）を対象としたアニメが多いように見える。|760|赤ずきんチャチャ、花より男子、フルーツバスケット、ふしぎ遊戯、神風怪盗ジャンヌ、彼氏彼女の事情、カードキャプターさくら|
|Slice of Life|学園生活など日常を切り取った場面をベースに繰り広げられるアニメ。|1914|ハチミツとクローバー、彼氏彼女の事情、ラブひな、らんま1/2、ボーイズ・ビー、ごくせん|
|Shounen Ai|読んで字の如し|100|【略】|
|Shoujo Ai|読んで字の如し|79|【略】|
|Yaoi|読んで字の如し|42|【略】|
|Yuri|読んで字の如し|32|【略】|


ジャンルを特徴量として取得したいので、one-hotエンコーディングしてジャンルごとに１項目に置き換えます

~~~py
df_anime_genre = df_anime

for genre in sorted(genre_set):
  df_anime_genre['genre-' + genre] = df_anime['Genres'].str.contains(genre)
~~~

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
### ・ユーザー特徴量の作成

ユーザーがどのジャンルを視聴しているのかをカウントします。
またその合計を正規化します。（全体の合計で割ることで割合が分かるようになります。）

~~~py
# ユーザーごとのrating情報と、先ほどone-hotエンコーディングしたアニメのマスタ情報をマージ
df_dataset = pd.merge(df_rating.sample(n=1000000, random_state=0), 
                      df_anime_genre, left_on='anime_id', right_on='MAL_ID')

# 必要な情報（ジャンル情報のみ）に項目をしぼってユーザーごとにグルーピングして集計する。
df_dataset_user = df_dataset.set_index('user_id').iloc[:,38:]
df_dataset_user = df_dataset_user.groupby(level=0).sum()

# user特徴量の正規化。どのユーザーがどのジャンルを多く見てるのかが割合で示される。
df_dataset_user = (df_dataset_user.T/df_dataset_user.T.sum()).T
df_dataset_user
~~~

![5-050.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/86708a7f-ec6e-7dcd-b74b-e02c1277a6d4.png)

最大値が１で、それぞれジャンルごとに割合が示されているのが分かります。
user_id=2の人はコメディ好きのようですね。
user_id=7の人はエッチなジャンルをよくご覧になっているようです。

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
### ・学習データセットの作成

算出してきたユーザーの特徴量とアニメの特徴量をマージし、ひとつのDataFrameにします。
また必要に応じて、欠損値を含むデータへの対処もしていきます。
今回のデータにはNaNを含むデータが40件程度存在しました。NaNが含まれるとうまく処理できません。幸いデータ数が少なく大きな影響はないため、あらかじめNaNを含んだ行を落としておきます。


~~~py
# df_dataset_animeのindexとカラム名を設定。
df_dataset_anime = pd.DataFrame(H.T, index=anime_id_unique, 
                       columns=[f'MF-{i}' for i in range(H.shape[0])])

# それぞれ算出してきたuserの特徴量とanimeの特徴量をマージし、ひとつのDataFrameにする。
df_dataset = df_dataset[['user_id','anime_id','rating']]
df_dataset = pd.merge(df_dataset, df_dataset_user, left_on='user_id', right_index=True)
df_dataset = pd.merge(df_dataset, df_dataset_anime, left_on='anime_id', right_index=True) 

# 一部、NaNが含まれている項目があるためdropnaで行を落とす。
df_dataset = df_dataset.dropna(how='any')

~~~

データの下準備ができたら、X（説明変数）とY（目的変数）に分けていきます。

~~~py
# X（説明変数）とY（目的変数）に分けて、学習の準備をする

# Xは学習には不要な'user_id','anime_id',	'rating'を落としたもの
df_X = df_dataset.drop(columns=['user_id','anime_id','rating'])

# Yは評価結果
series_Y = df_dataset['rating']
~~~






<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
### ・モデルの定義と学習

学習データセットができたので、機械学習モデルを定義して実際に学習させていきます
今回は非常にポピュラーな「ランダムフォレスト」という手法を使っています。

~~~py
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error

#データを学習データとテストデータに分割する
X_train, X_test, y_train, y_test = train_test_split(df_X, series_Y, shuffle=True, train_size=0.8, random_state=0)

#モデルの定義
model = RandomForestRegressor(n_estimators=20, max_depth=4, random_state=0, n_jobs=-1, verbose=1, min_samples_split=3, min_samples_leaf=3, max_features='log2')

#モデルの学習
model.fit(X_train, y_train)

#予測を行い、精度を計測する
pred_train = model.predict(X_train)
pred_test  = model.predict(X_test)

print(mean_squared_error(y_train.values, pred_train, squared=False))
print(mean_squared_error(y_test.values, pred_test, squared=False))
~~~
精度は
train 1.6215739968095482
test  1.6235540960211639
と低めの結果となりました。

ランダムサーチやグリッドサーチ、optuna等を駆使してハイパーパラメータのチューニングをしてみましたが、そこまで劇的な改善が見られませんでした。。。
精度はそこまで高くないですが、過学習は発生していないので一旦これで進めてみようと思います。





<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
### ・推論

モデルができたのでいよいよ推論してみます。

これまでに見たことがあるアニメをいくつかチョイスし、モデルに渡しておすすめのアニメ一覧を推論させます。アニメは配列で何個でも渡せるのですが、ひとまず結果の精度が分かりやすいように「風の谷のナウシカ」で推論してみたいと思います。

アニメのタイトルから検索し、IDを調べます。
~~~py
search_word = '風の谷のナウシカ'
df_anime[(df_anime['Japanese name'].str.contains(search_word)) 
       | (df_anime['English name'].str.contains(search_word)) ]
~~~

風の谷のナウシカのIDは572でした。

IDをモデルに渡して候補生成、リランクし、レコメンドを表示します。

~~~py
MAL_ID_list = [572]

df_candidate_list = get_mf_candidate(MAL_ID_list,50)

df_candidate_concat = pd.concat(df_candidate_list, axis=0)
df_candidate_concat = df_candidate_concat.reset_index()
df_candidate_concat = df_candidate_concat.groupby('MAL_ID', group_keys=False).apply(lambda x: x.loc[x.score.idxmax()]).drop(columns='MAL_ID')

pd.set_option('display.max_rows', 500)
df_candidate_concat.sort_values('score', ascending=False)

df_predict_contents = df_candidate_concat
df_predict_contents = pd.merge(df_predict_contents, df_dataset_anime, left_on='MAL_ID', right_index=True).sort_values('score', ascending=False)

df_predict_user = df_anime_genre
df_predict_user = df_predict_user[df_predict_user['MAL_ID'].isin(MAL_ID_list)].iloc[:,36:]
arr_predict_user = df_predict_user.sum()
arr_predict_user = arr_predict_user/arr_predict_user.sum()

df_predict = df_predict_contents.copy(deep=True)
for i,v in arr_predict_user.items():
  df_predict[i] = v

df_predict_result = df_predict.copy(deep=True)
df_predict_result['rerank_score'] = model.predict(df_predict.drop(columns=['Japanese name','score']))

df_predict_result[['Japanese name','score','rerank_score']].sort_values('rerank_score', ascending=False)
~~~


<br><br>

推論の結果、このようなランキングとなりました。

![5-100.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/5ae25193-60b1-976f-7274-0ed0b1127955.png)


うーん。。。なんとも言えない結果ですねｗ

「蟲師」が９位にランクインしてますね。
確かに「王蟲」と蟲の字が被ってはいますが、アニメとしてはあまり類似点がないような・・・。

ちなみに
scoreはアニメの特徴量のみを用いた評価点。
rerank_scoreはユーザーの特徴量を用いた評価点です。

<b> rerank_scoreを見るとほとんど同じような値になってしまっていることが分かります。</b>

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# ６．分析

リランクの精度を上げるため、データを可視化しながら原因を分析していきます。

#### ・feature importance

feature importanceで特徴量の重要度を比較してみます。

~~~py
df_fi = pd.DataFrame(model.feature_importance(), 
                     index=df_X.columns, columns=['feature_importance'])
df_fi.plot.bar(figsize=(16, 9))
~~~
![6-005.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/c446765c-1a0b-7f75-7342-ed7b76855fd8.png)


向かって左側がユーザー特徴量（genre-XXXXX）、右側がアニメの特徴量（MF-X）です。

アニメの特徴量は大きく、ユーザーの特徴量は小さくなっており、
かなり偏りがあることが分かります。

<br><br>

#### ・ヒストグラム

ヒストグラムでユーザーデータがどの程度複雑に入っているのかを確認してみます。
~~~
df_dataset['rating'].hist()
~~~

![6-010.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/e7bfdf6a-7279-cbab-7487-42b58f100d28.png)

そもそも評価点自体が偏っているのが分かります。
ほとんどが6～10点をつけており、5点以下の低い点数をつけるひとはあまりいないようです。

<br><br>

同様にユーザーが見たジャンルのカウントにどれくらいばらつきがあるかをヒストグラムで確認します。


~~~py
df_dataset.set_index('user_id').iloc[:,38:].groupby(level=0).count().iloc[:,0].hist(bins=30)
~~~

![6-020.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/b7a3d5bb-6818-aa60-d459-1c00a5de3e3d.png)

ほとんどのユーザーは1ジャンル（1作品）しか見てないですね。
多くても2,3ジャンルです。



<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# ７．リランクその２（ユーザー特徴量次元削減による精度改善）

分析結果から以下の方針で再度リランクをしてみようと思います。
* ユーザー特徴量にゼロが多く密度が低いため、次元削減して密度を高める。
* アニメの特徴量に偏っている傾向があるので、アニメの特徴量は逆に次元を増やして密度を薄める。

<!--
~~~py
# 必要な情報（ジャンル情報のみ）に項目をしぼってユーザーごとにグルーピングして集計する。
df_dataset_user = df_dataset.set_index('user_id').iloc[:,38:]
df_dataset_user = df_dataset_user.groupby(level=0).sum()

# user特徴量の正規化。どのユーザーがどのジャンルを多く見てるのかが割合で示される。
df_dataset_user = (df_dataset_user.T/df_dataset_user.T.sum()).T
df_dataset_user

# 一部、NaNが含まれている項目があるためdropnaで行を落とす。
df_dataset_user = df_dataset_user.dropna(how='any')
df_dataset_user
~~~
-->

ジャンルは全部で44個もあります。つまり44次元のユーザー特徴量ということです。
これを10次元に減らします。可視化をしたときと同じ方式でPCAを使います。

~~~py
from sklearn.decomposition import PCA

# 主成分分析のインスタンスを生成。
pca = PCA(n_components=10)

# データを学習し、変換する。
user_pca = pca.fit_transform(df_dataset_user)

df_dataset_user = pd.DataFrame(user_pca, index=df_dataset_user.index, columns=[f'user_genre_pca_{i}' for i in range(10)])
~~~

<br>
また、「３．候補生成」でアニメの特徴量を作成する時に次元指定を10次元にしていましたが、これを15次元に変更してみます。

~~~py
# NMFのモデルを作成しデータを学習させる（n_componentsは何次元に次元削減するのかの指定）
nmf = NMF(n_components=15, init='random', max_iter=30)

W = nmf.fit_transform(X)
H = nmf.components_
~~~


以降、手順としては「５．リランク」と同じことをやっていきます。

　　・ユーザー特徴量の作成
　　・学習データセットの作成
　　・モデルの定義と学習
　　・推論

feature importanceの結果は下記のようになりました。
全体的に要素がシンプルになり、ユーザー特徴量に重きが置かれるようになりました。

![7-005.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/75abcc0d-4a18-0409-92db-50d6887aace6.png)


また、精度は
train　1.4419250735955922
test　1.4736103072904003
まで上がりました。




<br><br><br>
さて、いよいよ推論結果です。

![7-010.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/f41a885e-ddf2-7d4d-b086-9fede4d6e00c.png)


全体的にジブリ作品が増えているのと、rerank_scoreが前よりはバラけているので推論の精度としては上がってそうですが、１位がまさかの「パンティ＆ストッキングwithガーターベルト」という結果になりました。

私はこのアニメを見たことがなく、タイトルの感じから「もしや・・・」と思ったのですが、ネットで調べるとどうやらここに掲載できるアニメのようですので少しホッとしました。

自分が作ったAIの激推しなので一度見てみたいと思います。
（ナウシカと類似しているかどうかはさておき）

https://www.youtube.com/watch?v=jtn8jIIdTfk

https://www.youtube.com/watch?v=PKDoT1CZhSY

---

せっかくなので子供に過去に見たことがある好きなアニメを３つ選択してもらい
そこから推論してみました。

* 僕のヒーローアカデミア →31964
* 約束のネバーランド →37779
* 地縛少年花子くん →39534

~~~py
MAL_ID_list = [31964,37779,39534]

df_candidate_list = get_mf_candidate(MAL_ID_list,50)

df_candidate_concat = pd.concat(df_candidate_list, axis=0)
df_candidate_concat = df_candidate_concat.reset_index()
df_candidate_concat = df_candidate_concat.groupby('MAL_ID', group_keys=False).apply(lambda x: x.loc[x.score.idxmax()]).drop(columns='MAL_ID')

pd.set_option('display.max_rows', 500)
df_candidate_concat.sort_values('score', ascending=False)

df_predict_contents = df_candidate_concat
df_predict_contents = pd.merge(df_predict_contents, df_dataset_anime, left_on='MAL_ID', right_index=True).sort_values('score', ascending=False)

df_predict_user = df_anime_genre
df_predict_user = df_predict_user[df_predict_user['MAL_ID'].isin(MAL_ID_list)].iloc[:,36:]
arr_predict_user = df_predict_user.sum()
arr_predict_user = arr_predict_user/arr_predict_user.sum()

df_predict = df_predict_contents.copy(deep=True)
for i,v in arr_predict_user.items():
  df_predict[i] = v

df_predict_result = df_predict.copy(deep=True)
df_predict_result['rerank_score'] = model.predict(df_predict.drop(columns=['Japanese name','score']))

df_predict_result[['Japanese name','score','rerank_score']].sort_values('rerank_score', ascending=False)

~~~
<br><br><br>
「パンティ＆ストッキングwithガーターベルト」がランクインしてこないことを祈ります。
説明がめんどくさいので・・・。

<br><br><br>
結果は・・・

![7-100.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712562/e9c2c7db-84f1-77ba-f759-00096928a156.png)


なかなか手堅いところをチョイスしてきてる気がしますね。
やれば出来るヤツではないか。

この後しばらく子供と遊んで盛り上がりました。

<!-- ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ -->
# ８．まとめ

今回はアニメのデータセットを使ったレコメンド機能に挑戦してみました。

全くの初心者ですのでAidemyのチューターさんに教えていただきながら、なんとか形にすることができました。ひとつひとつ丁寧に指導してくださったチューターさんにこの場を借りて感謝申し上げます。おかげで楽しく機械学習の勉強をすることができ、普段何気なく目にして使っている「レコメンド」がどのようにして作られているのか仕組みを理解することができました。本当にいろいろな場所で機械学習が活用されているんですね。

時間も限られていたので決して精度の高いレコメンド機能とは言えませんでしたが、これを最初の一歩として機械学習の勉強に取り組んでいければと思います。


今後は今回作ったものを土台として、下記のようなことにもチャレンジしてみたいです。

・ モデルの精度を上げる。
・ 勾配ブースティング決定木（XGBoost、LightGBM）など他の手法でもやってみる。
・ WEB画面を作って操作できるようにしてみる。
・ アニメ以外（映画、音楽、ECの商品）のレコメンドに応用してみる。

最後まで読んでいただきありがとうございました。