これまで示した断片的なコードをまとめて全体像を示します.なお,以前のページのコードにあった不要な箇所を削除したり,順序を入れ替えたりしています.
import pandas as pd
import numpy as np
np.set_printoptions(precision=3) # 小数点以下の表示桁数を設定
np.set_printoptions(suppress=True) # 指数表示を行わないように
"""
パラメータの設定
↓↓↓↓↓ここから↓↓↓↓↓
"""
# データのURLを指定
url_items = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_items.csv?raw=true"
url_customers = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_customers.csv?raw=true"
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings.csv?raw=true"
# どの顧客のどの商品について分析したいかを指定する
# customer_id は 推定したい顧客(またはユーザ)
# item_id は 推定したい商品(またはアイテム)
# 次の4行のうち,いずれかの行を有効にする
# customer_id = 0; item_id = 3;
# customer_id= 0; item_id = 4;
customer_id= 2; item_id = 3;
# customer_id= 2; item_id = 4;
# 近傍顧客の数を設定する
k = 3
"""
パラメータの設定
↑↑↑↑↑ここまで↑↑↑↑↑
"""
def get_Y_il(y, i, l):
"""
顧客 i と顧客 l の両方によって評価された評価の集合を返す
"""
# まずは,yi と yl だけを取り出して2行の行列を生成
yil = np.concatenate((y[i], y[l])).reshape(2,y.shape[1])
# NaNが含まれる列のインデックスを初期化(あとでNaNが含まれている列は削除したい)
nan_idx = []
for i in range(yil.shape[1]):
if np.isnan(yil[0][i]) or np.isnan(yil[1][i]):
nan_idx.append(i) # どちらかの顧客が未評価 (NaN) なら削除する列のリストに追加する
# NaN が含まれる列を削除する
return np.delete(yil, nan_idx, 1)
def get_sim_i_l(y, i, l):
"""
顧客 i と 顧客 l の類似度を返す関数
"""
y_il = get_Y_il(y, i, l)
mean0 = np.mean(y_il[0])
mean1 = np.mean(y_il[1])
term1 = np.sum((y_il[0] - mean0) * (y_il[1] - mean1))
term2 = np.sqrt(np.sum((y_il[0] - mean0)**2)) * np.sqrt(np.sum((y_il[1] - mean1)**2))
if term2 == 0:
return 0
return term1 / term2
def get_sij(y, w, i, j):
"""
顧客 i の 商品 j についてのスコアを返す関数
"""
y_mean = np.nanmean(y, axis=1)
y_mean_i = y_mean[i]
# NaNが含まれている行,および自身の行は削除したい
nan_idx = [i]
for k in range(len(w)):
if np.isnan(y[k][j]):
nan_idx.append(k)
y = np.delete(y, nan_idx, 0)
y_mean = np.delete(y_mean, nan_idx)
w = np.delete(w, nan_idx)
# j 列目だけを取り出す
y = y[:,j]
sij = y_mean_i + np.sum(w * (y - y_mean)) / np.sum(w)
return sij
"""
ここから実質的なメイン
"""
# CSVデータを読み込む
df_items = pd.read_csv(url_items)
df_customers = pd.read_csv(url_customers )
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')
# NumPy 配列に格納し,レイティング行列 y を生成する
y = df.values
"""
類似度の計算
"""
# 定式化での記号に合わせて内部的には i, と j を使う
i = customer_id
j = item_id
# y についてNaNを除いた平均を求める
y_mean = np.nanmean(y, axis=1)
# すべての顧客について,類似度を計算します.
sim_matrix = np.zeros((y.shape[0], y.shape[0]))
for u in range(sim_matrix.shape[0]):
for v in range(sim_matrix.shape[1]):
sim_matrix[u, v] = get_sim_i_l(y, u, v)
# sim ベクトルの順位
customer_rank = np.argsort(np.argsort(sim_matrix[i])[::-1])
print(customer_rank)
# (sim は 1 のはずの) 顧客 i 自身も含んだ近傍顧客のインデックスを取得する
idx = np.arange(len(sim_matrix[i]))
near_customer_ids = idx[customer_rank <= k]
# 類似度 sim ベクトルから近傍顧客の類似度だけを取り出す
near_sim = sim_matrix[i][near_customer_ids]
# レイティング行列から近傍顧客のレイティングだけを取り出す
near_y = y[near_customer_ids]
# 変数 i が近傍顧客のみの集団での customer_id のインデックスとなるように更新する
i = np.where(near_customer_ids == i)[0][0]
"""
レイティングの推定
"""
sij = get_sij(near_y, near_sim, i, j)
"""
結果の表示
"""
print(df)
print(f"商品 {item_id} ({df.columns[item_id]}) に対する顧客 {customer_id } のスコアは {sij:5.3f} です.")
item A B C D E id name 0 Eto 5.0 4.0 3.0 NaN NaN 1 Sato NaN 2.0 4.0 2.0 3.0 2 Kato 2.0 3.0 4.0 NaN NaN 3 Muto 2.0 3.0 2.0 NaN 2.0 4 Kito 4.0 3.0 3.0 4.0 NaN 5 Goto 2.0 5.0 NaN 2.0 3.0 6 Bito 3.0 3.0 2.0 NaN 1.0 7 Saito 4.0 4.0 5.0 NaN 5.0 8 Naito 5.0 5.0 4.0 2.0 1.0 9 Koto 3.0 NaN 2.0 2.0 2.0 商品 3 (D) に対する顧客 2 のスコアは 2.125 です.
なお,顧客 0 (Eto さん) と 顧客 2 (Kato さん) について商品 D, E のスコアをそれぞれ計算すると次のようになりました.これは上のコードの19行目から22行目のいずれか1行が有効になるように変更して実行するだけで結果が得られるはずです.この結果,Eto さんには商品 D を優先的に,Kato さんには商品 E を優先的に推薦すべきであることがわかりました.
item A B C D E id name 0 Eto 5.0 4.0 3.0 NaN NaN 1 Sato NaN 2.0 4.0 2.0 3.0 2 Kato 2.0 3.0 4.0 NaN NaN 3 Muto 2.0 3.0 2.0 NaN 2.0 4 Kito 4.0 3.0 3.0 4.0 NaN 5 Goto 2.0 5.0 NaN 2.0 3.0 6 Bito 3.0 3.0 2.0 NaN 1.0 7 Saito 4.0 4.0 5.0 NaN 5.0 8 Naito 5.0 5.0 4.0 2.0 1.0 9 Koto 3.0 NaN 2.0 2.0 2.0 商品 3 (D) に対する顧客 0 のスコアは 3.216 です. 商品 4 (E) に対する顧客 0 のスコアは 2.751 です. 商品 3 (D) に対する顧客 2 のスコアは 2.125 です. 商品 4 (E) に対する顧客 2 のスコアは 3.238 です.
行列分解のコードもまとめておきます.なお,CSVファイルは行列形式ではなく個別のデータを読み込んでいます.また,前のページとは異なり,\(\lambda_1\) と \(\lambda_2\) には 0.001 という値を設定しています.この結果,前のページの結果よりもさらによい解が得られているように感じられます.具体的にはスコアで負の値や5を大幅に超える値がなくなっています.さらに「Eto さんには商品 D を優先的に,Kato さんには商品 E を優先的に推薦すべきである」という結果となり,これは顧客間の類似度に基づいた手法の結果と同じ結果になりました.
import pandas as pd
import numpy as np
import scipy.optimize as optimize
np.set_printoptions(precision=3) # 小数点以下の表示桁数を設定
np.set_printoptions(suppress=True) # 指数表示を行わないように
"""
パラメータの設定
↓↓↓↓↓ここから↓↓↓↓↓
"""
# データのURLを指定
url_items = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_items.csv?raw=true"
url_customers = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_customers.csv?raw=true"
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings.csv?raw=true"
# 潜在因子数
L = 3
print("潜在因子数 L =", L)
# lambda1 と lambda2
lam1 = 1.0E-03
lam2 = 1.0E-03
print("lambda1, lambda2 = ", lam1, ", ", lam2)
"""
パラメータの設定
↑↑↑↑↑ここまで↑↑↑↑↑
"""
def get_L2rss_UV(UV, Y, M, N, L, lam1, lam2):
"""
UとVを一つの一次元配列にした UV を受け取り,行列 U と V に変換して演算する
"""
u, v = np.split(UV, [M*L])
u = u.reshape(M, L)
v = v.reshape(N, L)
S = np.dot(u, v.T)
# L2 ノルム
ul2 = np.linalg.norm(u, ord=2)
vl2 = np.linalg.norm(v, ord=2)
return np.nansum((Y-S)**2 + lam1 * ul2 + lam2 * vl2)
"""
ここから実質的なメイン
"""
# CSVデータを読み込む
df_items = pd.read_csv(url_items)
df_customers = pd.read_csv(url_customers )
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')
# NumPy 配列に格納し,レイティング行列 y を生成する
y = df.values
M, N = y.shape
print("顧客数 M =", M)
print("アイテム数 N =",N)
# 乱数列の初期化
# rng = np.random.default_rng()
rng = np.random.default_rng(seed=1)
# -1, 1 の一様乱数
U = 2.0 * rng.random((M, L)) - 1.0
V = 2.0 * rng.random((N, L)) - 1.0
# 行列 U, V を一次元配列化して,連結する
UV = np.concatenate([U,V]).ravel()
results = optimize.minimize(
get_L2rss_UV, UV, args=(y, M, N, L, lam1, lam2),
method='Nelder-Mead',
tol=0.001,
options={'maxiter': 5000000}
)
print(' fun:', results.fun)
print('message:', results.message)
print(' status:', results.status)
print(' success:', results.success)
u, v = np.split(results.x, [M*L])
u = u.reshape(M, L)
v = v.reshape(N, L)
print('----- u -------')
print(u)
print('----- v -------')
print(v)
s = np.dot(u, v.T)
print('----- s -------')
print(s)
print('----- y -------')
print(y)
print('----- RSS -------')
print(np.nansum((y-s)**2))
潜在因子数 L = 3 lambda1, lambda2 = 0.001 , 0.001 顧客数 M = 10 アイテム数 N = 5 fun: 6.6525765325362 message: Optimization terminated successfully. status: 0 success: True ----- u ------- [[-0.144 2.21 1.31 ] [ 0.438 2.604 0.133] [ 0.354 2.992 0.363] [-1.251 5.091 0.161] [ 2.384 -2.775 1.642] [-1.094 6.639 0.159] [ 4.597 -8.509 2.271] [ 2.281 0.051 1.261] [-1.57 6.345 0.819] [ 0.18 1.805 0.709]] ----- v ------- [[-0.088 0.337 3.025] [ 0.582 0.646 2.307] [ 2.018 0.997 0.494] [ 1.207 0.448 1.459] [ 2.6 0.972 -1.046]] ----- s ------- [[4.719 4.365 2.56 2.727 0.402] [1.241 2.244 3.547 1.889 3.531] [2.075 2.976 3.877 2.296 3.447] [2.314 2.933 2.631 1.005 1.525] [3.823 3.385 2.856 4.031 1.785] [2.816 4.021 4.491 1.885 3.438] [3.598 2.418 1.915 5.051 1.31 ] [3.63 4.269 5.277 4.614 4.66 ] [4.755 5.076 3.563 2.142 1.225] [2.738 2.908 2.513 2.061 1.479]] ----- y ------- [[ 5. 4. 3. nan nan] [nan 2. 4. 2. 3.] [ 2. 3. 4. nan nan] [ 2. 3. 2. nan 2.] [ 4. 3. 3. 4. nan] [ 2. 5. nan 2. 3.] [ 3. 3. 2. nan 1.] [ 4. 4. 5. nan 5.] [ 5. 5. 4. 2. 1.] [ 3. nan 2. 2. 2.]] ----- RSS ------- 5.8802738100979655