| 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
|
|
|
|
|