Графы всегда легче видеть, как они выглядят, чем просто ими оперировать. В этой статье поговорим о визуализации графов GraphFrames с помощью Graphviz. Читайте в этой статье: синтаксис Graphviz, а также отображение графов в Jupyter Notebook.
Немного о пакете Graphviz
Graphviz — это пакет для визуализации графов и сетей. Он включает коллекцию программ-фильтров:
dotдля рисования направленных графов,neatoдля рисования ненаправленных графов,circoдля рисования круговых диаграмм и др [1]
Эти программы располагают элементы по-разному, мы далее это ещё увидим. Синтаксис Graphviz достаточно простой. Создавать направленные графы будем с помощью объекта digraph. Например, граф с врешинами a и b и связью, идущей из первой до второй, в Graphviz выражается так:
digraph {
a -> b
}
Теперь если сохранить это в файле c расширением gv, например, g.gv, то его можно компилировать одной из программ-фильтров:
$ dot g.gv -Tpng -o g.png
Здесь мы экспортируем в png, другие форматы также поддерживаются (дайте команду man dot и вы увидите доступные форматы в разделе Output formats).
Форма вершин изменяется через атрибут node, а форма связей (стрелок) — через edge. Например, определение:
digraph {
node [shape=box, fontsize=10];
edge[arrowsize=.4];
a -> b
}
— сделает вершины прямоугольными с шрифтом размера 10, а у стрелок будет размер 0.4.
Отдельные связи также можно изменять под себя. Делается это сразу после описания связи в квадратных скобках. Ниже связь a->b будет иметь подпись с размером 12.
digraph {
a -> b [label="This is label", fontsize=12];
b -> c;
}
Комментарии такие же, как и в С++: многострочный /* com */ или однострочный // com.
Рисование графов GraphFrames
Самое простое, что мы можем сделать, так это записать в файл связи графа. Воспользуемся графом GraphFrames, который мы использовали в одной из прошлой статьи:
from graphframes import GraphFrame
v = spark.createDataFrame([
(1, 'Anton', 23),
(2, 'Anna', 27),
(3, 'Andry', 24),
(4, 'Alex', 32),
(5, 'Boris', 55),
(6, 'Vera', 64), ],
['id', 'name', 'age'])
e = spark.createDataFrame([
(1, 2, 'friend'),
(2, 1, 'friend'),
(1, 3, 'friend'),
(3, 1, 'friend'),
(2, 3, 'friend'),
(3, 2, 'friend'),
(4, 2, 'follows'),
(5, 6, 'follows'), ],
['src', 'dst', 'type'])
g = GraphFrame(v, e)
С помощью поиска по шаблону мы можем воссоздать все связи между вершинами, а затем проитерировать по каждому полю (можно было бы просто пройтись по ребрам, поиск по шаблону испольуем для напоминания его синтаксиса):
ab = g.find("(a)-[]->(c)")
rows = ab.collect()
for row in rows:
print(f"{row.a.name} -> {row.c.name}")
Результат:
Boris -> Vera Andry -> Anton Anna -> Anton Anton -> Andry Anna -> Andry Anton -> Anna Andry -> Anna Alex -> Anna
Именно так мы и поступим. Для наглядности снабдим табами \t каждую связь и добавим перход на новую строку \n в конце, а затем запишем в файл graph.gv. Код на Python для записи графа GraphFrames в файл выглядит следующим образом:
with open("graph.gv","w") as f:
res = ''
for row in rows:
res += f"\t{row.a.name} -> {row.c.name};\n"
f.write("digraph G {\n" + res + "}")
Результат записи в файл (знак ! показывает, что запускается интерпретатор Bash изнутри Jupyter Notebook):
!cat graph.gv
digraph G {
Boris -> Vera;
Andry -> Anton;
Anna -> Anton;
Anton -> Andry;
Anna -> Andry;
Anton -> Anna;
Andry -> Anna;
Alex -> Anna;
}
Этот файл теперь можно откомпилировать с помощью программ-фильтров Graphviz:
!dot graph.gv -Tpng -o dot_graph.png
Как отобразить граф в виде изображения в Jupyter Notebook
В Jupyter Notebook вывести изображения можно разными способами. Мы поговорим о самых простых: через язык разметки Markdown и через модуль IPython.display.
Чтобы сделать ячейку Markdown-ячейкой, либо нажмите ESC (ячейка должна быть подсвечена синим цветом), а затем нажмите M; либо в контекстном меню Cell -> Cell Type -> Markdown. Затем добавьте изображение в соответсвии с синтаксисом Markdown:
 В моем случае: 
— и запустите ячейку.

Проблем этого метода в том, что после перекомпиляции с тем же названием, отображаться будет старая картинка. Поэтому компилировать нужно с измененным названием, например, dot_graph2.png. Зато этот метод поддерживает формат SVG, в отличие от следующего.
Модуль IPython.display содержит класс Image, с помощью которого выводится изображение:
from IPython.display import Image Image(filename="dot_graph.png")
Используем другие фильтры Graphviz
Если результат визуализации нас смущает, то мы можем прибегнуть к другим фильтрам Graphviz. Фильтр neato подходит для ненаправленных графов, а circo для циклических и круговых диаграмм. Мы, тем не менее, посмотрим, как они отобразят наш граф.
!circo graph.gv -Tpng -o circo_graph.png !neato graph.gv -Tpng -o neato_graph.png

Как видим, circo для нашего конкретного графа справился лучше, чем остальные.
В следующей статье поговорим о Graphviz как о библиотеки Python, проведем визуализацию на примере алгоритма PageRank. Ещё больше подробностей о графах GraphFramesб их визуализации вы узнаете на наших образовательных курсах в лицензированном учебном центре обучения и повышения квалификации руководителей и ИТ-специалистов (менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data) в Москве:



