Векторизация или ускорение нечеткого сопоставления строк в столбце PANDAS

Я пытаюсь найти потенциальные совпадения в столбце PANDAS, полном названий организаций. В настоящее время я использую iterrows(), но он очень медленный для фрейма данных с ~ 70 000 строк. После просмотра StackOverflow я попытался реализовать метод лямбда-строки (применить), но это, похоже, едва ускоряет работу, если вообще ускоряет.

Первые четыре строки фрейма данных выглядят так:

index  org_name
0   cliftonlarsonallen llp minneapolis MN
1   loeb and troper llp newyork NY
2   dauby o'connor and zaleski llc carmel IN
3   wegner cpas llp madison WI

Следующий блок кода работает, но его обработка заняла около пяти дней:

org_list = df['org_name']
from fuzzywuzzy import process
for index, row in df.iterrows():
    x = process.extract(row['org_name'], org_list, limit=2)[1]
    if x[1]>93:
        df.loc[index, 'fuzzy_match'] = x[0]
        df.loc[index, 'fuzzy_match_score'] = x[1]

По сути, для каждой строки я сравниваю название организации со списком всех названий организаций, беру два верхних совпадения, затем выбираю второе наилучшее совпадение (поскольку верхнее совпадение будет таким же именем), а затем устанавливаю условие что оценка должна быть выше 93 для создания новых столбцов. Причина, по которой я создаю дополнительные столбцы, заключается в том, что я не хочу просто заменять значения — я хотел бы сначала перепроверить результаты.

Есть ли способ ускорить это? Я прочитал несколько сообщений в блогах и вопросы StackOverflow, в которых говорилось о «векторизации» этого кода, но мои попытки не увенчались успехом. Я также подумал о том, чтобы просто создать матрицу расстояний Левенштейна размером 70 000 x 70 000, а затем извлечь оттуда информацию. Есть ли более быстрый способ создать наилучшее соответствие для каждого элемента в списке или столбце PANDAS?


person Gregory Saxton    schedule 03.10.2018    source источник
comment
Итак, для каждой строки вы хотите использовать столбец org_name этой строки в качестве запроса, а затем использовать весь список названий организаций из полного столбца org_name в качестве вариантов соответствия?   -  person rahlf23    schedule 03.10.2018
comment
Да, это правильно.   -  person Gregory Saxton    schedule 03.10.2018


Ответы (3)


Учитывая вашу задачу, вы сравниваете 70 тыс. строк друг с другом с помощью fuzz.WRatio, поэтому у вас есть в общей сложности 4 900 000 000 сравнений, причем каждое из этих сравнений использует расстояние Левенштейна внутри fuzzywuzzy, что является операцией O (N * M). fuzz.WRatio представляет собой комбинацию нескольких различных коэффициентов соответствия строк, которые имеют разные веса. Затем он выбирает наилучшее соотношение между ними. Поэтому ему даже приходится несколько раз вычислять расстояние Левенштейна. Таким образом, одна цель должна состоять в том, чтобы уменьшить пространство поиска, исключив некоторые возможности, используя более быстрый алгоритм сопоставления. Другая проблема заключается в том, что строки предварительно обрабатываются для удаления знаков препинания и перевода строк в нижний регистр. Хотя это требуется для сопоставления (например, слово в верхнем регистре становится равным слову в нижнем регистре), мы можем сделать это заранее. Таким образом, нам нужно предварительно обработать 70 тыс. строк только один раз. Я буду использовать здесь RapidFuzz вместо FuzzyWuzzy, так как он немного быстрее (автор я).

Следующая версия работает более чем в 10 раз быстрее, чем ваше предыдущее решение в моих экспериментах, и применяет следующие улучшения:

  1. он предварительно обрабатывает строки заранее

  2. он передает score_cutoff в ExtractOne, чтобы он мог пропустить вычисления, когда он уже знает, что они не могут достичь этого соотношения.

import pandas as pd, numpy as np
from rapidfuzz import process, utils

org_list = df['org_name']
processed_orgs = [utils.default_process(org) for org in org_list]

for (i, processed_query) in enumerate(processed_orgs):
    # None is skipped by extractOne, so we set the current element to None an
    # revert this change after the comparision
    processed_orgs[i] = None
    match = process.extractOne(processed_query, processed_orgs, processor=None, score_cutoff=93)
    processed_orgs[i] = processed_query
    if match:
        df.loc[i, 'fuzzy_match'] = org_list[match[2]]
        df.loc[i, 'fuzzy_match_score'] = match[1]

