Python: Быстрое извлечение пересечений среди всех возможных 2-комбинаций в большом количестве списков

У меня есть набор данных ок. 9К списков переменной длины (от 1 до 100К элементов). Мне нужно рассчитать длину пересечения всех возможных комбинаций из двух списков в этом наборе данных. Обратите внимание, что элементы в каждом списке уникальны, поэтому их можно хранить как наборы в python.

Каков наиболее эффективный способ выполнить это в python?

Изменить Я забыл указать, что мне нужна возможность сопоставлять значения пересечения с соответствующей парой списков. Спасибо всем за оперативный ответ и извините за сумбур!


person radrat    schedule 18.11.2009    source источник
comment
Я попробовал решение RedGlyph (# 2), и оно работает как шарм (также достаточно быстро). Так что я буду придерживаться этого, спасибо всем за большой и быстрый ответ.   -  person radrat    schedule 19.11.2009


Ответы (3)


Если ваши наборы хранятся в s, например:

s = [set([1, 2]), set([1, 3]), set([1, 2, 3]), set([2, 4])]

Затем вы можете использовать itertools.combinations, чтобы взять их два на два и вычислить пересечение (обратите внимание, что, как указал Алекс, combinations доступно только с версии 2.6). Здесь со списком (просто для примера):

from itertools import combinations
[ i[0] & i[1] for i in combinations(s,2) ]

Или в цикле, что, вероятно, вам нужно:

for i in combinations(s, 2):
    inter = i[0] & i[1]
    # processes the intersection set result "inter"

Итак, чтобы иметь длину каждого из них, эта «обработка» будет:

    l = len(inter)

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


Изменить. Обратите внимание, что при использовании этого метода каждый набор в списке "s" может фактически быть чем-то другим, что возвращает набор, например генератором. Сам список может быть просто генератором, если у вас мало памяти. Это может быть намного медленнее, в зависимости от того, как вы генерируете эти элементы, но вам не нужно будет одновременно иметь весь список наборов в памяти (не то чтобы это должно быть проблемой в вашем случае).

Например, если каждый набор сделан из функции gen:

def gen(parameter):
    while more_sets():
        # ... some code to generate the next set 'x'
        yield x

with open("results", "wt") as f_results:
    for i in combinations(gen("data"), 2):
        inter = i[0] & i[1]
        f_results.write("%d\n" % len(inter))

Редактировать 2: Как собирать индексы (после комментария Redrat).

Помимо быстрого решения, на которое я ответил в комментарии, более эффективным способом сбора установленных индексов было бы иметь список (index, set) вместо списка set.

Пример с новым форматом:

s = [(0, set([1, 2])), (1, set([1, 3])), (2, set([1, 2, 3]))]

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

with open("results", "wt") as f_results:
    for i in combinations(s, 2):
        inter = i[0][1] & i[1][1]
        f_results.write("length of %d & %d: %d\n" % (i[0][0],i[1][0],len(inter))

В цикле i[0] и i[1] будут кортежем (index, set), поэтому i[0][1] — это первый набор, i[0][0] его индекс.

person RedGlyph    schedule 18.11.2009
comment
› в зависимости от того, как вы создаете эти элементы. Наборы импортируются из плоских текстовых файлов и, к сожалению, не могут быть сгенерированы на лету. - person radrat; 18.11.2009
comment
Они могли быть, но это было бы очень медленно (прыгать по кешированным позициям, читать их... действительно было бы немного сложно). В этом случае я бы придерживался первого метода, который в любом случае не увеличивает потребление памяти - пока вы можете хранить исходный список наборов в памяти, все в порядке. Идея части Edit заключалась в том, чтобы показать гибкость. - person RedGlyph; 18.11.2009
comment
@RedGlyph, используя комбинации и описанный выше метод, я могу действительно получить пересечения между любыми двумя элементами и длину пересечения. Однако я понимаю, что не смогу получить доступ к позиции в s двух сравниваемых элементов, верно? Мой окончательный результат должен быть серией троек, содержащих идентификаторы двух наборов (например, если они определены в словаре или, по крайней мере, их позиции в s) и длину их пересечения. Можно ли использовать для этой цели ваше решение на базе itertools? - person radrat; 19.11.2009
comment
@redrat: Если вам также нужны индексы для всех длин, вы можете использовать комбинацию индексов вместо комбинации наборов: for x,y in combinations(xrange(0, n), 2), и тогда наборы будут доступны через них: inter = s[x] & s[y]. Если вам нужно только несколько индексов, например, максимальная длина, вы можете найти ее с помощью: j = s.index(imax), imax, являющихся набором в цикле итерации. Этого не было в вашем описании, поэтому вам придется уточнить, что вам нужно, если вам нужна дополнительная помощь. - person RedGlyph; 19.11.2009
comment
@redrat, это (paste.ubuntu.com/322146) является продолжением идеи RedGlyph, за исключением используя enumerate для сбора индексов. Поскольку enumerate возвращает итератор, его можно красиво связать с itertools.combinations. Я не проверял, но думаю, что это быстрее, чем доступ к элементам s через индексацию: s[x]. - person unutbu; 19.11.2009
comment
@redrat, я добавил пример с индексами, который более эффективен, чем мой последний ответ (было немного поздно...). Не забудьте также проверить код unutbu в предыдущем комментарии, он более элегантный IMO :-) - person RedGlyph; 19.11.2009
comment
Спасибо, ребята, я попробую все вышеперечисленные варианты и отчитаюсь. (ох и неудачный выбор radrat в качестве ника...) - person radrat; 19.11.2009

Поскольку вам нужно создать матрицу (N на N/2) результатов, то есть O(N в квадрате) выходных данных, ни один подход не может быть меньше, чем O(N в квадрате) — конечно, на любом языке. (N в вашем вопросе «около 9K»). Итак, я не вижу ничего более быстрого, чем (а) создание N наборов, которые вам нужны, и (б) итерация по ним для получения вывода, т. е. простейший подход. ИОВ:

def lotsofintersections(manylists):
  manysets = [set(x) for x in manylists]
  moresets = list(manysets)
  for  s in reversed(manysets):
    moresets.pop()
    for z in moresets:
      yield s & z

Этот код уже пытается добавить некоторую незначительную оптимизацию (например, избегая нарезки или извлечения из начала списков, что может добавить другие факторы O (N в квадрате)).

Если у вас есть много доступных ядер и/или узлов и вы ищете параллельные алгоритмы, это, конечно, другой случай — если это ваш случай, можете ли вы упомянуть тип кластера, который у вас есть, его размер, как узлы и ядра могут лучше всего взаимодействовать , и так далее?

Редактировать: как ОП случайно упомянул в комментарии (!), что им действительно нужны номера пересекаемых наборов (действительно, зачем опускать такие важные части спецификаций ?! хотя бы отредактируйте вопрос чтобы уточнить их...), для этого потребуется только изменить это на:

  L = len(manysets)
  for i, s in enumerate(reversed(manysets)):
    moresets.pop()
    for j, z in enumerate(moresets):
      yield L - i, j + 1, s & z

(если вам нужно «считать от 1» для прогрессивных идентификаторов - в противном случае очевидное изменение).

Но если это часть спецификации, вы можете использовать более простой код — забудьте о дополнительных наборах и:

  L = len(manysets)
  for i xrange(L):
    s = manysets[i]
    for j in range(i+1, L):
      yield i, j, s & manysets[z]

на этот раз предположим, что вместо этого вы хотите «считать с 0», просто для разнообразия ;-)

person Alex Martelli    schedule 18.11.2009
comment
@Alex Martelli: Не могли бы вы объяснить подробнее, почему этот код быстрее, чем itertools.combination? Мои тесты timeit показывают, что это так, но я нахожу это тревожным, поскольку единственный очевидный способ сделать это (itertools.combination) не кажется самым быстрым способом сделать это в этом случае. Следует ли всегда использовать свой код вместо комбинаций? - person unutbu; 18.11.2009
comment
Спасибо, Алекс, я не планирую запускать это на кластере, так что это действительно может быть довольно сложно. Мне тоже интересно узнать об известных различиях в производительности между разными методами. - person radrat; 18.11.2009
comment
@Alex: тебе не нужно исключать идентичные наборы с if s is not z: yield s & z? - person RedGlyph; 18.11.2009
comment
@unutbu: я провел несколько измерений, и оба они дают очень близкие значения времени (в пределах 0,1% для минутных тестов), независимо от размеров списка и/или набора. Вы наблюдали что-то совсем другое? - person RedGlyph; 18.11.2009
comment
@RedGlyph: я должен отозвать свой предыдущий комментарий. Два проверенных мной алгоритма не дают одинаковых результатов даже с if s is not z: yield s & z. Так что сравнивать времена бессмысленно. Вероятно, это ошибка, которую я как-то ввел, но пока не смог ее найти. Вы можете посмотреть мой код здесь: paste.ubuntu.com/321906 - person unutbu; 19.11.2009
comment
@unutbu: Хорошо, я понимаю, как это объясняет разницу. Вероятно, это также немного зависит от реализации (я использую CPython 2.6.4 на Win32), но я также заметил, что itertools.combinations имеет тенденцию использовать меньше памяти, хотя я не проверял, как он был на самом деле закодирован, чтобы найти формальное объяснение. Спасибо за перепроверку :-) - person RedGlyph; 19.11.2009
comment
@RedGlyph, я только что отредактировал ответ, чтобы избежать пересечений с самим собой (что буквально может нарушить спецификации в Q, которые не определяют комбинации 2-различных-списков, а скорее все< /b> возможные, но, вероятно, именно это имел в виду ОП). Я не удивлен, если itertools, тем не менее, все еще немного быстрее, поскольку он закодирован так хорошо и так быстро (использование дополнительной памяти, менее 36 КБ для моего случая, shd в любом случае тривиально, так как необходимая часть памяти, поэтому я не не беспокойтесь об этом ;-). Но: я часто использую App Engine, так что 2.5-совместимые решения по крайней мере мне все еще очень интересны ;-). - person Alex Martelli; 19.11.2009
comment
@Alex: я колебался с интерпретацией из-за акцента на все возможное, но просто предположил, что это не будет иметь особого смысла, но я могу ошибаться. И да, combinations() появился с версии 2.6, вы правы! Я должен был упомянуть об этом. - person RedGlyph; 19.11.2009

Попробуй это:

_lists = [[1, 2, 3, 7], [1, 3], [1, 2, 3], [1, 3, 4, 7]]
_sets = map( set, _lists )
_intersection = reduce( set.intersection, _sets )

И чтобы получить индексы:

_idxs = [ map(_i.index, _intersection ) for _i in _lists ]

Ваше здоровье,

Хосе Мария Гарсия

PS: Извините, я неправильно понял вопрос

person Jose M.    schedule 22.12.2009