Размито съпоставяне на низове в 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 хиляди имена и отнема почти цял час.

Данните от теста са разделени на.

  • Име
  • Година Месец Дата на раждане

И аз ги сравнявам по кофи, където техните YM са в една и съща кофа.

Възможно ли е проблемът да се дължи на модула FuzzyWuzzy, който използвам? Оценявам всяка помощ.


person BernardL    schedule 16.08.2016    source източник
comment
Можете да опитате да нормализирате имената и да сравните нормализираните форми. Това става доста паралелно.   -  person Alec    schedule 16.08.2016
comment
Как бихте препоръчали нормализиране? Има ли стандартен метод, който мога да приложа? Оценявам вашите отзиви.   -  person BernardL    schedule 16.08.2016
comment
Е, зависи от вида на разликите в именуването? Като прост пример, съпоставяне на имена на компании, мога да премахна фрази като LTD или INC и може би дори небукви.   -  person Alec    schedule 16.08.2016
comment
Предполагам, че това е трудността с имената, можем да нормализираме, но това няма да помогне за намаляване на броя на повторенията.   -  person BernardL    schedule 16.08.2016
comment
Ами не, ще стане. Вие нормализирате всички записи, след което считате записите за дублиращи се един на друг, ако техните нормализирани форми са еднакви. Това е само едно голямо намаление на картата.   -  person Alec    schedule 16.08.2016
comment
Можете да опитате да uniqueing и двата списъка, за да се надяваме да намалите и двата с няколко порядъка, така че квадратичните (размити) сравнения да не бъдат толкова болезнени.   -  person acdr    schedule 16.08.2016
comment
Можете ли да публикувате някои данни от списъка си, които биха били полезни при намирането на решение. Какви са малко по-различните конвенции за именуване например? Кои две имена се считат за 95% съвпадение?   -  person DhruvPathak    schedule 16.08.2016
comment
Ако нямате нищо против O(mn) време на изпълнение, можете да изчислите разстоянието на Левенщайн между двата низа и да видите дали те са в рамките на вашия праг на приемане   -  person Nick Zuber    schedule 16.08.2016
comment
@NickZuber: O(mn), когато m и n са › 10^6 е от порядъка на трилион. И като се има предвид, че разстоянието на Левенщайн е O(xy), където x и y са дължините на отделните низове, подозирам, че решението ще отнеме много дни, за да бъде завършено. Дори и да сте готови да чакате дни, съмнявам се, че дистанцията на Левенщайн би дала полезни резултати.   -  person Jim Mischel    schedule 16.08.2016
comment
Трябва да ни предоставите повече информация за вашите данни. Опитвате ли се да направите съпоставяне едно към едно между двата списъка? Или има няколко елемента в един списък, които могат да се считат за еднакви? Каква е природата на разликите? Има ли съкращения? Долни черти или други разделители на думи? Опитвате ли се да коригирате правописни грешки? Колкото повече знаем за естеството на вашите данни, толкова по-добре можем да дадем предложения.   -  person Jim Mischel    schedule 16.08.2016
comment
@JimMischel n и m са представителни за съответните дължини на низове, съжалявам, ако това не е ясно. И низовете не са дълги милиони знаци. Можете да изчислите милиони низове с алгоритъма на Levenshtein за секунди   -  person Nick Zuber    schedule 16.08.2016
comment
@NickZuber: Но трябва да направите от порядъка на 10^12 изчисления на Левенщайн, за да сравните всеки низ в списък А с всеки низ в списък Б. Дори и да можете да направите милион в секунда, това пак са около 12 дни.   -  person Jim Mischel    schedule 16.08.2016
comment
@JimMischel о, да, правенето на трилиони думи може да отнеме известно време :)   -  person Nick Zuber    schedule 16.08.2016
comment
Здравейте на всички, опитвам се да направя карта едно към едно. Добре съм с проста логика за съвпадение на низове, като например сравняването на „abcd“ и „abcf“ ще върне 75% увереност. Актуализирах въпроса си с някои опити. Все още има проблеми с времето за обработка.   -  person BernardL    schedule 21.08.2016
comment
@BernardL Можете ли да споделите пълния си код, заедно с основно профилиране с помощта на time.time(), за да покажете колко време отнема всеки сегмент?   -  person DhruvPathak    schedule 19.09.2016
comment
@BernardL Добавихме също раздел Начален мач, за да оптимизираме това повече.   -  person DhruvPathak    schedule 19.09.2016


Отговори (3)


