REK-proj-1/project_1_recommender_and_evaluation.ipynb
Aleksander Piotrowski 8bdc851fae Remove redundant stuff
2021-05-21 21:36:24 +02:00

61 KiB

%matplotlib inline
%load_ext autoreload
%autoreload 2

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import Markdown, display, HTML
from collections import defaultdict

# Fix the dying kernel problem (only a problem in some installations - you can remove it, if it works without it)
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'

######################
# I have added hyperopt package to environment.yml. 
######################

import warnings
warnings.filterwarnings("ignore")
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
Collecting package metadata (current_repodata.json): failed

CondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://conda.anaconda.org/conda-forge/linux-64/current_repodata.json>
Elapsed: -

An HTTP error occurred when trying to retrieve this URL.
HTTP errors are often intermittent, and a simple retry will get you on your way.
'https://conda.anaconda.org/conda-forge/linux-64'


Load the dataset for recommenders

data_path = os.path.join("data", "hotel_data")

interactions_df = pd.read_csv(os.path.join(data_path, "hotel_data_interactions_df.csv"), index_col=0)
preprocessed_data = pd.read_csv(os.path.join(data_path, "hotel_data_preprocessed.csv"), index_col=0)

base_item_features = ['term', 'length_of_stay_bucket', 'rate_plan', 'room_segment', 'n_people_bucket', 'weekend_stay']

column_values_dict = {
    'term': ['WinterVacation', 'Easter', 'OffSeason', 'HighSeason', 'LowSeason', 'MayLongWeekend', 'NewYear', 'Christmas'],
    'length_of_stay_bucket': ['[0-1]', '[2-3]', '[4-7]', '[8-inf]'],
    'rate_plan': ['Standard', 'Nonref'],
    'room_segment': ['[0-160]', '[160-260]', '[260-360]', '[360-500]', '[500-900]'],
    'n_people_bucket': ['[1-1]', '[2-2]', '[3-4]', '[5-inf]'],
    'weekend_stay': ['True', 'False']
}

interactions_df.loc[:, 'term'] = pd.Categorical(
    interactions_df['term'], categories=column_values_dict['term'])
interactions_df.loc[:, 'length_of_stay_bucket'] = pd.Categorical(
    interactions_df['length_of_stay_bucket'], categories=column_values_dict['length_of_stay_bucket'])
interactions_df.loc[:, 'rate_plan'] = pd.Categorical(
    interactions_df['rate_plan'], categories=column_values_dict['rate_plan'])
interactions_df.loc[:, 'room_segment'] = pd.Categorical(
    interactions_df['room_segment'], categories=column_values_dict['room_segment'])
interactions_df.loc[:, 'n_people_bucket'] = pd.Categorical(
    interactions_df['n_people_bucket'], categories=column_values_dict['n_people_bucket'])
interactions_df.loc[:, 'weekend_stay'] = interactions_df['weekend_stay'].astype('str')
interactions_df.loc[:, 'weekend_stay'] = pd.Categorical(
    interactions_df['weekend_stay'], categories=column_values_dict['weekend_stay'])

display(HTML(interactions_df.head(15).to_html()))
user_id item_id term length_of_stay_bucket rate_plan room_segment n_people_bucket weekend_stay
0 1 0 WinterVacation [2-3] Standard [260-360] [5-inf] True
1 2 1 WinterVacation [2-3] Standard [160-260] [3-4] True
2 3 2 WinterVacation [2-3] Standard [160-260] [2-2] False
3 4 3 WinterVacation [4-7] Standard [160-260] [3-4] True
4 5 4 WinterVacation [4-7] Standard [0-160] [2-2] True
5 6 5 Easter [4-7] Standard [260-360] [5-inf] True
6 7 6 OffSeason [2-3] Standard [260-360] [5-inf] True
7 8 7 HighSeason [2-3] Standard [160-260] [1-1] True
8 9 8 HighSeason [2-3] Standard [0-160] [1-1] True
9 8 7 HighSeason [2-3] Standard [160-260] [1-1] True
10 8 7 HighSeason [2-3] Standard [160-260] [1-1] True
11 10 9 HighSeason [2-3] Standard [160-260] [3-4] True
12 11 9 HighSeason [2-3] Standard [160-260] [3-4] True
13 12 10 HighSeason [8-inf] Standard [160-260] [3-4] True
14 14 11 HighSeason [2-3] Standard [0-160] [3-4] True

