<jupyter_start><jupyter_text>Copyright (c) Microsoft Corporation. All rights reserved.Licensed under the MIT License.  Evaluation Evaluation with offline metrics is pivotal to assess the quality of a recommender before it goes into production. Usually, evaluation metrics are carefully chosen based on the actual application scenario of a recommendation system. It is hence important to data scientists and AI developers that build recommendation systems to understand how each evaluation metric is calculated and what it is for.This notebook deep dives into several commonly used evaluation metrics, and illustrates how these metrics are used in practice. The metrics covered in this notebook are merely for off-line evaluations.  0 Global settings Most of the functions used in the notebook can be found in the `reco_utils` directory.<jupyter_code># set the environment path to find Recommenders
import sys
sys.path.append("../../")
import pandas as pd
import pyspark
from sklearn.preprocessing import minmax_scale

from reco_utils.common.spark_utils import start_or_get_spark
from reco_utils.evaluation.spark_evaluation import SparkRankingEvaluation, SparkRatingEvaluation
from reco_utils.evaluation.python_evaluation import auc, logloss
from reco_utils.recommender.sar.sar_singlenode import SARSingleNode
from reco_utils.dataset.download_utils import maybe_download
from reco_utils.dataset.python_splitters import python_random_split

print("System version: {}".format(sys.version))
print("Pandas version: {}".format(pd.__version__))
print("PySpark version: {}".format(pyspark.__version__))<jupyter_output>System version: 3.6.0 | packaged by conda-forge | (default, Feb  9 2017, 14:36:55) 
[GCC 4.8.2 20140120 (Red Hat 4.8.2-15)]
Pandas version: 0.23.4
PySpark version: 2.3.1<jupyter_text>Note to successfully run Spark codes with the Jupyter kernel, one needs to correctly set the environment variables of `PYSPARK_PYTHON` and `PYSPARK_DRIVER_PYTHON` that point to Python executables with the desired version. Detailed information can be found in the setup instruction document [SETUP.md](../../SETUP.md).<jupyter_code>COL_USER = "UserId"
COL_ITEM = "MovieId"
COL_RATING = "Rating"
COL_PREDICTION = "Rating"

