"""
# Курсовой проект студента GeekBrains
# Автор: Ельцов Артем Александрович
# Ник на kaggle: virus_madison
# Задание для курсового проекта
# Метрика: MAP@5 - средневзвешенная точность рекомендаций
Сдача проекта:

Целевая метрика - MAP@5. Порог для уcпешной сдачи проекта - бэйзлайн в лидерборде (Most popular items baseline), если вам удастся его обойти вы успешно закрыли курс.

Датасет разбит на две части private/public в равных пропорциях.
Учтите что в тестовой выборке могут попстаться новые пользователи которые не видела ваша модель (для коллаборативной фильтрации)
НЕ обязательно, но крайне желательно использовать 2-ух уровневые рекоммендательные системы в проекте
Вы сдаете код проекта в виде github репозитория и указываете свой ник в kaggle.
Данные
Все данные знакомы вам из занятий:

* hh_demographic.csv - фичи пользователей.
* product.csv - фичи товарова.
* retail_train.csv - взаимодействие пользователей с товарами.
* test_users.csv - список пользователей для которого необходимо предоставить предсказания.
* sample_submission.csv - пример данных для загрузки на kaggle для валидации.

Для валидации необходимо загрузить предсказания для пользователей из файла test_users.csv.
"""
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" 
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
import matplotlib.pyplot as plt
%matplotlib inline

# Для работы с матрицами
from scipy.sparse import csr_matrix, coo_matrix

# Матричная факторизация
from implicit.als import AlternatingLeastSquares
from implicit.nearest_neighbours import bm25_weight, tfidf_weight
from implicit.bpr import BayesianPersonalizedRanking
from implicit.nearest_neighbours import ItemItemRecommender  # нужен для одного трюка
from lightfm import LightFM
from lightfm.evaluation import precision_at_k, recall_at_k

# Модель второго уровня
from lightgbm import LGBMClassifier

# Детерминированные алгоритмы
from implicit.nearest_neighbours import ItemItemRecommender, CosineRecommender, TFIDFRecommender, BM25Recommender
# Для работы с матрицами
from scipy.sparse import csr_matrix

