YELP — зарубежная сеть, которая помогает людям находить местные предприятия и услуги, основываясь на отзывах, предпочтениях и рекомендациях. В текущей статей будет проведен определенный ее анализ с использованием платформы Neo4j, относящаяся к графовым СУБД, а также язык python.

Что посмотрим:

  • как работать с Neo4j и объемными датасетами на примере YELP;
  • чем может быть полезен YELP dataset;
  • частично: какие особенности в новых версиях Neo4j и почему книга «Графовые алгоритмы» 2019 года от O'REILLY уже устарела.

Что такое YELP и yelp dataset


Сеть YELP на текущий момент охватывает 30 стран, РФ пока не входит в их число. Русский язык сетью не поддерживается. Сама сеть содержит достаточно объемное количество сведений о различного рода предприятиях, а также отзывах о них. Также yelp можно смело назвать социальной сетью, так как в ней имеются данные о пользователях, оставлявших отзывы. Никаких персональных данных там нет, только имена. Тем не менее пользователи образуют сообщества, группы или же могут быть в дальнейшем в эти группы и сообщества объединены по различным признакам. Например по количеству звезд (stars), которые поставили той точке (ресторану, заправке и т.п.), которую посетили.

Сама себя YELP описывает следующим образом:

  • 8,635,403 отзывов
  • 160,585 предприятий
  • 200,000 картинок
  • 8 мегаполисов

1,162,119 рекомендаций от 2,189,457 пользователей.

Более 1.2 миллиона бизнес-атрибутики: часы работы, парковка, доступность и т.п.

С 2013 года Yelp регулярно проводит конкурс Yelp Dataset, призывая всех желающих
исследовать и изучать открытый набор данных Yelp.

Сам датасет доступен по ссылке
Датасет достаточно объемный и после распаковки представляет из себя 5 файлов формата json:



Все бы ничего, да вот только YELP выкладывает «сырые» (raw), необработанные данные и, чтобы начать с ними работать, потребуется предобработка.

Установка и быстрая настройка Neo4j


Для анализа будет использоваться Neo4j, используем возможности графовой СУБД и их незамысловатый язык cypher для работы с датасетом.

О Neo4j как графовой СУБД неоднократно писали на Habrе (здесь и здесь статьи для начинающих), поэтому повторно представлять ее нет смысла.

Для того, чтобы начать работу с платформой, необходимо скачать desktop версию (около 500Mb) либо поработать в online песочнице. На момент написания статьи доступна Neo4j Enterprise 4.2.6 for Developers, а также иные, более ранние версии для установки.

Далее будет использоваться вариант — работа в desktop версии в среде Windows (Neo4j Desktop 1.4.5, БД версии 4.2.5, 4.2.1).

Не смотря на то, что самая свежая версия 4.2.6, лучше ее пока не устанавливать, так как для нее еще не актуализированы все плагины, использующиеся в neo4j. Достаточно будет предыдущей версии — 4.2.5.

После установки скачанного пакета, необходимо будет:

  • создать новую локальную БД, указав пользователя neo4j и пароль 123 (почему именно их, объясню ниже),

картинка


  • установить плагины, которые понадобятся — APOC, Graph Data Science Library.

картинка


  • проверить, запускается ли БД и открывается ли браузер при нажатии на кнопку старт

картинка




*- включить offline режим, чтобы БД истово не пыталась предлагать новые версии.

картинка


Загружаем данные в Neo4j


Если с установкой Neo4j все прошло гладко, можно двигаться дальше и тут есть три пути.

Путь первый — пройти долгий путь от импорта данных в БД с нуля, включающий их первичную очистку и трансформацию.

Путь второй — загрузить готовую БД из дампа и начать с ней работать.

Третий путь — загрузить готовую БД в папку со вновь созданной БД напрямую.

В итоге во всех случаях должна получиться БД со следующими параметрами:



и итоговой схемой:



Чтобы пройти первый путь, лучше ознакомиться сперва со статьей на medium.

*Большое человеческое спасибо за это TRAN Ngoc Thach.

И воспользоваться готовым jupyter notebookом (адаптирован мною под windows) — ссылка.

Процесс импорта не из простых и занимает достаточно продолжительное время —



Проблем с памятью при этом не возникает даже при наличии всего лишь 8Гб Ram, так как используется пакетный импорт.

Однако потребуется создать swap файл размером на 10Гб, так как при проверке импортированных данных jupyter крашится, об этом моменте есть упоминание в вышеуказанной тетрадке jupyter.

Второй путь проще.
Создать БД, зайти в папку с ее neo4j-admin (у каждой БД он свой) и выполнить:
neo4j-admin load --from=G:\neo4j\dumps\neo4j.dump --database=neo4j --force

где G:\neo4j\dumps\neo4j.dump — путь к дампу БД.

