class AlsRecommender(OwnRecommender): """Модель, обученная ALS Input ----- ds: RecommenderDataset подготовленный RecommenderDataset обьект """ def fit(self, n_factors=20, regularization=0.001, iterations=15, num_threads=4): """Обучает ALS""" self.model = AlternatingLeastSquares(factors=n_factors, regularization=regularization, iterations=iterations, num_threads=num_threads) self.model.fit(self.ds.csr_matrix) return self def _similarItems(self, userId, N=5): """Рекомендуем товары, похожие на топ-N купленных юзером товаров""" if not self.ds.userExist(userId): return self.ds.extend([], N) def _get_similar_item(item_id): """Находит товар, похожий на item_id""" recs = self.model.similar_items(self.ds.itemid_to_id[item_id], N=2) if len(recs) > 1: top_rec = recs[1][0] return self.ds.id_to_itemid[top_rec] return item_id res = [_get_similar_item(item) for item in self.ds.userTop(userId, N)] return self.extend(res, N) def _similarUsers(self, userId, N=5): """Рекомендуем топ-N товаров, среди купленных похожими юзерами""" if not self.ds.userExist(userId): return self.ds.extend([], N) res = [] similar_users = [rec[0] for rec in self.model.similar_users(self.ds.userid_to_id[userId], N=N+1)] similar_users = similar_users[1:] for user in similar_users: res.extend(self.ds.userTop(userId, 1)) return self.extend(res, N) def items_embedings(self): emb = pd.DataFrame(data=self.model.item_factors).add_prefix('itm') emb['item_id'] = self.ds.itemids return emb def users_embedings(self): emb = pd.DataFrame(data=self.model.user_factors).add_prefix('usr') emb['user_id'] = self.ds.userids return emb
class MainRecommender: """Рекоммендации, которые можно получить из ALS Input ----- user_item_matrix: pd.DataFrame Матрица взаимодействий user-item """ def __init__(self, data, weighting=True): # Топ покупок каждого юзера self.top_purchases = data.groupby( ['user_id', 'item_id'])['quantity'].count().reset_index() self.top_purchases.sort_values('quantity', ascending=False, inplace=True) self.top_purchases = self.top_purchases[ self.top_purchases['item_id'] != 999999] # Топ покупок по всему датасету self.overall_top_purchases = data.groupby( 'item_id')['quantity'].count().reset_index() self.overall_top_purchases.sort_values('quantity', ascending=False, inplace=True) self.overall_top_purchases = self.overall_top_purchases[ self.overall_top_purchases['item_id'] != 999999] self.overall_top_purchases = self.overall_top_purchases.item_id.tolist( ) self.user_item_matrix = self._prepare_matrix(data) # pd.DataFrame self.id_to_itemid, self.id_to_userid, \ self.itemid_to_id, self.userid_to_id = self._prepare_dicts(self.user_item_matrix) if weighting: self.user_item_matrix = bm25_weight(self.user_item_matrix.T).T @staticmethod def _prepare_matrix(data): """Готовит user-item матрицу""" user_item_matrix = pd.pivot_table( data, index='user_id', columns='item_id', values='quantity', # Можно пробовать другие варианты aggfunc='count', fill_value=0) user_item_matrix = user_item_matrix.astype( float) # необходимый тип матрицы для implicit return user_item_matrix @staticmethod def _prepare_dicts(user_item_matrix): """Подготавливает вспомогательные словари""" userids = user_item_matrix.index.values itemids = user_item_matrix.columns.values matrix_userids = np.arange(len(userids)) matrix_itemids = np.arange(len(itemids)) id_to_itemid = dict(zip(matrix_itemids, itemids)) id_to_userid = dict(zip(matrix_userids, userids)) itemid_to_id = dict(zip(itemids, matrix_itemids)) userid_to_id = dict(zip(userids, matrix_userids)) return id_to_itemid, id_to_userid, itemid_to_id, userid_to_id def fit_own_recommender(self, user_item_matrix): """Обучает модель, которая рекомендует товары, среди товаров, купленных юзером""" self.own_recommender = ItemItemRecommender(K=1, num_threads=4) self.own_recommender.fit(csr_matrix(user_item_matrix).T.tocsr()) return self.own_recommender def fit_als(self, user_item_matrix, n_factors=20, regularization=0.001, iterations=15, num_threads=4, show_progress=True, use_gpu=True): """Обучает ALS""" self.model_als = AlternatingLeastSquares(factors=n_factors, regularization=regularization, iterations=iterations, use_gpu=use_gpu, num_threads=num_threads, random_state=0) self.model_als.fit(csr_matrix(user_item_matrix).T.tocsr(), show_progress=show_progress) return self.model_als def _update_dict(self, user_id): """Если появился новыю user / item, то нужно обновить словари""" if user_id not in self.userid_to_id.keys(): max_id = max(list(self.userid_to_id.values())) max_id += 1 self.userid_to_id.update({user_id: max_id}) self.id_to_userid.update({max_id: user_id}) def _get_similar_item(self, item_id): """Находит товар, похожий на item_id""" recs = self.model_als.similar_items( self.itemid_to_id[item_id], N=2) # Товар похож на себя -> рекомендуем 2 товара top_rec = recs[1][0] # И берем второй (не товар из аргумента метода) return self.id_to_itemid[top_rec] def _extend_with_top_popular(self, recommendations, N=5): """Если кол-во рекоммендаций < N, то дополняем их топ-популярными""" if len(recommendations) < N: recommendations.extend(self.overall_top_purchases[:N]) recommendations = recommendations[:N] return recommendations def _get_recommendations(self, user, model_als, N=5): """Рекомендации через стардартные библиотеки implicit""" self._update_dict(user_id=user) recs = model_als.recommend(userid=self.userid_to_id[user], user_items=csr_matrix( self.user_item_matrix).tocsr(), N=N, filter_already_liked_items=False, filter_items=[self.itemid_to_id[999999]], recalculate_user=True) res = [self.id_to_itemid[rec[0]] for rec in recs] res = self._extend_with_top_popular(res, N=N) assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res def get_als_recommendations(self, user, N=5): """Рекомендации через стардартные библиотеки implicit""" self._update_dict(user_id=user) return self._get_recommendations(user, model_als=self.model_als, N=N) def get_own_recommendations(self, user, N=5): """Рекомендуем товары среди тех, которые юзер уже купил""" self._update_dict(user_id=user) return self._get_recommendations(user, model_als=self.own_recommender, N=N) def get_similar_items_recommendation(self, user, N=5): """Рекомендуем товары, похожие на топ-N купленных юзером товаров""" top_users_purchases = self.top_purchases[self.top_purchases['user_id'] == user].head(N) res = top_users_purchases['item_id'].apply( lambda x: self._get_similar_item(x)).tolist() res = self._extend_with_top_popular(res, N=N) assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res def get_similar_users_recommendation(self, user, N=5): """Рекомендуем топ-N товаров, среди купленных похожими юзерами""" res = [] # Находим топ-N похожих пользователей similar_users = self.model_als.similar_users(self.userid_to_id[user], N=N + 1) similar_users = [rec[0] for rec in similar_users] similar_users = similar_users[1:] # удалим юзера из запроса for user in similar_users: userid = self.id_to_userid[ user] #own recommender works with user_ids res.extend(self.get_own_recommendations(userid, N=1)) res = self._extend_with_top_popular(res, N=N) assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res
class MainRecommender: """Рекоммендации, которые можно получить из ALS Input ----- user_item_matrix: pd.DataFrame Матрица взаимодействий user-item """ def __init__(self, data, data_product, weighting=True): # Топ покупок каждого юзера self.top_purchases = data.groupby(['user_id', 'item_id'])['quantity'].count().reset_index() self.top_purchases.sort_values('quantity', ascending=False, inplace=True) self.top_purchases = self.top_purchases[self.top_purchases['item_id'] != 999999] # Топ покупок по всему датасету self.overall_top_purchases = data.groupby('item_id')['quantity'].count().reset_index() self.overall_top_purchases.sort_values('quantity', ascending=False, inplace=True) self.overall_top_purchases = self.overall_top_purchases[self.overall_top_purchases['item_id'] != 999999] self.overall_top_purchases = self.overall_top_purchases.item_id.tolist() self.user_item_matrix = self.prepare_matrix(data) # pd.DataFrame self.id_to_itemid, self.id_to_userid,self.itemid_to_id, self.userid_to_id = self.prepare_dicts(self.user_item_matrix) # Словарь {item_id: 0/1}. 0/1 - факт принадлежности товара к СТМ self.item_id_to_ctm = self.prepare_ctm(data_product) # Own recommender обучается до взвешивания матрицы self.own_recommender = self.fit_own_recommender(self.user_item_matrix) if weighting: self.user_item_matrix = bm25_weight(self.user_item_matrix.T).T self.model = self.fit(self) @staticmethod def prepare_matrix(data): user_item_matrix = pd.pivot_table(data, index='user_id', columns='item_id', values='quantity', # Можно пробоват ьдругие варианты aggfunc='count', fill_value=0 ) user_item_matrix = user_item_matrix.astype(float) # необходимый тип матрицы для implicit return user_item_matrix @staticmethod def prepare_ctm(data_product): """Создаем словарь {item_id: 0/1}. 0/1 - факт принадлежности товара к СТМ""" ctm_items = data_product[['item_id', 'brand']] ctm_items['feature'] = data_product['brand'].isin(['Private']) ctm_items = ctm_items.replace(to_replace=[False, True], value=[0, 1]) return dict(zip(ctm_items['item_id'], ctm_items['feature'])) @staticmethod def prepare_dicts(user_item_matrix): """Подготавливает вспомогательные словари""" userids = user_item_matrix.index.values itemids = user_item_matrix.columns.values matrix_userids = np.arange(len(userids)) matrix_itemids = np.arange(len(itemids)) id_to_itemid = dict(zip(matrix_itemids, itemids)) id_to_userid = dict(zip(matrix_userids, userids)) itemid_to_id = dict(zip(itemids, matrix_itemids)) userid_to_id = dict(zip(userids, matrix_userids)) return id_to_itemid, id_to_userid, itemid_to_id, userid_to_id @staticmethod def fit_own_recommender(user_item_matrix): """Обучает модель, которая рекомендует товары, среди товаров, купленных юзером""" own_recommender = ItemItemRecommender(K=1, num_threads=4) own_recommender.fit(csr_matrix(user_item_matrix).T.tocsr()) return own_recommender @staticmethod def fit(self, n_factors=20, regularization=0.001, iterations=15, num_threads=4): """Обучает ALS""" self.model = AlternatingLeastSquares(factors=n_factors, regularization=regularization, iterations=iterations, num_threads=num_threads) self.model.fit(csr_matrix(self.user_item_matrix).T.tocsr()) return self.model def get_similar_items_recommendation(self, user, filter_ctm=True, N=5): """Рекомендуем товары, похожие на топ-N купленных юзером товаров""" # your_code # Практически полностью реализовали на прошлом вебинаре # Не забывайте, что нужно учесть параметр filter_ctm res = [] recommends = self.model.similar_items(self.itemid_to_id[user], N) for item in recommends: res.append(item[0]) return res def get_similar_users_recommendation(self, user, N=5): """Рекомендуем топ-N товаров, среди купленных похожими юзерами""" res = [] recommends = self.model.similar_users(self.userid_to_id[user], N) for item in recommends: res.append(item[0]) return res
class MainRecommender: own_recomender_defult_param = {'filter_already_liked_items':False, 'filter_items':False, "recalculate_user":True} model_als_defult_param ={'factors':50, 'regularization':15, 'iterations':15, 'num_threads':-1,'calculate_training_loss':False} def __init__(self, data,data_test=None,split_info=None): """ data - dataframe c данными data_test - даные для валидации, если нет и есть split_info то создаем split_info кортеж с инфрмацией как создать data_test (размер, поле деления) рассматривается только в слуяае отсутвя data_test """ self.top = 5000 self.data_validation={} self.data_validation['status'] = False self.user_item_matrix = {'status':False,'matrix':None,'params':None} self.own_recommender_is_fit= {'status':False,'params':None} self.als_recommender_is_fit= {'status':False,'params':None} self.data = data.copy() self.full_data_train = data.copy() #Оставим полный объем данный , если нужно будет предсказывать по полному объему данных self.data_train = data.copy() if data_test is not None: self.data_test = data_test.copy() else: self.data_test = None if split_info: self.data_train,self.data_test = self.train_test_split(test_size_num = split_info[0],split_column =split_info[1]) if self.data_test is not None: self.data_validation['data'] = self.get_validation_data() self.data_validation['status'] = True def prefiltr_1(self,my_data): df = my_data.copy() """Оставим только top самых популярных товаров остальные переименуем в 999999""" popularity = my_data.groupby('item_id')['quantity'].count().reset_index() popularity.rename(columns={'quantity': 'n_sold'}, inplace=True) top_5000 = popularity.sort_values('n_sold', ascending=False).head(self.top).item_id.tolist() df.loc[~df['item_id'].isin(top_5000), 'item_id'] = 999999 return df def prefiltr_2(self,data_train,n=5000): """Оставим только n самых популярных товаров, транзакции с остальными товрами удалим""" df = data_train.copy() popularity = df.groupby('item_id')['quantity'].count().reset_index() popularity.rename(columns={'quantity': 'n_sold'}, inplace=True) top_n = popularity.sort_values('n_sold', ascending=False).head(n).item_id.tolist() df = df.loc[df['item_id'].isin(top_n)] return df def prefiltr_3(self,data_train,n=5000): """транзакции с самыми не популярными n товрами удалим""" df = data_train.copy() not_popularity = df.groupby('item_id')['quantity'].count().reset_index() not_popularity.rename(columns={'quantity': 'n_sold'}, inplace=True) not_top_n = not_popularity.sort_values('n_sold').head(n).item_id.tolist() df = df.loc[~df['item_id'].isin(not_top_n)] return df def prefiltr_4(self,data_train,weeks = 50): """Удалим транзакции с товарами, которые не покупали более n недель""" df = data_train.copy() old_item = df.groupby('item_id')['week_no'].max().reset_index() old_item = old_item.loc[old_item['week_no']>weeks,'item_id'].tolist() df = df.loc[df['item_id'].isin(old_item)] return df def train_test_split(self,test_size_num,split_column): data_train = self.data[self.data[split_column] < self.data[split_column].max() - test_size_num] data_test = self.data[self.data[split_column] >= self.data[split_column].max() - test_size_num] return data_train, data_test def get_validation_data(self): result = self.data_test.groupby('user_id')['item_id'].unique().reset_index() users_train = self.data_train.user_id.unique() result = result[result.user_id.isin(users_train)] result['train'] = result['user_id'].map(self.data_train.groupby('user_id')['item_id'].unique()) result['full_train'] = result['user_id'].map(self.full_data_train.groupby('user_id')['item_id'].unique()) result.rename(columns={'item_id':'test'},inplace=True) result.reset_index(inplace=True,drop=True) return result def prepare_matrix(self,agg_column,full=None,filtr=None): my_data = self.data_train.copy() if full: my_data = self.full_data_train.copy() if filtr: for i in filtr: prefiltr = 'self.prefiltr_'+str(i)+'(my_data)' my_data = eval(prefiltr) user_item_matrix = pd.pivot_table(my_data, index='user_id', columns='item_id', values=agg_column[0], aggfunc=agg_column[1], fill_value=0 ) user_item_matrix = user_item_matrix.astype(float) self.prepare_dicts(user_item_matrix) self.current_working_data = my_data.copy() return user_item_matrix def prepare_dicts(self,user_item_matrix): """Подготавливает вспомогательные словари""" userids = user_item_matrix.index.values itemids = user_item_matrix.columns.values matrix_userids = np.arange(len(userids)) matrix_itemids = np.arange(len(itemids)) self.id_to_itemid = dict(zip(matrix_itemids, itemids)) self.id_to_userid = dict(zip(matrix_userids, userids)) self.itemid_to_id = dict(zip(itemids, matrix_itemids)) self.userid_to_id = dict(zip(userids, matrix_userids)) return self.id_to_itemid, self.id_to_userid, self.itemid_to_id, self.userid_to_id def make_data(self,agg_column,filtr=None,full =False,top = 5000): self.top = top self.full = full uim = self.prepare_matrix(agg_column=agg_column,full=full,filtr=filtr) uim_w = uim.copy() self.user_item_matrix['uim_matrix_w'] = csr_matrix(uim_w).tocsr() uim[uim>0]=1 self.user_item_matrix['uim_matrix'] = csr_matrix(uim).tocsr() self.user_item_matrix['ium_matrix_w_tfidf'] = tfidf_weight(csr_matrix(uim_w.T).tocsr()) self.user_item_matrix['ium_matrix_tfidf'] = tfidf_weight(csr_matrix(uim.T).tocsr()) self.user_item_matrix['ium_matrix_w_bm25'] = bm25_weight(csr_matrix(uim_w.T).tocsr()) self.user_item_matrix['ium_matrix_bm25'] = bm25_weight(csr_matrix(uim.T).tocsr()) self.user_item_matrix['status'] = True self.user_item_matrix['params'] = {'agg_column':agg_column,'filtr':filtr,'full':full} return self.user_item_matrix def precision_at_k(x, k=5): if len(x['predict']) == 0: return 0 bought_list = np.array(x['test']) recommended_list = np.array(x['predict'])[:k] flags = np.isin(bought_list, recommended_list) precision = flags.sum() / len(recommended_list) return precision def fit_own_recommender(self,weighting=False): """Обучает модель, которая рекомендует товары, среди товаров, купленных юзером""" assert self.user_item_matrix['status'], 'необходимо сначала выполнить метод make_data(self,agg_column,filtr=None,weighting=None,full =False)' ium = self.user_item_matrix['uim_matrix'].T if weighting: assert (weighting == 'tf_idf' or weighting == 'bm25'), 'необходимо указать weighting: tf_idf или bm25 или None' if weighting == 'tf_idf': ium = self.user_item_matrix['ium_matrix_tfidf'] else: ium = self.user_item_matrix['ium_matrix_bm25'] self.own_recommender = ItemItemRecommender(K=1, num_threads=-1) self.own_recommender.fit(ium) self.own_recommender_is_fit['status'] =True self.own_recommender_is_fit['params'] ={'model':'ItemItemRecommender(K=1, num_threads=-1)','weighting':weighting} self.own_recommender_is_fit['ium']=ium return self.own_recommender def predict_own_recommender(self,users,N=5,params=own_recomender_defult_param): param = params.copy() assert self.own_recommender_is_fit['status'], 'необходимо сначала выполнить метод fit_own_recommender()' assert type(users) == list, 'users - должен быть списком' uim = self.user_item_matrix['uim_matrix'] param['user_items'] = uim param['N'] = N answer = pd.DataFrame() answer['user_id']=users if param['filter_items']: param['filter_items']=[self.itemid_to_id[i] for i in params['filter_items']] rec=[] for user in users: param['userid'] = self.userid_to_id[user] rec.append( [self.id_to_itemid[i[0]] for i in self.own_recommender.recommend(**param)]) answer['result'] = rec return answer def validation_own_recommender(self,metric=precision_at_k,N=5,params=own_recomender_defult_param): assert self.data_validation['status'], 'тестовые данные не созданы' assert self.own_recommender_is_fit['status'], 'необходимо сначала выполнить метод fit_own_recommender()' df = self.data_validation['data'] users = df['user_id'].to_list() predict = self.predict_own_recommender(users = users,N=N,params=params) df['predict'] = predict['result'] return df.apply(metric,axis=1).mean() def fit_als(self, params = model_als_defult_param,weighting=False): """Обучает ALS""" assert self.user_item_matrix['status'], 'необходимо сначала выполнить метод make_data(self,agg_column,filtr=None,weighting=None,full =False)' ium = self.user_item_matrix['uim_matrix_w'].T if weighting: assert (weighting == 'tf_idf' or weighting == 'bm25'), 'необходимо указать weighting: tf_idf или bm25 или None' if weighting == 'tf_idf': ium = self.user_item_matrix['ium_matrix_w_tfidf'] else: ium = self.user_item_matrix['ium_matrix_w_bm25'] self.model_als = AlternatingLeastSquares(**params) self.model_als.fit(ium) self.als_recommender_is_fit['status'] = True self.als_recommender_is_fit['params'] = {'model':params,'weighting':weighting} self.als_recommender_is_fit['ium'] = ium return self.model_als def predict_als(self,users,N=5,params=own_recomender_defult_param): param = params.copy() assert self.als_recommender_is_fit['status'], 'необходимо сначала выполнить метод fit_als()' assert type(users) == list, 'users - должен быть списком' uim = self.user_item_matrix['uim_matrix_w'] param['user_items'] = uim param['N'] = N answer = pd.DataFrame() answer['user_id']=users if param['filter_items']: param['filter_items']=[self.itemid_to_id[i] for i in params['filter_items']] rec=[] for user in users: param['userid'] = self.userid_to_id[user] rec.append( [self.id_to_itemid[i[0]] for i in self.model_als.recommend(**param)]) answer['result'] = rec return answer def validation_als_recommender(self,metric=precision_at_k,N=5,params=own_recomender_defult_param): assert self.data_validation['status'], 'тестовые данные не созданы' assert self.als_recommender_is_fit['status'], 'необходимо сначала выполнить метод fit_als()' df = self.data_validation['data'].copy() users = df['user_id'].to_list() predict = self.predict_als(users = users,N=N,params=params) df['predict'] = predict['result'] return df.apply(metric,axis=1).mean() def get_recs(self,user,popularity,not_my=0): result = [] for item in popularity[popularity['user_id']==user]['item_id'].to_list(): recs_ = self.model_als.similar_items(self.itemid_to_id[item], N=3) recs = [self.id_to_itemid[i[0]] for i in recs_] if 999999 in recs: recs.remove(999999) result.append(recs[not_my]) return result def get_similar_items_recommendation(self, users,not_my=0, N=5): """Рекомендуем товары, похожие на топ-N купленных юзером товаров not_my =1 если хотим предсказать поекупку собственных товаров (вроде own_recomender), 0 - обратно""" assert self.als_recommender_is_fit['status'],'Модель als не обучена, используйте fit_als()' assert type(users)==list,'параметр users должен быть list' assert not_my in [0,1],'параметр not_my должен быть равен 0 или 1' my_data = self.current_working_data.copy() my_data = my_data[my_data['user_id'].isin(users)] popularity = my_data.groupby(['user_id', 'item_id'])['quantity'].count().reset_index() popularity.sort_values('quantity', ascending=False, inplace=True) popularity = popularity[popularity['item_id'] != 999999] popularity =popularity.groupby('user_id').head(N) popularity.sort_values(['user_id','quantity'], ascending=False, inplace=True) result = pd.DataFrame() result['user_id'] = users result['similar_recommendation'] = result['user_id'].apply(\ lambda x: self.get_recs(user = x,popularity = popularity,not_my=not_my)) return result def validation_similar_items_recommendation(self,metric=precision_at_k,N=5,not_my=0): assert self.data_validation['status'], 'тестовые данные не созданы' assert self.als_recommender_is_fit['status'], 'необходимо сначала выполнить метод fit_als()' assert not_my in [0,1],'параметр not_my должен быть равен 0 или 1' df = self.data_validation['data'].copy() users = df['user_id'].to_list() predict = self.get_similar_items_recommendation(users = users,N=N,not_my=not_my) df['predict'] = predict['similar_recommendation'] return df.apply(metric,axis=1).mean() def get_user(self,user): users = self.model_als.similar_users(self.userid_to_id[user], N=2) return self.id_to_userid[users[1][0]] def get_similar_users_recommendation(self, users, N=5,params=own_recomender_defult_param): """Рекомендуем топ-N товаров, среди купленных похожими юзерами""" assert self.als_recommender_is_fit['status'],'Модель als не обучена, используйте fit_als()' assert type(users)==list,'параметр users должен быть list' result = pd.DataFrame() result['user_id'] = users result['simular_user_id'] = result['user_id'].apply(self.get_user) result['similar_recommendation'] = self.predict_als(result['simular_user_id'].to_list(),N=5,params=params)['result'] return result def validation_similar_users_recommendation(self,metric=precision_at_k,N=5): assert self.data_validation['status'], 'тестовые данные не созданы' assert self.als_recommender_is_fit['status'], 'необходимо сначала выполнить метод fit_als()' df = self.data_validation['data'].copy() users = df['user_id'].to_list() predict = self.get_similar_users_recommendation(users = users,N=N) df['predict'] = predict['similar_recommendation'] return df.apply(metric,axis=1).mean()
class AlsEstimator(TransformerMixin, BaseEstimator): def __init__(self, recommendations='als', n_rec=5, n_rec_pre=100, n_new=2, n_exp=1, price_lte=7, filter_item_id=-99, filter=True, filter_post=True, postfilter_func=None, factors=50, regularization=0.01, iterations=10, matrix_values='quantity', matrix_aggfunc='count', weighting=True, use_native=True, use_gpu=False): self.n_rec = n_rec self.n_rec_pre = n_rec_pre self.n_new = n_new self.n_exp = n_exp self.price_lte = price_lte self.filter_item_id = filter_item_id self.filter = filter self.filter_post = filter_post self.postfilter_func = postfilter_func self.factors = factors self.regularization = regularization self.iterations = iterations self.matrix_values = matrix_values self.matrix_aggfunc = matrix_aggfunc self.recommendations = recommendations self.weighting = True self.use_native = use_native self.use_gpu = use_gpu def _reset(self): if hasattr(self, 'item_info'): del self.item_info if hasattr(self, 'user_history'): del self.user_history if hasattr(self, 'top_purchases'): del self.top_purchases if hasattr(self, 'overall_top_purchases'): del self.overall_top_purchases if hasattr(self, 'user_item_matrix'): del self.user_item_matrix if hasattr(self, 'id_to_itemid'): del self.id_to_itemid if hasattr(self, 'id_to_userid'): del self.id_to_userid if hasattr(self, 'itemid_to_id'): del self.itemid_to_id if hasattr(self, 'userid_to_id'): del self.userid_to_id if hasattr(self, '_fit'): del self._fit @staticmethod def _prepare_matrix(data: pd.DataFrame, values: str, aggfunc: str): """Готовит user-item матрицу""" user_item_matrix = pd.pivot_table(data, index='user_id', columns='item_id', values=values, aggfunc=aggfunc, fill_value=0) user_item_matrix = user_item_matrix.astype(float) return user_item_matrix @staticmethod def _prepare_dicts(user_item_matrix): """Подготавливает вспомогательные словари""" userids = user_item_matrix.index.values itemids = user_item_matrix.columns.values matrix_userids = np.arange(len(userids)) matrix_itemids = np.arange(len(itemids)) id_to_itemid = dict(zip(matrix_itemids, itemids)) id_to_userid = dict(zip(matrix_userids, userids)) itemid_to_id = dict(zip(itemids, matrix_itemids)) userid_to_id = dict(zip(userids, matrix_userids)) return id_to_itemid, id_to_userid, itemid_to_id, userid_to_id def fit(self, X, y=None): self._reset() self.item_info = X.groupby('item_id').agg({ 'price': 'max', 'SUB_COMMODITY_DESC': 'first' }) self.user_history = pd.DataFrame( X.groupby('user_id').item_id.unique().rename('history')) self.top_purchases = X.groupby(['user_id', 'item_id' ])['quantity'].count().reset_index() self.top_purchases.sort_values('quantity', ascending=False, inplace=True) self.top_purchases = self.top_purchases[ self.top_purchases['item_id'] != self.filter_item_id] # Топ покупок по всему датасету self.overall_top_purchases = X.groupby( 'item_id')['quantity'].count().reset_index() self.overall_top_purchases.sort_values('quantity', ascending=False, inplace=True) self.overall_top_purchases = self.overall_top_purchases[ self.overall_top_purchases['item_id'] != self.filter_item_id] self.overall_top_purchases = self.overall_top_purchases.item_id.tolist( ) self.user_item_matrix = self._prepare_matrix(X, self.matrix_values, self.matrix_aggfunc) self.id_to_itemid, self.id_to_userid, \ self.itemid_to_id, self.userid_to_id = self._prepare_dicts(self.user_item_matrix) if self.weighting: self.user_item_matrix = bm25_weight(self.user_item_matrix.T).T self.model = AlternatingLeastSquares( factors=self.factors, regularization=self.regularization, iterations=self.iterations, dtype=np.float32, use_native=self.use_native, use_gpu=self.use_gpu, ) self.model.fit(csr_matrix(self.user_item_matrix).T.tocsr()) self.model_own_recommender = ItemItemRecommender(K=1) self.model_own_recommender.fit( csr_matrix(self.user_item_matrix).T.tocsr()) self._fit = True def transform(self, X): if self._fit: X = X['user_id'].drop_duplicates() X.index = X.values return X def _update_dict(self, user_id): """Если появился новыю user / item, то нужно обновить словари""" if user_id not in self.userid_to_id.keys(): max_id = max(list(self.userid_to_id.values())) max_id += 1 self.userid_to_id.update({user_id: max_id}) self.id_to_userid.update({max_id: user_id}) def _get_similar_item(self, item_id): """Находит товар, похожий на item_id""" recs = self.model.similar_items( self.itemid_to_id[item_id], N=2) # Товар похож на себя -> рекомендуем 2 товара top_rec = recs[1][0] # И берем второй (не товар из аргумента метода) return self.id_to_itemid[top_rec] def _extend_with_top_popular(self, recommendations): """Если кол-во рекоммендаций < N, то дополняем их топ-популярными""" if self.filter_post: n_rec = self.n_rec_pre else: n_rec = self.n_rec if len(recommendations) < n_rec: recommendations.extend(self.overall_top_purchases[:n_rec]) recommendations = recommendations[:n_rec] return recommendations def _get_recommendations(self, user, model, n_rec): """Рекомендации через стардартные библиотеки implicit""" self._update_dict(user_id=user) try: res = [ self.id_to_itemid[rec[0]] for rec in model.recommend( userid=self.userid_to_id[user], user_items=csr_matrix(self.user_item_matrix).tocsr(), N=n_rec, filter_already_liked_items=False, filter_items=[self.itemid_to_id[self.filter_item_id]], recalculate_user=True) ] except: res = list() finally: res = self._extend_with_top_popular(res) assert len(res) == n_rec, 'Количество рекомендаций != {}'.format( n_rec) return res def get_als_recommendations(self, user): """Рекомендации через стардартные библиотеки implicit""" if self.filter_post: n_rec = self.n_rec_pre else: n_rec = self.n_rec self._update_dict(user_id=user) return self._get_recommendations(user, model=self.model, n_rec) def get_own_recommendations(self, user): """Рекомендуем товары среди тех, которые юзер уже купил""" self._update_dict(user_id=user) return self._get_recommendations(user, model=self.model_own_recommender) def get_similar_items_recommendations(self, user): """Рекомендуем товары, похожие на топ-N купленных юзером товаров""" if self.filter_post: n_rec = self.n_rec_pre else: n_rec = self.n_rec top_users_purchases = self.top_purchases[self.top_purchases['user_id'] == user].head(n_rec) res = top_users_purchases['item_id'].apply( lambda x: self._get_similar_item(x)).tolist() res = self._extend_with_top_popular(res) assert len(res) == n_rec, 'Количество рекомендаций != {}'.format(n_rec) return res def get_similar_users_recommendations(self, user): """Рекомендуем топ-N товаров, среди купленных похожими юзерами""" if self.filter_post: n_rec = self.n_rec_pre else: n_rec = self.n_rec res = [] # Находим топ-N похожих пользователей similar_users = self.model.similar_users(self.userid_to_id[user], N=n_rec + 1) similar_users = [rec[0] for rec in similar_users] similar_users = similar_users[1:] # удалим юзера из запроса for user in similar_users: user_rec = self._get_recommendations( user, model=self.model_own_recommender, n_rec=1) res.extend(user_rec) res = self._extend_with_top_popular(res) assert len(res) == n_rec, 'Количество рекомендаций != {}'.format(n_rec) return res def predict(self, X): X = self.transform(X) recommender = getattr(self, f'get_{self.recommendations}_recommendations') rec = X.swifter.progress_bar(False).apply( lambda item: recommender(user=item)) if self.postfilter_func is not None and self.filter_post: rec = self.postfilter_func( rec, item_info=self.item_info, user_history=self.user_history, n_rec=self.n_rec, n_new=self.n_new, n_exp=self.n_exp, price_lte=self.price_lte, ) assert (rec.swifter.progress_bar(False).apply(len) == self.n_rec).all( ), f'The number of recommendations is not equal {self.n_rec}.' return rec
class MainRecommender: """Рекоммендации, которые можно получить из ALS Input ----- user_item_matrix: pd.DataFrame Матрица взаимодействий user-item """ def __init__(self, data, user_features, item_features, items_to_filter=[999999], weighting=True): self.items_to_filter = items_to_filter # Топ покупок каждого юзера self.top_purchases = data.groupby( ['user_id', 'item_id'])['quantity'].count().reset_index() self.top_purchases.sort_values('quantity', ascending=False, inplace=True) self.top_purchases = self.top_purchases[ self.top_purchases['item_id'] != 999999] # Топ покупок по всему датасету self.overall_top_purchases = data.groupby( 'item_id')['quantity'].count().reset_index() self.overall_top_purchases.sort_values('quantity', ascending=False, inplace=True) self.overall_top_purchases = self.overall_top_purchases[ ~self.overall_top_purchases['item_id'].isin( self.items_to_filter)] # ~self.top_purchases отрицание self.overall_top_purchases = self.overall_top_purchases.item_id.tolist( ) self.user_item_matrix, self.sparse_user_item = self._prepare_matrix( data) # pd.DataFrame self.id_to_itemid, self.id_to_userid, \ self.itemid_to_id, self.userid_to_id = self._prepare_dicts(self.user_item_matrix) # LightFM не будет взвешен при такой конструкции self.user_feat_lightfm_fixed, self.item_feat_lightfm_fixed = self._prepare_user_item_feat_lightfm( self.user_item_matrix, user_features, item_features) self.user_item_matrix_lightfm = self.user_item_matrix.copy() if weighting: self.user_item_matrix = bm25_weight(self.user_item_matrix.T).T @staticmethod def _prepare_matrix(data): """Готовит user-item матрицу""" user_item_matrix = pd.pivot_table( data, index='user_id', columns='item_id', values='quantity', # Можно пробовать другие варианты aggfunc='count', fill_value=0) user_item_matrix = user_item_matrix.astype( float) # необходимый тип матрицы для implicit # переведем в формат sparse matrix (для LightFM) sparse_user_item = csr_matrix(user_item_matrix).tocsr() return user_item_matrix, sparse_user_item @staticmethod def _prepare_dicts(user_item_matrix): """Подготавливает вспомогательные словари""" userids = user_item_matrix.index.values itemids = user_item_matrix.columns.values matrix_userids = np.arange(len(userids)) matrix_itemids = np.arange(len(itemids)) id_to_itemid = dict(zip(matrix_itemids, itemids)) id_to_userid = dict(zip(matrix_userids, userids)) itemid_to_id = dict(zip(itemids, matrix_itemids)) userid_to_id = dict(zip(userids, matrix_userids)) return id_to_itemid, id_to_userid, itemid_to_id, userid_to_id @staticmethod def _prepare_user_item_feat_lightfm(user_item_matrix, user_features, item_features): """Готовит под нужный формат фичи для LightFM""" user_feat = pd.DataFrame(user_item_matrix.index) user_feat = user_feat.merge( user_features, on='user_id', how='left').drop(columns=['homeowner_desc']) user_feat.set_index('user_id', inplace=True) item_feat = pd.DataFrame(user_item_matrix.columns) item_feat = item_feat.merge( item_features, on='item_id', how='left').drop( columns=['sub_commodity_desc', 'curr_size_of_product']) item_feat.set_index('item_id', inplace=True) user_feat_lightfm_fixed = pd.get_dummies( user_feat, columns=user_feat.columns.tolist()) item_feat_lightfm_fixed = pd.get_dummies( item_feat, columns=item_feat.columns.tolist()) return user_feat_lightfm_fixed, item_feat_lightfm_fixed def fit_own_recommender(self): """Обучает модель, которая рекомендует товары, среди товаров, купленных юзером""" self.own_recommender = ItemItemRecommender(K=1, num_threads=4) self.own_recommender.fit(csr_matrix(self.user_item_matrix).T.tocsr()) return self.own_recommender def fit_als(self, n_factors=20, regularization=0.001, iterations=15, num_threads=4, show_progress=True, use_gpu=True, random_state=42): """Обучает ALS""" self.model_als = AlternatingLeastSquares(factors=n_factors, regularization=regularization, iterations=iterations, use_gpu=use_gpu, num_threads=num_threads, random_state=random_state) self.model_als.fit(csr_matrix(self.user_item_matrix).T.tocsr(), show_progress=show_progress) return self.model_als def fit_lightfm(self, no_components=16, loss='warp', learning_rate=0.05, item_alpha=0.2, user_alpha=0.05, random_state=42, epochs=15): """Обучает LightFM""" self.model_lightfm = LightFM( no_components=no_components, loss=loss, # или 'warp' - ниже в уроке описана разница learning_rate=learning_rate, item_alpha=item_alpha, user_alpha=user_alpha, random_state=random_state) self.model_lightfm.fit( (self.sparse_user_item > 0) * 1, # user-item matrix из 0 и 1 sample_weight=coo_matrix( self.user_item_matrix_lightfm), # матрица весов С user_features=csr_matrix( self.user_feat_lightfm_fixed.values).tocsr(), item_features=csr_matrix( self.item_feat_lightfm_fixed.values).tocsr(), epochs=epochs, num_threads=8) return self.model_lightfm def precision_at_k_lightfm(self, model_lightfm, sparse_user_item, k=5): """Precision встроенный в LightFM""" self.precision_res = precision_at_k( model_lightfm, sparse_user_item, user_features=csr_matrix( self.user_feat_lightfm_fixed.values).tocsr(), item_features=csr_matrix( self.item_feat_lightfm_fixed.values).tocsr(), k=k) return self.precision_res def recall_at_k_lightfm(self, model_lightfm, sparse_user_item, k=5): """Recall встроенный в LightFM""" self.recall_res = recall_at_k( model_lightfm, sparse_user_item, user_features=csr_matrix( self.user_feat_lightfm_fixed.values).tocsr(), item_features=csr_matrix( self.item_feat_lightfm_fixed.values).tocsr(), k=k) return self.recall_res def _update_dict(self, user_id): """Если появился новый user / item, то нужно обновить словари""" if user_id not in self.userid_to_id.keys(): max_id = max(list(self.userid_to_id.values())) max_id += 1 self.userid_to_id.update({user_id: max_id}) self.id_to_userid.update({max_id: user_id}) def _get_similar_item(self, item_id): """Находит товар, похожий на item_id""" recs = self.model_als.similar_items( self.itemid_to_id[item_id], N=2) # Товар похож на себя -> рекомендуем 2 товара top_rec = recs[1][0] # И берем второй (не товар из аргумента метода) return self.id_to_itemid[top_rec] def _extend_with_top_popular(self, recommendations, N=5): """Если кол-во рекоммендаций < N, то дополняем их топ-популярными""" if len(recommendations) < N: recommendations.extend(self.overall_top_purchases[:N]) recommendations = recommendations[:N] return recommendations def _get_recommendations(self, user, model, N=5): """Рекомендации через стардартные библиотеки implicit""" self._update_dict(user_id=user) recs = model.recommend( userid=self.userid_to_id[user], user_items=csr_matrix(self.user_item_matrix).tocsr(), N=N, filter_already_liked_items=False, filter_items=[ self.itemid_to_id[item] for item in self.items_to_filter if item in self.itemid_to_id.keys() ], # [self.itemid_to_id[item] for item in items_to_filter if item in self.itemid_to_id.keys()] recalculate_user=True) res = [self.id_to_itemid[rec[0]] for rec in recs] res = self._extend_with_top_popular(res, N=N) assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res def get_als_recommendations(self, user, N=5): """Рекомендации через стардартные библиотеки implicit""" self._update_dict(user_id=user) return self._get_recommendations(user, model=self.model_als, N=N) def get_lightfm_recommendations(self, user, N=5): """Рекомендации для библиотеки LightFM""" self._update_dict(user_id=user) test_item_ids = np.arange(len(self.itemid_to_id)) scores = self.model_lightfm.predict( user_ids=int( self.userid_to_id[user] ), # На <class 'numpy.int64'> ругается, поэтому в int перевожу item_ids=test_item_ids, user_features=csr_matrix( self.user_feat_lightfm_fixed.values).tocsr(), item_features=csr_matrix( self.item_feat_lightfm_fixed.values).tocsr(), num_threads=8) top_items = np.argsort(-scores) res = [ self.id_to_itemid[item] for item in top_items ][: N] # Конвертируем id обратно, делаем срез на нужное число, т.к. предсказания были для всех item res = self._extend_with_top_popular( res, N=N ) # Теоретически для этой модели не нужно, т.к. предсказания сразу для всех делает? assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res def get_own_recommendations(self, user, N=5): """Рекомендуем товары среди тех, которые юзер уже купил""" self._update_dict(user_id=user) return self._get_recommendations(user, model=self.own_recommender, N=N) def get_similar_items_recommendation(self, user, N=5): """Рекомендуем товары, похожие на топ-N купленных юзером товаров. Не фильтрует item_id!""" top_users_purchases = self.top_purchases[self.top_purchases['user_id'] == user].head(N) res = top_users_purchases['item_id'].apply( lambda x: self._get_similar_item(x)).tolist() res = self._extend_with_top_popular(res, N=N) assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res def get_similar_users_recommendation(self, user, N=5): """Рекомендуем топ-N товаров, среди купленных похожими юзерами. Не фильтрует item_id!""" res = [] # Находим топ-N похожих пользователей similar_users = self.model_als.similar_users(self.userid_to_id[user], N=N + 1) similar_users = [rec[0] for rec in similar_users] similar_users = similar_users[1:] # удалим юзера из запроса for user in similar_users: userid = self.id_to_userid[ user] # own recommender works with user_ids res.extend(self.get_own_recommendations(userid, N=1)) res = self._extend_with_top_popular(res, N=N) assert len(res) == N, 'Количество рекомендаций != {}'.format(N) return res