|
from implicit.als import AlternatingLeastSquares
|
|
from implicit.lmf import LogisticMatrixFactorization
|
|
from implicit.bpr import BayesianPersonalizedRanking
|
|
from implicit.nearest_neighbours import bm25_weight
|
|
from scipy.sparse import csr_matrix
|
|
from typing import Dict, Any
|
|
|
|
MODEL = {
|
|
"lmf": LogisticMatrixFactorization,
|
|
"als": AlternatingLeastSquares,
|
|
"bpr": BayesianPersonalizedRanking,
|
|
}
|
|
|
|
|
|
def _get_sparse_matrix(values, user_idx, product_idx):
|
|
return csr_matrix(
|
|
(values, (user_idx, product_idx)),
|
|
shape=(len(user_idx.unique()), len(product_idx.unique())),
|
|
)
|
|
|
|
|
|
def _get_model(name: str, **params):
|
|
model = MODEL.get(name)
|
|
if model is None:
|
|
raise ValueError("No model with name {}".format(name))
|
|
return model(**params)
|
|
|
|
|
|
class InternalStatusError(Exception):
|
|
pass
|
|
|
|
|
|
class Recommender:
|
|
def __init__(
|
|
self,
|
|
values,
|
|
user_idx,
|
|
product_idx,
|
|
):
|
|
self.user_product_matrix = _get_sparse_matrix(values, user_idx, product_idx)
|
|
self.user_idx = user_idx
|
|
self.product_idx = product_idx
|
|
|
|
|
|
self.model = None
|
|
self.fitted = False
|
|
|
|
def create_and_fit(
|
|
self,
|
|
model_name: str,
|
|
weight_strategy: str = "bm25",
|
|
model_params: Dict[str, Any] = {},
|
|
):
|
|
weight_strategy = weight_strategy.lower()
|
|
if weight_strategy == "bm25":
|
|
data = bm25_weight(
|
|
self.user_product_matrix,
|
|
K1=1.2,
|
|
B=0.75,
|
|
)
|
|
elif weight_strategy == "balanced":
|
|
|
|
|
|
total_size = (
|
|
self.user_product_matrix.shape[0] * self.user_product_matrix.shape[1]
|
|
)
|
|
sum = self.user_product_matrix.sum()
|
|
num_zeros = total_size - self.user_product_matrix.count_nonzero()
|
|
data = self.user_product_matrix.multiply(num_zeros / sum)
|
|
elif weight_strategy == "same":
|
|
data = self.user_product_matrix
|
|
else:
|
|
raise ValueError("Weight strategy not supported")
|
|
|
|
self.model = _get_model(model_name, **model_params)
|
|
self.fitted = True
|
|
|
|
self.model.fit(data)
|
|
|
|
return self
|
|
|
|
def recommend_products(
|
|
self,
|
|
user_id,
|
|
items_to_recommend=5,
|
|
):
|
|
"""Finds the recommended items for the user.
|
|
|
|
Returns:
|
|
(items, scores) pair, where item is already the name of the suggested item.
|
|
"""
|
|
|
|
if not self.fitted:
|
|
raise InternalStatusError(
|
|
"Cannot recommend products without previously fitting the model."
|
|
" Please, consider fitting the model before recommening products."
|
|
)
|
|
|
|
return self.model.recommend(
|
|
user_id,
|
|
self.user_product_matrix[user_id],
|
|
filter_already_liked_items=True,
|
|
N=items_to_recommend,
|
|
)
|
|
|
|
def explain_recommendation(
|
|
self,
|
|
user_id,
|
|
suggested_item_id,
|
|
recommended_items,
|
|
):
|
|
_, items_score_contrib, _ = self.model.explain(
|
|
user_id,
|
|
self.user_product_matrix,
|
|
suggested_item_id,
|
|
N=recommended_items,
|
|
)
|
|
|
|
return items_score_contrib
|
|
|
|
def similar_users(self, user_id):
|
|
return self.model.similar_users(user_id)
|
|
|
|
@property
|
|
def item_factors(self):
|
|
return self.model.item_factors
|
|
|
|
|