Как оптимизировать сравнение комбинаций в списке python - PullRequest
2 голосов
/ 08 января 2020

У меня есть список из 16 000 списков. Каждый подсписок содержит кортеж и оценку, например:

mylist = [
[('only','one','time'),10.54],
[('one','time'),3.21],
[('red','hot','chili','peppers'),0.223],
[('red','hot','chili'),1.98]
]  

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

[('one','time'),3.21],
[('red','hot','chili','peppers'),0.223]

, потому что ('one', 'time') является подмножеством из ('only', 'one', 'time' ) и между двумя, ('one', 'time') имеет самый низкий балл 10.54> 3.21.
('red', 'hot', 'chili', 'peppers') - это superset of ('red', 'hot', 'chili') и между двумя значениями 0,223 <1,98 </p>

Моим первоначальным решением было грубое насилие - выберите любую возможную комбинацию из списка, выберите 2, затем сравните кортежи для подмножеств, использующие функцию all (), затем отбрасывают элементы со счетом min ().
Это плохо работает из-за количества комбинаций для поиска:

    from itertools import combinations
    removelist = []
    for x,y in combinations(mylist,2):
        if (all(word in x[0] for word in y[0]) or all(word in y[0] for word in x[0])):
            smallest = min([x,y],key=itemgetter(1))
            removelist.append(smallest)
    removelist = set(removelist)
    outlist = [x for x in mylist if x not in removelist]
    return outlist  

возвращает:

outlist = [
[('only','one','time'),10.54],
[('red','hot','chili'),1.98]
]

Таким образом, для списка из ~ 16 000 подсписков это будет примерно:

combinations = n! / (r! * (n-r)!)  
combinations = 16,000! / (2! * (15998)!)  
combinations = 16,000 * 15999 / 2
combinations = 127,992,000  

Есть ли более разумный способ сделать это, сократив 127 миллионов элементов, которые мне нужно проверить?

Ответы [ 2 ]

3 голосов
/ 09 января 2020

Это может быть в тысячу раз быстрее, чем у вас. Сначала я преобразую наборы слов в наборы для более простых и быстрых проверок подмножеств, например, @Alexander. Затем я сортирую по заданному размеру, поэтому мне не нужно проверять наличие надмножества. (Потому что если | A | ≤ | B |, то единственный способ, которым A является надмножеством B, - это если равен B, и в этом случае это также подмножество из B) .

И вот мой главный трюк. Допустим, у нас есть слово set {'red', 'hot', 'chili'}, и мы хотим найти наборы слов, подмножеством которых оно является. Нужно ли проверять все остальные (большие или одинаковые) наборы? Нет. Достаточно проверить только те наборы, которые содержат слово «красный». Или только те, у кого «жарко». Или только с чили. Давайте возьмем слово редчайшее , т. Е. Слово в наименьшем количестве наборов (в данном случае я бы предположил «чили»).

Я решил назвать ваши списки «песнями», приятно поговорить о них.

from collections import defaultdict

def process_list_stefan(mylist):

    # Change to sets, attach the index, and sort by number of words (many to few)
    songs = [(i, set(words), score) for i, (words, score) in enumerate(mylist)]
    songs.sort(key=lambda song: -len(song[1]))

    # Check songs against others, identify the ones to remove
    remove = set()
    songs_with_word = defaultdict(list)
    for song in songs:
        i, words1, score1 = song
        # Pick the song's rarest word
        word = min(words1, key=lambda word: len(songs_with_word[word]))
        # Go through songs containing that word
        for j, words2, score2 in songs_with_word[word]:
            if words1 <= words2:
                # Lower score loses. In case of tie, lower index loses.
                remove.add(min((score1, i), (score2, j))[1])
        # Make this song available as superset candidate
        for word in words1:
            songs_with_word[word].append(song)

    # Apply the removals
    return [song for i, song in enumerate(mylist) if i not in remove]

Обновление: На самом деле, вместо того, чтобы просто использовать редчайшее слово песни и просмотреть все ее «надмножества» (наборы, содержащие это слово), рассмотрим всех слов в текущей песне и используйте пересечение их "надмножеств". В моем тестировании с использованием готовых данных это даже быстрее примерно в 1,6 раза:

from collections import defaultdict

def process_list_stefan(mylist):

    # Change to sets, attach the index, and sort by number of words (many to few)
    songs = [(i, set(words), score) for i, (words, score) in enumerate(mylist)]
    songs.sort(key=lambda song: -len(song[1]))

    # Helper: Intersection of sets
    def intersect(sets):
        s = next(sets).copy()
        for t in sets:
            s &= t
        return s

    # Check songs against others, identify the ones to remove
    remove = set()
    songs_with_word = defaultdict(set)
    for song in songs:
        i, words1, score1 = song
        for j in intersect(songs_with_word[word] for word in words1):
            # Lower score loses. In case of tie, lower index loses.
            remove.add(min((score1, i), (mylist[j][1], j))[1])
        # Make this song available as superset candidate
        for word in words1:
            songs_with_word[word].add(i)

    # Apply the removals
    return [song for i, song in enumerate(mylist) if i not in remove]
1 голос
/ 08 января 2020

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

Перечисляет каждый набор слов и оценок в этом новом списке и сравнивает против всех оставшихся сетов и очков. Используя наборы, мы можем обнаружить подмножества через s1.issubset(s2), а суперсеты через s1.issuperset(s2).

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

После того, как мы обработали все записи, мы используем условное понимание списка для создания новый список всех записей, которые нужно сохранить.

С точки зрения сравнений подмножеств, сложность времени наихудшего случая равна O (n ^ 2) / 2, которая по-прежнему равна O (n ^ 2). Конечно, каждое сравнение подмножеств имеет свою собственную временную сложность, основанную на количестве уникальных слов в каждом подсписке. Таким образом, это решение выполняет то же количество сравнений, что и метод OP for x,y in combinations(mylist,2), но сравнения подмножеств / надмножеств выполняются с использованием наборов, а не списков. В результате этот метод все еще должен быть значительно быстрее.

def process_list(my_list):
    # Convert tuples to sets.
    my_sets = [(set(tuples), score) for tuples, score in my_list]

    idx_to_remove = set()
    for i, (subset1, score1) in enumerate(my_sets):
        for j, (subset2, score2) in enumerate(my_sets[(i + 1):], start=i + 1):
            if subset1.issubset(subset2) | subset1.issuperset(subset2):  
                # Subset/Superset detected.
                idx_to_remove.add(i if score1 < score2 else j)

    # Remove filtered items from list and return filtered list.
    return [tup for n, tup in enumerate(my_list) if n not in idx_to_remove]

# TEST CASES

# Case 1.
mylist = [
    [('only','one','time'), 10.54],
    [('one','time'), 3.21],
    [('red','hot','chili','peppers'), 0.223],
    [('red','hot','chili'), 1.98],
] 
>>> process_list(mylist)
[[('only', 'one', 'time'), 10.54], [('red', 'hot', 'chili'), 1.98]]

# Case 2.
# ('a', 'b', 'd') is superset of ('a', 'b') and has a lower score, so remove former.
# ('a', 'b') is a subset of ('a', 'b', 'c') and has a lower score, so remove former.
mylist = [[('a', 'b', 'c'), 3], [('a', 'b', 'd'), 1], [('a', 'b'), 2]]
>>> process_list(mylist)
[[('a', 'b', 'c'), 3]]

# Case 3.  Same items as Case 2, but different order.  Same logic as Case 2.
mylist = [[('a', 'b'), 2], [('a', 'b', 'c'), 3], [('a', 'b', 'd'), 1]]
>>> process_list(mylist)
[[('a', 'b', 'c'), 3]]

# Case 4.
# ('a', 'b', 'c') is a superset of ('a', 'b') and has a lower score, so remove former.
# ('d','c') is a subset of ('d','c','w') and has a lower score, so remove former.
mylist = [[('a', 'b'), 2], [('a', 'b', 'c'), 1], [('d','c','w'), 4], [('d','c'), 2]]
>>> process_list(mylist)
[[('a', 'b'), 2], [('d', 'c', 'w'), 4]]
...