Нечеткого сопоставления строк в Python

у меня есть 2 списка более миллиона имен с немного разными соглашениями об именах. Цель здесь в том, чтобы соответствовать тем записям, которые похожи, с логикой 95% уверенности.

я узнал, что есть библиотеки, которые я могу использовать, такие как модуль FuzzyWuzzy в Python.

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

есть ли другие более эффективные методы для этой проблемы?

обновление:

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

for n in list(dftest['YM'].unique()):
    n = str(n)
    frame = dftest['Name'][dftest['YM'] == n]
    print len(frame)
    print n
    for names in tqdm(frame):
            closest = process.extractOne(names,frame)

используя pythons pandas, данные загружаются в меньшие ведра, сгруппированные по годам, а затем с помощью модуля FuzzyWuzzy,process.extractOne используется для получения хороший матч.

результаты все еще несколько разочаровывают. Во время теста приведенный выше код используется на фрейме тестовых данных, содержащем всего 5 тысяч имен и занимает почти целый час.

тестовые данные разделены.

  • имя
  • год месяц Дата рождения

и я сравниваю их ведрами, где их YMs находятся в одном ведре.

может ли проблема быть из-за модуля FuzzyWuzzy I я использую? Ценю любую помощь.

3 ответов


здесь возможно несколько уровней оптимизации, чтобы превратить эту проблему из O (n^2) в меньшую временную сложность.

  • предварительная обработка : сортировка списка в первом проходе, создание выходной карты для каждой строки, они ключ для карты может быть нормализована строка. Нормализация может включать:

    • строчные преобразования,
    • нет пробелов, удаление специальных символов,
    • преобразование Unicode для эквивалентов ascii если возможно, используйте unicodedata.нормализовать или unidecode модуль )

    в результате "Andrew H Smith", "andrew h. smith", "ándréw h. smith" генерация же ключ "andrewhsmith", и уменьшит ваш набор миллионов имен до меньшего набора уникальных / похожих сгруппированных имен.

вы можете использовать этот метод utlity для нормализации строки (не включает часть unicode, хотя) :

def process_str_for_similarity_cmp(input_str, normalized=False, ignore_list=[]):
    """ Processes string for similarity comparisons , cleans special characters and extra whitespaces
        if normalized is True and removes the substrings which are in ignore_list)
    Args:
        input_str (str) : input string to be processed
        normalized (bool) : if True , method removes special characters and extra whitespace from string,
                            and converts to lowercase
        ignore_list (list) : the substrings which need to be removed from the input string
    Returns:
       str : returns processed string
    """
    for ignore_str in ignore_list:
        input_str = re.sub(r'{0}'.format(ignore_str), "", input_str, flags=re.IGNORECASE)

    if normalized is True:
        input_str = input_str.strip().lower()
        #clean special chars and extra whitespace
        input_str = re.sub("\W", "", input_str).strip()

    return input_str
  • теперь подобные строки уже будут лежать в одном ведре, если их нормализованный ключ одинаковый.

  • для дальнейшего сравнения, вам нужно будет сравнить только ключи, а не имена. например andrewhsmith и andrewhsmeeth, так как это сходство для имен потребуется нечеткое сопоставление строк, кроме нормализованных сравнение сделано выше.

  • группируете : вы действительно нужно сравнить ключ 5 символов с ключом 9 символов, чтобы увидеть, если это 95% матч ? Нет, не знаешь. Таким образом, вы можете создавать ведра, соответствующие вашим строкам. например, 5 имен символов будут соответствовать 4-6 именам символов, 6 имен символов с 5-7 символами и т. д. Ограничение N+1,N-1 символов для ключа N символов является достаточно хорошим ведром для наиболее практического сопоставления.

  • начало матча: большинство вариантов имен будет иметь то же самое первое символ в нормализованном формате (e.g Andrew H Smith, ándréw h. smith и Andrew H. Smeeth генерировать ключи andrewhsmith,andrewhsmith и andrewhsmeeth соответственно. Они, как правило, не отличаются в первом символе, так что вы можете запустить соответствие для ключей, начиная с a на другие клавиши, которые начинаются с a, и падают в ведра длины. Это сильно уменьшило бы ваше соответствуя время. Нет необходимости соответствовать ключ andrewhsmith to bndrewhsmith как такое изменение имени с первой буквой будет редко существовать.

тогда вы можете использовать что-то на линиях этого метод (или модуль FuzzyWuzzy) чтобы найти процент сходства строк, вы можете исключить один из jaro_winkler или difflib для оптимизации скорости и качества результата:

def find_string_similarity(first_str, second_str, normalized=False, ignore_list=[]):
    """ Calculates matching ratio between two strings
    Args:
        first_str (str) : First String
        second_str (str) : Second String
        normalized (bool) : if True ,method removes special characters and extra whitespace
                            from strings then calculates matching ratio
        ignore_list (list) : list has some characters which has to be substituted with "" in string
    Returns:
       Float Value : Returns a matching ratio between 1.0 ( most matching ) and 0.0 ( not matching )
                    using difflib's SequenceMatcher and and jellyfish's jaro_winkler algorithms with
                    equal weightage to each
    Examples:
        >>> find_string_similarity("hello world","Hello,World!",normalized=True)
        1.0
        >>> find_string_similarity("entrepreneurship","entreprenaurship")
        0.95625
        >>> find_string_similarity("Taj-Mahal","The Taj Mahal",normalized= True,ignore_list=["the","of"])
        1.0
    """
    first_str = process_str_for_similarity_cmp(first_str, normalized=normalized, ignore_list=ignore_list)
    second_str = process_str_for_similarity_cmp(second_str, normalized=normalized, ignore_list=ignore_list)
    match_ratio = (difflib.SequenceMatcher(None, first_str, second_str).ratio() + jellyfish.jaro_winkler(unicode(first_str), unicode(second_str)))/2.0
    return match_ratio

вы должны индексировать или нормализовать строки, чтобы избежать запуска O(n^2). В принципе, вы должны сопоставить каждую строку с нормальной формой и построить обратный словарь со всеми словами, связанными с соответствующими нормальными формами.

Normalized -> [word1, word2, word3], например:
"world" <-> Normalized('world')
"word"  <-> Normalized('wrd')

to:

Normalized('world') -> ["world", "word"]

там вы идете-все элементы (списки) в нормализованном dict, которые имеют более одного значения-являются подобранные слова.

алгоритм нормализации зависит от данных, т. е. слов. Рассмотрим один из многих:

  • Soundex
  • метафон
  • Двойной Метафон
  • NYSIIS
  • Caverphone
  • Кельн Фонетический
  • MRA codex

специфично для fuzzywuzzy, обратите внимание, что в настоящее время процесс.extractOne по умолчанию WRatio, который является самым медленным из их алгоритмов, и процессор по умолчанию utils.полный процесс. Если вы проходите в пух.QRatio как ваш Бомбардир будет идти намного быстрее, но не так мощно в зависимости от того, что вы пытаетесь соответствовать. Может быть, просто хорошо для имен. Мне лично повезло с token_set_ratio, который, по крайней мере, несколько быстрее, чем WRatio. Вы также можете запустить утилиты.full_process () на всех ваш выбор заранее, а затем запустить его с fuzz.соотношение как ваш бомбардир и процессор=нет, чтобы пропустить шаг обработки. (см. ниже)Если вы просто используете основную функцию соотношения fuzzywuzzy, вероятно, излишне. Fwiw у меня есть порт JavaScript (fuzzball.js), где вы можете предварительно вычислить наборы токенов и использовать их вместо пересчета каждый раз.)

Это не сокращает количество сравнений, но это помогает. (BK-дерево для этого возможно? Искал же ситуация сам)

также обязательно установите python-Levenshtein, чтобы использовать более быстрый расчет.

**поведение ниже может измениться, открыть обсуждаемые вопросы и т. д.**

fuzz.ratio не запускает полный процесс, а функции token_set и token_sort принимают full_process=False param, и если вы не установите Processor=None, функция extract все равно попытается запустить полный процесс. Можно использовать частичные функции, чтобы сказать pass in fuzz.token_set_ratio с full_process=False в качестве бомбардира и запустить utils.full_process на вашем выборе заранее.