Motif Finding, или как найти заданную структуру в графе

В прошлый раз мы говорили об основных операциях над графами 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]|
+-------------+-------------+--------------+
Граф GraphFrames (Apache Spark)
Подграфы, совпадающие с шаблоном

Поскольку результатом поиска по шаблону является 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 в Москве.

Записаться на курс

Смотреть раcписание

Источники
  1. https://en.wikipedia.org/wiki/Network_motif
  2. https://graphframes.github.io/graphframes/docs/_site/user-guide.html
  3. https://docs.python.org/3/library/functools.html#functools.reduce

Добавить комментарий

Поиск по сайту