Нечеткое совпадение регулярных выражений на миллионе строк Pandas df

Я пытаюсь проверить нечеткое совпадение между строковым столбцом и списком ссылок. Серия строк содержит более 1 млн строк, а список ссылок содержит более 10 тыс. записей.

Например:

df['NAMES'] = pd.Series(['ALEXANDERS', 'NOVA XANDER', 'SALA MANDER', 'PARIS HILTON', 'THE HARIS DOWNTOWN', 'APARISIAN', 'PARIS', 'MARIN XO']) # 1mil rows

ref_df['REF_NAMES'] = pd.Series(['XANDER','PARIS']) #10 k rows

###Output should look like 

df['MATCH'] = pd.Series([Nan, 'XANDER', 'MANDER', 'PARIS', 'HARIS', Nan, 'PARIS', Nan])

Он должен сгенерировать совпадение, если слово появляется отдельно в строке (и внутри него разрешена замена до 1 символа)

Например, «PARIS» может соответствовать «PARIS HILTON», «THE HARIS DOWNTOWN», но не «APARISIAN».

Точно так же «XANDER» соответствует «NOVA XANDER» и «SALA MANDER» (MANDER отличается от XANDER на 1 символ), но не создает совпадения с «ALEXANDERS».

На данный момент мы написали логику для того же самого (показанного ниже), хотя матч занимает более 4 часов. Нужно сделать это менее чем за 30 минут.

Текущий код -

tags_regex = ref_df['REF_NAMES'].tolist()
tags_ptn_regex = '|'.join([f'\s+{tag}\s+|^{tag}\s+|\s+{tag}$' for tag in tags_regex])


def search_it(partyname):
    m = regex.search("("+tags_ptn_regex+ ")"+"{s<=1:[A-Z]}",partyname):
    if m is not None:
        return m.group()
    else:
        return None
    
df['MATCH'] = df['NAMES'].str.apply(search_it)

Кроме того, поможет ли многопроцессорность с регулярным выражением? Спасибо заранее!

Как насчет "НОВА XNDER"? Подходит ли «XANDER»?

Dani Mesejo 14.12.2020 10:58

Да, допускается до 1 замены, удаления или добавления. Спасибо!

StarMunch 14.12.2020 11:05

Прежде всего, вы можете упростить регулярное выражение: \s+{tag}\s+|^{tag}\s+|\s+{tag}$ эквивалентно \b{tag}\b, где \b — граница слова. 2) почему вы успешно запускаете regex.search во второй раз? Хотя я думаю, что оба улучшения имеют относительно небольшой эффект

Yaroslav Fyodorov 14.12.2020 11:08

2) Я не думаю, что ваше регулярное выражение вообще работает - попробовал его на «АЛЕКСАНДЕ», и оно совпадает. Значит, у вас проблемы с корректностью в первую очередь. Я не думаю, что вы можете легко сделать «до 1 замены, удаления или добавления» с регулярным выражением.

Yaroslav Fyodorov 14.12.2020 11:17

На данный момент работает до 1 замены. Добавление/удаление приятно иметь. Я отредактировал скрипт так, чтобы regex.search был только один раз, .. как вы сказали .. не так много улучшений, хотя

StarMunch 14.12.2020 11:19

По поводу исправления - думаю нужно сделать так tags_ptn_regex = '|'.join(tags_regex) а потом regex.search("\\b("+tags_ptn_regex+ ")"+"{s<=1:[A-Z]}\\b",partyname) иначе как я уже сказал он находит совпадение на "Александре" игнорируя \s+, к тому же это немного проще и возможно быстрее. В искровом мире я бы предложил разбить ваши ИМЕНА на слова, затем взорвать, а затем сопоставить отдельные слова, но я не уверен, принесет ли это какую-либо пользу в пандах - в конце концов, ваши критерии соответствия сложны.

Yaroslav Fyodorov 14.12.2020 11:33

