Contents
はじめに
今回はKaggleに公開されているアニメのデータセットを使っていきたいとおもいます。今回は、クラスタリングを使って推薦システムを作っていきたいと思います。クラスタリングを使って推薦システムを作ること自体は興味があったんですが、なかなかやる機会がなかったので今回次のカーネルを参考にして勉強していきたいと思います。
今回のデータセットのリンク
Anime Recommendations Database
今回の参考カーネル
User Clustering for anime recommendation
合わせて読みたい記事:
データセットの説明
データセットの内訳
- 総アニメ数:12,294
- ユーザー数:73,516
Anime.csv
- anime_id:アニメの固有ID
- name:アニメの名前
- genre:アニメのジャンル
- type:アニメの形式(例:movie, TV, OVA)
- episodes:何話あるか(映画の場合1).
- rating:平均レート(10段階評価)
- members:このアニメの「グループ」に含まれるコミュニティメンバーの数
Rating.csv
- user_id:ランダムに生成されたユーザーID
- anime_id:このユーザーが評価したアニメID
- rating:このユーザーが割り当てた10段階の評価(ユーザーがそのアニメを見たが、評価をしなかった場合は-1)
Anime Recommendations Database
今回参考にしたカーネルでは次のものを扱っています。
- Data wrangling
- K mean clustering
- Characteristic of each cluster
ソースコード
まず、GoogleColaboratory上でデータを扱えるようにします。データセットリンクよりダウンロードしてきたzipファイルを、GoogleColaboratory上の「表示」から「目次」を選び、「ファイル」タブを選択します。そうしたら、「アップロード」という表記が見えると思うので、そこからzipファイルを選択し、アップロードしてください。
データセットの解凍
In[1]:
!unzip anime-recommendations-database.zip
ライブラリの準備
In[2]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline
plt.rcParams['figure.figsize'] = (6, 4)
plt.style.use('ggplot')
%config InlineBackend.figure_formats = {'png', 'retina'}
データ読み込み
In[3]:
anime.head()
「君の名は」が1位で人気がすさまじいですね。ちなみに私は見ていないのでわかりません。
In[4]:
print(anime.shape)
ユーザーのレーティング情報の方も読み込んで出力させてみましょう。
In[5]:
user = pd.read_csv('rating.csv')
user.head(10)
In[7]:
print(user.shape)
好きなものの定義
ユーザー1の平均評価はマイナスでした。
In[8]:
user[user['user_id']==1].rating.mean()
user[user['user_id']==2].rating.mean()
user[user['user_id']==5].rating.mean()
ユーザーあたりの平均評価を計算する
MRPU = user.groupby(['user_id']).mean().reset_index()
MRPU['mean_rating'] = MRPU['rating']
MRPU.drop(['anime_id','rating'],axis=1, inplace=True)
MRPU.head(10)
データフレームuserにMRPUをkeyをuser_idとしてマージします。
user = pd.merge(user,MRPU,on=['user_id','user_id'])
user.head(5)
これで、データフレームuserにmean_ratingを追加することができました。次に平均評価より低い評価の物を取り除きます。
user = user.drop(user[user.rating < user.mean_rating].index)
user[user['user_id']== 1].head(10)
ユーザー1の評価が-1の物が取り除かれているのが確認できます。
user[user['user_id']== 2].head(10)
これにより、ユーザー2のお気に入りのアニメが1つであることがわかりました。
user[user['user_id']== 5].head(10)
print(user.shape)
user["user_id"].unique()
user = user.rename({'rating':'userRating'}, axis='columns')
2つのデータセットを組み合わせる
anime_idをkeyにしてマージします。また、user_idが20,000以下のみにします。
mergedata = pd.merge(anime,user,on=['anime_id','anime_id'])
mergedata= mergedata[mergedata.user_id <= 20000]
mergedata.head(10)
len(mergedata['anime_id'].unique())
len(anime['anime_id'].unique())
ユーザーidで制限をかけた分減っているのがわかりますね。
クロステーブルを作る
pandas.crosstab()
関数を使うとクロス集計分析ができます。
user_anime = pd.crosstab(mergedata['user_id'], mergedata['name'])
user_anime.head(10)
user_anime.shape
主成分分析
目的としては、クラスタリングと可視化のためにデータの次元を減らすことにあります。
In [27]:
from sklearn.decomposition import PCA
pca = PCA(n_components=3)
pca.fit(user_anime)
pca_samples = pca.transform(user_anime)
ps = pd.DataFrame(pca_samples)
ps.head()
tocluster = pd.DataFrame(ps[[0,1,2]])
plt.rcParams['figure.figsize'] = (16, 9)
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(tocluster[0], tocluster[2], tocluster[1])
plt.title('Data points in 3D PCA axis', fontsize=20)
plt.show()
kの数を選ぶ
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
scores = []
inertia_list = np.empty(8)
for i in range(2,8):
kmeans = KMeans(n_clusters=i)
kmeans.fit(tocluster)
inertia_list[i] = kmeans.inertia_
scores.append(silhouette_score(tocluster, kmeans.labels_))
plt.plot(range(0,8),inertia_list,'-o')
plt.xlabel('Number of cluster')
plt.axvline(x=4, color='blue', linestyle='--')
plt.ylabel('Inertia')
plt.show()
In [33]:
plt.plot(range(2,8), scores);
plt.title('Results KMeans')
plt.xlabel('n_clusters');
plt.axvline(x=4, color='blue', linestyle='--')ylabel('Silhouette Score');
plt.show()
k:クラスタの数は4で行きたいと思います。
K mean clustering
どのように動作するか説明をここに入れようとおもいましたが、以下のリンクで動作原理を理解するために可視化していたので、こちらを紹介しておきます。文で表現するより直観的に理解できると思います。
In[34]:
from sklearn.cluster import KMeans
clusterer = KMeans(n_clusters=4,random_state=30).fit(tocluster)
centers = clusterer.cluster_centers_
c_preds = clusterer.predict(tocluster)
print(centers)
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(tocluster[0], tocluster[2], tocluster[1], c = c_preds)
plt.title('Data points in 3D PCA axis', fontsize=20)
plt.show()