Define user features based on reservations

The content-based recommenders will be forecasting the probability of interaction between user and item based on user features vector and item features vector:

$$ r_{u, i} = f(user\_features, item\_features) $$

Task:
Design numerical user features based on user reservations. Code the following method which for a given interactions DataFrame (it will be used in the fit method of the recommender) returns a DataFrame with user_id and user features as well as a list with names of user features (this will be important to select the right columns for an ML algorithm). Remember to name the columns differently than item features which you will create in the next task. Validate your features on users with several interactions (sample user ids are already given below).

Ideas for user features:

  • Find the vector of most popular feature values from all user reservations and encode every feature with one-hot encoding.
  • For every reservation feature calculate the probability distribution of its values among all user's reservations.
  • For numerical buckets (length_of_stay, room_segment, n_people) you can calculate the average value for every user from their reservations (you will have to map the buckets back to numerical values before averaging them).

Remember that you will have to select the best features (with the highest explanatory power). Using all above features at once would make the number of variables too large for this dataset and would also introduce too much correlations between features.

You can also prepare several version of the prepare_users_df method and test which works best in your recommender.

def prepare_users_df(interactions_df):
    users_df = interactions_df[['user_id']]
    users_df = users_df.drop_duplicates()
    
    users_df['n_people'] = preprocessed_data.groupby('user_id')['n_people'].transform('mean').reindex()
    users_df['length_of_stay'] = preprocessed_data.groupby('user_id')['length_of_stay'].transform('mean').reindex()
    users_df['night_price'] = preprocessed_data.groupby('user_id')['night_price'].transform('mean').reindex()
    
    user_features = list(users_df.columns)
    return users_df, user_features
    

users_df, user_features = prepare_users_df(interactions_df)

print(user_features)

display(HTML(users_df.loc[users_df['user_id'].isin([706, 1736, 7779, 96, 1, 50, 115])].head(15).to_html()))
['user_id', 'n_people', 'length_of_stay', 'night_price']
user_id n_people length_of_stay night_price
0 1 2.434783 3.434783 161.930000
52 50 4.043478 2.695652 270.541739
103 96 3.000000 2.291667 154.016250
150 115 1.409091 1.909091 15.909091
700 706 3.913947 3.827893 54.159792
1758 1736 1.896552 2.655172 202.308621
8097 7779 4.259259 4.333333 33.788889

Prepare numerical item features

Task:
Code the prepare_items_df method which will be used in the recommender fit and recommend methods to map items to numerical features. This method should take the interactions_df DataFrame as input and return a DataFrame containing one record per item_id with item_id column and numerical item feature columns.

You can try turning all item features into on-hot representations. You can use the get_dummies method from pandas. It will return the same columns on any dataset of interactions because of the categorical variables with all possible values have been defined in the second cell in this notebook.

You are welcome to design your own numerical item features.

def prepare_items_df(interactions_df):
    items_df = interactions_df[[
        'term',
        'length_of_stay_bucket',
        'rate_plan',
        'room_segment',
        'n_people_bucket',
        'weekend_stay'
    ]]
    
    onehot_df = pd.get_dummies(items_df)
    onehot_df['item_id'] = interactions_df['item_id']
    
    onehot_df = onehot_df.drop_duplicates()
    
    
    item_features = list(onehot_df.columns)
    return onehot_df, item_features


items_df, item_features = prepare_items_df(interactions_df)