HEADER = {
    "col_user": COL_USER,
    "col_item": COL_ITEM,
    "col_rating": COL_RATING,
    "col_prediction": COL_PREDICTION,
}<jupyter_output><empty_output><jupyter_text>1 Prepare data  1.1 Prepare dummy data For illustration purpose, a dummy data set is created for demonstrating how different evaluation metrics work. The data has the schema that can be frequently found in a recommendation problem, that is, each row in the dataset is a (user, item, rating) tuple, where "rating" can be an ordinal rating score (e.g., discrete integers of 1, 2, 3, etc.) or an numerical float number that quantitatively indicates the preference of the user towards that item. For simplicity reason, the column of rating in the dummy dataset we use in the example represent some ordinal ratings.<jupyter_code>df_true = pd.DataFrame(
        {
            COL_USER: [1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
            COL_ITEM: [1, 2, 3, 1, 4, 5, 6, 7, 2, 5, 6, 8, 9, 10, 11, 12, 13, 14],
            COL_RATING: [5, 4, 3, 5, 5, 3, 3, 1, 5, 5, 5, 4, 4, 3, 3, 3, 2, 1],
        }
    )
df_pred = pd.DataFrame(
    {
        COL_USER: [1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
        COL_ITEM: [3, 10, 12, 10, 3, 5, 11, 13, 4, 10, 7, 13, 1, 3, 5, 2, 11, 14],
        COL_PREDICTION: [14, 13, 12, 14, 13, 12, 11, 10, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5]
    }
)<jupyter_output><empty_output><jupyter_text>Take a look at ratings of the user with ID "1" in the dummy dataset.<jupyter_code>df_true[df_true[COL_USER] == 1]
df_pred[df_pred[COL_USER] == 1]<jupyter_output><empty_output><jupyter_text>1.2 Prepare Spark data Spark framework is sometimes used to evaluate metrics given datasets that are hard to fit into memory. In our example, Spark DataFrames can be created from the Python dummy dataset.<jupyter_code>spark = start_or_get_spark("EvaluationTesting", "local")

dfs_true = spark.createDataFrame(df_true)
dfs_pred = spark.createDataFrame(df_pred)
dfs_true.filter(dfs_true[COL_USER] == 1).show()
dfs_pred.filter(dfs_pred[COL_USER] == 1).show()<jupyter_output>+------+-------+------+
|UserId|MovieId|Rating|
+------+-------+------+
|     1|      3|    14|
|     1|     10|    13|
|     1|     12|    12|
+------+-------+------+<jupyter_text>2 Evaluation metrics  2.1 Rating metricsRating metrics are similar to regression metrics used for evaluating a regression model that predicts numerical values given input observations. In the context of recommendation system, rating metrics are to evaluate how accurate a recommender is to predict ratings that users may give to items. Therefore, the metrics are **calculated exactly on the same group of (user, item) pairs that exist in both ground-truth dataset and prediction dataset** and **averaged by the total number of users**. 2.1.1 Use casesRating metrics are effective in measuring the model accuracy. However, in some cases, the rating metrics are limited if* **the recommender is to predict ranking instead of explicit rating**. For example, if the consumer of the recommender cares about the ranked recommended items, rating metrics do not apply directly. Usually a relevancy function such as top-k will be applied to generate the ranked list from predicted ratings in order to evaluate the recommender with other metrics. * **the recommender is to generate recommendation scores that have different scales with the original ratings (e.g., the SAR algorithm)**. In this case, the difference between the generated scores and the original scores (or, ratings) is not valid for measuring accuracy of the model. 2.1.2 How-to with the evaluation utilitiesA few notes about the interface of the Rating evaluator class:1. The columns of user, item, and rating (prediction) should be present in the ground-truth DataFrame (prediction DataFrame).2. There should be no duplicates of (user, item) pairs in the ground-truth and the prediction DataFrames, othewise there may be unexpected behavior in calculating certain metrics.3. Default column names for user, item, rating, and prediction are "UserId", "ItemId", "Rating", and "Prediciton", respectively.In our examples below, to calculate rating metrics for input data frames in Spark, a Spark object, `SparkRatingEvaluation` is initialized. The input data schemas for the ground-truth dataset and the prediction dataset are* Ground-truth dataset.|Column|Data type|Description||-------------|------------|-------------||`COL_USER`||User ID||`COL_ITEM`||Item ID||`COL_RATING`||Rating or numerical value of user preference.|* Prediction dataset.|Column|Data type|Description||-------------|------------|-------------||`COL_USER`||User ID||`COL_ITEM`||Item ID||`COL_RATING`||Predicted rating or numerical value of user preference.|<jupyter_code>spark_rate_eval = SparkRatingEvaluation(dfs_true, dfs_pred, **HEADER)<jupyter_output><empty_output><jupyter_text>2.1.3 Root Mean Square Error (RMSE)RMSE is for evaluating the accuracy of prediction on ratings. RMSE is the most widely used metric to evaluate a recommendation algorithm that predicts missing ratings. The benefit is that RMSE is easy to explain and calculate.<jupyter_code>print("The RMSE is {}".format(spark_rate_eval.rmse()))<jupyter_output>The RMSE is 7.254309064273455<jupyter_text>2.1.4 R Squared (R2)R2 is also called "coefficient of determination" in some context. It is a metric that evaluates how well a regression model performs, based on the proportion of total variations of the observed results.<jupyter_code>print("The R2 is {}".format(spark_rate_eval.rsquared()))<jupyter_output>The R2 is -31.699029126213595<jupyter_text>2.1.5 Mean Absolute Error (MAE)MAE evaluates accuracy of prediction. It computes the metric value from ground truths and prediction in the same scale. Compared to RMSE, MAE is more explainable.<jupyter_code>print("The MAE is {}".format(spark_rate_eval.mae()))<jupyter_output>The MAE is 6.375<jupyter_text>2.1.6 Explained Variance Explained variance is usually used to measure how well a model performs with regard to the impact from the variation of the dataset.<jupyter_code>print("The explained variance is {}".format(spark_rate_eval.exp_var()))<jupyter_output>The explained variance is -6.446601941747574<jupyter_text>2.1.7 Summary |Metric|Range|Selection criteria|Limitation|Reference||------|-------------------------------|---------|----------|---------||RMSE|$> 0$|The smaller the better.|May be biased, and less explainable than MSE|[link](https://en.wikipedia.org/wiki/Root-mean-square_deviation)||R2|$\leq 1$|The closer to $1$ the better.|Depend on variable distributions.|[link](https://en.wikipedia.org/wiki/Coefficient_of_determination)||MSE|$\geq 0$|The smaller the better.|Dependent on variable scale.|[link](https://en.wikipedia.org/wiki/Mean_absolute_error)||Explained variance|$\leq 1$|The closer to $1$ the better.|Depend on variable distributions.|[link](https://en.wikipedia.org/wiki/Explained_variation)|  2.2 Ranking metrics "Beyond-accuray evaluation" was proposed to evaluate how relevant recommendations are for users. In this case, a recommendation system is a treated as a ranking system. Given a relency definition, recommendation system outputs a list of recommended items to each user, which is ordered by relevance. The evaluation part takes ground-truth data, the actual items that users interact with (e.g., liked, purchased, etc.), and the recommendation data, as inputs, to calculate ranking evaluation metrics.  2.2.1 Use casesRanking metrics are often used when hit and/or ranking of the items are considered:* **Hit** - defined by relevancy, a hit usually means whether the recommended "k" items hit the "relevant" items by the user. For example, a user may have clicked, viewed, or purchased an item for many times, and a hit in the recommended items indicate that the recommender performs well. Metrics like "precision", "recall", etc. measure the performance of such hitting accuracy.* **Ranking** - ranking metrics give more explanations about, for the hitted items, whether they are ranked in a way that is preferred by the users whom the items will be recommended to. Metrics like "mean average precision", "ndcg", etc., evaluate whether the relevant items are ranked higher than the less-relevant or irrelevant items.  2.2.2 How-to with evaluation utilitiesA few notes about the interface of the Rating evaluator class:1. The columns of user, item, and rating (prediction) should be present in the ground-truth DataFrame (prediction DataFrame). The column of timestamp is optional, but it is required if certain relevanc function is used. For example, timestamps will be used if the most recent items are defined as the relevant one.2. There should be no duplicates of (user, item) pairs in the ground-truth and the prediction DataFrames, othewise there may be unexpected behavior in calculating certain metrics.3. Default column names for user, item, rating, and prediction are "UserId", "ItemId", "Rating", and "Prediciton", respectively.  2.2.1 Relevancy of recommendationRelevancy of recommendation can be measured in different ways:* **By ranking** - In this case, relevant items in the recommendations are defined as the top ranked items, i.e., top k items, which are taken from the list of the recommended items that is ordered by the predicted ratings (or other numerical scores that indicate preference of a user to an item). * **By timestamp** - Relevant items are defined as the most recently viewed k items, which are obtained from the recommended items ranked by timestamps.* **By rating** - Relevant items are defined as items with ratings (or other numerical scores that indicate preference of a user to an item) that are above a given threshold. Similarly, a ranking metric object can be initialized as below. The input data schema is* Ground-truth dataset.|Column|Data type|Description||-------------|------------|-------------||`COL_USER`||User ID||`COL_ITEM`||Item ID||`COL_RATING`||Rating or numerical value of user preference.||`COL_TIMESTAMP`||Timestamps.|* Prediction dataset.|Column|Data type|Description||-------------|------------|-------------||`COL_USER`||User ID||`COL_ITEM`||Item ID||`COL_RATING`||Predicted rating or numerical value of user preference.||`COL_TIMESTAM`||Timestamps.|In this case, in addition to the input datasets, there are also other arguments used for calculating the ranking metrics:|Argument|Data type|Description||------------|------------|--------------||`k`||Number of items recommended to user.||`revelancy_method`||Methonds that extract relevant items from the recommendation list| For example, the following code initializes a ranking metric object that calculates the metrics.<jupyter_code>spark_rank_eval = SparkRankingEvaluation(dfs_true, dfs_pred, k=3, relevancy_method="top_k", **HEADER)<jupyter_output><empty_output><jupyter_text>A few ranking metrics can then be calculated.  2.2.1 PrecisionPrecision@k is a metric that evaluates how many items in the recommendation list are relevant (hit) in the ground-truth data. For each user the precision score is normalized by `k` and then the overall precision scores are averaged by the total number of users. Note it is apparent that the precision@k metric grows with the number of `k`.<jupyter_code>print("The precision at k is {}".format(spark_rank_eval.precision_at_k()))<jupyter_output>The precision at k is 0.3333333333333333<jupyter_text>2.2.2 RecallRecall@k is a metric that evaluates how many relevant items in the ground-truth data are in the recommendation list. For each user the recall score is normalized by the total number of ground-truth items and then the overall recall scores are averaged by the total number of users.<jupyter_code>print("The recall at k is {}".format(spark_rank_eval.recall_at_k()))<jupyter_output>The recall at k is 0.2111111111111111<jupyter_text>2.2.3 Normalized Discounted Cumulative Gain (NDCG)NDCG is a metric that evaluates how well the recommender performs in recommending ranked items to users. Therefore both hit of relevant items and correctness in ranking of these items matter to the NDCG evaluation. The total NDCG score is normalized by the total number of users.<jupyter_code>print("The ndcg at k is {}".format(spark_rank_eval.ndcg_at_k()))<jupyter_output>The ndcg at k is 0.3333333333333333<jupyter_text>2.2.4 Mean Average Precision (MAP)MAP is a metric that evaluates the average precision for each user in the datasets. It also penalizes ranking correctness of the recommended items. The overall MAP score is normalized by the total number of users.<jupyter_code>print("The map at k is {}".format(spark_rank_eval.map_at_k()))<jupyter_output>The map at k is 0.15<jupyter_text>2.2.5 ROC and AUCROC, as well as AUC, is a well known metric that is used for evaluating binary classification problem. It is similar in the case of binary rating typed recommendation algorithm where the "hit" accuracy on the relevant items is used for measuring the recommender's performance. To demonstrate the evaluation method, the original data for testing is manipuldated in a way that the ratings in the testing data are arranged as binary scores, whilst the ones in the prediction are scaled in 0 to 1.<jupyter_code># Convert the original rating to 0 and 1.
df_true_bin = df_true.copy()
df_true_bin[COL_RATING] = df_true_bin[COL_RATING].apply(lambda x: 1 if x > 3 else 0)

df_true_bin
# Convert the predicted ratings into a [0, 1] scale.
df_pred_bin = df_pred.copy()
df_pred_bin[COL_PREDICTION] = minmax_scale(df_pred_bin[COL_PREDICTION].astype(float))

df_pred_bin
# Calculate the AUC metric
auc_score = auc(
    df_true_bin,
    df_pred_bin,
    col_user = COL_USER,
    col_item = COL_ITEM,
    col_rating = COL_RATING,
    col_prediction = COL_RATING
)
print("The auc score is {}".format(auc_score))<jupyter_output>The auc score is 0.3333<jupyter_text>It is worth mentioning that in some literature there are variants of the original AUC metric, that considers the effect of **the number of the recommended items (k)**, **grouping effect of users (compute AUC for each user group, and take the average across different groups)**. These variants are applicable to various different scenarios, and choosing an appropriate one depends on the context of the use case itself.  2.3.2 Logistic loss Logistic loss (sometimes it is called simply logloss, or cross-entropy loss) is another useful metric to evaluate the hit accuracy. It is defined as the negative log-likelihood of the true labels given the predictions of a classifier.<jupyter_code># Calculate the logloss metric
logloss_score = logloss(
    df_true_bin,
    df_pred_bin,
    col_user = COL_USER,
    col_item = COL_ITEM,
    col_rating = COL_RATING,
    col_prediction = COL_RATING
)

print("The logloss score is {}".format(logloss_score))<jupyter_output>The logloss score is 4.1061<jupyter_text>It is worth noting that logloss may be sensitive to the class balance of datasets, as it penalizes heavily classifiers that are confident about incorrect classifications. To demonstrate, the ground truth data set for testing is manipulated purposely to unbalance the binary labels. For example, the following binarizes the original rating data by using a lower threshold, i.e., 2, to create more positive feedback from the user.<jupyter_code>df_true_bin_pos = df_true.copy()
df_true_bin_pos[COL_RATING] = df_true_bin_pos[COL_RATING].apply(lambda x: 1 if x > 2 else 0)

df_true_bin_pos<jupyter_output><empty_output><jupyter_text>By using threshold of 2, the labels in the ground truth data is not balanced, and the ratio of 1 over 0 is<jupyter_code>one_zero_ratio = df_true_bin_pos[COL_PREDICTION].sum() / (df_true_bin_pos.shape[0] - df_true_bin_pos[COL_PREDICTION].sum())

print('The ratio between label 1 and label 0 is {}'.format(one_zero_ratio))<jupyter_output>The ratio between label 1 and label 0 is 5.0<jupyter_text>Another prediction data is also created, where the probabilities for label 1 and label 0 are fixed. Without loss of generity, the probability of predicting 1 is 0.6. The data set is purposely created to make the precision to be 100% given an presumption of cut-off equal to 0.5.<jupyter_code>prob_true = 0.6

df_pred_bin_pos = df_true_bin_pos.copy()
df_pred_bin_pos[COL_PREDICTION] = df_pred_bin_pos[COL_PREDICTION].apply(lambda x: prob_true if x==1 else 1-prob_true)

df_pred_bin_pos<jupyter_output><empty_output><jupyter_text>Then the logloss is calculated as follows.<jupyter_code># Calculate the logloss metric
logloss_score_pos = logloss(
    df_true_bin_pos,
    df_pred_bin_pos,
    col_user = COL_USER,
    col_item = COL_ITEM,
    col_rating = COL_RATING,
    col_prediction = COL_RATING
)

print("The logloss score is {}".format(logloss_score))<jupyter_output>The logloss score is 4.1061<jupyter_text>For comparison, a similar process is used with a threshold value of 3 to create a more balanced dataset. Another prediction dataset is also created by using the balanced dataset. Again, the probabilities of predicting label 1 and label 0 are fixed as 0.6 and 0.4, respectively. **NOTE**, same as above, in this case, the prediction also gives us a 100% precision. The only difference is the proportion of binary labels.<jupyter_code>prob_true = 0.6

df_pred_bin_balanced = df_true_bin.copy()
df_pred_bin_balanced[COL_PREDICTION] = df_pred_bin_balanced[COL_PREDICTION].apply(lambda x: prob_true if x==1 else 1-prob_true)

df_pred_bin_balanced<jupyter_output><empty_output><jupyter_text>The ratio of label 1 and label 0 is<jupyter_code>one_zero_ratio = df_true_bin[COL_PREDICTION].sum() / (df_true_bin.shape[0] - df_true_bin[COL_PREDICTION].sum())

print('The ratio between label 1 and label 0 is {}'.format(one_zero_ratio))<jupyter_output>The ratio between label 1 and label 0 is 1.0<jupyter_text>It is perfectly balanced. Applying the logloss function to calculate the metric gives us a more promising result, as shown below.<jupyter_code># Calculate the logloss metric
logloss_score = logloss(
    df_true_bin,
    df_pred_bin_balanced,
    col_user = COL_USER,
    col_item = COL_ITEM,
    col_rating = COL_RATING,
    col_prediction = COL_RATING
)

print("The logloss score is {}".format(logloss_score))<jupyter_output>The logloss score is 0.5108
{'fraction_non_alphanumeric': 0.09227761485826003, 'fraction_numerical': 0.02238514173998045, 'mean_word_length': 4.879597701149425, 'pattern_counts': {'":': 4, '<': 81, '<?xml version=': 0, '>': 84, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 3, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '24', 'dir': 'jupyter-structured-clean-dedup', 'id': '5954', 'license': 'MIT', 'n_tokens_mistral': 6246, 'n_tokens_neox': 5736, 'n_words': 2480, 'path': 'notebooks/03_evaluate/evaluation.ipynb', 'repo_name': 'bethz/Recommenders', 'size': '20460'}