Как агрегировать совпадающие пары в "связанные компоненты" в 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
))
таким образом, по крайней мере, в этом надуманном сценарии есть измеримое ускорение. Это, по общему признанию, не новаторский, и я уверен, что оставил некоторые биты производительности на столе в своей реализации, но, возможно, это поможет вам подумать о других альтернативах?