{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp metrics.ndcg"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# NDCG\n",
    "> Normalized Discounted Cumulative Gain.\n",
    "\n",
    "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.\n",
    "\n",
    "nDCG has three parts. First is ‘CG’ which stands for Cumulative Gains. It deals with the fact that most relevant items are more useful than somewhat relevant items that are more useful than irrelevant items. It sums the items based on its relevancy, hence, the term cumulative.\n",
    "\n",
    "But CG doesn’t account for the position of the items on the list. And hence, changing the item's position won’t change the CG. This is where the second part of nDCG comes in to play i.e. ‘D’. Discounted Cumulative Gain, DCG for short, penalized the items that appear lower in the list. A relevant item appearing at the end of the list is a result of a bad recommender system and hence that item should be discounted to indicate the bad performance of the model.\n",
    "\n",
    "nDCG normalized the DCG values of the different number of the items lists. To do so we sort the item list by relevancy and calculate the DCG for that list. This will be the perfect DCG score as items are sorted by their relevancy score.\n",
    "\n",
    "<img src='https://github.com/recohut/reco-static/raw/master/media/images/metrics/ndcg.png'>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def ndcg_at_k(y_true_list, y_reco_list, users=None, k=10, next_item=False,\n",
    "              all_item=False):\n",
    "    if next_item:\n",
    "        ndcg_all = []\n",
    "        y_true_list = y_true_list.tolist()\n",
    "        y_reco_list = y_reco_list.tolist()\n",
    "        for y_true, y_reco in zip(y_true_list, y_reco_list):\n",
    "            if y_true in y_reco:\n",
    "                index = y_reco.index(y_true)\n",
    "                ndcg = 1. / np.log2(index + 2)\n",
    "            else:\n",
    "                ndcg = 0.\n",
    "            ndcg_all.append(ndcg)\n",
    "        return np.mean(ndcg_all)\n",
    "\n",
    "    elif all_item:\n",
    "        ndcg_all = []\n",
    "        users = users.tolist()\n",
    "        y_reco_list = y_reco_list.tolist()\n",
    "        for i in range(len(y_reco_list)):\n",
    "            y_true = y_true_list[users[i]]\n",
    "            y_reco = y_reco_list[i]\n",
    "            ndcg_all.append(ndcg_one(y_true, y_reco, k))\n",
    "        return np.mean(ndcg_all)\n",
    "\n",
    "    else:\n",
    "        ndcg_all = list()\n",
    "        for u in users:\n",
    "            y_true = y_true_list[u]\n",
    "            y_reco = y_reco_list[u]\n",
    "            ndcg_all.append(ndcg_one(y_true, y_reco, k))\n",
    "        return np.mean(ndcg_all)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def ndcg_one(y_true, y_reco, k):\n",
    "    rank_list = np.zeros(k)\n",
    "    common_items, indices_in_true, indices_in_reco = np.intersect1d(\n",
    "        y_true, y_reco, assume_unique=False, return_indices=True)\n",
    "\n",
    "    if common_items.size > 0:\n",
    "        rank_list[indices_in_reco] = 1\n",
    "        ideal_list = np.sort(rank_list)[::-1]\n",
    "        #  np.sum(rank_list / np.log2(2, k+2))\n",
    "        dcg = np.sum(rank_list / np.log2(np.arange(2, k + 2)))\n",
    "        idcg = np.sum(ideal_list / np.log2(np.arange(2, k + 2)))\n",
    "        ndcg = dcg / idcg\n",
    "    else:\n",
    "        ndcg = 0.\n",
    "    return ndcg"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def dcg_at_k(r, k, method=0):\n",
    "    \"\"\"\n",
    "    Args:\n",
    "        r: Relevance scores (list or numpy) in rank order\n",
    "            (first element is the first item)\n",
    "        k: Number of results to consider\n",
    "        method: If 0 then weights are [1.0, 1.0, 0.6309, 0.5, 0.4307, ...]\n",
    "                If 1 then weights are [1.0, 0.6309, 0.5, 0.4307, ...]\n",
    "    Returns:\n",
    "        Discounted cumulative gain\n",
    "    \"\"\"\n",
    "    r = np.asfarray(r)[:k]\n",
    "    if r.size:\n",
    "        if method == 0:\n",
    "            return r[0] + np.sum(r[1:] / np.log2(np.arange(2, r.size + 1)))\n",
    "        elif method == 1:\n",
    "            return np.sum(r / np.log2(np.arange(2, r.size + 2)))\n",
    "        else:\n",
    "            raise ValueError('method must be 0 or 1.')\n",
    "    return 0."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "8.097171433256849"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "result = dcg_at_k([3, 2, 3, 0, 1, 2], 6)\n",
    "result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "assert np.round(result,4)==8.0972"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def ndcg_at_k_v2(r, k, method=0):\n",
    "    dcg_max = dcg_at_k(sorted(r, reverse=True), k, method)\n",
    "    if not dcg_max:\n",
    "        return 0.\n",
    "    return dcg_at_k(r, k, method) / dcg_max"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.9608081943360617"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "result = ndcg_at_k_v2([3, 2, 3, 0, 1, 2], 6, method=1)\n",
    "result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "assert np.round(result,4)==0.9608"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Author: Sparsh A.\n",
      "\n",
      "Last updated: 2021-12-24 06:57:01\n",
      "\n",
      "Compiler    : GCC 7.5.0\n",
      "OS          : Linux\n",
      "Release     : 5.4.144+\n",
      "Machine     : x86_64\n",
      "Processor   : x86_64\n",
      "CPU cores   : 2\n",
      "Architecture: 64bit\n",
      "\n",
      "IPython: 5.5.0\n",
      "numpy  : 1.19.5\n",
      "\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "%reload_ext watermark\n",
    "%watermark -a \"Sparsh A.\" -m -iv -u -t -d"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
