Как агрегировать совпадающие пары в "связанные компоненты" в Python

реальные проблемы:

у меня есть данные о директорах многих фирм, но иногда "Джон Смит, директор XYZ" и "Джон Смит, директор ABC" - это один и тот же человек, иногда это не так. Кроме того," Джон Дж.Смит, директор XYZ "и" Джон Смит, директор ABC " могут быть одним и тем же человеком или не быть. Часто изучение дополнительной информации (например, Сравнение биографических данных "Джон Смит, директор XYZ" и "Джон Смит, директор ABC") делает это возможно решить, являются ли два наблюдения одним и тем же человеком или нет.

концептуальная версия проблемы:

в этом духе, я собираю данные, которые позволят выявить совпадающие пары. Например, предположим, что у меня есть следующие совпадающие пары:{(a, b), (b, c), (c, d), (d, e), (f, g)}. Я хочу использовать свойство транзитивности отношения "тот же человек, что и" для генерации "связанных компонентов"{{a, b, c, d, e}, {f, g}}. Это {a, b, c, d, e} человек и {f, g} это другое. (Более ранняя версия вопрос относится к "кликам", которые, по-видимому, являются чем-то другим; это объясняет, почему find_cliques на networkx давал "неправильные" результаты (для моих целей).

следующий код Python выполняет эту работу. Но мне интересно: есть ли лучший (менее дорогостоящий) подход (например, использование стандартных или доступных библиотек)?

есть примеры здесь и там, которые кажутся связанными (например,клики в python), но они неполные, поэтому я не уверен на какие библиотеки они ссылаются или как настроить мои данные для их использования.

пример кода Python 2:

def get_cliques(pairs):
    from sets import Set

    set_list = [Set(pairs[0])]

    for pair in pairs[1:]:
        matched=False
        for set in set_list:
            if pair[0] in set or pair[1] in set:
                set.update(pair)
                matched=True
                break
        if not matched:
            set_list.append(Set(pair))

    return set_list

pairs = [('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e'), ('f', 'g')]

print(get_cliques(pairs))

это дает желаемый результат:[Set(['a', 'c', 'b', 'e', 'd']), Set(['g', 'f'])].

пример кода Python 3:

это производит [set(['a', 'c', 'b', 'e', 'd']), set(['g', 'f'])]):

def get_cliques(pairs):

    set_list = [set(pairs[0])]

    for pair in pairs[1:]:
        matched=False
        for a_set in set_list:
            if pair[0] in a_set or pair[1] in a_set:
                a_set.update(pair)
                matched=True
                break
        if not matched:
            set_list.append(set(pair))

    return set_list

pairs = [('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e'), ('f', 'g')]

print(get_cliques(pairs))

4 ответов


С networkX:

import networkx as nx
G1=nx.Graph()
G1.add_edges_from([("a","b"),("b","c"),("c","d"),("d","e"),("f","g")])
sorted(nx.connected_components(G1), key = len, reverse=True)

даем:

[['a', 'd', 'e', 'b', 'c'], ['f', 'g']]

теперь вы должны проверить самый быстрый алгоритм ...

OP:

это прекрасно работает! Теперь у меня есть это в моей базе данных PostgreSQL. Просто организуйте пары в таблицу с двумя столбцами, затем используйте array_agg() перейти к функции PL/Python get_connected(). Спасибо.

CREATE OR REPLACE FUNCTION get_connected(
    lhs text[],
    rhs text[])
  RETURNS SETOF text[] AS
$BODY$
    pairs = zip(lhs, rhs)

    import networkx as nx
    G=nx.Graph()
    G.add_edges_from(pairs)
    return sorted(nx.connected_components(G), key = len, reverse=True)

$BODY$ LANGUAGE plpythonu;

(Примечание: я отредактировал ответ, так как думал, что этот шаг может быть полезным дополнением, но слишком длинным для комментария.)


Я не верю (поправьте меня, если я ошибаюсь), что это напрямую связано с самой большой проблемой клики. Определение клик (Википедия) говорит, что клика "в неориентированном графе является подмножеством его вершин, так что каждые две вершины в подмножестве связаны ребром". В этом случае нам нужно найти, какие узлы могут достигать друг друга (даже косвенно).

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

from collections import defaultdict

