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