In[36]:
fig = plt.figure(figsize=(10,8))
plt.scatter(tocluster[1],tocluster[0],c = c_preds)
for ci,c in enumerate(centers):
plt.plot(c[1], c[0], 'o', markersize=8, color='red', alpha=1)
plt.xlabel('x_values')
plt.ylabel('y_values')
plt.title('Data points in 2D PCA axis', fontsize=20)
plt.show()
次に、クロステーブルにクラスタカラムを追加しておきます。
In[37]:
user_anime['cluster'] = c_preds
user_anime.head(10)
user_anime.info()
各クラスタの特性
In[39]:
c0 = user_anime[user_anime['cluster']==0].drop('cluster',axis=1).mean()
c1 = user_anime[user_anime['cluster']==1].drop('cluster',axis=1).mean()
c2 = user_anime[user_anime['cluster']==2].drop('cluster',axis=1).mean()
c3 = user_anime[user_anime['cluster']==3].drop('cluster',axis=1).mean()
Cluster 0
このクラスターの特徴を説明するトップ15のアニメを出力させてみます。
In[40]:
c0.sort_values(ascending=False)[0:15]
エピソード、ジャンル、メンバー、レーティングといったアニメの情報をそれぞれリストにして返す関数を作ります。
In[41]:
def createAnimeInfoList(animelist):
episode_list = list()
genre_list = list()
member_list = list()
rating_list= list()
for x in anime['name']:
if x in animelist:
episode_list.append(anime[anime['name']==x].episodes.values.astype(int))
member_list.append(anime[anime['name']==x].members.values.astype(int))
rating_list.append(anime[anime['name']==x].rating.values.astype(int))
for y in anime[anime['name']==x].genre.values:
genre_list.append(y)
return genre_list,episode_list,rating_list,member_list
def count_word(df, ref_col, liste):
keyword_count = dict()
for s in liste: keyword_count[s] = 0
for liste_keywords in df[ref_col].str.split(','):
if type(liste_keywords) == float and pd.isnull(liste_keywords): continue
for s in [s for s in liste_keywords if s in liste]:
if pd.notnull(s): keyword_count[s] += 1
#______________________________________________________________________
# convert the dictionary in a list to sort the keywords by frequency
keyword_occurences = []
for k,v in keyword_count.items():
keyword_occurences.append([k,v])
keyword_occurences.sort(key = lambda x:x[1], reverse = True)
return keyword_occurences, keyword_count
animelist = list(c0.index)
data = pd.DataFrame()
data['genre'],data['episode'],data['rating'],data['member'] = createAnimeInfoList(animelist)
set_keywords = set()
for liste_keywords in data['genre'].str.split(',').values:
if isinstance(liste_keywords, float): continue # only happen if liste_keywords = NaN
set_keywords = set_keywords.union(liste_keywords)
このクラスタのお気に入りのジャンル
!pip install wordcloud
from wordcloud import WordCloud
def makeCloud(Dict,name,color):
words = dict()
for s in Dict:
words[s[0]] = s[1]
wordcloud = WordCloud(
width=1500,
height=500,
background_color=color,
max_words=20,
max_font_size=500,
normalize_plurals=False)
wordcloud.generate_from_frequencies(words)
fig = plt.figure(figsize=(12, 8))
plt.title(name)
plt.imshow(wordcloud)
plt.axis('off')
plt.show()

In [46]:
c0_animelist = list(c0.sort_values(ascending=False)[0:15].index)
c0_data = pd.DataFrame()
c0_data['genre'],c0_data['episode'],c0_data['rating'],c0_data['member'] = createAnimeInfoList(c0_animelist)
c0_data.iloc[:,1:4] = c0_data.iloc[:,1:4].astype(int) # change to numeric object to integer
keyword_occurences, dum = count_word(c0_data, 'genre', set_keywords)
makeCloud(keyword_occurences[0:10],"cluster 0","lemonchiffon")
キーワードと頻度が高い順で5件出力させてみます。
In[47]:
keyword_occurences[0:5]
Out[47]:
[['Action', 10],
[' Drama', 8],
[' Fantasy', 5],
[' Supernatural', 5],
[' Romance', 5]]
このクラスタのユーザが好きなアニメの情報の平均
In[48]:
print('cluster 0\nAVG episode : {0}\nAVG movie rating : {1}\nAVG member : {2}'
.format(c0_data['episode'].mean(), c0_data['rating'].mean(),c0_data['member'].mean()))
Cluster 1
他のクラスタもみていきます。やることはクラスタ0と同じです。
In[49]:
c1.sort_values(ascending=False)[0:15]
c1_animelist = list(c1.sort_values(ascending=False)[0:15].index)
c1_data = pd.DataFrame()
c1_data['genre'],c1_data['episode'],c1_data['rating'],c1_data['member'] = createAnimeInfoList(c1_animelist)
c1_data.iloc[:,1:4] = c1_data.iloc[:,1:4].astype(int)
keyword_occurences, dum = count_word(c1_data, 'genre', set_keywords)
makeCloud(keyword_occurences[0:10],"cluster 1","white")

keyword_occurences[0:5]
print('cluster 1\nAVG episode : {0}\nAVG movie rating : {1}\nAVG member : {2}'
.format(c1_data['episode'].mean(), c1_data['rating'].mean(),c1_data['member'].mean())
クラスタ0と比べて、エピソードの平均が短く、平均評価が高いですね。アクションものはエピソードが長いものが多そうな気がします。
Cluster 2
In[53]:
c2.sort_values(ascending=False)[0:15]
c2_animelist = list(c2.sort_values(ascending=False)[0:15].index)
c2_data = pd.DataFrame()
c2_data['genre'],c2_data['episode'],c2_data['rating'],c2_data['member'] = createAnimeInfoList(c2_animelist)
c2_data.iloc[:,1:4] = c2_data.iloc[:,1:4].astype(int)
keyword_occurences, dum = count_word(c2_data, 'genre', set_keywords)
makeCloud(keyword_occurences[0:10],"cluster 2","black")
keyword_occurences[0:5]
c2_data['episode'].mean()
print('cluster 2\nAVG episode : {0}\nAVG movie rating : {1}\nAVG member : {2}'
.format(c2_data['episode'].mean(), c2_data['rating'].mean(),c2_data['member'].mean()))
平均評価は今まで出てきたクラスタはどれもあまり変わらないですね。先ほど、アクションだとエピソードが長い?と予想を立てましたが、そうではないみたいです。クラスタ0との違いはロマンス、ファンタジーといったものなので、アクションとファンタジー、ロマンスが重なったらエピソードが長い傾向にあるのかもしれません。
Cluster 3
In[58]:
c3.sort_values(ascending=False)[0:15]
c3_animelist = list(c3.sort_values(ascending=False)[0:15].index)
c3_data = pd.DataFrame()
c3_data['genre'],c3_data['episode'],c3_data['rating'],c3_data['member'] = createAnimeInfoList(c3_animelist)
c3_data.iloc[:,1:4] = c3_data.iloc[:,1:4].astype(int)
keyword_occurences, dum = count_word(c3_data, 'genre', set_keywords)
makeCloud(keyword_occurences[0:10],"cluster 3","snow")
keyword_occurences[0:5]
print('cluster 3\nAVG episode : {0}\nAVG movie rating : {1}\nAVG member : {2}'
.format(c3_data['episode'].mean(), c3_data['rating'].mean(),c3_data['member'].mean()))
こちらも平均エピソードはあまり変わらず、ということは、アクションとロマンスがエピソード数に関わってくるのでしょうか?
長くなってしまったので、これらを用いた推薦システムは次回書きたいと思います。
最後まで読んでいただきありがとうございました。よろしければこの記事をシェアしていただけると励みになります。よろしくお願いします。