display(HTML(items_df.loc[items_df['item_id'].isin([0, 1, 2, 3, 4, 5, 6])].head(15).to_html()))
term_WinterVacation term_Easter term_OffSeason term_HighSeason term_LowSeason term_MayLongWeekend term_NewYear term_Christmas length_of_stay_bucket_[0-1] length_of_stay_bucket_[2-3] length_of_stay_bucket_[4-7] length_of_stay_bucket_[8-inf] rate_plan_Standard rate_plan_Nonref room_segment_[0-160] room_segment_[160-260] room_segment_[260-360] room_segment_[360-500] room_segment_[500-900] n_people_bucket_[1-1] n_people_bucket_[2-2] n_people_bucket_[3-4] n_people_bucket_[5-inf] weekend_stay_True weekend_stay_False item_id
0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 1 0 0 0 0 0 1 1 0 0
1 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 0 0 0 0 1 0 1 0 1
2 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 0 0 0 1 0 0 0 1 2
3 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 1 0 1 0 3
4 1 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 1 0 0 1 0 4
5 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 0 0 0 0 1 1 0 5
6 0 0 1 0 0 0 0 0 0 1 0 0 1 0 0 0 1 0 0 0 0 0 1 1 0 6
print("interactions", len(interactions_df))
print("items",len(items_df))
print("users", len(users_df))
interactions 16102
items 772
users 14188

Content-based recommender

Task:
Code the content-based recommender. User features should be calculated within the fit method based on available training data and should be saved in the object for later use in the recommend method. Overwrite the users_df variable. Item features should be calculated both in the fit method (from interactions_df) and in the recommend method (from items_df - the items to be evaluated).

In the fit method you have to randomly generate non-existing interactions and add them to the training data for the regressor. You should add the target variable to interactions - equal to 1 for real interactions and equal to 0 for those newly added interactions. Generate several negative interactions per every positive interactions (n_neg_per_pos). Treat the proportion as a tunable parameter of the model.

Remember to keep control over randomness - in the init method add seed as a parameter and use initialize the random seed generator with that seed:

self.seed = seed
self.rng = np.random.RandomState(seed=seed)

Below the base content-based recommender class there are several classes which inherit from the base class and use different ML models:

  • LinearRegressionCBUIRecommender - based on linear regression,

  • SVRCBUIRecommender - based on Support Vector Regressor (if you want to test it, sample the data in the fit method, as the training can take many hours on the entire dataset of interactions),

  • RandomForestCBUIRecommender - based on Random Forest,

  • XGBoostCBUIRecommender - based on XGBoost.

    There is no need to change anything in those inheriting classes, although you can experiment with other tunable parameters of the underlying models.

You are encouraged to experiment with:

  • Other numerical user and item features (but always train and evaluate the model on buckets defined in the first notebook).
  • Other ML models, e.g. Huber regression, Lasso regression, Ridge regression, LARS regression, Linear SVR, Decision Tree, Naive Bayes, Neural Networks or any model of your choice.
  • A different approach where you treat each item as a class, you train directly on categorical features of items and users (you would have to design appropriate categorical features for users) and you fit classifiers (e.g. Decision Tree classifier, Naive Bayes classifier etc.) instead of regressors.
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor

from recommenders.recommender import Recommender


class ContentBasedUserItemRecommender(Recommender):
    """
    Linear recommender class based on user and item features.
    """
    
    def __init__(self, seed=6789, n_neg_per_pos=5):
        """
        Initialize base recommender params and variables.
        """
        self.model = LinearRegression()
        self.n_neg_per_pos = n_neg_per_pos
        
        self.recommender_df = pd.DataFrame(columns=['user_id', 'item_id', 'score'])
        self.users_df = None
        self.user_features = None
        
        self.seed = seed
        self.rng = np.random.RandomState(seed=seed)
    
    def gen_neg_interactions(self):
        user_ids = interactions_df['user_id']
        item_ids = interactions_df['item_id']
        
        while True:
            user_id = user_ids.sample().item()
            item_id = item_ids.sample().item()
            
            found_interaction = interactions_df[(interactions_df['item_id'] == item_id) & (interactions_df['user_id'] == user_id)]
            if found_interaction.empty:
                return (user_id, item_id, 0)

    
    def fit(self, interactions_df, users_df, items_df):
        """
        Training of the recommender.
        
        :param pd.DataFrame interactions_df: DataFrame with recorded interactions between users and items 
            defined by user_id, item_id and features of the interaction.
        :param pd.DataFrame users_df: DataFrame with users and their features defined by user_id and the user feature columns.
        :param pd.DataFrame items_df: DataFrame with items and their features defined by item_id and the item feature columns.
        """
        
        interactions_df = interactions_df.copy()
        
        # Prepare users_df and items_df
        
        users_df, user_features = prepare_users_df(interactions_df)
        
        self.users_df = users_df
        self.user_features = user_features
        
        items_df, item_features = prepare_items_df(interactions_df)
