Примечания:
Как предлагается в других ответах, используйте collections.Counter
для подсчета. Так как он ведет себя как dict
, ему требуются хешируемые типы. {a,b}
не хэшируемый, потому что это набор. Замена его на кортеж устраняет проблему с хэш-памятью, но вводит возможные дубликаты (например, ('a', 'b')
и ('b', 'a')
). Чтобы решить эту проблему, просто сортируйте кортеж.
, поскольку sorted
возвращает list
, нам нужно превратить это обратно в кортеж: tuple(sorted((a,b)))
. Немного громоздко, но удобно в сочетании с Counter
.
Быстрое и простое ускорение: понимание вместо циклов
При перестановке ваши вложенные циклы можно заменить следующим пониманием:
sets = [ sorted((a,b)) for lst in df['letters'] for i,a in enumerate(lst) for b in lst[i:] if not a == b ]
В Python есть оптимизация для выполнения понимания, так что это уже принесет некоторое ускорение.
Бонус: если вы объедините его с Counter
, вам даже не понадобится результат в виде списка, но вместо этого вы можете использовать выражение генератора (вместо этого почти не используется дополнительная памятьхранения всех пар):
Counter( tuple(sorted((a, b))) for lst in lists for i,a in enumerate(lst) for b in lst[i:] if not a == b ) # note the lack of [ ] around the comprehension
Оценка: Каков более быстрый подход?
Как обычно, когда речь идет о производительности, окончательный ответ должен прийти из тестирования различных подходов ивыбирая лучший. Здесь я сравниваю (очень элегантный и читабельный) подход на основе itertools
от @yatu, оригинальную вложенность и понимание. Все тесты выполняются на одних и тех же данных примера, сгенерированных случайным образом в соответствии с приведенным примером.
from timeit import timeit
setup = '''
import numpy as np
import random
from collections import Counter
from itertools import combinations, chain
random.seed(42)
np.random.seed(42)
DF_SIZE = 50000 # make it big
MAX_LEN = 6
list_lengths = np.random.randint(1, 7, DF_SIZE)
letters = 'abcdefghijklmnopqrstuvwxyz'
lists = [ random.sample(letters, ln) for ln in list_lengths ] # roughly equivalent to df.letters.tolist()
'''
#################
comprehension = '''Counter( tuple(sorted((a, b))) for lst in lists for i,a in enumerate(lst) for b in lst[i:] if not a == b )'''
itertools = '''Counter(chain.from_iterable(combinations(sorted(i), r=2) for i in lists))'''
original_for_loop = '''
sets = []
for lst in lists:
for i, a in enumerate(lst):
for b in lst[i:]:
if not a == b:
sets.append(tuple(sorted((a, b))))
Counter(sets)
'''
print(f'Comprehension: {timeit(setup=setup, stmt=comprehension, number=10)}')
print(f'itertools: {timeit(setup=setup, stmt=itertools, number=10)}')
print(f'nested for: {timeit(setup=setup, stmt=original_for_loop, number=10)}')
При запуске приведенного выше кода на моем компьютере (python 3.7) печатается:
Comprehension: 1.6664735930098686
itertools: 0.5829475829959847
nested for: 1.751666523006861
Итакоба предложенных подхода улучшены по сравнению с вложенными циклами for, но в этом случае itertools действительно быстрее.