Има няколко възможни нива на оптимизация тук, за да се превърне този проблем от O(n^2) в по-малка времева сложност.

  • Предварителна обработка: Сортирайте списъка си при първото преминаване, създавайки изходна карта за всеки низ, те ключът за картата може да бъде нормализиран низ. Нормализациите могат да включват:

    • lowercase conversion,
    • без бели интервали, премахване на специални знаци,
    • трансформирайте unicode в ascii еквиваленти, ако е възможно, използвайте unicodedata.normalize или модул за унидекод)

    Това би довело до "Andrew H Smith", "andrew h. smith", "ándréw h. smith" генериране на същия ключ "andrewhsmith" и би намалило вашия набор от милиони имена до по-малък набор от уникални/подобни групирани имена.

Можете да използвате този полезен метод, за да нормализирате своя низ (все пак не включва частта от 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 знака е сравнително добра група за най-практично съвпадение.

  • Начално съвпадение: Повечето варианти на имена ще имат един и същ първи знак в нормализирания формат (напр. Andrew H Smith, ándréw h. smith и Andrew H. Smeeth генерират съответно ключове andrewhsmith, andrewhsmith и andrewhsmeeth. Те обикновено няма да се различават по първия знак , така че можете да стартирате съпоставяне за ключове, започващи с a, към други ключове, които започват с a и попадат в кофите с дължина. Това значително ще намали времето ви за съпоставяне. Няма нужда да съпоставяте ключ andrewhsmith до bndrewhsmith като такъв вариант на име с first писмо рядко ще съществува.

След това можете да използвате нещо в съответствие с този метод (или модул 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
person DhruvPathak    schedule 16.08.2016
comment
Здравейте, изпробвах вашия метод с изместване и нормализиране. Което имаше много смисъл, но все още остана във времето за обработка, което отнема твърде много време. Почти час за обработка на 5 хиляди имена. В момента се използва модулът FuzzyWuzzy с функцията process.extractOne. Всяка помощ е много ценена. - person BernardL; 21.08.2016

Трябва да индексирате или нормализирате низовете, за да избегнете изпълнението на O(n^2). По принцип трябва да картографирате всеки низ към нормална форма и да изградите обратен речник с всички думи, свързани със съответните нормални форми.

Нека приемем, че нормалните форми на „свят“ и „дума“ са едни и същи. И така, първо изградете обърнат речник на Normalized -> [word1, word2, word3],, напр.:

"world" <-> Normalized('world')
"word"  <-> Normalized('wrd')

to:

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

Ето ви - всички елементи (списъци) в нормализирания dict, които имат повече от една стойност - са съвпадащите думи.

Алгоритъмът за нормализиране зависи от данните, т.е. думите. Помислете за един от многото:

  • Soundex
  • Метафон
  • Двоен метафон
  • NYSIIS
  • Caverphone
  • Кьолн фонетичен
  • MRA кодекс
person Zaur Nasibov    schedule 16.08.2016
comment
Разбирам, ще публикувам актуализация, ако накарам решението да работи. Индексирането въз основа на друга информация и нормализирането изглежда добър подход. - person BernardL; 16.08.2016

Специфично за fuzzywuzzy, имайте предвид, че в момента process.extractOne по подразбиране е WRatio, което е най-бавният от техните алгоритми, а процесорът по подразбиране е utils.full_process. Ако преминете, да речем, fuzz.QRatio като ваш резултат, той ще върви много по-бързо, но не толкова мощно в зависимост от това, което се опитвате да съпоставите. Все пак може да е добре за имена. Аз лично имам късмет с token_set_ratio, който е поне малко по-бърз от WRatio. Можете също така да стартирате utils.full_process() на всичките си избори предварително и след това да го стартирате с fuzz.ratio като ваш резултат и процесор=Няма, за да пропуснете стъпката на обработка. (вижте по-долу) Ако просто използвате функцията за основно съотношение, fuzzywuzzy вероятно е прекалено много. Fwiw Имам JavaScript порт (fuzzball.js), където можете също да изчислите предварително наборите токени и да ги използвате, вместо да преизчислявате всеки път.)

Това не намалява големия брой сравнения, но помага. (BK-дърво за това вероятно? Аз самият разглеждах същата ситуация)

Също така се уверете, че имате инсталиран python-Levenshtein, за да използвате по-бързото изчисление.

**Поведението по-долу може да се промени, открити проблеми в процес на обсъждане и т.н.**

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

person nbkap    schedule 03.01.2017