Мы надеемся, что вы видели телевизионный рекламный ролик рекламной кампании « Человек, который может пахнуть ваш человек », созданной Old Spice, и вы, возможно, видели более 100 видеороликов, которые Исаия Мустафа показывал в ответ на комментарии, сделанные в Твиттере. Это отличный пример персонализации, и сегодня вы узнаете, как внести некоторую персонализацию в свое приложение, и вам не понадобятся мышцы или лошадь.
Мы собираемся стереть с нуля проект Neoflix с начала года и добавим несколько функций. Он был обновлен для работы на Neo4j версии 1.7 и позволяет искать фильмы, которые имеют цитату. Спасибо Дженн Алонс и Винсу Сима за исправления ошибок во время WindyCityDB .
Стратегии персонализации
Когда мы смотрим на незарегистрированного пользователя (кто-то просто просматривает сайт), используя уже разработанную рекомендацию на основе элементов, это все, что нам нужно. Когда пользователь регистрируется и дает нам некоторую информацию о себе, мы можем использовать его свойства, чтобы рекомендовать фильмы, которые понравились другим пользователям с похожими свойствами. После того, как зарегистрированный пользователь оценил несколько фильмов, мы можем отказаться от того, чтобы полагаться исключительно на их свойства, и порекомендовать фильмы, которые были высоко оценены пользователями, которые имеют похожую историю оценок и, следовательно, тот же вкус в фильмах.
Чтобы персонализировать Neoflix, нам нужно сначала найти пользователя. Если вы посмотрите на создание графа, вы заметите, что мы индексируем узлы по индексу «вершины» и устанавливаем свойство «тип», равное «Пользователь». Давайте возьмем нескольких пользователей и посмотрим, с чем нам предстоит работать.
START users = node:vertices(type="User") RETURN users LIMIT 5
Мы используем Cypher здесь, и это возвращает таблицу:
==> +----------------------------------------------------------+ ==> | users | ==> +----------------------------------------------------------+ ==> | Node[3902]{type->"User",userId->1,gender->"F",age->1} | ==> | Node[4065]{type->"User",userId->143,gender->"M",age->18} | ==> | Node[4064]{type->"User",userId->142,gender->"M",age->25} | ==> | Node[4067]{type->"User",userId->145,gender->"M",age->18} | ==> | Node[4066]{type->"User",userId->144,gender->"M",age->25} | ==> +----------------------------------------------------------+
Похоже, у нас есть выборка пользователей с опорным полем userId, но у наших пользователей также есть некоторая дополнительная информация. Мы можем видеть их пол и возраст. Каково возрастное распределение наших пользователей?
START users = node:vertices(type = "User") RETURN users.age, COUNT(users.age) ORDER by users.age
Мы используем функцию Cypher COUNT для выполнения эквивалента GROUP BY в SQL, и ниже приведены наши результаты:
==> +------------------------------+ ==> | users.age | COUNT(users.age) | ==> +------------------------------+ ==> | 1 | 134 | ==> | 18 | 581 | ==> | 25 | 942 | ==> | 35 | 581 | ==> | 45 | 275 | ==> | 50 | 263 | ==> | 56 | 224 | ==> +------------------------------+
Как подозревается, это не их реальный возраст, а возрастной диапазон (1-17, 18-24, 25-34, 35-44, 45-49, 50-54, 56+). Мы можем сделать то же самое, чтобы получить гендерное распределение наших пользователей.
START users = node:vertices(type = "User") RETURN users.gender, COUNT(users.gender) ORDER by users.gender
Что дает нам эту таблицу:
==> +------------------------------------+ ==> | users.gender | COUNT(users.gender) | ==> +------------------------------------+ ==> | "F" | 821 | ==> | "M" | 2179 | ==> +------------------------------------+
Если посмотреть на наши цифры, то у нас, по-видимому, почти в 3 раза больше пользователей выборок среди мужчин, чем выборок среди женщин. Давайте скомбинируем эти две метрики и добавим количество фильмов с оценкой, а также количество оценок по каждому сегменту.
START users = node:vertices(type = "User") MATCH users -[r1:rated]-> movies RETURN users.gender, users.age, COUNT(DISTINCT users) AS user_cnt, COUNT(DISTINCT movies) AS mov_cnt, COUNT(r1) AS rtg_cnt ORDER by users.gender, users.age
и посмотрим, что мы получим:
==> +---------------------------------------------------------+ ==> | users.gender | users.age | user_cnt | mov_cnt | rtg_cnt | ==> +---------------------------------------------------------+ ==> | "F" | 1 | 42 | 1804 | 5292 | ==> | "F" | 18 | 153 | 2544 | 23247 | ==> | "F" | 25 | 238 | 2967 | 39240 | ==> | "F" | 35 | 157 | 2711 | 23493 | ==> | "F" | 45 | 94 | 2161 | 9643 | ==> | "F" | 50 | 77 | 2103 | 9568 | ==> | "F" | 56 | 60 | 1544 | 5432 | ==> | "M" | 1 | 92 | 2012 | 12058 | ==> | "M" | 18 | 428 | 3083 | 74656 | ==> | "M" | 25 | 704 | 3367 | 140239 | ==> | "M" | 35 | 424 | 3209 | 74902 | ==> | "M" | 45 | 181 | 2908 | 28118 | ==> | "M" | 50 | 186 | 2628 | 26396 | ==> | "M" | 56 | 164 | 2286 | 15472 | ==> +---------------------------------------------------------+
Чтобы подтвердить нашу идею использования демографии для улучшения наших рекомендаций, нам нужно немного покопаться в наших данных. Давайте начнем с одного пользователя и выясним, кто такие пользователи. Давайте попробуем userId 1, если вы помните, что этот пользователь был женщиной в возрасте от 1 до 17 лет. Чтобы найти похожих пользователей, мы возьмем фильмы, которые оценила эта молодая девушка, и посмотрим, какие другие пользователи оценили те же фильмы в пределах одной звезды ее рейтинга. Это даст нам пользователей, которые любят те же фильмы, что и она, и ненавидят те же фильмы, что и она.
START me = node:vertices(userId = "1") MATCH me -[r1:rated]-> movies <-[r2:rated]- similar_users WHERE ABS(r1.stars-r2.stars) <= 1 RETURN similar_users, COUNT(*) AS cnt ORDER BY cnt DESC LIMIT 10
Мы добавили предложение WHERE, чтобы мы могли ограничить абсолютные значения наших звездных рейтингов с точностью до одного рейтинга.
==> +-----------------------------------------------------------------+ ==> | similar_users | cnt | ==> +-----------------------------------------------------------------+ ==> | Node[5010]{type->"User",userId->1088,gender->"F",age->1} | 45 | ==> | Node[5863]{type->"User",userId->1941,gender->"M",age->35} | 45 | ==> | Node[4600]{type->"User",userId->678,gender->"M",age->25} | 44 | ==> | Node[5995]{type->"User",userId->2073,gender->"F",age->18} | 44 | ==> | Node[4346]{type->"User",userId->424,gender->"M",age->25} | 43 | ==> | Node[5902]{type->"User",userId->1980,gender->"M",age->35} | 42 | ==> | Node[5527]{type->"User",userId->1605,gender->"F",age->18} | 41 | ==> | Node[5042]{type->"User",userId->1120,gender->"M",age->18} | 40 | ==> | Node[5535]{type->"User",userId->1613,gender->"M",age->18} | 39 | ==> | Node[4937]{type->"User",userId->1015,gender->"M",age->35} | 39 | ==> +-----------------------------------------------------------------+
Глядя на эти данные, мы видим, что она больше всего похожа на другого пользователя (1088), который делится ее демографией, и, вероятно, на отца другого пользователя (1941), который делится ее демографией. Вопрос достоверности предоставленных пользователем данных усложняет ситуацию, и мы пока проигнорируем это, но подумайте о том, как такой сервис, как Netflix, работает с несколькими пользователями, имеющими общий аккаунт (мама любит иностранные романсы, Джуниору нравятся боевики, дико отличающиеся друг от друга) рейтинги вносят беспорядок в рекомендации).
Давайте объединим некоторые запросы и посмотрим на демографию похожих пользователей на пользователя 1 и количество подходящих оценок.
START me = node:vertices(userId = "1") MATCH me -[r1:rated]-> movies <-[r2:rated]- similar_users WHERE ABS(r1.stars-r2.stars) <= 1 RETURN similar_users.gender, similar_users.age, COUNT(DISTINCT similar_users.userId) AS user_cnt, COUNT(r2) AS rtg_cnt ORDER BY similar_users.gender, similar_users.age
==> +---------------------------------------------------------------+ ==> | similar_users.gender | similar_users.age | user_cnt | rtg_cnt | ==> +---------------------------------------------------------------+ ==> | "F" | 1 | 37 | 368 | ==> | "F" | 18 | 150 | 1403 | ==> | "F" | 25 | 229 | 2074 | ==> | "F" | 35 | 150 | 1296 | ==> | "F" | 45 | 86 | 559 | ==> | "F" | 50 | 74 | 508 | ==> | "F" | 56 | 58 | 362 | ==> | "M" | 1 | 88 | 697 | ==> | "M" | 18 | 408 | 3747 | ==> | "M" | 25 | 677 | 6520 | ==> | "M" | 35 | 410 | 3687 | ==> | "M" | 45 | 176 | 1385 | ==> | "M" | 50 | 178 | 1277 | ==> | "M" | 56 | 156 | 838 | ==> +---------------------------------------------------------------+
Давайте сравним количество подходящих оценок пользователя 1 с другими сегментами.
gender age user 1 total matching percent of max F 1 368 5292 0.069538927 1 F 18 1403 23247 0.060351873 0.867886179 F 25 2074 39240 0.05285423 0.760066813 F 35 1296 23493 0.055165368 0.793301983 F 45 559 9643 0.057969512 0.83362678 F 50 508 9568 0.053093645 0.763509706 F 56 362 5432 0.066642121 0.958342671 M 1 697 12058 0.057803948 0.831245898 M 18 3747 74656 0.050190206 0.72175698 M 25 6520 140239 0.04649206 0.668576036 M 35 3687 74902 0.04922432 0.70786712 M 45 1385 28118 0.049256704 0.708332818 M 50 1277 26396 0.048378542 0.695704471 M 56 838 15472 0.054162358 0.778878254
START users = node:vertices(type = "User") MATCH (users)-[rating:rated]->(movies) WHERE users.age = 1 AND users.gender = "F" AND rating.stars > 3 RETURN movies.title, COUNT(rating) AS cnt ORDER BY cnt DESC LIMIT 10
Предполагая, что новый пользователь зарегистрирован с той же демографией, что и пользователь 1, мы можем дать ему следующие 10 рекомендуемых фильмов:
==> +-------------------------------------------+ ==> | movies.title | cnt | ==> +-------------------------------------------+ ==> | "Toy Story 2 (1999)" | 19 | ==> | "Toy Story (1995)" | 17 | ==> | "Sixth Sense, The (1999)" | 16 | ==> | "Aladdin (1992)" | 15 | ==> | "Shakespeare in Love (1998)" | 14 | ==> | "Bug's Life, A (1998)" | 14 | ==> | "Beauty and the Beast (1991)" | 13 | ==> | "Clueless (1995)" | 13 | ==> | "Men in Black (1997)" | 13 | ==> | "E.T. the Extra-Terrestrial (1982)" | 12 | ==> +-------------------------------------------+
А как насчет мужчин в возрасте 25-34 лет?
START users = node:vertices(type = "User") MATCH (users)-[rating:rated]->(movies) WHERE users.age = 25 AND users.gender = "M" AND rating.stars > 3 RETURN movies.title, COUNT(rating) AS cnt ORDER BY cnt DESC LIMIT 10
Предполагая, что новый пользователь зарегистрирован с этими демографическими данными, мы можем дать ему следующие 10 рекомендуемых фильмов:
==> +---------------------------------------------------------------+ ==> | movies.title | cnt | ==> +---------------------------------------------------------------+ ==> | "American Beauty (1999)" | 388 | ==> | "Star Wars: Episode V - The Empire Strikes Back (1980)" | 370 | ==> | "Star Wars: Episode IV - A New Hope (1977)" | 366 | ==> | "Terminator 2: Judgment Day (1991)" | 328 | ==> | "Silence of the Lambs, The (1991)" | 326 | ==> | "Raiders of the Lost Ark (1981)" | 323 | ==> | "Matrix, The (1999)" | 320 | ==> | "Saving Private Ryan (1998)" | 313 | ==> | "Braveheart (1995)" | 300 | ==> | "Star Wars: Episode VI - Return of the Jedi (1983)" | 296 | ==> +---------------------------------------------------------------+
Это совсем другой список, и он сразу же улучшит взаимодействие с пользователем, если мы сможем получить только эти фрагменты информации от пользователя.
Теперь мы можем предсказать, какой звездный рейтинг даст новый пользователь, прежде чем смотреть фильм, с учетом его демографии.
START movie = node:vertices(title="Toy Story 2 (1999)"), users = node:vertices(type = "User") MATCH (users)-[rating:rated]->(movie) WHERE users.age = 1 AND users.gender = "F" RETURN AVG(rating.stars)
==> +-------------------+ ==> | AVG(rating.stars) | ==> +-------------------+ ==> | 4.142857142857143 | ==> +-------------------+
После того, как недавно зарегистрированный пользователь оценил несколько фильмов, мы можем перейти к действительно персональным рекомендациям. Здесь мы прогнозируем ее рейтинг одного фильма, который она еще не видела.
START me = node:vertices(userId = "1"), movie = node:vertices(title="101 Dalmatians (1961)") MATCH me -[r1:rated]-> movies <-[r2:rated]- similar_users -[r3:rated]-> movie WHERE ABS(r1.stars-r2.stars) <= 1 RETURN AVG(r3.stars)
==> +-------------------+ ==> | AVG(rating.stars) | ==> +-------------------+ ==> | 3.600526612751551 | ==> +-------------------+
Мы также можем рекомендовать 10 фильмов, которые должен увидеть пользователь 1:
START me = node:vertices(userId = "1") MATCH me -[r1:rated]-> movies <-[r2:rated]- similar_users -[r3:rated]-> new_movies WHERE ABS(r1.stars-r2.stars) <= 1 AND r3.stars > 3 AND NOT((me)-[:rated]->(new_movies)) RETURN new_movies.title, COUNT(r3) AS cnt ORDER BY cnt DESC LIMIT 10
Этот запрос может занять очень много времени для возврата, так как мы собираемся охватить почти все отношения на графике. Лучший способ справиться с этим — взять 10 лучших похожих пользователей и заставить наш обход просто использовать их.
START me = node:vertices(userId = "1"), similar_users = node(5010,5863,4600,5995,4346,5902,5527,5042,5535,4937) MATCH similar_users -[rating:rated]-> new_movies WHERE rating.stars > 3 AND NOT((me)-[:rated]->(new_movies)) RETURN new_movies.title, COUNT(rating) AS cnt ORDER BY cnt DESC LIMIT 10
Наши результаты сейчас:
==> +------------------------------------------+ ==> | new_movies.title | cnt | ==> +------------------------------------------+ ==> | "Usual Suspects, The (1995)" | 10 | ==> | "101 Dalmatians (1961)" | 10 | ==> | "Casablanca (1942)" | 10 | ==> | "It's a Wonderful Life (1946)" | 10 | ==> | "Forrest Gump (1994)" | 10 | ==> | "Elizabeth (1998)" | 10 | ==> | "Shawshank Redemption, The (1994)" | 10 | ==> | "American Beauty (1999)" | 9 | ==> | "Few Good Men, A (1992)" | 9 | ==> | "Shakespeare in Love (1998)" | 9 | ==> +------------------------------------------+
Теперь мы знаем, как заставить Neo4j давать нам персонализированные рекомендации для фильмов. Мы добавим их в наше приложение Neoflix в следующем сообщении в блоге. Просто будьте осторожны, ваши пользователи не шутят с вами.