def get_cliques(pairs):
    # Build a graph using the pairs
    nodes = defaultdict(lambda: [])
    for a, b in pairs:
        if b is not None:
            nodes[a].append((b, nodes[b]))
            nodes[b].append((a, nodes[a]))
        else:
            nodes[a]  # empty list

    # Add all neighbors to the same group    
    visited = set()
    def _build_group(key, group):
        if key in visited:
            return
        visited.add(key)
        group.add(key)
        for key, _ in nodes[key]:
            _build_group(key, group)

    groups = []
    for key in nodes.keys():
        if key in visited: continue
        groups.append(set())
        _build_group(key, groups[-1])

    return groups

if __name__ == '__main__':
    pairs = [
        ('a', 'b'), ('b', 'c'), ('b', 'd'), # a "tree"
        ('f', None),                        # no relations
        ('h', 'i'), ('i', 'j'), ('j', 'h')  # circular
    ]
    print get_cliques(pairs)
    # Output: [set(['a', 'c', 'b', 'd']), set(['f']), set(['i', 'h', 'j'])]

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


комментарий DSM заставил меня искать алгоритмы консолидации набора в Python. Розетта Код есть две версии одного и того же алгоритма. Пример использования (нерекурсивная версия):

[('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e'), ('f', 'g')]

# Copied from Rosetta Code
def consolidate(sets):
    setlist = [s for s in sets if s]
    for i, s1 in enumerate(setlist):
        if s1:
            for s2 in setlist[i+1:]:
                intersection = s1.intersection(s2)
                if intersection:
                    s2.update(s1)
                    s1.clear()
                    s1 = s2
    return [s for s in setlist if s]

print consolidate([set(pair) for pair in pairs])
# Output: [set(['a', 'c', 'b', 'd']), set([None, 'f']), set(['i', 'h', 'j'])]

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

# Modified to use a dictionary
from collections import defaultdict

def get_cliques2(pairs):
  maxClique = 1
  clique = defaultdict(int)
  for (a, b) in pairs:
    currentClique = max(clique[i] for i in (a,b))
    if currentClique == 0:
      currentClique = maxClique
      maxClique += 1
    clique[a] = clique[b] = currentClique
  reversed = defaultdict(list)
  for (k, v) in clique.iteritems(): reversed[v].append(k)
  return reversed

и просто убедить себя, что он возвращает правильный результат (get_cliques1 вот ваше оригинальное решение Python 2):

>>> from cliques import *
>>> get_cliques1(pairs) # Original Python 2 solution
[Set(['a', 'c', 'b', 'e', 'd']), Set(['g', 'f'])]
>>> get_cliques2(pairs) # Dictionary-based alternative
[['a', 'c', 'b', 'e', 'd'], ['g', 'f']]

информация о времени в секундах (с 10 миллионами повторений):

$ python get_times.py 
get_cliques: 75.1285209656
get_cliques2: 69.9816100597

для полноты и ссылки, это полный список обоих cliques.py и get_times.py сроки сценарий:

# cliques.py
# Python 2.7
from collections import defaultdict
from sets import Set  # I moved your import out of the function to try to get closer to apples-apples

# Original Python 2 solution
def get_cliques1(pairs):

    set_list = [Set(pairs[0])]

    for pair in pairs[1:]:
        matched=False
        for set in set_list:
            if pair[0] in set or pair[1] in set:
                set.update(pair)
                matched=True
                break
        if not matched:
            set_list.append(Set(pair))

    return set_list

# Modified to use a dictionary
def get_cliques2(pairs):
  maxClique = 1
  clique = defaultdict(int)
  for (a, b) in pairs:
    currentClique = max(clique[i] for i in (a,b))
    if currentClique == 0:
      currentClique = maxClique
      maxClique += 1
    clique[a] = clique[b] = currentClique
  reversed = defaultdict(list)
  for (k, v) in clique.iteritems(): reversed[v].append(k)
  return reversed.values()

pairs = [('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e'), ('f', 'g')]


# get_times.py
# Python 2.7
from timeit import timeit

REPS = 10000000

print "get_cliques: " + str(timeit(
  stmt='get_cliques1(pairs)', setup='from cliques import get_cliques1, pairs',
  number=REPS
))
print "get_cliques2: " + str(timeit(
  stmt='get_cliques2(pairs)', setup='from cliques import get_cliques2, pairs',
  number=REPS
))

таким образом, по крайней мере, в этом надуманном сценарии есть измеримое ускорение. Это, по общему признанию, не новаторский, и я уверен, что оставил некоторые биты производительности на столе в своей реализации, но, возможно, это поможет вам подумать о других альтернативах?