Netflix が、すでに観た映画をもとに映画を提案してくれるのを不思議に思ったことはないだろうか。また、eコマースサイトでは、「よく一緒に購入されている商品」などがどのように表示されるのでしょうか。これらは比較的単純な選択肢に見えるかもしれないが、舞台裏では複雑な統計アルゴリズムが実行され、これらの推薦を予測しているのである。このようなシステムは、レコメンダーシステム、レコメンデーションシステム、レコメンデーションエンジンなどと呼ばれている。レコメンダーシステムは、データサイエンスと機械学習の最も有名なアプリケーションの1つである。
レコメンダーシステムは、エンティティ間の類似性、または以前にそれらのエンティティを評価したユーザー間の類似性に基づいて、特定のエンティティに対するユーザーの評価を予測しようとする統計的アルゴリズムを採用している。直感的には、同じようなタイプのユーザーは、一連のエンティティに対して同じような評価をする可能性が高いと考えられる。
現在、多くの大手ハイテク企業が何らかの形でレコメンダーシステムを利用している。Amazon(商品の推薦)からYouTube(ビデオの推薦)、Facebook(友達の推薦)まで、あらゆるところで見かけることができます。ユーザーに関連性の高い製品やサービスを推薦する機能は、企業にとって大きな後押しとなるため、多くのサイトでこの技術が採用されているのである。
今回は、Pythonで簡単なレコメンダーシステムを構築する方法を見ていきましょう。
推薦システムの種類
レコメンダーシステムを構築するアプローチには、大きく分けて2つある。コンテンツベースフィルタリングと協調フィルタリングである。
コンテンツベースフィルタリング
コンテンツベースフィルタリングでは、異なる商品間の類似性を商品の属性に基づいて計算する。例えば、コンテンツベースの映画推薦システムでは、映画のジャンル、出演者、監督などの属性に基づいて、映画間の類似度が計算される。
コラボレイティブ・フィルタリング
協調フィルタリングは、群衆の力を利用する。協調フィルタリングの直感は、あるユーザーAが商品XとYを気に入り、別のユーザーBが商品Xを気に入れば、かなりの確率で商品Yも気に入ってくれるというものです。
例えば、映画のリコメンデーションシステムを考えてみましょう。膨大な数のユーザが映画XとYに同じ評価をつけているとします。そこに、映画Xに同じ評価をつけているが映画Yはまだ見ていない新しいユーザがやってきます。協調フィルタリングシステムは、彼に映画Yを推薦します。
Pythonによる映画推薦システムの実装
このセクションでは、異なる映画に割り当てられた評価の相関を使用して、映画間の類似性を見つけるために、Pythonで非常に単純な映画推薦システムを開発します。
この問題のために使用するデータセットは MovieLens Dataset です。これは実際の映画データセットのサブセットで、700人のユーザによる9000本の映画に対する100000件のレーティングが含まれています。
ダウンロードしたファイルを解凍すると、”links.csv”, “movies.csv”, “ratings.csv”, “tags.csv” ファイルと “README” というドキュメントが表示されます。今回は、”movies.csv “と “rating.csv “のファイルを使用することにします。
本記事のスクリプトでは、解凍した「ml-latest-small」フォルダを、「E」ドライブの「Datasets」フォルダ内に配置しました。
データの可視化と前処理
あらゆるデータサイエンス問題の最初のステップは、データを可視化し、前処理をすることです。私たちも同じように、まず「rating.csv」ファイルをインポートして、その中身を確認してみましょう。以下のスクリプトを実行します。
import numpy as np
import pandas as pd
ratings_data = pd.read_csv("E:\Datasets\ml-latest-small\ratings.csv")
ratings_data.head()
上記のスクリプトでは、Pandasライブラリの read_csv()
メソッドを使用して “ratings.csv” ファイルを読み込んでいます。次に、read_csv()
関数が返すdataframeオブジェクトからhead()
メソッドを呼び出し、データセットの最初の5行を表示させます。
出力はこのようになります。
userId | movieId | レーティング | タイムスタンプ | |
---|---|---|---|---|
0 | 1 | 31 | 2.5 | 1260759144 |
1 | 1 | 1029 | 3.0 | 1260759179 |
2 | 1 | 1061 | 3.0 | 1260759182 |
3 | 1 | 1129 | 2.0 | 1260759185 |
4 | 1 | 1172 | 4.0 | 1260759205 |
出力から、”rating.csv” ファイルが userId, movieId, ratings, and timestamp 属性を含んでいることがわかります。データセットの各行は、1つのレーティングに対応しています。userId カラムには、レーティングを残したユーザーの ID が含まれています。movieId カラムは映画の ID を、rating カラムはユーザが残した評価を含んでいます。レーティングは 1 から 5 までの値を持つことができます。そして最後に、timestampはユーザがレーティングを残した時刻を表しています。
このデータセットには1つの問題があります。このデータセットには映画のIDは含まれていますが、タイトルは含まれていません。推薦する映画には映画の名前が必要です。映画の名前は「movies.csv」ファイルに格納されています。このファイルをインポートして、どのようなデータが入っているか見てみましょう。以下のスクリプトを実行してください。
movie_names = pd.read_csv("E:\Datasets\ml-latest-small\movies.csv")
movie_names.head()
出力はこのようになります。
| ムービーID|タイトル|ジャンル||||など。
| — | — | — | — |
| 0|1|『トイ・ストーリー』(1995)|アドベンチャー|アニメ|チルドレン|コメディ|ファンタジー
| 1|2|『ジュマンジ』(1995)|アドベンチャー|こども|ファンタジー
| 2|3|『グランピア・オールドメン』(1995年)|コメディ|ロマンス|映画
| 3|4|『息を吐くまでに』(1995)|コメディ|ドラマ|ロマンス|映画
| 4|5|『花嫁の父 Part II』(1995)|コメディー|ロマンス
見てわかるように、このデータセットには movieId と映画のタイトル、そしてジャンルが含まれています。userId、映画のタイトル、レーティングを含むデータセットが必要です。この情報は、2つのdataframeオブジェクトに格納されています。「ratings_data” と “movie_names” です。目的の情報を1つのdataframeで取得するために、2つのdataframeオブジェクトをmovieIdカラムでマージすることができます。
これは、以下のようにPandasライブラリの merge()
関数を使用して行うことができます。
movie_data = pd.merge(ratings_data, movie_names, on='movieId')
では、新しいデータフレームを表示してみましょう。
movie_data.head()
出力はこのようになります。
| | userId | movieId | rating | timestamp | title | genres | 。
| — | — | — | — | — | — | — |
| 0|1|31|2.5|1260759144|デンジャラス・マインド(1995年)|ドラマ||です。
| 1|7|31|3.0|851868750|デンジャラス・マインズ(1995)|ドラマ
| 2|31|31|4.0|12703541953|デンジャラス・マインズ(1995)|ドラマ
| 3|32|31|4.0|834828440|デンジャラス・マインズ(1995)|ドラマ
| 4|36|31|3.0|847057202|デンジャラス・マインズ(1995)|ドラマ||です。
新しく作成したデータフレームには、必要に応じて、userId、タイトル、映画のレーティングが含まれていることがわかります。
では、各ムービーの平均レーティングを見てみましょう。そのために、映画のタイトルでデータセットをグループ化し、各映画のレーティングの平均を計算することができます。そして、head()
メソッドを使って最初の5つの映画を平均レーティングとともに表示します。次のスクリプトを見てください。
movie_data.groupby('title')['rating'].mean().head()
出力はこのようになります。
title
"Great Performances" Cats (1998) 1.750000
$9.99 (2008) 3.833333
'Hellboy': The Seeds of Creation (2004) 2.000000
'Neath the Arizona Skies (1934) 0.500000
'Round Midnight (1986) 2.250000
Name: rating, dtype: float64
平均レーティングがソートされていないことがわかります。平均評価の降順にソートしてみましょう。
movie_data.groupby('title')['rating'].mean().sort_values(ascending=False).head()
上記のスクリプトを実行すると、次のような出力が得られます。
title
Burn Up! (1991) 5.0
Absolute Giganten (1999) 5.0
Gentlemen of Fortune (Dzhentlmeny udachi) (1972) 5.0
Erik the Viking (1989) 5.0
Reality (2014) 5.0
Name: rating, dtype: float64
これで映画はレーティングの昇順でソートされました。しかし、問題があります。ある映画は、たった一人のユーザが5つの星をつけたとしても、上記のリストのトップになることができます。そのため、上記の統計は誤解を招く可能性があります。通常、本当に良い映画は、多くのユーザーによって高い評価を得ます。
では、ある映画のレーティングの総数をプロットしてみましょう。
movie_data.groupby('title')['rating'].count().sort_values(ascending=False).head()
上のスクリプトを実行すると、次のような出力が得られます。
title
Forrest Gump (1994) 341
Pulp Fiction (1994) 324
Shawshank Redemption, The (1994) 311
Silence of the Lambs, The (1991) 304
Star Wars: Episode IV - A New Hope (1977) 291
Name: rating, dtype: int64
これで、本当に良い映画が上位にあることがわかります。上のリストは、良い映画は通常より高いレーティングを受けるという我々の指摘を裏付けています。さて、映画あたりの平均レーティングと映画あたりのレーティング数の両方が、重要な属性であることがわかりました。では、これらの属性を含む新しいデータフレームを作成してみましょう。
以下のスクリプトを実行して ratings_mean_count
データフレームを作成し、まず各映画の平均レーティングをこのデータフレームに追加します。
ratings_mean_count = pd.DataFrame(movie_data.groupby('title')['rating'].mean())
次に、映画のレーティング数を ratings_mean_count
データフレームに追加する必要があります。以下のスクリプトを実行してください。
ratings_mean_count['rating_counts'] = pd.DataFrame(movie_data.groupby('title')['rating'].count())
では、新しく作成したデータフレームを見てみましょう。
ratings_mean_count.head()
出力はこのようになります。
| タイトル|レーティング|レーティングカウント
| — | — | — |
| “偉大なパフォーマンス “キャッツ(1998)|1.750000|2|。
| $9.99 (2008) | 3.833333 | 3 |
| 「ヘルボーイ」。創造の種 (2004年)|||||||||||||||||||||||||||。
映画の共通点を見つける
データの可視化と前処理にかなりの時間を費やしました。今度は、映画間の類似性を見つける番です。
ここでは、映画のレーティングの相関を類似性の指標として使用することにします。映画のレーティングの相関を求めるには、各列が映画名で、各行がその映画に特定のユーザが割り当てたレーティングを含む行列を作成する必要があります。すべての映画がすべてのユーザによって評価されているわけではないので、この行列は多くのヌル値を持つことに留意してください。
映画のタイトルとそれに対応するユーザのレーティングの行列を作成するには、次のスクリプトを実行します。
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('dark')
%matplotlib inline
plt.figure(figsize=(8,6))
plt.rcParams['patch.force_edgecolor'] = True
ratings_mean_count['rating_counts'].hist(bins=50)
plt.figure(figsize=(8,6))
plt.rcParams['patch.force_edgecolor'] = True
ratings_mean_count['rating'].hist(bins=50)
| タイトル|「Great Performances」Cats (1998) | $9.99 (1998) | 「Hellboy」: 創造の種』(2008)|『アリゾナの空の下に』(1934)|『ラウンド・ミッドナイト』(1986)|『セイラムズ・ロット』(2004)|『ティル・ゼア・ワズ・ユー』(1997)|『バーブ、ザ』(1989)|『ナイトマザー』(1986)|(500)日のサマー(2009)| …等。| ズールー (1964)|ズールー (2013)|…
| — | — | — | — | — | — | — | — | — | — | — | — | — | — |
| ユーザーID
| 1|NaN|NaN|NaN|NaN|NaN|NaN|NaN| …。| NaN | NaN |…
| 2 | NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|… | NaN|NaN|NaN|NaN
| 3|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN| … | NaN|NaN|NaN|NaN
| 4 | NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|…. | NaN|NaN|NaN|NaN
| 5|NaN|NaN|NaN|NaN|NaN|NaN|NaN|・・・|。| NaN|NaN|NaN|NaN
各列は特定の映画に対するすべてのユーザー評価を含んでいることがわかります。映画「フォレスト・ガンプ (1994)」に対するすべてのユーザー評価を見つけ、それに類似した映画を見つけよう。この映画を選んだのは、レーティング数が最も多く、レーティング数の多い映画間の相関を調べたいからです。
Forrest Gump (1994)」に対するユーザー評価を調べるには、以下のスクリプトを実行します。
plt.figure(figsize=(8,6))
plt.rcParams['patch.force_edgecolor'] = True
sns.jointplot(x='rating', y='rating_counts', data=ratings_mean_count, alpha=0.4)
上記のスクリプトは、Pandasの系列を返します。どのように見えるか見てみましょう。
user_movie_rating = movie_data.pivot_table(index='userId', columns='title', values='rating')
user_movie_rating.head()
では、「フォレスト・ガンプ (1994)」に類似する映画をすべて取得してみましょう。以下のように corrwith()
関数を用いて、”Forest Gump (1994)” と他のすべての映画に対するユーザ評価の相関を求めることができます。
forrest_gump_ratings = user_movie_rating['Forrest Gump (1994)']
上記のスクリプトでは、まず corrwith()
関数を使用して、”Forrest Gump (1994)” に関連するすべての映画のリストを、その相関値とともに取得します。次に、映画のタイトルと相関のカラムを含むデータフレームを作成した。そして、データフレームからNAの値をすべて削除し、head
関数を用いて最初の5行を表示しました。
出力はこのようになります。
タイトル | 相関関係 |
---|---|
$9.99 (2008) | 1.000000 |
‘バーブズ, The (1989) | 0.044946 |
(500)日のサマー(2009) | 0.624458|。 |
Ⅻ*電池がない(1987) | 0.603023 |
…そして、正義のために(1979) | 0.173422 |
相関の高い映画を上位に表示するために、相関の降順にソートしてみましょう。以下のスクリプトを実行します。
forrest_gump_ratings.head()
上のスクリプトの出力は以下の通りです。
| $9.99 (2008) | 1.0 |
| 作品名:Say It Isn’t So (2001) | 1.0 | 1.0 | 1.0
| メトロポリス(2001)|1.0||。
| 1.0点|悪を見ず、悪を聞かず(1989)|1.0点|1.0点
| ミドルメン(2009) | 1.0
| ウォーター・フォー・エレファンツ(2011)|1.0倍
| ウォッチ(2012)|1.0倍
| チーチ&チョンの次回作(1980)|1.0倍
| フォレスト・ガンプ(1994)|1.0倍
| ウォーリアー(2011)|1.0倍
出力から、”Forrest Gump (1994) “と高い相関を持つ映画は、あまり知られていないことがわかります。これは、”フォレストガンプ (1994) “と他の1つの映画だけを観て、両方を5と評価したユーザーが存在しうるため、相関関係だけでは類似性の良い指標とはならないことを示しています。
この問題を解決するには、少なくとも50以上のレーティングを持つ、相関のある映画だけを取り出すことです。そのためには、rating_mean_count
データフレームから rating_counts
カラムを corr_forrest_gump
データフレームに追加することになります。そのためには、以下のスクリプトを実行します。
userId
1 NaN
2 3.0
3 5.0
4 5.0
5 4.0
Name: Forrest Gump (1994), dtype: float64
出力は以下のような感じです。
| タイトル| 相関| rating_counts
| $9.99 (2008) | 1.000000 | 3 |
| バーブス(1989)|0.044946|19|(500)日目の夏(2008
| (500)日のサマー(2009)|0.624458|45|。
| Ⅻ*電池がない (1987) | 0.603023 | 7|。
| …そして、正義のために(1979年)|0.173422|13||。
最も相関の高い”$9.99 “は、3つの評価しかないことがわかります。これは、”Forest Gump (1994)”, “$9.99 “に同じ評価を与えたユーザが3人しかいないことを意味します。しかし、3つの評価だけでは、ある映画が他の映画と類似しているとは言えないことが推測されます。そこで “raating_counts “カラムを追加しました。では、”Forest Gump (1994) “に関連する映画で、50以上のレーティングを持つものをフィルタリングしてみましょう。以下のコードで行います。
movies_like_forest_gump = user_movie_rating.corrwith(forrest_gump_ratings)
corr_forrest_gump = pd.DataFrame(movies_like_forest_gump, columns=['Correlation'])
corr_forrest_gump.dropna(inplace=True)
corr_forrest_gump.head()
スクリプトの出力は、次のようになります。
| フォレスト・ガンプ(1994)|1.000000|341||||。
| マイ・ビッグ・ファット・グリーク・ウェディング (200
結論
今回は、レコメンダーシステムとは何か、そしてPythonでPandasライブラリだけを使ってどのように作ることができるかを勉強しました。ここで重要なのは、今回作成したレコメンダーシステムは非常にシンプルであるということです。実際のレコメンダーシステムは非常に複雑なアルゴリズムを使っており、これについては後の記事で説明します。
もしレコメンダーシステムについてもっと学びたいのであれば、Practical Recommender Systems と Recommender Systems という本をチェックすることをお勧めします。教科書です。これらの本では、このトピックについてより深く掘り下げており、この記事で紹介したものよりも複雑で正確な方法をカバーしています。