#         items_df = items_df.loc[:, ['item_id'] + item_features]
        
        # Generate negative interactions
        
        interactions_df = interactions_df.loc[:, ['user_id', 'item_id']]
        
        interactions_df.loc[:, 'interacted'] = 1
        
        negative_interactions = []
        
        # Write your code here
        # Generate tuples (user_id, item_id, 0) for pairs (user_id, item_id) which do not
        # appear in the interactions_df and add those tuples to the list negative_interactions.
        # Generate self.n_neg_per_pos * len(interactions_df) negative interactions 
        # (self.n_neg_per_pos per one positive).
        # Make sure the code is efficient and runs fast, otherwise you will not be able to properly tune your model.
        
        num_of_neg = int(self.n_neg_per_pos * len(interactions_df))
        for i in range(num_of_neg):
            negative_interactions.append(self.gen_neg_interactions())
            
        interactions_df = pd.concat(
            [interactions_df, pd.DataFrame(negative_interactions, columns=['user_id', 'item_id', 'interacted'])])
        
        # Get the input data for the model
        
#         print("==================================")
#         print(users_df)
#         print("==================================")
#         print(items_df)
#         print("==================================")
        
        interactions_df = pd.merge(interactions_df, users_df, on=['user_id'])
        interactions_df = pd.merge(interactions_df, items_df, on=['item_id'])
        
        x = interactions_df.loc[:, user_features + item_features].values
        y = interactions_df['interacted'].values
    
        self.model.fit(x, y)
    
    def recommend(self, users_df, items_df, n_recommendations=1):
        """
        Serving of recommendations. Scores items in items_df for each user in users_df and returns 
        top n_recommendations for each user.
        
        :param pd.DataFrame users_df: DataFrame with users and their features for which recommendations should be generated.
        :param pd.DataFrame items_df: DataFrame with items and their features which should be scored.
        :param int n_recommendations: Number of recommendations to be returned for each user.
        :return: DataFrame with user_id, item_id and score as columns returning n_recommendations top recommendations 
            for each user.
        :rtype: pd.DataFrame
        """
        
        # Clean previous recommendations (iloc could be used alternatively)
        self.recommender_df = self.recommender_df[:0]
        
        # Write your code here
        # Prepare users_df and items_df
        # For users_df you just need to merge user features from self.users_df to users_df 
        # (the users for which you generate recommendations)
        users_df = pd.merge(self.users_df, users_df, on='user_id')
        # For items you have to apply the prepare_items_df method to items_df.
        items_df, item_features = prepare_items_df(items_df)
        
        # Score the items
    
        recommendations = pd.DataFrame(columns=['user_id', 'item_id', 'score'])
        
        for ix, user in users_df.iterrows():
            
            # Write your code here
            # Create a Carthesian product of users from users_df and items from items_df
            # https://stackoverflow.com/questions/13269890/cartesian-product-in-pandas
            carthesian_df = users_df.merge(items_df, how='cross')

            # Write your code here
            # Use self.model.predict method to calculate scores for all records in the just created DataFrame
            # of users and items
            scores = self.model.predict(carthesian_df)
                         
            # Write your code here
            # Obtain item ids with the highest score and save those ids under the chosen_ids variable
            # Do not exclude already booked items.
            chosen_ids = np.argsort(scores)[-n_recommendations:]
            
            recommendations = []
            if len(chosen_ids) == 0:
                print("empty chosen_ids")
                
            for item_id in chosen_ids:
                recommendations.append(
                    {
                        'user_id': user['user_id'],
                        'item_id': item_id,
                        'score': scores[item_id]
                    }
                )
            
            user_recommendations = pd.DataFrame(recommendations)

            self.recommender_df = pd.concat([self.recommender_df, user_recommendations])

        return self.recommender_df
    
    
class LinearRegressionCBUIRecommender(ContentBasedUserItemRecommender):
    """
    Linear regression recommender class based on user and item features.
    """
    
    def __init__(self, seed=6789, n_neg_per_pos=5, **model_params):
        """
        Initialize base recommender params and variables.
        """
        super().__init__(seed=seed, n_neg_per_pos=n_neg_per_pos)
        self.model = LinearRegression()
        
        
