{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Данные" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import csv\n", "from collections import defaultdict\n", "import random\n", "from metrics import apk\n", "random.seed(42)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [], "source": [ "#Словари для основной выборки\n", "user_to_items = defaultdict(set)\n", "item_to_users = defaultdict(set)\n", "\n", "#Словари для тестовой выборки\n", "test_user_to_items = defaultdict(set)\n", "test_item_to_users = defaultdict(set)\n", "\n", "\n", "with open(\"data/train_likes.csv\") as datafile:\n", " for like in csv.DictReader(datafile):\n", " # Кидаем монетку. В зависимости от результата кладём в обучение или тест\n", " if random.random() < 0.5:\n", " user_to_items[like['user_id']].add(like['item_id'])\n", " item_to_users[like['item_id']].add(like['user_id'])\n", " else:\n", " test_user_to_items[like['user_id']].add(like['item_id'])\n", " test_item_to_users[like['item_id']].add(like['user_id'])" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [], "source": [ "all_items = set(item_to_users.keys()) | set(test_item_to_users.keys())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Фильтрация пользователей\n", "* Значительная часть пользователей имеет всего 1-2 лайка. При всём желании, рекоммендовать им что-либо осмысленное при помощи рассматриваемого здесь метода мы вряд ли сможем. Для простоты вычислений, удалим их из выборки.\n", "* Важно понимать, что качество на оставшихся пользователях скорее всего будет выше, чем на первоначальной выборке." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [], "source": [ "min_items_per_user = 2\n", "from copy import copy\n", "for user in copy(test_user_to_items).keys():\n", " \n", " n_items_per_user = len(user_to_items[user]) + len(test_user_to_items[user])\n", " \n", " if n_items_per_user <= min_items_per_user:\n", " del user_to_items[user]\n", " del test_user_to_items[user]\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Рекоммендующая функция\n", "Позволим себе немного вольности: наша функция будет возвращать не вероятности, а список фильмов в порядке убывания \"рекомендованности\".\n", "\n", "* Рекоммендованность фильма item пользователю user посчитаем так:\n", " * Для каждого фильма, полайканного пользователем user, найдём других людей, которым понравился фильм.\n", " * Сложим всех таких \"друзей по лайкам\" вместе и назовём соседями (__neighborhood__) пользователя.\n", " * Для фильма item узнаем его аудиторию - множество пользователей, которые его лайкнули\n", " * Пригодность фильма пользователю - то, насколько \"друзьям по лайкам\" пользователя нравится этот фильм.\n", "\n", "Для примера, будем использовать косинусную меру расстояния\n", " \n", "$ cos(u_{film}, u_{neighborhood}) = $ =$ u_{film} \\cdot u_{neighborhood} \\over |u_{film}| |u_{neighborhood}| $\n", "\n", "\n", "$u_{neighborhood}$ зависит только от пользователя, но не от фильма, поэтому при сравнении фильмов по пригодности для одного пользователя, его можно исключить из формулы для простоты вычислений.\n", "\n", "$ similarity(u_{film}, u_{neighborhood}) = $ $ u_{film} \\cdot u_{neighborhood} \\over |u_{film}| $\n", " \n", " \n", "Распишем формулу подробно:\n", "\n", "$ similarity(u_{film}, u_{neighborhood}) = $ $ \\sum _{u_i} [u_i \\in u_{film}] \\cdot [u_i \\in u_{neighborhood}] \\over |u_{film}| $\n", "\n", "* u_i - очередной пользователь (в цикле по всем пользователям)\n", " \n", "Выражение $[u_i \\in u_neighborhood]$ здесь означает \"сколько раз очередной пользователь входит в множество друзей по лайкам\"\n", " \n", " " ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from math import sqrt\n", "from collections import Counter\n", "\n", "def recommend(user, n_best = 10):\n", " \"\"\"Функция, которая возвращает список рекоммендованных фильмов\n", " user - id пользователя\n", " n_best - сколько максимум фильмов можно рекоммендовать\n", " remove_already_liked - если True, \"\"\"\n", " user_items = user_to_items[user]\n", " \n", " #соседи пользователя {User_id -> сколько раз сосед}\n", " #Что такое counter - https://pymotw.com/2/collections/counter.html\n", " neighborhood = Counter()\n", " for item in user_items:\n", " neighborhood.update(item_to_users[item])\n", " \n", " #словарь {фильм -> пригодность фильма пользователю}\n", " item_similarities = {}\n", " \n", " for item in all_items:\n", " #пропустим те фильмы, которые пользователь уже лайкал, если нас об этом попросили\n", " if item in user_items: continue\n", " \n", " #пользователи, лайкавшие фильм item\n", " item_users = item_to_users[item]\n", " \n", " #Если фильм никто не лайкал, пропускаем\n", " if len(item_users) == 0: continue\n", " \n", " #число соседей user (кому мы рекоммендуем), лайкнувших фильм item\n", " n_common_users = sum(neighborhood[user] for user in item_users)\n", " \n", " #похожесть на интересы пользователя = число соседей, кому он понравился, делённое на общее число \n", " similarity = float(n_common_users) / sqrt(len(item_users))\n", " \n", " item_similarities[item] = similarity\n", " \n", " #Отсортируем все фильмы по убыванию их похожести на интересы пользователя\n", " items_sorted = sorted(all_items, key = lambda x: item_similarities.get(x, 0),reverse = True)\n", " \n", " #вернём n_best наиболее пригодных\n", " return items_sorted[:n_best]" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['44327280355abfc1c58fa9ad8c41a2cc',\n", " 'e6e53f41066b37fb5b80bd118dc800be',\n", " '1e540fdc6ef7c62c3d3ef6d78a6abcab',\n", " '13ffc4b8348356546cc44358b6c999c1',\n", " 'f049e6c94383af7a3c480a7469a5efd4']" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Порекоммендуем топ-5 фильмов какому-то юзверю\n", "recommend('d8c2794b01531ca807bc2b28d171f22d', n_best=5)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['44327280355abfc1c58fa9ad8c41a2cc',\n", " 'e6e53f41066b37fb5b80bd118dc800be',\n", " '1e540fdc6ef7c62c3d3ef6d78a6abcab']" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Ему же топ-3 фильма. Совпадает с первыми 3-мя из топ-5\n", "recommend('d8c2794b01531ca807bc2b28d171f22d', n_best=3)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "# Оценка качества - map@k" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0 / 6108\n", "100 / 6108\n", "200 / 6108\n", "300 / 6108\n", "400 / 6108\n", "500 / 6108\n", "600 / 6108\n", "700 / 6108\n", "800 / 6108\n", "900 / 6108\n", "1000 / 6108\n", "1100 / 6108\n", "1200 / 6108\n", "1300 / 6108\n", "1400 / 6108\n", "1500 / 6108\n", "1600 / 6108\n", "1700 / 6108\n", "1800 / 6108\n", "1900 / 6108\n", "2000 / 6108\n", "2100 / 6108\n", "2200 / 6108\n", "2300 / 6108\n", "2400 / 6108\n", "2500 / 6108\n", "2600 / 6108\n", "2700 / 6108\n", "2800 / 6108\n", "2900 / 6108\n", "3000 / 6108\n", "3100 / 6108\n", "3200 / 6108\n", "3300 / 6108\n", "3400 / 6108\n", "3500 / 6108\n", "3600 / 6108\n", "3700 / 6108\n", "3800 / 6108\n", "3900 / 6108\n", "4000 / 6108\n", "4100 / 6108\n", "4200 / 6108\n", "4300 / 6108\n", "4400 / 6108\n", "4500 / 6108\n", "4600 / 6108\n", "4700 / 6108\n", "4800 / 6108\n", "4900 / 6108\n", "5000 / 6108\n", "5100 / 6108\n", "5200 / 6108\n", "5300 / 6108\n", "5400 / 6108\n", "5500 / 6108\n", "5600 / 6108\n", "5700 / 6108\n", "5800 / 6108\n", "5900 / 6108\n", "6000 / 6108\n", "6100 / 6108\n", "AP@10 = 0.005945258691169598\n" ] } ], "source": [ "#сколько рекоммендаций рассматриваем\n", "K = 10\n", "\n", "#по какой части тестовых пользователей считаем map@k\n", "max_n_users = len(test_user_to_items)\n", "#warnung, для max_n_users = len(test_user_to_items) цикл может считаться несколько минут\n", "#для отладки рекоммендуется использовать меньшее число\n", "\n", "\n", "APatK_per_user = []\n", "#для всех юзверей\n", "for i, user in enumerate(test_user_to_items):\n", " \n", " #фильмы, которые пользователю на самом деле нравятся\n", " test_items = test_user_to_items[user]\n", " \n", " #Выдать топ-K рекоммендаций\n", " recommendation_list = recommend(user,n_best=K)\n", " \n", " #Посчитать ap@k\n", " user_APatK = apk(test_items, recommendation_list,k=K)\n", " \n", " #и сложить в коробку\n", " APatK_per_user.append(user_APatK)\n", " \n", " #Progress bar\n", " if i % 100 ==0:\n", " print(i,'/',max_n_users)\n", " \n", " if i > max_n_users:\n", " break\n", " \n", "print('mAP@{} = {}'.format(K, sum(APatK_per_user)/len(APatK_per_user)))\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Notes\n", "* Кроме качества рекоммендаций, map@k ещё зависит от доли тестовой выборки, фильтрации и от самого K. Сравнивать качество разных алгоритмов имеет смысл только при одинаковом K и тестовой выборке.\n", "* Давать полезные рекоммендации пользователям с малым числом лайков тоже можно: например, можно выдавать наиболее популярные в целом фильмы.\n", "* Разделение на обучение/тест честнее делать на по времени: первые 70% (например) лайков в обучение, остальные в тест. Это ближе к реальной жизни, когда вы сначала обучаете модель на логах, а потом применяете на новых сессиях пользователей." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.2" } }, "nbformat": 4, "nbformat_minor": 0 }