В прошлый раз мы говорили об основных операциях над графами GraphFrames в Apache Spark. Сегодня рассмотрим поиск по шаблону (motif finding). Что такое motif finding, как задается в GraphFrames, какие правила и ограничения, всё это с примерами в нашей статье.
Что такое поиск по шаблону, или motif finding
Поиск по шаблону (morif finding) — это способ нахождения заданных структурных шаблонов в графе. В GraphFrames для поиска по шаблону используется свой очень простой предметно-ориентированный язык для выражения запросов к графу. Запрос с шаблоном передается методу find
. Например, запрос:
graph.find("(a)-[e]->(b); (b)-[e2]->(a)")
— найдет все подграфы, состоящие из двух вершин a
и b
, которые соединены друг с другом в обоих направлениях. Результатом запроса будет DataFrame со столбцами a
, e
, b
, e2
, где e
и e2
— ребра. Все эти столбцы имеют тип StructField
, который эквивалентен исходному датфрейму вершин или ребер.
К сожалению, GraphFrames не предоставляет выбора алгоритма поиска, хотя их существует огромное множество [1].
Правила шаблонов
Шаблоны с запросом к графу GraphFrame должны удовлетворять следующим условиям [2]:
- Вершины должны быть заключены в круглые скобки, ребра — в квадратные. Имена результирующих столбцов будут такие же, какие имена заключены в скобках.
- Единицей шаблона является ребро. Ребра разделяются точкой с запятой. Направление у связи всегда правое (знак больше): (откуда)-[ребро]->(куда). Например, шаблон
(a)-[e]->(b); (b)-[e2]->(c)
обозначает связьa
доb
иb
доc
. Шаблоны(a)-[e]->(b)-[e2]->(c)
и(a)<-[e]->(b)
НЕ допустимы. - Разрешается опускать названия ребер и/или вершин. Например, шаблон
(a)-[]->(b)
выражает связь междуa
иb
, в результирующем датафрейме столбцаe
не будет; а в шаблоне(a)-[]->()
будут опущены и ребро, и вершина, в которую входит ребро. Такие вершины и ребра называются анонимными. - Связь может содержать отрицание через
!
. Таким образом обозначается, что связь не должна присутствовать в графе. Например,(a)-[]->(b); !(b)-[]->(a)
соответствует тем связям, в котором есть путь изa
вb
, но нет пути изb
вa
. - Имена не обозначают уникальные компоненты. Например, в шаблоне
(a)-[e]->(b); (b)-[e2]->(c)
именаa
иc
могут относится к одной и той же вершине. Чтобы получить уникальные значения, отфильтруйте результирующий DataFrame, например, вот такres.filter("a.id != c.id")
.
При этом у шаблонов есть ограничения:
- В шаблоне не должны все элементы быть анонимными. Поэтому шаблон
()-[]->()
и!()-[]->()
запрещены. - Шаблон с отрицанием не должен иметь именованное ребро. Поэтому шаблон
!(a)-[ab]->(b)
неправильный, а шаблон!(a)-[]->(b)
— правильный.
Пример использования поиска по шаблону
Воспользуемся тем же графом, который мы создали в прошлой статье. Итак, найдем те подграфы GraphFrame, состоящие из 3 вершин, где первые две имеют только прямой путь, а обратного нет (см. рисунок ниже). Запрос по такому шаблону в Apache Spark будет следующим:
>>> motif = g.find("(a)-[]->(b); !(b)-[]->(a); (b)-[]->(c)") >>> motif.show() +-------------+-------------+--------------+ | a| b| c| +-------------+-------------+--------------+ |[4, Alex, 32]|[2, Anna, 27]|[1, Anton, 23]| |[4, Alex, 32]|[2, Anna, 27]|[3, Andry, 24]| +-------------+-------------+--------------+
Поскольку результатом поиска по шаблону является DataFrame, то к нему можно применять методы фильтрации:
>>> motif.filter("c.age > 23").show() +-------------+-------------+--------------+ | a| b| c| +-------------+-------------+--------------+ |[4, Alex, 32]|[2, Anna, 27]|[3, Andry, 24]| +-------------+-------------+--------------+
Поиск по шаблону Graphframes с условием
Приведенный выше пример достаточно прост. Поиск по шаблону отбирает те подграфы, у которых есть та или иная связь, соответствующая этому шаблону. Дело усложняется, когда заходит речь о фильтрации по атрибутам (столбцам) вершины или ребра. Поэтому приходится отбирать нужные значения самостоятельно.
Рассмотрим пример. Допустим, требуется найти подграфы, которые состоят из 3 вершин и имеют в своей связи как минимум 2 друзей. Тогда нам понадобится:
- отобрать все подграфы с тремя вершинами;
- определить функцию по подсчету друзей;
- применить эту функцию к ребрам;
- указать условие для отбора.
Ниже приведен код для нахождения тех связей в графе GraphFrames, которые удовлетворяют заданному условию (имеют более чем 2 друзей). Мы использовали функцию reduce
, которая применит к ребрам заданную функцию [3].
import pyspark.sql.functions as F chain3 = g.find("(a)-[ab]->(b); (b)-[bc]->(c)") # Если `friend`, то +1, если нет, то оставь так count_friends = (lambda n, relation: F.when(relation == "friend", n+1).otherwise(n) ) # Применить функцию для столбцов `ab` и `bc`. # `lit(0)` задает начальное значение в 0 cond = (reduce(lambda n, e: count_friends(n, F.col(e).type), ["ab", "bc"], F.lit(0)) ) chain3.where(cond > 1).show()
+--------------+--------------+--------------+--------------+--------------+ | a| ab| b| bc| c| +--------------+--------------+--------------+--------------+--------------+ |[1, Anton, 23]|[1, 3, friend]|[3, Andry, 24]|[3, 1, friend]|[1, Anton, 23]| | [2, Anna, 27]|[2, 3, friend]|[3, Andry, 24]|[3, 1, friend]|[1, Anton, 23]| |[1, Anton, 23]|[1, 2, friend]| [2, Anna, 27]|[2, 1, friend]|[1, Anton, 23]| |[3, Andry, 24]|[3, 2, friend]| [2, Anna, 27]|[2, 1, friend]|[1, Anton, 23]| |[3, Andry, 24]|[3, 1, friend]|[1, Anton, 23]|[1, 3, friend]|[3, Andry, 24]| | [2, Anna, 27]|[2, 1, friend]|[1, Anton, 23]|[1, 3, friend]|[3, Andry, 24]| |[1, Anton, 23]|[1, 2, friend]| [2, Anna, 27]|[2, 3, friend]|[3, Andry, 24]| |[3, Andry, 24]|[3, 2, friend]| [2, Anna, 27]|[2, 3, friend]|[3, Andry, 24]| |[3, Andry, 24]|[3, 1, friend]|[1, Anton, 23]|[1, 2, friend]| [2, Anna, 27]| | [2, Anna, 27]|[2, 1, friend]|[1, Anton, 23]|[1, 2, friend]| [2, Anna, 27]| |[1, Anton, 23]|[1, 3, friend]|[3, Andry, 24]|[3, 2, friend]| [2, Anna, 27]| | [2, Anna, 27]|[2, 3, friend]|[3, Andry, 24]|[3, 2, friend]| [2, Anna, 27]| +--------------+--------------+--------------+--------------+--------------+
Недостатком поиска по шаблону является задание фиксированной длины графа. К счастью, можно всегда переиспользовать код. Кроме того, можно придумать свой скрипт, который будет генерировать последовательность, например, такая функция на Python:
from string import ascii_lowercase as a def format_path(alph_idx): return "({})-[{}]->({}); ".format( a[alph_idx], a[alph_idx] + a[alph_idx+1], a[alph_idx+1] ) def generate_motif_finding(n): if n > 25: print("Too much vertices, use `ascii_letters`") return "".join([format_path(i) for i in range(n)])[:-2]
В следующей статье расскажем о том, как перевести граф GraphFrames в рисунки формата PNG и визуализировать их в Jupyter Notebook. Ещё больше подробностей о motif finding вы узнаете на специализированном курсе по машинному обучению «Графовые алгоритмы в Apache Spark» в лицензированном учебном центре обучения и повышения квалификации разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве.