Как можно ускорить обработку текста в Python

У меня есть *.csv файлик, в котором около 40000 наименований товаров. Из них 2-3k уникальных названий имеют по 2-3 схожих (слегка видоизмененных) наименования, но не полностью похожи. Я их сравниваю при помощи библиотеки difflib, проходя циклами по наименованиям следующим образом:

from transliterate import translit
import difflib
import pandas as pd

def similarity(s1, s2):
  normalized1 = s1.lower()
  normalized2 = s2.lower()
  matcher = difflib.SequenceMatcher(None, normalized1, normalized2)
  return matcher.ratio()

names = pd.read_csv(r'path to file with names')
similar = []
a = 1
df = pd.DataFrame()
for a_name in names['names']:
  print(a)
  a += 1
  for b_name in names['names']:
    if similarity(translit(a_name, "ru", reversed=True).lower(),translit(b_name, "ru", reversed=True).lower()) >= 0.7:
      similar.append([a_name, b_name])
      df = df.append([a_name, b_name])
print(similar)
df.to_csv('similar.csv')

Цикл для a_name отрабатывает за 3-5 секунд, а это значит, что для выполнения всего скрипта потребуется около двух дней беспрерывной работы, что достаточно долго. Потому возникает вопрос: нет ли способа ускорить как-то выполнение скрипта?


Ответы (2 шт):

Автор решения: CrazyElf

Советы по ускорению:

  1. Надеюсь, имена у вас уже отобраны уникальные, а если нет, то нужно взять из списка имён уникальные имена, без повторений
  2. Закешируйте работу функции translit (плюс lower()), либо самостоятельно записывая аргументы и результат в словарь, либо с помощью декоратора lru_cache. Если непонятно, объясню потом подробнее.
  3. Закешируйте работу функции similarity, но тут декоратором не обойтись, потому что нужно будет выдавать одинаковый результат для аргументов (a, b) и (b, a), так что тут видимо самостоятельно через словарь надо будет сделать.
  4. Никогда не конкатенируйте датафрейм внутри цикла! Это очень долго. Тем более, что вы же параллельно собираете ту же самую инфу в список. Просто уберите df.append, просто после окончания цикла создайте датафрейм из списка similar - pd.DataFrame(similar), это будет то же самое, но быстро.

Всё это вместе должно значительно ускорить работу кода.

Итоговый код будет примерно такой:

from transliterate import translit
import difflib
import pandas as pd
from functools import lru_cache
from tqdm.auto import tqdm

names = pd.read_csv(r'path to file with names')
words = names['names'].unique()

@lru_cache(None)
def trans(word):
    return translit(word, "ru", reversed=True).lower()

sim_cache = {}

def similarity(s1, s2):
  if s1 == s2:
     return 1

  if (s1,s2) in sim_cache:
      return sim_cache[(s1,s2)]

  normalized1, normalized2 = trans(a_name), trans(b_name)
  matcher = difflib.SequenceMatcher(None, normalized1, normalized2)
  r = matcher.ratio()
  sim_cache[(s1,s2)] = r
  sim_cache[(s2,s1)] = r
  return r

similar = []
df = pd.DataFrame()
for a_name in tqdm(words):
  for b_name in words:
    if similarity(a_name,b_name) >= 0.7:
      similar.append([a_name, b_name])
print(similar)
df = pd.DataFrame(similar)
df.to_csv('similar.csv')

Если же у вас именно уникальных слов 40000, то тогда нужно использовать совсем другие метрики, например, косинусное расстояние, для его быстрого и массового вычисления есть специальные библиотеки, я как-то об этом уже писал, если нужно будет, поищу. difflib с таким объёмом уже никак не сможет справиться.

→ Ссылка
Автор решения: SergFSM

в дополнение

я бы еще максимально облегчил цикл, вынес из него все что можно векторизовать, вот что-то вроде псевдокода (не тестировался по понятным причинам, просто мысли)

на выходе должны получить датафрейм, где в колонке 'names' наименования, в колонке 'simil' список с похожими на него наименованиями:

def similarity(s1, s2):
    matcher = difflib.SequenceMatcher(None, s1, s2)
    return matcher.ratio()

names = pd.read_csv(r'path to file with names').drop_duplicates()

transl = lambda x: translit(x, "ru", reversed=True)

names['transl'] = names['names'].str.lower().map(transl)
names['simil'] = names['transl'].map(lambda x: [names.loc[i,'names'] for i,y in names['transl'].items() if similarity(x,y)>=0.7])
→ Ссылка