Вот список наиболее важных улучшений RapidFuzz, чтобы сделать его быстрее, чем FuzzyWuzzy в этом примере:

  1. Он полностью реализован на C++, в то время как большая часть FuzzyWuzzy реализована на Python.

  2. При расчете расстояния Левенштейна учитывается score_cutoff для выбора оптимизированной реализации на основе. Например. когда разница в длине между строками слишком велика, она может завершиться за O(1).

  3. FuzzyWuzzy использует Python-Levenshtein для вычисления сходства между двумя строками, в котором для замен используется взвешенное расстояние Левенштейна с весом 2. Это реализовано с помощью Вагнера-Фишера. RapidFuzz, с другой стороны, использует для этого битпараллельную реализацию на основе BitPal, т.е. Быстрее

  4. fuzz.WRatio объединяет результаты нескольких других алгоритмов сопоставления строк, таких как fuzz.ratio, fuzz.token_sort_ratio и fuzz.token_set_ratio, и берет максимальный результат после их взвешивания. Таким образом, в то время как fuzz.ratio имеет вес 1, fuzz.token_sort_ratio и fuzz.token_set_ratio имеют один из 0,95. Когда score_cutoff больше 95, fuzz.token_sort_ratio и fuzz.token_set_ratio больше не рассчитываются, так как результаты гарантированно будут меньше, чем score_cutoff

  5. В process.extractOne RapidFuzz по возможности избегает вызовов через Python и заранее обрабатывает запрос. Например. алгоритм BitPal требует, чтобы одна из двух сравниваемых строк сохранялась в битовом векторе, который занимает большую часть времени выполнения алгоритма. В process.extractOne запрос сохраняется в этом битовом векторе только один раз, после чего битовый вектор используется повторно, что значительно ускоряет алгоритм.

  6. поскольку extractOne ищет только наилучшее совпадение, для следующих элементов используется отношение текущего наилучшего совпадения как score_cutoff. Таким образом, он может быстро отбросить больше элементов, используя улучшения расчета расстояния Левенштейна из 2) во многих случаях. Когда он находит элемент со сходством 100, он завершает работу раньше, поскольку после этого не может быть лучшего совпадения.

person maxbachmann    schedule 22.04.2020
comment
Я люблю библиотеку! Это сократило время сопоставления с 66 часов до примерно 90 минут. - person isht3; 13.05.2020
comment
Эта библиотека фантастическая, у меня были улучшения производительности, аналогичные приведенным выше. - person tom; 04.06.2020
comment
Отличная библиотека. Только что попробовал это на задаче, и это сократило время в 16 раз по сравнению с fuzzywuzzy. - person DarrylG; 24.07.2020
comment
Работает ли ваша библиотека с многомерными партитурами? т.е. оценки, содержащие кортеж? ФВ делает. Я просмотрел ваши источники, и похоже, что они имеют дело исключительно со скалярными оценками, верно? - person keepAlive; 11.08.2020
comment
Да, это по существу напоминает поведение fuzzywuzzy, которое также должно работать только со скалярными значениями (и парами ключ-значение, такими как словари или наборы данных pandas). На самом деле у меня еще не было этого требования, и особенно я действительно не знаю, как работать с матрицами с сопоставлением строк таким образом, чтобы это работало для всех (если у вас есть хорошая идея для этого, не стесняйтесь открывать вопрос об этом;) ). - person maxbachmann; 11.08.2020
comment
Спасибо за быстрый ответ :) FuzzyWuzzy работает с оценками кортежей. Попробуйте, например, fzp.extractOne(query='stack', choices=['stick', 'stok'], processor=None, scorer=lambda s1, s2: (fzz.WRatio(s1, s2), fzz.QRatio(s1, s2)), score_cutoff=(0, 0)). Этот подход позволяет лексикографически упорядочивать совпадения. Скажем, два матча имеют одинаковый первый счет, а второй может не иметь такого же результата. - person keepAlive; 11.08.2020
comment
На самом деле два набора строк, которые предотвращают оценку кортежей, это (170:172) и (189:191) в process.py. Имея дело с идентичными оценками, fuzzywuzzy просто возвращает первое, что он находит в (что кажется) неупорядоченной итерации... По общему признанию, спорно. - person keepAlive; 11.08.2020
comment
Я согласен с вами, что это использование должно быть разрешено. Я открыл на этом сайте github.com/maxbachmann/rapidfuzz/issues/39. - person maxbachmann; 11.08.2020
comment
@maxbachmann: Мне понравился ваш пакет rapidfuzz. Только один запрос, у меня есть аналогичная проблема, как описано выше, но вместо 1-го совпадения.. Я хочу установить ограничение как 10 с порогом › 80. Что мне делать с rapidfuzz? - person Rahul Agarwal; 24.09.2020
comment
@RahulAgarwal попробуйте process.extract(query, choices, limit=10, score_cutoff=80) - person maxbachmann; 24.09.2020
comment
Да .. спасибо ... увидел ваши другие ответы и понял !!! :) - person Rahul Agarwal; 24.09.2020

Это решение использует apply() и должно демонстрировать разумные улучшения производительности. Не стесняйтесь поиграть с scorer и изменить threshold в соответствии с вашими потребностями:

import pandas as pd, numpy as np
from fuzzywuzzy import process, fuzz

df = pd.DataFrame([['cliftonlarsonallen llp minneapolis MN'],
        ['loeb and troper llp newyork NY'],
        ["dauby o'connor and zaleski llc carmel IN"],
        ['wegner cpas llp madison WI']],
        columns=['org_name'])

org_list = df['org_name']

threshold = 40