class SVRCBUIRecommender(ContentBasedUserItemRecommender):
    """
    SVR recommender class based on user and item features.
    """
    
    def __init__(self, seed=6789, n_neg_per_pos=5, **model_params):
        """
        Initialize base recommender params and variables.
        """
        super().__init__(seed=seed, n_neg_per_pos=n_neg_per_pos)
        if 'kernel' in model_params:
            self.kernel = model_params['kernel']
        else:
            self.kernel = 'rbf'
        if 'C' in model_params:
            self.C = model_params['C']
        else:
            self.C = 1.0
        if 'epsilon' in model_params:
            self.epsilon = model_params['epsilon']
        else:
            self.epsilon = 0.1
        self.model = SVR(kernel=self.kernel, C=self.C, epsilon=self.epsilon)
        
    
class RandomForestCBUIRecommender(ContentBasedUserItemRecommender):
    """
    Random forest recommender class based on user and item features.
    """
    
    def __init__(self, seed=6789, n_neg_per_pos=5, **model_params):
        """
        Initialize base recommender params and variables.
        """
        super().__init__(seed=seed, n_neg_per_pos=n_neg_per_pos)
        if 'n_estimators' in model_params:
            self.n_estimators = int(model_params['n_estimators'])
        else:
            self.n_estimators = 100
        if 'max_depth' in model_params:
            self.max_depth = int(model_params['max_depth'])
        else:
            self.max_depth = 30
        if 'min_samples_split' in model_params:
            self.min_samples_split = int(model_params['min_samples_split'])
        else:
            self.min_samples_split = 30
        self.model = RandomForestRegressor(
            n_estimators=self.n_estimators, max_depth=self.max_depth, min_samples_split=self.min_samples_split)
    
    
class XGBoostCBUIRecommender(ContentBasedUserItemRecommender):
    """
    XGBoost recommender class based on user and item features.
    """
    
    def __init__(self, seed=6789, n_neg_per_pos=5, **model_params):
        """
        Initialize base recommender params and variables.
        """
        super().__init__(seed=seed, n_neg_per_pos=n_neg_per_pos)
        if 'n_estimators' in model_params:
            self.n_estimators = int(model_params['n_estimators'])
        else:
            self.n_estimators = 100
        if 'max_depth' in model_params:
            self.max_depth = int(model_params['max_depth'])
        else:
            self.max_depth = 30
        if 'min_samples_split' in model_params:
            self.min_samples_split = int(model_params['min_samples_split'])
        else:
            self.min_samples_split = 30
        if 'learning_rate' in model_params:
            self.learning_rate = model_params['learning_rate']
        else:
            self.learning_rate = 30
        self.model = GradientBoostingRegressor(
            n_estimators=self.n_estimators, max_depth=self.max_depth, min_samples_split=self.min_samples_split,
            learning_rate=self.learning_rate)  
                         
                         
recommender = ContentBasedUserItemRecommender()
# print(recommender.gen_neg_interactions())
# items_df = interactions_df.loc[:, ['item_id'] + base_item_features].drop_duplicates()
# recommender.fit(interactions_df.copy(), users_df.copy(), items_df.copy())
# recommender.recommend(users_df.copy(), items_df.copy())

Quick test of the recommender

items_df = interactions_df.loc[:, ['item_id'] + base_item_features].drop_duplicates()
# Fit method
cb_user_item_recommender = RandomForestCBUIRecommender()
cb_user_item_recommender.fit(interactions_df, None, None)
# Recommender method

recommendations = cb_user_item_recommender.recommend(pd.DataFrame([[1], [2], [3], [4], [5]], columns=['user_id']), interactions_df, 10)

recommendations = pd.merge(recommendations, items_df, on='item_id', how='left')
display(HTML(recommendations.to_html()))

Tuning method

from evaluation_and_testing.testing import evaluate_train_test_split_implicit

seed = 6789
from hyperopt import hp, fmin, tpe, Trials
import traceback

