{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp transforms.sampling"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Sampling\n",
    "> Data sampling methods."
   ]
  },
  {
   "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",
    "from abc import *\n",
    "from pathlib import Path\n",
    "import pickle\n",
    "import os\n",
    "from tqdm import trange\n",
    "from collections import Counter\n",
    "import numpy as np\n",
    "import pandas as pd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def simple_negative_sampling(data,\n",
    "                             num_negatives=4,\n",
    "                             binarization=False,\n",
    "                             feedback_column='RATING'):\n",
    "  \n",
    "  # Get a list of all Item IDs\n",
    "  all_itemsIds = data['ITEMID'].unique()\n",
    "\n",
    "  # Placeholders that will hold the data\n",
    "  users, items, labels = [], [], []\n",
    "\n",
    "  if binarization:\n",
    "    data.loc[:,feedback_column] = 1\n",
    "\n",
    "  user_item_set = set(zip(data['USERID'], data['ITEMID'], data[feedback_column]))\n",
    "\n",
    "  for (u, i, r) in user_item_set:\n",
    "    users.append(u)\n",
    "    items.append(i)\n",
    "    labels.append(r)\n",
    "    for _ in range(num_negatives):\n",
    "      # randomly select an item\n",
    "      negative_item = np.random.choice(all_itemsIds) \n",
    "      # check that the user has not interacted with this item\n",
    "      while (u, negative_item) in user_item_set:\n",
    "          negative_item = np.random.choice(all_itemsIds)\n",
    "      users.append(u)\n",
    "      items.append(negative_item)\n",
    "      labels.append(0) # items not interacted with are negative\n",
    "  ns_data = pd.DataFrame(list(zip(users, items, labels)),\n",
    "                         columns=['USERID','ITEMID',feedback_column])\n",
    "  return ns_data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class AbstractNegativeSampler(metaclass=ABCMeta):\n",
    "    def __init__(self, train, val, test, user_count, item_count, sample_size, seed, flag, save_folder):\n",
    "        self.train = train\n",
    "        self.val = val\n",
    "        self.test = test\n",
    "        self.user_count = user_count\n",
    "        self.item_count = item_count\n",
    "        self.sample_size = sample_size\n",
    "        self.seed = seed\n",
    "        self.flag = flag\n",
    "        self.save_path = save_path\n",
    "\n",
    "    @classmethod\n",
    "    @abstractmethod\n",
    "    def code(cls):\n",
    "        pass\n",
    "\n",
    "    @abstractmethod\n",
    "    def generate_negative_samples(self):\n",
    "        pass\n",
    "\n",
    "    def get_negative_samples(self):\n",
    "        savefile_path = self._get_save_path()\n",
    "        print(\"Negative samples don't exist. Generating.\")\n",
    "        seen_samples, negative_samples = self.generate_negative_samples()\n",
    "        with savefile_path.open('wb') as f:\n",
    "            pickle.dump([seen_samples, negative_samples], f)\n",
    "        return seen_samples, negative_samples\n",
    "\n",
    "    def _get_save_path(self):\n",
    "        folder = Path(self.save_path)\n",
    "        if not folder.is_dir():\n",
    "            folder.mkdir(parents=True)\n",
    "        # filename = '{}-sample_size{}-seed{}-{}.pkl'.format(\n",
    "        #     self.code(), self.sample_size, self.seed, self.flag)\n",
    "        filename = 'negative_samples_{}.pkl'.format(self.flag)\n",
    "        return folder.joinpath(filename)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class RandomNegativeSampler(AbstractNegativeSampler):\n",
    "    @classmethod\n",
    "    def code(cls):\n",
    "        return 'random'\n",
    "\n",
    "    def generate_negative_samples(self):\n",
    "        assert self.seed is not None, 'Specify seed for random sampling'\n",
    "        np.random.seed(self.seed)\n",
    "        num_samples = 2 * self.user_count * self.sample_size\n",
    "        all_samples = np.random.choice(self.item_count, num_samples) + 1\n",
    "\n",
    "        seen_samples = {}\n",
    "        negative_samples = {}\n",
    "        print('Sampling negative items randomly...')\n",
    "        j = 0\n",
    "        for i in trange(self.user_count):\n",
    "            user = i + 1\n",
    "            seen = set(self.train[user])\n",
    "            seen.update(self.val[user])\n",
    "            seen.update(self.test[user])\n",
    "            seen_samples[user] = seen\n",
    "\n",
    "            samples = []\n",
    "            while len(samples) < self.sample_size:\n",
    "                item = all_samples[j % num_samples]\n",
    "                j += 1\n",
    "                if item in seen or item in samples:\n",
    "                    continue\n",
    "                samples.append(item)\n",
    "            negative_samples[user] = samples\n",
    "\n",
    "        return seen_samples, negative_samples"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class PopularNegativeSampler(AbstractNegativeSampler):\n",
    "    @classmethod\n",
    "    def code(cls):\n",
    "        return 'popular'\n",
    "\n",
    "    def generate_negative_samples(self):\n",
    "        assert self.seed is not None, 'Specify seed for random sampling'\n",
    "        np.random.seed(self.seed)\n",
    "        popularity = self.items_by_popularity()\n",
    "        items = list(popularity.keys())\n",
    "        total = 0\n",
    "        for i in range(len(items)):\n",
    "            total += popularity[items[i]]\n",
    "        for i in range(len(items)):\n",
    "            popularity[items[i]] /= total\n",
    "        probs = list(popularity.values())\n",
    "        num_samples = 2 * self.user_count * self.sample_size\n",
    "        all_samples = np.random.choice(items, num_samples, p=probs)\n",
    "\n",
    "        seen_samples = {}\n",
    "        negative_samples = {}\n",
    "        print('Sampling negative items by popularity...')\n",
    "        j = 0\n",
    "        for i in trange(self.user_count):\n",
    "            user = i + 1\n",
    "            seen = set(self.train[user])\n",
    "            seen.update(self.val[user])\n",
    "            seen.update(self.test[user])\n",
    "            seen_samples[user] = seen\n",
    "\n",
    "            samples = []\n",
    "            while len(samples) < self.sample_size:\n",
    "                item = all_samples[j % num_samples]\n",
    "                j += 1\n",
    "                if item in seen or item in samples:\n",
    "                    continue\n",
    "                samples.append(item)\n",
    "            negative_samples[user] = samples\n",
    "\n",
    "        return seen_samples, negative_samples\n",
    "\n",
    "    def items_by_popularity(self):\n",
    "        popularity = Counter()\n",
    "        self.users = sorted(self.train.keys())\n",
    "        for user in self.users:\n",
    "            popularity.update(self.train[user])\n",
    "            popularity.update(self.val[user])\n",
    "            popularity.update(self.test[user])\n",
    "\n",
    "        popularity = dict(popularity)\n",
    "        popularity = {k: v for k, v in sorted(popularity.items(), key=lambda item: item[1], reverse=True)}\n",
    "        return popularity"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Author: Sparsh A.\n",
      "\n",
      "Last updated: 2021-12-18 09:51:57\n",
      "\n",
      "Compiler    : GCC 7.5.0\n",
      "OS          : Linux\n",
      "Release     : 5.4.104+\n",
      "Machine     : x86_64\n",
      "Processor   : x86_64\n",
      "CPU cores   : 2\n",
      "Architecture: 64bit\n",
      "\n",
      "pandas : 1.1.5\n",
      "IPython: 5.5.0\n",
      "numpy  : 1.19.5\n",
      "\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "!pip install -q watermark\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
}