Третий путь самый быстрый и был обнаружен случайно. Он подразумевает копирование уже готовой БД neo4j в существующую БД neo4j напрямую. Из минусов (пока обнаруженных) — нельзя произвести backup БД средствами Neo4j (neo4j-admin dump --database=neo4j --to=D:\neo4j\neo4j.dump). Однако, это может быть связано с различиями в версиях — в версии 4.2.1 была скопирована БД от версии 4.2.5. Кроме того, на общей схеме БД появляются артефакты, которые тем не менее не влияют на ее работу.

Как реализуется этот метод:

  • открыть вкладку Manage БД, куда будет произведен импорт

картинка




  • перейти в папку с БД и скопировать туда папку data, перезаписав возможные совпадения

картинка


При этом сама БД, куда произведено копирование не должна быть запущена.
  • Перезапустить Neo4j.

И вот здесь пригодятся логин-пароль, которые ранее были использованы (neo4j,123) для избежания конфликтов.

После старта скопированной БД будет доступна БД c yelp-датасетом:



Смотрим YELP


Изучать YELP можно как из Neo4j браузера, так и отправляя запросы в БД из того же jupyter notebook.

Благодаря тому, что БД графовая, в браузере будет сопровождать приятная наглядная картинка, на которой эти графы и будут отображаться.

Приступая к ознакомлению с YELP необходимо оговориться, что в БД будут только 3 страны US,KG и CA:



Посмотреть схему БД можно написав запрос на языке cypher в браузере neo4j:

CALL db.schema.visualization()




Как читать эту схему? Выглядит все следующим образом. Вершина User имеет связь сама с собой типа FRIENDS, а также связь WROTE с вершиной Review. Rewiew в свою очередь имеет связь REVIEWS с Business и так далее. Посмотреть на это можно наглядно после нажатия на одной из вершин (node labels), например на User:



БД выберет любых 25 пользователей и покажет их:



Если нажать на соответствующий значок прямо на пользователе, то будут показаны все идущие от него прямые связи, а так как связи у User двух типов — FRIENDS и REVIEW, то все они появятся:



Это удобно и неудобно одновременно. С одной стороны о пользователе можно посмотреть всю информацию одним кликом, но в то же время этим кликом нельзя убрать лишнее.

Но здесь нет ничего страшного, можно по id найти этого пользователя и только всех его друзей:

MATCH p=(:User {user_id:"u-CFWELen3aWMSiLAa_9VANw"}) -[r:FRIENDS]->() RETURN p LIMIT 25



Точно так же можно посмотреть какие отзывы написал данный человек:



YELP хранит отзывы аж от 2010 года! Сомнительная полезность, но тем не менее.

Чтобы почитать эти отзывы необходимо переключиться в вид текста, нажав на А —



Посмотрим на место, о котором писала Sandy 10 лет назад и найдем его на yelp.com —

Такое место действительно существует — www.yelp.com/biz/cafe-sushi-cambridge,
а вот и сама Sandy co своим отзывом — www.yelp.com/biz/cafe-sushi-cambridge?q=I%20was%20really%20excited

картинка


Python запросы в БД Neo4j из jupyter notebook


Здесь будут частично использованы сведения из упомянутой свободно распространяемой книги «Графовые алгоритмы» 2019 года от O'REILLY. Частично, потому как синтаксис из книги во многих местах устарел.

База, с которой мы будем работать должна быть запущена, при этом сам neo4j браузер запускать нет необходимости.

Импорт библиотек:

from neo4j import GraphDatabase
import pandas as pd
from tabulate import tabulate
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

Подключение к БД:

driver = GraphDatabase.driver("bolt://localhost", auth=("neo4j", "123"))

Подсчитаем количество вершин для каждой метки в БД:

result = {"label": [], "count": []}
with driver.session() as session:
    labels = [row["label"] for row in session.run("CALL db.labels()")]
    for label in labels:
        query = f"MATCH (:`{label}`) RETURN count(*) as count"
        count = session.run(query).single()["count"]
        result["label"].append(label)
        result["count"].append(count)
df = pd.DataFrame(data=result)
print(tabulate(df.sort_values("count"), headers='keys',
tablefmt='psql', showindex=False))

На выходе:

+----------+---------+
| label | count |
|----------+---------|
| Country | 3 |
| Area | 15 |
| City | 355 |
| Category | 1330 |
| Business | 160585 |
| User | 2189457 |
| Review | 8635403 |
+----------+---------+
Похоже на правду, в нашей базе 3 страны, как мы увидели ранее через neo4j браузер.

А этот код подсчитает количество связей (ребер):

result = {"relType": [], "count": []}
with driver.session() as session:
    rel_types = [row["relationshipType"] for row in session.run
    ("CALL db.relationshipTypes()")]
    for rel_type in rel_types:
        query = f"MATCH ()-[:`{rel_type}`]->() RETURN count(*) as count"
        count = session.run(query).single()["count"]
        result["relType"].append(rel_type)
        result["count"].append(count)