# Матричная факторизация
from implicit.als import AlternatingLeastSquares
from implicit.nearest_neighbours import ItemItemRecommender  # нужен для одного трюка
from implicit.nearest_neighbours import bm25_weight, tfidf_weight


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

        self.model = self.fit(self.user_item_matrix)
        self.own_recommender = self.fit_own_recommender(self.user_item_matrix)

    @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

    @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(user_item_matrix, n_factors=50, regularization=0.001, iterations=15, num_threads=4):
        """Обучает ALS"""

        model = AlternatingLeastSquares(factors=n_factors,
                                        regularization=regularization,
                                        iterations=iterations,
                                        num_threads=num_threads)
        model.fit(csr_matrix(user_item_matrix).T.tocsr())

        return model

    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=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)
        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,
                                        filter_already_liked_items=False,
                                        filter_items=[self.itemid_to_id[999999]],
                                        recalculate_user=True)]

        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, N=N)

    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 купленных юзером товаров"""

        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.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:
            res.extend(self.get_own_recommendations(user, N=1))

        res = self._extend_with_top_popular(res, N=N)

        assert len(res) == N, 'Количество рекомендаций != {}'.format(N)
        return res
"""
# Filters
"""
def prefilter_items(data, take_n_popular=5000, item_features=None):
    # Уберем самые популярные товары (их и так купят)
    popularity = data.groupby('item_id')['user_id'].nunique().reset_index() / data['user_id'].nunique()
    popularity.rename(columns={'user_id': 'share_unique_users'}, inplace=True)

    top_popular = popularity[popularity['share_unique_users'] > 0.2].item_id.tolist()
    data = data[~data['item_id'].isin(top_popular)]

    # Уберем самые НЕ популярные товары (их и так НЕ купят)
    top_notpopular = popularity[popularity['share_unique_users'] < 0.02].item_id.tolist()
    data = data[~data['item_id'].isin(top_notpopular)]

    # Уберем не интересные для рекоммендаций категории (department)
    if item_features is not None:
        department_size = pd.DataFrame(item_features.\
                                        groupby('department')['item_id'].nunique().\
                                        sort_values(ascending=False)).reset_index()

        department_size.columns = ['department', 'n_items']
        rare_departments = department_size[department_size['n_items'] < 150].department.tolist()
        items_in_rare_departments = item_features[item_features['department'].isin(rare_departments)].item_id.unique().tolist()

        data = data[~data['item_id'].isin(items_in_rare_departments)]


    # Уберем слишком дешевые товары (на них не заработаем). 1 покупка из рассылок стоит 60 руб.
    data['price'] = data['sales_value'] / (np.maximum(data['quantity'], 1))
    data = data[data['price'] > 2]

    # Уберем слишком дорогие товарыs
    data = data[data['price'] < 50]

    # Возьмем топ по популярности
    popularity = data.groupby('item_id')['quantity'].sum().reset_index()
    popularity.rename(columns={'quantity': 'n_sold'}, inplace=True)

    top = popularity.sort_values('n_sold', ascending=False).head(take_n_popular).item_id.tolist()
    
    # Заведем фиктивный item_id (если юзер покупал товары из топ-5000, то он "купил" такой товар)
    data.loc[~data['item_id'].isin(top), 'item_id'] = 999999
    
    # ...

    return data
    

def postfilter(recommendations, item_info, N=5):
    """Пост-фильтрация товаров
    
    Input
    -----
    recommendations: list
        Ранжированный список item_id для рекомендаций
    item_info: pd.DataFrame
        Датафрейм с информацией о товарах
    """
    
    # Уникальность
#     recommendations = list(set(recommendations)) - неверно! так теряется порядок
    unique_recommendations = []
    [unique_recommendations.append(item) for item in recommendations if item not in unique_recommendations]
    
    # Разные категории
    categories_used = []
    final_recommendations = []
    
    CATEGORY_NAME = 'sub_commodity_desc'
    for item in unique_recommendations:
        category = item_features.loc[item_features['item_id'] == item, CATEGORY_NAME].values[0]
        
        if category not in categories_used:
            final_recommendations.append(item)
            
        unique_recommendations.remove(item)
        categories_used.append(category)
    
    n_rec = len(final_recommendations)
    if n_rec < N:
        final_recommendations.extend(unique_recommendations[:N - n_rec])
    else:
        final_recommendations = final_recommendations[:N]
    
    assert len(final_recommendations) == N, 'Количество рекомендаций != {}'.format(N)
    return final_recommendations
"""
# Metrics
"""
def precision_at_k(recommended_list, bought_list, k=5):
    
    bought_list = np.array(bought_list)
    recommended_list = np.array(recommended_list)
    
    bought_list = bought_list  # Тут нет [:k] !!
    recommended_list = recommended_list[:k]
    
    flags = np.isin(bought_list, recommended_list)
    
    precision = flags.sum() / len(recommended_list)
    
    
    return precision
def ap_k(recommended_list, bought_list, k=5):
    
    bought_list = np.array(bought_list)
    recommended_list = np.array(recommended_list)
    
    flags = np.isin(recommended_list, bought_list)
    
    if sum(flags) == 0:
        return 0
    
    sum_ = 0
    for i in range(1, k+1):
        
        if flags[i - 1] == True:
            p_k = precision_at_k(recommended_list, bought_list, k=i)
            
            sum_ += p_k
            
    result = sum_ / min(len(recommended_list), k)
    
    return result
def map_k(recommended_list_list, bought_list_list, k=5):
    
    ap = list()
    for i in range(len(recommended_list_list)):
        ap.append(ap_k(recommended_list_list[i], bought_list_list[i], k=k))
    
    map_metric = np.mean(ap)
    
    return map_metric
def ma_k_var2(predicted, actual, k=5):
    return np.mean([ap_k(a,p,k) for a,p in zip(actual, predicted)])
"""
# Open data and their transform
"""
data = pd.read_csv('/kaggle/input/gb-recsys-project/retail_train.csv')
item_features = pd.read_csv('/kaggle/input/gb-recsys-project/product.csv')
user_features = pd.read_csv('/kaggle/input/gb-recsys-project/hh_demographic.csv')
test_df = pd.read_csv('/kaggle/input/gb-recsys-project/test_users.csv')

# column processing
item_features.columns = [col.lower() for col in item_features.columns]
user_features.columns = [col.lower() for col in user_features.columns]

item_features.rename(columns={'product_id': 'item_id'}, inplace=True)
user_features.rename(columns={'household_key': 'user_id'}, inplace=True)


# Важна схема обучения и валидации!
# -- давние покупки -- | -- 6 недель -- | -- 3 недель -- 
# подобрать размер 2-ого датасета (6 недель) --> learning curve (зависимость метрики recall@k от размера датасета)
val_lvl_1_size_weeks = 6
val_lvl_2_size_weeks = 3

data_train_lvl_1 = data[data['week_no'] < data['week_no'].max() - (val_lvl_1_size_weeks + val_lvl_2_size_weeks)]
data_val_lvl_1 = data[(data['week_no'] >= data['week_no'].max() - (val_lvl_1_size_weeks + val_lvl_2_size_weeks)) &
                      (data['week_no'] < data['week_no'].max() - (val_lvl_2_size_weeks))]

data_train_lvl_2 = data_val_lvl_1.copy()  # Для наглядности. Далее мы добавим изменения, и они будут отличаться
data_val_lvl_2 = data[data['week_no'] >= data['week_no'].max() - val_lvl_2_size_weeks]

data_train_lvl_1.head(2)
users, items, interactions = data.user_id.nunique(), data.item_id.nunique(), data.shape[0]

print('# users: ', users) #пользователи
print('# items: ', items) #товары
print('# interactions: ', interactions) #взаимодействия
"""
### Train-test split
"""
# Используем train-test split по времени, а не случайно. Возьмем последние 3 недели в качестве теста
test_size_weeks = 3

data_train = data[data['week_no'] < data['week_no'].max() - test_size_weeks]
data_test = data[data['week_no'] >= data['week_no'].max() - test_size_weeks]
data_train.shape[0], data_test.shape[0]
"""
# 1. Бейзлайны
"""
"""
Создадим датафрейм с покупками юзеров на тестовом датасете (последние 3 недели)
"""
result = data_test.groupby('user_id')['item_id'].unique().reset_index()
result.columns=['user_id', 'actual']
result.head(2)
test_users = result.shape[0]
new_test_users = len(set(data_test['user_id']) - set(data_train['user_id']))

print('В тестовом дата сете {} юзеров'.format(test_users))
print('В тестовом дата сете {} новых юзеров'.format(new_test_users))
"""
# 1.1 Popularity-based recommendation
"""
popularity = data.groupby('item_id')['user_id'].nunique().reset_index() / data['user_id'].nunique()
popularity.rename(columns={'user_id': 'share_unique_users'}, inplace=True)

top_popular = popularity[popularity['share_unique_users'] > 0.3].item_id.tolist()

def popularity_recommendation(data, n=5):
    """Топ-n популярных товаров"""
    
    popular = data.groupby('item_id')['sales_value'].sum().reset_index()
    popular.sort_values('sales_value', ascending=False, inplace=True)
    
    recs = popular.head(n).item_id
    
    return recs.tolist()
%%time

# Можно так делать, так как рекомендация не зависит от юзера
popular_recs = popularity_recommendation(data_train, n=5)

result['popular_recommendation'] = result['user_id'].apply(lambda x: popular_recs)
result.head(2)
"""
# 1.2. Main Recommender
"""
n_items_before = data_train_lvl_1['item_id'].nunique()

data_train_lvl_1 = prefilter_items(data_train_lvl_1, item_features=item_features, take_n_popular=5000)

n_items_after = data_train_lvl_1['item_id'].nunique()
print('Decreased # items from {} to {}'.format(n_items_before, n_items_after))
recommender = MainRecommender(data_train_lvl_1)
recommender.get_als_recommendations(2375, N=5)
recommender.get_own_recommendations(2375, N=5)
recommender.get_similar_items_recommendation(2375, N=5)
recommender.get_similar_users_recommendation(2375, N=5)
# В тестовом дата сетеесть "холодные" покупатели на которах модель не обучалась
# Найдем данные по таким покупателям, для них по умолчанию будем рекомендовать самые популярные товары
list_out = result.loc[~result.user_id.isin(data_train_lvl_1.user_id), 'user_id'].tolist()
list_out
def rule(x, y, model):
    if model == 'als':
        if x in y:
            return list([840361, 1029743, 995242, 981760, 1082185])
        else:
            return recommender.get_als_recommendations(x, N=5)
    elif model == 'own':
        if x in y:
            return list([840361, 1029743, 995242, 981760, 1082185])
        else:
            return recommender.get_own_recommendations(x, N=5)
    elif model == 'similar_items':
        if x in y:
            return list([840361, 1029743, 995242, 981760, 1082185])
        else:
            return recommender.get_similar_items_recommendation(x, N=5)
    elif model == 'similar_users':
        if x in y:
            return list([840361, 1029743, 995242, 981760, 1082185])
        else:
            return recommender.get_similar_users_recommendation(x, N=5)
result['als'] = result['user_id'].apply(lambda x: rule(x, list_out, model='als'))
result['own'] = result['user_id'].apply(lambda x: rule(x, list_out, model='own'))
result['similar_items'] = result['user_id'].apply(lambda x: rule(x, list_out, model='similar_items'))
result.apply(lambda row: precision_at_k(row['als'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: precision_at_k(row['own'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: precision_at_k(row['similar_items'], row['actual']), axis=1).mean() * 100
result.head(2)
# Для примера генерации в тестовый дата сет
# test_df['own'] = test_df['user_id'].apply(lambda x: rule(x, list_out, model='own'))
"""
# Cross validation
"""
# user_item_matrix = recommender.user_item_matrix
# list_out = data_test.loc[~data_test.user_id.isin(data_train_lvl_1.user_id), 'user_id'].tolist()
# for factors in [50]:
#     recommender.fit(user_item_matrix, n_factors=factors, regularization=0.001, iterations=15, num_threads=4)
#     result['als'] = result['user_id'].apply(lambda x: rule(x, list_out, model='als'))
# result.apply(lambda row: precision_at_k(row['als'], row['actual']), axis=1).mean() * 100
"""
# Обучаем модель 2-ого уровня на выбранных кандидатах
"""
# -- давние покупки -- | -- 6 недель -- | -- 3 недель --

users_lvl_2 = pd.DataFrame(data_train_lvl_2['user_id'].unique())
users_lvl_2.columns = ['user_id']

# Пока только warm start
train_users = data_train_lvl_1['user_id'].unique()
users_lvl_2 = users_lvl_2[users_lvl_2['user_id'].isin(train_users)]

users_lvl_2['candidates'] = users_lvl_2['user_id'].apply(lambda x: recommender.get_own_recommendations(x, N=50))
users_lvl_2.head(2)
s = users_lvl_2.apply(lambda x: pd.Series(x['candidates']), axis=1).stack().reset_index(level=1, drop=True)
s.name = 'item_id'

users_lvl_2 = users_lvl_2.drop('candidates', axis=1).join(s)
users_lvl_2['flag'] = 1

users_lvl_2.head(4)
targets_lvl_2 = data_train_lvl_2[['user_id', 'item_id']].copy()
targets_lvl_2['target'] = 1  # тут только покупки 

targets_lvl_2 = users_lvl_2.merge(targets_lvl_2, on=['user_id', 'item_id'], how='left')

targets_lvl_2['target'].fillna(0, inplace= True)
targets_lvl_2.drop('flag', axis=1, inplace=True)
targets_lvl_2.head(2)
df = data_train.groupby('user_id')['item_id'].apply(list).reset_index()
print (df)
# Фильтрация полученных рекомендаций
# result = [postfilter(recommendations, item_info=item_features, N=5) for recommendations in df['item_id']]
"""
# 1.0. Filter Items
"""
# n_items_before = data_train['item_id'].nunique()

# data_train = prefilter_items(data_train, item_features=item_features)

# n_items_after = data_train['item_id'].nunique()
# print('Decreased # items from {} to {}'.format(n_items_before, n_items_after))
"""
# 1.2. Возьмем 1 наиболее близкого соседа
"""
# popularity = data_train.groupby('item_id')['quantity'].sum().reset_index()
# popularity.rename(columns={'quantity': 'n_sold'}, inplace=True)

# popularity.head()
# top_5000 = popularity.sort_values('n_sold', ascending=False).head(5000).item_id.tolist()
# Заведем фиктивный item_id (если юзер покупал товары из топ-5000, то он "купил" такой товар)
# data_train.loc[~data_train['item_id'].isin(top_5000), 'item_id'] = 999999

user_item_matrix = pd.pivot_table(data_train, 
                                  index='user_id', columns='item_id', 
                                  values='quantity',
                                  aggfunc='count', 
                                  fill_value=0
                                 )

user_item_matrix[user_item_matrix > 0] = 1 # так как в итоге хотим предсказать 
user_item_matrix = user_item_matrix.astype(float) # необходимый тип матрицы для implicit

# переведем в формат saprse matrix
sparse_user_item = csr_matrix(user_item_matrix).tocsr()

user_item_matrix.head(3)
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))
%%time

model = ItemItemRecommender(K=1, num_threads=4) # K - кол-во ближайших соседей

model.fit(csr_matrix(user_item_matrix).T.tocsr(), 
          show_progress=True)
%%time

result['own_purchases'] = result['user_id'].\
    apply(lambda x: [id_to_itemid[rec[0]] for rec in 
                    model.recommend(userid=userid_to_id[x], 
                                    user_items=sparse_user_item,   # на вход user-item matrix
                                    N=5, 
                                    filter_already_liked_items=False, 
                                    filter_items=None,
                                    recalculate_user=False)])
result.head(5)
"""
# 1.3. Alternating Least Squares (ALS)
"""
def get_recommendations(user, model, N=5):
    res = [id_to_itemid[rec[0]] for rec in 
                    model.recommend(userid=userid_to_id[user], 
                                    user_items=sparse_user_item,   # на вход user-item matrix
                                    N=N, 
                                    filter_already_liked_items=False, 
                                    filter_items=None, 
                                    recalculate_user=True)]
    return res
%%time

model = AlternatingLeastSquares(factors=50, 
                                regularization=0.001,
                                iterations=15, 
                                calculate_training_loss=True, 
                                num_threads=4)

model.fit(csr_matrix(user_item_matrix).T.tocsr(),  # На вход item-user matrix
          show_progress=True)

result['als_var'] = result['user_id'].apply(lambda x: get_recommendations(x, model=model, N=5))
"""
# 1.3. TF-IDF взвешивание
"""
user_item_matrix = tfidf_weight(user_item_matrix.T).T  # Применяется к item-user матрице ! 
%%time

model = AlternatingLeastSquares(factors=50, 
                                regularization=0.001,
                                iterations=15, 
                                calculate_training_loss=True, 
                                num_threads=4)

model.fit(csr_matrix(user_item_matrix).T.tocsr(),  # На вход item-user matrix
          show_progress=True)

result['als_tfidf'] = result['user_id'].apply(lambda x: get_recommendations(x, model=model, N=5))
"""
# 1.4. BM25 взвешивание
"""
# Заведем фиктивный item_id (если юзер покупал товары из топ-5000, то он "купил" такой товар)
# data_train.loc[~data_train['item_id'].isin(top_5000), 'item_id'] = 999999

user_item_matrix = pd.pivot_table(data_train, 
                                  index='user_id', columns='item_id', 
                                  values='quantity', # Можно пробоват ьдругие варианты
                                  aggfunc='count', 
                                  fill_value=0
                                 )

user_item_matrix = user_item_matrix.astype(float) # необходимый тип матрицы для implicit

# переведем в формат saprse matrix
sparse_user_item = csr_matrix(user_item_matrix).tocsr()

user_item_matrix.head(3)
user_item_matrix = bm25_weight(user_item_matrix.T).T  # Применяется к item-user матрице ! 
%%time

model = AlternatingLeastSquares(factors=50, 
                                regularization=0.001,
                                iterations=15, 
                                calculate_training_loss=True, 
                                num_threads=4) # K - кол-во билжайших соседей

model.fit(csr_matrix(user_item_matrix).T.tocsr(),  # На вход item-user matrix
          show_progress=True)

result['als_bm25'] = result['user_id'].apply(lambda x: get_recommendations(x, model=model, N=5))
"""
# Filter Items and model LFM
"""
# n_items_before = data_train['item_id'].nunique()

# data_train = prefilter_items(data_train, item_features=item_features)

# n_items_after = data_train['item_id'].nunique()
# print('Decreased # items from {} to {}'.format(n_items_before, n_items_after))
# user_item_matrix = pd.pivot_table(data_train, 
#                                   index='user_id', columns='item_id', 
#                                   values='quantity', # Можно пробоват ьдругие варианты
#                                   aggfunc='count', 
#                                   fill_value=0
#                                  )

# user_item_matrix = user_item_matrix.astype(float) # необходимый тип матрицы для implicit

# # переведем в формат saprse matrix
# sparse_user_item = csr_matrix(user_item_matrix).tocsr()

# user_item_matrix.head(2)
# data_test = data_test[data_test['item_id'].isin(data_train['item_id'].unique())]
# test_user_item_matrix = pd.pivot_table(data_test, 
#                                   index='user_id', columns='item_id', 
#                                   values='quantity', # Можно пробоват ьдругие варианты
#                                   aggfunc='count', 
#                                   fill_value=0
#                                  )

# test_user_item_matrix = user_item_matrix.astype(float) # необходимый тип матрицы для implicit
# 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))
# user_feat = pd.DataFrame(user_item_matrix.index)
# user_feat = user_feat.merge(user_features, on='user_id', how='left')
# 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')
# item_feat.set_index('item_id', inplace=True)

# user_feat.head(2)
# user_feat_lightfm = pd.get_dummies(user_feat, columns=user_feat.columns.tolist())
# item_feat_lightfm = pd.get_dummies(item_feat, columns=item_feat.columns.tolist())
# user_feat_lightfm.head(2)
# model = LightFM(no_components=30,
#                 loss='bpr', # 'warp'
#                 learning_rate=0.05, 
#                 item_alpha=0.1, user_alpha=0.1, 
#                 random_state=42)

# model.fit((sparse_user_item > 0) * 1,  # user-item matrix из 0 и 1
#           sample_weight=coo_matrix(user_item_matrix),
#           user_features=csr_matrix(user_feat_lightfm.values).tocsr(),
#           item_features=csr_matrix(item_feat_lightfm.values).tocsr(),
#           epochs=15, 
#           num_threads=4) 
# user_emb = model.get_user_representations(features=csr_matrix(user_feat_lightfm.values).tocsr())
# train_precision = precision_at_k(model, sparse_user_item, 
#                                  user_features=csr_matrix(user_feat_lightfm.values).tocsr(),
#                                  item_features=csr_matrix(item_feat_lightfm.values).tocsr(),
#                                  k=5).mean()

# train_precision
"""
# Измерим качество по precision@5
"""
result.head(2)
result.apply(lambda row: precision_at_k(row['popular_recommendation'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: precision_at_k(row['own_purchases'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: precision_at_k(row['als_var'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: precision_at_k(row['als_tfidf'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: precision_at_k(row['als_bm25'], row['actual']), axis=1).mean() * 100
"""
# Измерим качество по MAP@k (Mean Average Precision@k)  
- Показывает средневзвешенную точность рекомендаций
"""
result.apply(lambda row: ap_k(row['popular_recommendation'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: ap_k(row['own_purchases'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: ap_k(row['als'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: ap_k(row['als_tfidf'], row['actual']), axis=1).mean() * 100
result.apply(lambda row: ap_k(row['als_bm25'], row['actual']), axis=1).mean() * 100
"""
# 8. Прогнозирование на тестовом датасете
"""
"""
Для удобства перевода данных в подходящий для загрузки формат:
"""
def transform_data_for_eval(dataset, rec_col, user_col='user_id'):
    '''
    Func for transforming recommendations into kaggle evaluation format

    Parameters:
    dataset (pd.DataFrame): Dataset with 2 required columns:
        rec_col - column with recommendations should be iterable
        user_col - columns with user id

    rec_col (str): name of column in dataset with recommendations

    user_col (str): name of column in dataset with user id

    Returns:
    pd.DataFrame: DataFrame in suitable format

   '''
    eval_dataset = dataset[[user_col, rec_col]].copy()
    eval_dataset[rec_col] = eval_dataset[rec_col].apply(lambda x: ' '.join([str(i) for i in x]))
    eval_dataset.rename(columns={
        user_col: 'UserId',
        rec_col: 'Predicted'
    }, inplace=True)
    return eval_dataset
submit = pd.read_csv('/kaggle/input/gb-recsys-project/sample_submission.csv')
submit.head()
"""
# Эффективней других оказалась идея найти 1 ближашего соседа и фильтровать по топ 5000, ее и применим.
"""
def get_recommendations(user, model, N=5):
    res = [id_to_itemid[rec[0]] for rec in 
                    model.recommend(userid=userid_to_id[user], 
                                    user_items=sparse_user_item,   # на вход user-item matrix
                                    N=N, 
                                    filter_already_liked_items=False, 
                                    filter_items=None, 
                                    recalculate_user=True)]
    return res
user_item_matrix = pd.pivot_table(data_train, 
                                  index='user_id', columns='item_id', 
                                  values='quantity',
                                  aggfunc='count', 
                                  fill_value=0
                                 )

user_item_matrix[user_item_matrix > 0] = 1 # так как в итоге хотим предсказать 
user_item_matrix = user_item_matrix.astype(float) # необходимый тип матрицы для implicit

# переведем в формат saprse matrix
sparse_user_item = csr_matrix(user_item_matrix).tocsr()
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))
%%time

model = ItemItemRecommender(K=1, num_threads=4) # K - кол-во ближайших соседей

model.fit(csr_matrix(user_item_matrix).T.tocsr(), 
          show_progress=True)
model
list_out = test_df.loc[~test_df.user_id.isin(data_train.user_id), 'user_id'].tolist()
list_out
def rule(x, y):
    if x in y:
         return list([840361, 1029743, 995242, 981760, 1082185])
    else:
         return get_recommendations(x, model=model, N=5)
test_df['own_purchases'] = test_df['user_id'].apply(lambda x: rule(x, list_out))
test_df.head(2)
sampl_df = transform_data_for_eval(test_df, rec_col='own_purchases', user_col='user_id')
sampl_df.head(2)
sampl_df.to_csv('sample_test_submission.csv', index=False)