前のページでは行列分解によって評価データからレイティングの予測値を求めました.ここでは,アイテム数を10個に増加させたデータを利用してユーザごとに推薦アイテムのリストを作成することを考えます.
まずは,データを確認するために,GitHub のリポジトリ から新たなサンプルデータを読み込み,それらをここで説明した手順で結合します.
import pandas as pd
import numpy as np
url_items = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_items2.csv?raw=true"
df_items = pd.read_csv(url_items)
url_customers = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_customers.csv?raw=true"
df_customers = pd.read_csv(url_customers )
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings2.csv?raw=true"
df_ratings = pd.read_csv(url_ratings)
tbl = pd.merge(
pd.merge(
df_ratings, df_items,
left_on = 'item',
right_on = 'id',
),
df_customers,
left_on = 'customer', right_on = 'id'
)
tbl = tbl.rename(columns={'item_y': 'item', 'id_y': 'id'})
tbl = tbl.loc[:,['id', 'name','item','rating']]
df = tbl.pivot_table('rating', index=['id', 'name'], columns='item', aggfunc='sum')
print(df)
item A B C D E F G H I J id name 0 Eto 5.0 4.0 3.0 NaN NaN 2.0 4.0 2.0 3.0 5.0 1 Sato NaN 2.0 4.0 2.0 3.0 1.0 3.0 4.0 3.0 2.0 2 Kato 2.0 3.0 4.0 NaN NaN 1.0 3.0 4.0 3.0 2.0 3 Muto 2.0 3.0 2.0 NaN 2.0 2.0 2.0 2.0 2.0 1.0 4 Kito 4.0 3.0 3.0 4.0 NaN 4.0 4.0 3.0 2.0 4.0 5 Goto 2.0 5.0 NaN 2.0 3.0 3.0 3.0 2.0 4.0 1.0 6 Bito 3.0 3.0 2.0 NaN 1.0 4.0 1.0 2.0 2.0 4.0 7 Saito 4.0 4.0 5.0 NaN 5.0 2.0 5.0 5.0 4.0 4.0 8 Naito 5.0 5.0 4.0 2.0 1.0 1.0 2.0 2.0 4.0 5.0 9 Koto 3.0 NaN 2.0 2.0 2.0 3.0 2.0 1.0 2.0 3.0
上に表示された評価値のデータのうち,ハイライトされた数値はまだ観測されていないものとして残りのデータを訓練データとして利用します.そのうえで4人のユーザに対して,予測値が高い順番に推薦アイテムのリストを提示することを考えます.
まずはプログラムの全体像とその実行結果を示します.
プログラムの全体像
import pandas as pd
import numpy as np
from surprise import Dataset, Reader, SVD
from surprise.model_selection import train_test_split
from collections import defaultdict
np.set_printoptions(precision=3) # 小数点以下の表示桁数を設定
np.set_printoptions(suppress=True) # 指数表示を行わないように
def get_top_n(predictions, n=10):
"""
ユーザごとにレイティングの予測値が高いN個の推薦リストを作成する
"""
# ユーザごとの予測値リストを作成
top_n = defaultdict(list)
for uid, iid, true_r, est, _ in predictions:
top_n[uid].append((iid, est))
# ソートして上位 n個のリストを作成する
for uid, user_ratings in top_n.items():
user_ratings.sort(key=lambda x: x[1], reverse=True)
top_n[uid] = user_ratings[:n]
return top_n
# データの読み込み
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings2.csv?raw=true"
df_ratings = pd.read_csv(url_ratings)
# レイティングのスケールを設定する
reader = Reader(rating_scale=(1, 5))
# 列名を指定してデータフレームからデータセットを読み込む
data = Dataset.load_from_df(df_ratings[["customer", "item", "rating"]], reader)
# 19件をテストデータにする
trainset, testset = train_test_split(data, test_size=0.21, shuffle=False) # シャッフルなし
# trainset, testset = train_test_split(data, test_size=0.21) # シャッフルあり
# 学習データの数をカウントする
n_train = sum(len(d) for d in trainset.ur.values())
# データ数などを確認する
print(f"number of trainset customers ={trainset.n_users:4d}")
print(f"number of trainset items ={trainset.n_items:4d}")
print(f"number of elements in matrix ={trainset.n_users * trainset.n_items:4d}")
print(f"number of null ratings ={trainset.n_users * trainset.n_items - df_ratings['rating'].shape[0]:4d}")
print(f"number of ratings in data ={df_ratings['rating'].shape[0]:4d}")
print(f"number of ratings in trainset ={n_train:4d}")
print(f"number of ratings in testset ={len(testset):4d}")
# 学習データを行列形式に変換し,元の順番に並べ替えた行列も作成する
trainset_matrix = np.zeros((trainset.n_users, trainset.n_items))
train_raw_matrix = np.zeros((trainset.n_users, trainset.n_items)) # 元のデータ
trainset_matrix[:,:] = np.nan
train_raw_matrix[:,:] = np.nan
for u, vals in trainset.ur.items():
for i, rating in vals:
trainset_matrix[u, i] = rating
train_raw_matrix[trainset.to_raw_uid(u), trainset.to_raw_iid(i)] = rating
# 因子数
n_factors = 5
# エポック数
n_epochs = 100000
# バイアスを利用するか
biased = False
# biased = True
# 学習率
lr_all = 0.005
# 正則化パラメータ
reg_all = 0.02
# モデルの設定
mf = SVD(n_factors=n_factors,
n_epochs=n_epochs,
biased=biased,
lr_all=lr_all,
reg_all=reg_all
)
# モデルの当てはめ(学習)を行う
mf.fit(trainset)
# 予測評価値のマトリックス
predict_matrix = np.zeros((trainset.n_users, trainset.n_items))
for uid in range(trainset.n_users):
for iid in range(trainset.n_items):
pred = mf.predict(uid, iid, verbose=False)
predict_matrix[uid, iid] = pred.est
# テストデータのマトリックス
test_raw_matrix = np.zeros((trainset.n_users, trainset.n_items))
test_raw_matrix[:] = np.nan
for r, c, v in testset:
test_raw_matrix[r, c] = v
print("----- trainset -------")
print(train_raw_matrix)
print("----- testset -----")
print(test_raw_matrix)
print("----- s -------")
print(predict_matrix)
print("----- train_predict -------")
train_predict_matrix = np.copy(train_raw_matrix)
train_predict_matrix = np.where(~np.isnan(train_predict_matrix), predict_matrix, np.nan)
print(train_predict_matrix)
print("----- test_predict -------")
test_predict_matrix = np.copy(test_raw_matrix)
test_predict_matrix = np.where(~np.isnan(test_predict_matrix), predict_matrix, np.nan)
print(test_predict_matrix)
# テストデータに対しての予測を実行
test_predictions = mf.test(testset)
top_n = get_top_n(test_predictions, n=5) # ここで抽出するアイテム数 n を設定する
print("---- ユーザごとの推薦アイテム ----")
for uid, user_ratings in top_n.items():
print(f"ユーザ {uid} への推薦アイテム: {[iid for (iid, _) in user_ratings]}")
number of trainset customers = 10 number of trainset items = 10 number of elements in matrix = 100 number of null ratings = 11 number of ratings in data = 89 number of ratings in trainset = 70 number of ratings in testset = 19 ----- trainset ------- [[ 5. 4. 3. nan nan nan nan nan nan nan] [nan 2. 4. 2. 3. nan nan nan nan nan] [ 2. 3. 4. nan nan nan nan nan nan nan] [ 2. 3. 2. nan 2. 2. nan nan nan nan] [ 4. 3. 3. 4. nan 4. 4. 3. 2. 4.] [ 2. 5. nan 2. 3. 3. 3. 2. 4. 1.] [ 3. 3. 2. nan 1. 4. 1. 2. 2. 4.] [ 4. 4. 5. nan 5. 2. 5. 5. 4. 4.] [ 5. 5. 4. 2. 1. 1. 2. 2. 4. 5.] [ 3. nan 2. 2. 2. 3. 2. 1. 2. 3.]] ----- testset ----- [[nan nan nan nan nan 2. 4. 2. 3. 5.] [nan nan nan nan nan 1. 3. 4. 3. 2.] [nan nan nan nan nan 1. 3. 4. 3. 2.] [nan nan nan nan nan nan 2. 2. 2. 1.] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan]] ----- s ------- [[4.937 3.993 3.01 2.736 1.836 2.556 2.369 1.595 2.642 5. ] [2.155 2.037 3.891 2.033 2.976 1. 3.113 4.036 2.813 2.374] [2.026 3. 3.919 1.806 2.796 1. 2.955 3.77 3.48 2.029] [2.001 2.959 2.004 1.643 1.977 1.994 2.028 1.412 2.273 1.642] [3.934 3.015 2.981 3.876 4.201 3.947 3.956 2.987 2.046 4.02 ] [2.01 4.895 2.9 1.973 3.001 2.956 2.98 1.991 3.941 1.084] [2.999 2.985 2.009 1.874 1.048 3.901 1.049 1.929 1.984 3.945] [3.993 3.985 5. 3.812 4.921 2.042 4.946 4.908 3.955 3.95 ] [4.909 4.915 3.967 1.958 1.077 1.062 2.007 2.002 3.962 4.958] [3.023 3.182 1.901 2.132 1.877 2.932 1.982 1.137 2.005 2.896]] ----- test_predict ------- [[ nan nan nan nan nan 2.556 2.369 1.595 2.642 5. ] [ nan nan nan nan nan 1. 3.113 4.036 2.813 2.374] [ nan nan nan nan nan 1. 2.955 3.77 3.48 2.029] [ nan nan nan nan nan nan 2.028 1.412 2.273 1.642] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan]] ---- ユーザごとの推薦アイテム ---- ユーザ 0 への推薦アイテム: [9, 8, 5, 6, 7] ユーザ 1 への推薦アイテム: [7, 6, 8, 9, 5] ユーザ 2 への推薦アイテム: [7, 8, 6, 9, 5] ユーザ 3 への推薦アイテム: [8, 6, 9, 7]
上のコード35行目では,読み込んだデータ data
を訓練データとテストデータに分割しています.なお,GitHub から読み込んだ collaborative_filtering_ratings2.csv というファイルにおいて,テストデータとして利用したいデータを最後の19行に記録しているので,データをシャフルせず,最後の21%(つまり19件)のデータがテストデータになるようにしています.通常は shuffle=False
を指定せずにデータをシャッフルしてから訓練データとテストデータに分割するとよいでしょう.あるいは,評価データに評価日時の情報もあれば評価日時でソートしてからシャッフルせずに分割する方法も考えられるでしょう.
19件をテストデータにする
trainset, testset = train_test_split(data, test_size=0.21, shuffle=False) # シャッフルなし
# trainset, testset = train_test_split(data, test_size=0.21) # シャッフルあり
訓練データとテストデータに分割した後,思った通りに分割されているかを上の38行目からのコードで確認します.訓練データには10人の顧客と10種類のアイテムがあり,100個の要素からなる評価値行列が作成されています.このうち,11個には評価が存在せず,89個の評価値データがあります.89個の評価値のうち,70個が訓練データとして利用され,19個がテストデータとして利用されることが確認できました.
正しく分割されていることを確認する
# 学習データの数をカウントする
n_train = sum(len(d) for d in trainset.ur.values())
# データ数などを確認する
print(f"number of trainset customers ={trainset.n_users:4d}")
print(f"number of trainset items ={trainset.n_items:4d}")
print(f"number of elements in matrix ={trainset.n_users * trainset.n_items:4d}")
print(f"number of null ratings ={trainset.n_users * trainset.n_items - df_ratings['rating'].shape[0]:4d}")
print(f"number of ratings in data ={df_ratings['rating'].shape[0]:4d}")
print(f"number of ratings in trainset ={n_train:4d}")
print(f"number of ratings in testset ={len(testset):4d}")
number of trainset customers = 10 number of trainset items = 10 number of elements in matrix = 100 number of null ratings = 11 number of ratings in data = 89 number of ratings in trainset = 70 number of ratings in testset = 19
上のコードにおいて50行目から86行目までは前のページと同様です.88行目から97行目ではテストデータのみの行列を作成し,訓練データと並べて表示ています.この部分は必ずしも必要ではありませんが,結果を視覚的に確認しやすくなる利点があるでしょう.
# テストデータのマトリックス
test_raw_matrix = np.zeros((trainset.n_users, trainset.n_items))
test_raw_matrix[:] = np.nan
for r, c, v in testset:
test_raw_matrix[r, c] = v
print("----- trainset -------")
print(train_raw_matrix)
print("----- testset -----")
print(test_raw_matrix)
----- trainset ------- [[ 5. 4. 3. nan nan nan nan nan nan nan] [nan 2. 4. 2. 3. nan nan nan nan nan] [ 2. 3. 4. nan nan nan nan nan nan nan] [ 2. 3. 2. nan 2. 2. nan nan nan nan] [ 4. 3. 3. 4. nan 4. 4. 3. 2. 4.] [ 2. 5. nan 2. 3. 3. 3. 2. 4. 1.] [ 3. 3. 2. nan 1. 4. 1. 2. 2. 4.] [ 4. 4. 5. nan 5. 2. 5. 5. 4. 4.] [ 5. 5. 4. 2. 1. 1. 2. 2. 4. 5.] [ 3. nan 2. 2. 2. 3. 2. 1. 2. 3.]] ----- testset ----- [[nan nan nan nan nan 2. 4. 2. 3. 5.] [nan nan nan nan nan 1. 3. 4. 3. 2.] [nan nan nan nan nan 1. 3. 4. 3. 2.] [nan nan nan nan nan nan 2. 2. 2. 1.] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan]]
99行目から110行目では,レイティング予測値の行列から,訓練データだけの予測値とテストデータだけの予測値を作成しています.この部分も必ずしも必要はありませんが,視覚的に理解をすることが目的です.なお,下の6行目や11行目では np.where
と ~np.isnan
を利用していることに注意してください.
print("----- s -------")
print(predict_matrix)
print("----- train_predict -------")
train_predict_matrix = np.copy(train_raw_matrix)
train_predict_matrix = np.where(~np.isnan(train_predict_matrix), predict_matrix, np.nan)
print(train_predict_matrix)
print("----- test_predict -------")
test_predict_matrix = np.copy(test_raw_matrix)
test_predict_matrix = np.where(~np.isnan(test_predict_matrix), predict_matrix, np.nan)
print(test_predict_matrix)
----- s ------- [[4.937 3.993 3.01 2.736 1.836 2.556 2.369 1.595 2.642 5. ] [2.155 2.037 3.891 2.033 2.976 1. 3.113 4.036 2.813 2.374] [2.026 3. 3.919 1.806 2.796 1. 2.955 3.77 3.48 2.029] [2.001 2.959 2.004 1.643 1.977 1.994 2.028 1.412 2.273 1.642] [3.934 3.015 2.981 3.876 4.201 3.947 3.956 2.987 2.046 4.02 ] [2.01 4.895 2.9 1.973 3.001 2.956 2.98 1.991 3.941 1.084] [2.999 2.985 2.009 1.874 1.048 3.901 1.049 1.929 1.984 3.945] [3.993 3.985 5. 3.812 4.921 2.042 4.946 4.908 3.955 3.95 ] [4.909 4.915 3.967 1.958 1.077 1.062 2.007 2.002 3.962 4.958] [3.023 3.182 1.901 2.132 1.877 2.932 1.982 1.137 2.005 2.896]] ----- train_predict ------- [[4.937 3.993 3.01 nan nan nan nan nan nan nan] [ nan 2.037 3.891 2.033 2.976 nan nan nan nan nan] [2.026 3. 3.919 nan nan nan nan nan nan nan] [2.001 2.959 2.004 nan 1.977 1.994 nan nan nan nan] [3.934 3.015 2.981 3.876 nan 3.947 3.956 2.987 2.046 4.02 ] [2.01 4.895 nan 1.973 3.001 2.956 2.98 1.991 3.941 1.084] [2.999 2.985 2.009 nan 1.048 3.901 1.049 1.929 1.984 3.945] [3.993 3.985 5. nan 4.921 2.042 4.946 4.908 3.955 3.95 ] [4.909 4.915 3.967 1.958 1.077 1.062 2.007 2.002 3.962 4.958] [3.023 nan 1.901 2.132 1.877 2.932 1.982 1.137 2.005 2.896]] ----- test_predict ------- [[ nan nan nan nan nan 2.556 2.369 1.595 2.642 5. ] [ nan nan nan nan nan 1. 3.113 4.036 2.813 2.374] [ nan nan nan nan nan 1. 2.955 3.77 3.48 2.029] [ nan nan nan nan nan nan 2.028 1.412 2.273 1.642] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan]]
上のコード113行目ではテストデータでテストを行っています.その結果は test_predictions
次のとおり格納されています.
test_predictions = mf.test(testset)
test_predictions
[Prediction(uid=0, iid=5, r_ui=2.0, est=2.555887634896381, details={'was_impossible': False}), Prediction(uid=1, iid=5, r_ui=1.0, est=1, details={'was_impossible': False}), Prediction(uid=2, iid=5, r_ui=1.0, est=1, details={'was_impossible': False}), Prediction(uid=0, iid=6, r_ui=4.0, est=2.3688414499823414, details={'was_impossible': False}), Prediction(uid=1, iid=6, r_ui=3.0, est=3.11335734405957, details={'was_impossible': False}), Prediction(uid=2, iid=6, r_ui=3.0, est=2.954813314836426, details={'was_impossible': False}), Prediction(uid=3, iid=6, r_ui=2.0, est=2.027915113978186, details={'was_impossible': False}), Prediction(uid=0, iid=7, r_ui=2.0, est=1.5945790718983173, details={'was_impossible': False}), Prediction(uid=1, iid=7, r_ui=4.0, est=4.035744538184163, details={'was_impossible': False}), Prediction(uid=2, iid=7, r_ui=4.0, est=3.77035814328444, details={'was_impossible': False}), Prediction(uid=3, iid=7, r_ui=2.0, est=1.4122252335038672, details={'was_impossible': False}), Prediction(uid=0, iid=8, r_ui=3.0, est=2.6418791394427394, details={'was_impossible': False}), Prediction(uid=1, iid=8, r_ui=3.0, est=2.812874758153092, details={'was_impossible': False}), Prediction(uid=2, iid=8, r_ui=3.0, est=3.47959405725432, details={'was_impossible': False}), Prediction(uid=3, iid=8, r_ui=2.0, est=2.2728922587523437, details={'was_impossible': False}), Prediction(uid=0, iid=9, r_ui=5.0, est=5, details={'was_impossible': False}), Prediction(uid=1, iid=9, r_ui=2.0, est=2.373842411543232, details={'was_impossible': False}), Prediction(uid=2, iid=9, r_ui=2.0, est=2.028637946005516, details={'was_impossible': False}), Prediction(uid=3, iid=9, r_ui=1.0, est=1.6420496970184506, details={'was_impossible': False})]
上で求めた test_predictions
を利用して,ユーザごとに予測値が高いアイテムを順に最大5件取り出します.なお,上のコード11行目からの関数 get_top_n
の実装については,こちらを参照してください.
def get_top_n(predictions, n=10):
"""
ユーザごとにレイティングの予測値が高いN個の推薦リストを作成する
"""
# ユーザごとの予測値リストを作成
top_n = defaultdict(list)
for uid, iid, true_r, est, _ in predictions:
top_n[uid].append((iid, est))
# ソートして上位 n個のリストを作成する
for uid, user_ratings in top_n.items():
user_ratings.sort(key=lambda x: x[1], reverse=True)
top_n[uid] = user_ratings[:n]
return top_n
...(中略)...
print(test_predict_matrix)
...(中略)...
top_n = get_top_n(test_predictions, n=5) # ここで抽出するアイテム数 n を設定する
print("---- ユーザごとの推薦アイテム ----")
for uid, user_ratings in top_n.items():
print(f"ユーザ {uid} への推薦アイテム: {[iid for (iid, _) in user_ratings]}")
----- test_predict ------- [[ nan nan nan nan nan 2.556 2.369 1.595 2.642 5. ] [ nan nan nan nan nan 1. 3.113 4.036 2.813 2.374] [ nan nan nan nan nan 1. 2.955 3.77 3.48 2.029] [ nan nan nan nan nan nan 2.028 1.412 2.273 1.642] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan]] ---- ユーザごとの推薦アイテム ---- ユーザ 0 への推薦アイテム: [9, 8, 5, 6, 7] ユーザ 1 への推薦アイテム: [7, 6, 8, 9, 5] ユーザ 2 への推薦アイテム: [7, 8, 6, 9, 5] ユーザ 3 への推薦アイテム: [8, 6, 9, 7]
つまり,ユーザ 0 (Etoさん) への推薦アイテムトップ5は 9(商品J),8(商品I),5(商品F),6(商品G),7(商品H)であることがわかりました.ただし,テストデータのサイズが小さいことから,レイティングの予測値が低い商品であっても推薦アイテムのリストに含まれる可能性があることには注意してください.特にユーザ 1 や 2 への推薦アイテムの中に評価値が最低ランク(1)であるアイテムが含まれていることに注意してください.
また,このページでは行列分解による推薦モデルに基づいてユーザごとの推薦アイテムリストを生成しました.しかしながら,surprise に利用できる他のモデルであっても同じ様にユーザごとの推薦アイテムリストを生成できることにも注意してください.