@YaroslavFyodorov .. Спасибо за исправление! К сожалению... это на выделенном сервере Py :(

StarMunch 14.12.2020 11:39

По общему правилу, многопоточность кажется применимой здесь либо путем разделения вашего фрейма данных на несколько фрагментов и выполнения их независимо, затем объединения результатов, либо путем разделения ваших REF_NAMES и запуска их по отдельности. Не уверен, конкретно о питоне. Можете ли вы запустить несколько процессов Python и передать каждому из них часть вашего df?

Yaroslav Fyodorov 14.12.2020 11:44

Я бы попытался оценить нижнюю границу времени выполнения, запустив без замен (и, возможно, только часть REF_NAMES). Если это превышает ваш 30-минутный лимит...

Yaroslav Fyodorov 14.12.2020 11:47

Без нечеткого бита это намного меньше 10 минут, но нечеткая часть помогает с точностью

StarMunch 14.12.2020 11:58
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
10
506
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваш шаблон довольно неэффективен, так как вы повторяете шаблон tag трижды в регулярном выражении. Вам просто нужно создать шаблон с так называемыми границами пробелов, (?<!\S) и (?!\S), и вам понадобится только один шаблон tag.

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

Чтобы уменьшить этот возврат, вам понадобится regex trie.

Вот рабочий фрагмент:

import regex
import pandas as pd

## Class to build a regex trie, see https://stackoverflow.com/a/42789508/3832970
class Trie():
    """Regex::Trie in Python. Creates a Trie out of a list of words. The trie can be exported to a Regex pattern.
    The corresponding Regex should match much faster than a simple Regex union."""

    def __init__(self):
        self.data = {}

    def add(self, word):
        ref = self.data
        for char in word:
            ref[char] = char in ref and ref[char] or {}
            ref = ref[char]
        ref[''] = 1

    def dump(self):
        return self.data

    def quote(self, char):
        return regex.escape(char)

    def _pattern(self, pData):
        data = pData
        if "" in data and len(data.keys()) == 1:
            return None

        alt = []
        cc = []
        q = 0
        for char in sorted(data.keys()):
            if isinstance(data[char], dict):
                try:
                    recurse = self._pattern(data[char])
                    alt.append(self.quote(char) + recurse)
                except:
                    cc.append(self.quote(char))
            else:
                q = 1
        cconly = not len(alt) > 0

        if len(cc) > 0:
            if len(cc) == 1:
                alt.append(cc[0])
            else:
                alt.append('[' + ''.join(cc) + ']')

        if len(alt) == 1:
            result = alt[0]
        else:
            result = "(?:" + "|".join(alt) + ")"

        if q:
            if cconly:
                result += "?"
            else:
                result = "(?:%s)?" % result
        return result

    def pattern(self):
        return self._pattern(self.dump())

## Start of main code
df = pd.DataFrame()
df['NAMES'] = pd.Series(['ALEXANDERS', 'NOVA XANDER', 'SALA MANDER', 'PARIS HILTON', 'THE HARIS DOWNTOWN', 'APARISIAN', 'PARIS', 'MARIN XO']) # 1mil rows
ref_df = pd.DataFrame()
ref_df['REF_NAMES'] = pd.Series(['XANDER','PARIS']) #10 k row

trie = Trie()
for word in ref_df['REF_NAMES'].tolist():
    trie.add(word)

tags_ptn_regex = regex.compile(r"(?:(?<!\S)(?:{})(?!\S)){{s<=1:[A-Z]}}".format(trie.pattern()), regex.IGNORECASE)

def search_it(partyname):
    m = tags_ptn_regex.search(partyname)
    if m is not None:
        return m.group()
    else:
        return None
    
df['MATCH'] = df['NAMES'].apply(search_it)

Впечатляющий. Я думал предложить построить конечный автомат, но потом решил, что это слишком сложно и в любом случае не будет эффективно. Это хороший способ сделать это. Интересно, насколько быстро это на этих конкретных данных.

Yaroslav Fyodorov 16.12.2020 15:39

@YaroslavFyodorov Конечно, нечеткий квантификатор замедляет работу, но доказано, что сам подход хорошо работает с поисковыми фразами разной длины до 50 000.

Wiktor Stribiżew 16.12.2020 15:58

Другие вопросы по теме