df = pd.DataFrame(data=result)
print(tabulate(df.sort_values("count"), headers='keys',
tablefmt='psql', showindex=False))

Выход:

+-------------+---------+
| relType | count |
|-------------+---------|
| IN_COUNTRY | 15 |
| IN_AREA | 355 |
| IN_CITY | 160585 |
| IN_CATEGORY | 708884 |
| REVIEWS | 8635403 |
| WROTE | 8635403 |
| FRIENDS | 8985774 |
+-------------+---------+
Думаю, принцип понятен. В завершение напишем запрос и визуализируем его.

Top 10 отелей Ванкувера с наибольшим количеством отзывов
# Find the 10 hotels with the most reviews
query = """
MATCH (review:Review)-[:REVIEWS]->(business:Business),
      (business)-[:IN_CATEGORY]->(category:Category {category_id: $category}),
      (business)-[:IN_CITY]->(:City {name: $city})
RETURN business.name AS business, collect(review.stars) AS allReviews
ORDER BY size(allReviews) DESC
LIMIT 10
"""
#MATCH (review:Review)-[:REVIEWS]->(business:Business),
#(business)-[:IN_CATEGORY]->(category:Category {category_id: "Hotels"}),
#(business)-[:IN_CITY]->(:City {name: "Vancouver"})
#RETURN business.name AS business, collect(review.stars) AS allReviews
#ORDER BY size(allReviews) DESC
#LIMIT 10


fig = plt.figure()
fig.set_size_inches(10.5, 14.5)
fig.subplots_adjust(hspace=0.4, wspace=0.4)

with driver.session() as session:
    params = { "city": "Vancouver", "category": "Hotels"}
    result = session.run(query, params)
    for index, row in enumerate(result):        
        business = row["business"]
        stars = pd.Series(row["allReviews"])
        #print(dir(stars))

        total = stars.count()
        #s = pd.concat([pd.Series(x['A']) for x in data]).astype(float)
        s = pd.concat([pd.Series(row['allReviews'])]).astype(float)
        average_stars = s.mean().round(2)

        # Calculate the star distribution
        stars_histogram = stars.value_counts().sort_index()
        stars_histogram /= float(stars_histogram.sum())

        # Plot a bar chart showing the distribution of star ratings
        ax = fig.add_subplot(5, 2, index+1)
        stars_histogram.plot(kind="bar", legend=None, color="darkblue",
                             title=f"{business}\nAve:{average_stars}, Total: {total}")
                            
        #print(business)
        #print(stars)

plt.tight_layout()
plt.show()


Результат должен получиться следующий —



Ось X представляет рейтинг отеля в звездах, а ось Y – общий процент каждого рейтинга.

Чем может быть полезен YELP dataset


Из плюсов можно выделить следующие:

  • достаточно богатое информационное поле по содержательной составляющей. В частности можно просто насобирать отзывы со звездами 1.0 или 5.0 и заспамить какой-либо бизнес. Гм. Немного не в ту сторону, но вектор понятен;
  • датасет объемен, что создает дополнительные приятные трудности в плане тестирования производительности различных платформ по анализу данных;
  • представленные данные имеют определенную ретроспективу и в принципе возможно понять, как менялось предприятие, исходя из отзывов о нем;
  • данные можно использовать как ориентиры по предприятиям, учитывая, что имеются адреса;
  • пользователи в датасете зачастую образуют интересные взаимосвязанные структуры, которые можно брать как есть, не формируя пользователей в искусственную соц. сеть и не собирая данную сеть из иных существующих соц. сетей.

Из минусов:

  • всего лишь три страны представлены из 30-ти и есть подозрение, что и то не полностью,
  • отзывы хранятся по 10 лет, что может искажать и зачастую портить характеристику существующего бизнеса,
  • о пользователях мало данных, они обезличены, поэтому, рекомендательные системы на базе датасета будут явно хромать,
  • в связях FRIENDS используются направленные графы, то есть Аня дружит -> Петей. Получается, что Петя не дружит с Аней. Это решается программно, но все равно это неудобно.
  • датасет выкладывается «сырой» и требуется значительные усилия для его предобработки.

Несколько слов об особенностях новых версий Neo4j


Neo4j динамично обновляется и новая версия интерфейса, используемого в Neo4j Desktop 1.4.5 не совсем удобна, на мой взгляд. В частности не хватает наглядности в части сведений о количестве нод и связей в БД, что было в предыдущих версиях. Кроме того, интерфейс перемещения по вкладкам при работе с БД был изменен и к нему тоже необходимо привыкнуть.

Главная неприятность в обновлениях — интеграция графовых алгоритмов в плагин Graph Data Science Library. Ранее они именовались neo4j-graph-algorithms.

После интеграции многие алгоритмы значительно изменили синтаксис. По этой причине, изучение книги «Графовые алгоритмы» 2019 года от O'REILLY может быть затруднено.

Дамп БД yelp для neo4j — скачать.