def tune_recommender(recommender_class, interactions_df, items_df, 
                     param_space, max_evals=1, show_progressbar=True, seed=6789):
    # Split into train_validation and test sets

    shuffle = np.arange(len(interactions_df))
    rng = np.random.RandomState(seed=seed)
    rng.shuffle(shuffle)
    shuffle = list(shuffle)

    train_test_split = 0.8
    split_index = int(len(interactions_df) * train_test_split)

    train_validation = interactions_df.iloc[shuffle[:split_index]]
    test = interactions_df.iloc[shuffle[split_index:]]

    # Tune

    def loss(tuned_params):
        recommender = recommender_class(seed=seed, **tuned_params)
        hr1, hr3, hr5, hr10, ndcg1, ndcg3, ndcg5, ndcg10 = evaluate_train_test_split_implicit(
            recommender, train_validation, items_df, seed=seed)
        return -hr10

    n_tries = 1
    succeded = False
    try_id = 0
    while not succeded and try_id < n_tries:
        try:
            trials = Trials()
            best_param_set = fmin(loss, space=param_space, algo=tpe.suggest, 
                                  max_evals=max_evals, show_progressbar=show_progressbar, trials=trials, verbose=True)
            succeded = True
        except:
            traceback.print_exc()
            try_id += 1
            
    if not succeded:
        return None
        
    # Validate
    
    recommender = recommender_class(seed=seed, **best_param_set)

    results = [[recommender_class.__name__] + list(evaluate_train_test_split_implicit(
        recommender, {'train': train_validation, 'test': test}, items_df, seed=seed))]

    results = pd.DataFrame(results, 
                           columns=['Recommender', 'HR@1', 'HR@3', 'HR@5', 'HR@10', 'NDCG@1', 'NDCG@3', 'NDCG@5', 'NDCG@10'])

    display(HTML(results.to_html()))
    
    return best_param_set

Tuning of the recommender

Task:
Tune your models using the code below. You only need to put the class name of your recommender and choose an appropriate parameter space.

param_space = {
    'n_neg_per_pos': hp.quniform('n_neg_per_pos', 1, 10, 1)
}

best_param_set_LinearRegressionCBUIRecommender = tune_recommender(LinearRegressionCBUIRecommender, interactions_df, items_df,
                                  param_space, max_evals=10, show_progressbar=True, seed=seed)

print("Best parameters:")
print(best_param_set_LinearRegressionCBUIRecommender)
100%|██████████| 10/10 [35:55<00:00, 215.58s/trial, best loss: -0.0024580090126997134]
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 LinearRegressionCBUIRecommender 0.000329 0.000329 0.000329 0.001645 0.000329 0.000329 0.000329 0.000756
Best parameters:
{'n_neg_per_pos': 4.0}
param_space = {
    'n_neg_per_pos': hp.quniform('n_neg_per_pos', 1, 10, 1),
    'C': hp.loguniform('C', np.log(0.01), np.log(100.0))
}

best_param_set_SVRCBUIRecommender = tune_recommender(SVRCBUIRecommender, interactions_df, items_df,
                                  param_space, max_evals=10, show_progressbar=True, seed=seed)

print("Best parameters:")
print(best_param_set_SVRCBUIRecommender)
100%|██████████| 10/10 [2:03:32<00:00, 741.28s/trial, best loss: -0.0061450225317492835]
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 SVRCBUIRecommender 0.000658 0.001645 0.001645 0.002962 0.000658 0.001195 0.001195 0.001608
Best parameters:
{'C': 0.21020345682666736, 'n_neg_per_pos': 7.0}
param_space = {
    'n_neg_per_pos': hp.quniform('n_neg_per_pos', 1, 10, 1),
    'n_estimators': hp.quniform('n_estimators', 30, 300, 1),
    'max_depth': hp.quniform('max_depth', 2, 10, 1),
    'min_samples_split': hp.quniform('min_samples_split', 2, 30, 1)
}

best_param_set_RandomForestCBUIRecommender = tune_recommender(RandomForestCBUIRecommender, interactions_df, items_df,
                                  param_space, max_evals=100, show_progressbar=True, seed=seed)