def find_match(x):

  match = process.extract(x, org_list, limit=2, scorer=fuzz.partial_token_sort_ratio)[1]
  match = match if match[1]>threshold else np.nan
  return match

df['match found'] = df['org_name'].apply(find_match)

Возвращает:

                                   org_name                                     match found
0     cliftonlarsonallen llp minneapolis MN             (wegner cpas llp madison WI, 50, 3)
1            loeb and troper llp newyork NY             (wegner cpas llp madison WI, 46, 3)
2  dauby o'connor and zaleski llc carmel IN                                             NaN
3                wegner cpas llp madison WI  (cliftonlarsonallen llp minneapolis MN, 50, 0)

Если вы просто хотите вернуть совпадающую строку, вы можете изменить ее следующим образом:

match = match[0] if match[1]>threshold else np.nan

Я также добавил комментарий @user3483203, относящийся к пониманию списка, в качестве альтернативного варианта:

df['match found'] = [find_match(row) for row in df['org_name']]

Обратите внимание, что process.extract() предназначен для обработки одной строки запроса и применения переданного алгоритма оценки к этому запросу и предоставленным параметрам соответствия. По этой причине вам придется сравнить этот запрос со всеми 70 000 вариантов совпадения (так, как вы сейчас настроили свой код). Таким образом, вы будете оценивать len(match_options)**2 (или 4 900 000 000) сравнений строк. Поэтому я думаю, что наилучшие улучшения производительности могут быть достигнуты путем ограничения потенциальных вариантов совпадения с помощью более обширной логики в функции find_match(), например. обеспечение того, чтобы параметры соответствия начинались с той же буквы, что и запрос, и т. д.

person rahlf23    schedule 03.10.2018
comment
Вы получите лучшую производительность, используя понимание списка, чем применяя. Что-то вроде [find_match(row) for row in df.org_name] - person user3483203; 03.10.2018
comment
Спасибо, ralf23. Благодарю. Тем не менее, я упомянул, что уже пробовал подход с применением строк без увеличения производительности. С вашим кодом, примененным к первым двум строкам, это заняло 11,7 секунды, а с подходом iterrows - 11,6 секунды. - person Gregory Saxton; 03.10.2018
comment
Вы действительно проверяете каждый запрос по всем 70 000 вариантов соответствия? Я уверен, что вы можете найти способ ограничить параметры соответствия для каждого запроса. - person rahlf23; 03.10.2018
comment
Спасибо, @user3483203. Это кажется многообещающим, но не могли бы вы заполнить для меня пробелы о том, как я буду использовать это для создания нового столбца с совпадением? - person Gregory Saxton; 03.10.2018
comment
@ rahlf23 -- да, поэтому я и прошу здесь помощи. Какие-либо предложения? - person Gregory Saxton; 03.10.2018
comment
Вот где ваше знание ваших данных имеет решающее значение. Можете ли вы, например, ограничить параметры соответствия, чтобы они начинались с той же буквы, что и ваш запрос? - person rahlf23; 03.10.2018
comment
Да, спасибо. Это может сработать. Меня все еще интересует, есть ли способ ускорить код с помощью подхода векторизации или чего-то подобного. - person Gregory Saxton; 03.10.2018
comment
Я не думаю, что это так, потому что process.extract() предназначен для обработки одной строки запроса и применения переданного алгоритма оценки к этому запросу и предоставленным параметрам соответствия. Я добавлю некоторые детали в свой ответ в другом редактировании. - person rahlf23; 03.10.2018
comment
Спасибо, @ralf23. Я не думал о чем-то настолько простом, как ограничение соответствия первым буквам. Это сократило время на 100 строк с 11 минут до 1 минуты. - person Gregory Saxton; 03.10.2018
comment
Большой! Просто в качестве мысленного эксперимента: если вы предположите, что ваш список вариантов соответствия содержит равное количество строк, начинающихся с каждой из 26 букв алфавита, вы теоретически можете повысить производительность в этом случае в 26 раз. Как я уже сказал, вы знаете набор данных лучше, чем я, поэтому я уверен, что вы можете придумать другие способы ограничить параметры сопоставления и еще больше повысить производительность. - person rahlf23; 03.10.2018
comment
Еще раз спасибо, и очень признателен. - person Gregory Saxton; 03.10.2018
comment
Вы пробовали вложенный цикл, для меня 3000 строк с результатами 2000 строк приходят между 30-45 секундами. И я также сравниваю только названия организаций - person Rahul Agarwal; 09.10.2018

Использование iterrows() не рекомендуется для фреймов данных, вместо этого вы можете использовать apply(). Но вряд ли это сильно ускорило бы процесс. Что медленно, так это метод извлечения fuzzywuzzy, в котором ваш ввод сравнивается со всеми 70 тыс. строк (методы расстояния между строками требуют больших вычислительных ресурсов). Поэтому, если вы намерены придерживаться fuzzywuzzy, одним из решений будет ограничение поиска, например, только теми, у которых одна и та же первая буква. Или, если у вас есть другой столбец в ваших данных, который можно использовать в качестве подсказки (штат, город, ...)

person pedram bashiri    schedule 21.04.2020