Статьи

Персонализация приложений в Cypher

Мы надеемся, что вы видели телевизионный рекламный ролик рекламной кампании « Человек, который может пахнуть ваш человек », созданной 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 в следующем сообщении в блоге. Просто будьте осторожны, ваши пользователи не шутят с вами.