print("Best parameters:")
print(best_param_set_RandomForestCBUIRecommender)
100%|██████████| 100/100 [5:28:02<00:00, 196.83s/trial, best loss: -0.04629250307251127]  
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 RandomForestCBUIRecommender 0.002632 0.007897 0.014478 0.0487 0.002632 0.005653 0.0084 0.019035
Best parameters:
{'max_depth': 10.0, 'min_samples_split': 11.0, 'n_estimators': 277.0, 'n_neg_per_pos': 1.0}
# This tuning may take around 12 hours

param_space = {
    'n_neg_per_pos': hp.quniform('n_neg_per_pos', 1, 10, 1),
    'n_estimators': hp.quniform('n_estimators', 10, 300, 1),
    'max_depth': hp.quniform('max_depth', 2, 10, 1),
    'min_samples_split': hp.quniform('min_samples_split', 2, 30, 1),
    'learning_rate': hp.loguniform('learning_rate', np.log(0.001), np.log(0.1))
}

best_param_set_XGBoostCBUIRecommender = tune_recommender(XGBoostCBUIRecommender, interactions_df, items_df,
                                  param_space, max_evals=20, show_progressbar=True, seed=seed)

print("Best parameters:")
print(best_param_set_XGBoostCBUIRecommender)
100%|██████████| 20/20 [1:24:13<00:00, 252.67s/trial, best loss: -0.039737812371978695]
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 XGBoostCBUIRecommender 0.002962 0.006252 0.014149 0.043435 0.002962 0.004779 0.007993 0.017166
Best parameters:
{'learning_rate': 0.03474119783812193, 'max_depth': 8.0, 'min_samples_split': 12.0, 'n_estimators': 71.0, 'n_neg_per_pos': 4.0}

Final evaluation

Task:
Run the final evaluation of your recommender and present its results against the Amazon recommender's results. You can present results for several of your recommenders. You just need to give the class name of your recommender and its tuned parameters below. If you present results for several recommenders, you should add a separate cell for each recommender and change the names of the DataFrames containing results.

# cb_user_item_recommender = LinearRegressionCBUIRecommender(**best_param_set_LinearRegressionCBUIRecommender)
# cb_user_item_recommender = SVRCBUIRecommender(**best_param_set_SVRCBUIRecommender)
# cb_user_item_recommender = RandomForestCBUIRecommender(**best_param_set_RandomForestCBUIRecommender)
cb_user_item_recommender = XGBoostCBUIRecommender(**best_param_set_XGBoostCBUIRecommender)

# Give the name of your recommender in the line below
linear_cbui_tts_results = [['My recomender'] + list(evaluate_train_test_split_implicit(
    cb_user_item_recommender, interactions_df, items_df))]

linear_cbui_tts_results = pd.DataFrame(
    linear_cbui_tts_results, columns=['Recommender', 'HR@1', 'HR@3', 'HR@5', 'HR@10', 'NDCG@1', 'NDCG@3', 'NDCG@5', 'NDCG@10'])

display(HTML(linear_cbui_tts_results.to_html()))
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 My recomender 0.002632 0.01053 0.019085 0.052978 0.002632 0.006926 0.010409 0.020894
from recommenders.amazon_recommender import AmazonRecommender

amazon_recommender = AmazonRecommender()

amazon_tts_results = [['AmazonRecommender'] + list(evaluate_train_test_split_implicit(
    amazon_recommender, interactions_df, items_df))]

amazon_tts_results = pd.DataFrame(
    amazon_tts_results, columns=['Recommender', 'HR@1', 'HR@3', 'HR@5', 'HR@10', 'NDCG@1', 'NDCG@3', 'NDCG@5', 'NDCG@10'])

display(HTML(amazon_tts_results.to_html()))
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 AmazonRecommender 0.042119 0.10464 0.140507 0.199408 0.042119 0.076826 0.091797 0.110705
tts_results = pd.concat([linear_cbui_tts_results, amazon_tts_results]).reset_index(drop=True)
display(HTML(tts_results.to_html()))
Recommender HR@1 HR@3 HR@5 HR@10 NDCG@1 NDCG@3 NDCG@5 NDCG@10
0 My recomender 0.002632 0.01053 0.019085 0.052978 0.002632 0.006926 0.010409 0.020894
1 AmazonRecommender 0.042119 0.10464 0.140507 0.199408 0.042119 0.076826 0.091797 0.110705