чи можна зробити нечітке злиття збігів з пандами python?


78

У мене є два DataFrames, які я хочу об’єднати на основі стовпця. Однак через альтернативні написання, різну кількість пробілів, відсутність / наявність діакритичних знаків, я хотів би мати можливість об’єднатися, доки вони подібні один до одного.

Підійде будь-який алгоритм подібності (soundex, Levenshtein, difflib's).

Скажімо, один DataFrame має такі дані:

df1 = DataFrame([[1],[2],[3],[4],[5]], index=['one','two','three','four','five'], columns=['number'])

       number
one         1
two         2
three       3
four        4
five        5

df2 = DataFrame([['a'],['b'],['c'],['d'],['e']], index=['one','too','three','fours','five'], columns=['letter'])

      letter
one        a
too        b
three      c
fours      d
five       e

Тоді я хочу отримати отриманий DataFrame

       number letter
one         1      a
two         2      b
three       3      c
four        4      d
five        5      e

Так, з d6tjoin дивіться блокнот
MergeTop1

1
Прийняте рішення не вдається у випадках, коли не знайдено близьких збігів. Для простого способу перегляду дивіться цю альтернативу
яту

Відповіді:


86

Подібно до пропозиції @locojay, ви можете застосувати difflibs get_close_matchesдо df2індексу, а потім застосувати join:

In [23]: import difflib 

In [24]: difflib.get_close_matches
Out[24]: <function difflib.get_close_matches>

In [25]: df2.index = df2.index.map(lambda x: difflib.get_close_matches(x, df1.index)[0])

In [26]: df2
Out[26]: 
      letter
one        a
two        b
three      c
four       d
five       e

In [31]: df1.join(df2)
Out[31]: 
       number letter
one         1      a
two         2      b
three       3      c
four        4      d
five        5      e

.

Якби це були стовпці, тим самим чином ви могли б застосувати до стовпця тоді merge:

df1 = DataFrame([[1,'one'],[2,'two'],[3,'three'],[4,'four'],[5,'five']], columns=['number', 'name'])
df2 = DataFrame([['a','one'],['b','too'],['c','three'],['d','fours'],['e','five']], columns=['letter', 'name'])

df2['name'] = df2['name'].apply(lambda x: difflib.get_close_matches(x, df1['name'])[0])
df1.merge(df2)

1
Хтось знає, чи є спосіб зробити це між рядками одного стовпця? Я намагаюся знайти дублікати, які можуть мати помилки
As3adTintin

2
ви можете використовувати n = 1, щоб обмежити результати до 1. docs.python.org/3/library/…
Бастіан

2
Як це зробити, якщо два кадри даних мають різну довжину?
famargar

Це рішення багато в чому не вдається, і я віддаю перевагу github.com/d6t/d6tjoin . Ви можете налаштувати функцію подібності, affinegap - це краща метрика подібності, вона багатоядерна для швидшого обчислення, має справу з дублікатами збігів тощо
citynorman

Це рішення не вдається у випадках, коли не знайдено близьких збігів. Ось простий спосіб
обійтись

34

Використовуючи fuzzywuzzy

Відповідь 2019

Оскільки прикладів з fuzzywuzzyпакетом немає, ось функція, яку я написав, повертає всі збіги на основі порогу, який ви можете встановити як користувач:


Приклад datframe

df1 = pd.DataFrame({'Key':['Apple', 'Banana', 'Orange', 'Strawberry']})
df2 = pd.DataFrame({'Key':['Aple', 'Mango', 'Orag', 'Straw', 'Bannanna', 'Berry']})

# df1
          Key
0       Apple
1      Banana
2      Orange
3  Strawberry

# df2
        Key
0      Aple
1     Mango
2      Orag
3     Straw
4  Bannanna
5     Berry

Функція нечіткого збігу

def fuzzy_merge(df_1, df_2, key1, key2, threshold=90, limit=2):
    """
    :param df_1: the left table to join
    :param df_2: the right table to join
    :param key1: key column of the left table
    :param key2: key column of the right table
    :param threshold: how close the matches should be to return a match, based on Levenshtein distance
    :param limit: the amount of matches that will get returned, these are sorted high to low
    :return: dataframe with boths keys and matches
    """
    s = df_2[key2].tolist()

    m = df_1[key1].apply(lambda x: process.extract(x, s, limit=limit))    
    df_1['matches'] = m

    m2 = df_1['matches'].apply(lambda x: ', '.join([i[0] for i in x if i[1] >= threshold]))
    df_1['matches'] = m2

    return df_1

Використання нашої функції на кадрах даних: # 1

from fuzzywuzzy import fuzz
from fuzzywuzzy import process

fuzzy_merge(df1, df2, 'Key', 'Key', threshold=80)

          Key       matches
0       Apple          Aple
1      Banana      Bannanna
2      Orange          Orag
3  Strawberry  Straw, Berry

Використання нашої функції на кадрах даних: # 2

df1 = pd.DataFrame({'Col1':['Microsoft', 'Google', 'Amazon', 'IBM']})
df2 = pd.DataFrame({'Col2':['Mcrsoft', 'gogle', 'Amason', 'BIM']})

fuzzy_merge(df1, df2, 'Col1', 'Col2', 80)

        Col1  matches
0  Microsoft  Mcrsoft
1     Google    gogle
2     Amazon   Amason
3        IBM         

Встановлення:

Піп

pip install fuzzywuzzy

Анаконда

conda install -c conda-forge fuzzywuzzy

4
чи є спосіб перенести всі стовпці df2 на збіг? скажімо, c - це первинний або зовнішній ключ, який ви хотіли б зберегти в таблиці 2 (df2)
Tinkinc

@Tinkinc ти придумав, як це зробити?
Фатіма

Ей, Ерфане, коли ти отримуєш мо, думаєш, що ти міг би оновити це для використання з pandas 1.0? Цікаво, яке підвищення продуктивності це отримало б, якщо б ви змінили двигун на Cython або Numba
Манакін

Це рішення виглядає дуже перспективним і для моєї проблеми. Але чи можете ви пояснити, як це буде працювати, коли у мене немає спільного стовпця в обох наборах даних? Як я можу створити стовпець збігів в одному з двох наборів даних, що дає мені оцінку? Я використав ваше рішення №2. Я не впевнений, чому для запуску потрібно так багато часу.
Django0602

1
Якщо вам потрібні також відповідні ключі, ви можете використовуватиs = df_2.to_dict()[key2]
suricactus

17

Я написав пакет Python, метою якого є вирішення цієї проблеми:

pip install fuzzymatcher

Ви можете знайти репо тут, а документи тут .

Основне використання:

Враховуючи два кадри даних df_leftі df_right, які ви хочете нечітко приєднати, ви можете написати наступне:

from fuzzymatcher import link_table, fuzzy_left_join

# Columns to match on from df_left
left_on = ["fname", "mname", "lname",  "dob"]

# Columns to match on from df_right
right_on = ["name", "middlename", "surname", "date"]

# The link table potentially contains several matches for each record
fuzzymatcher.link_table(df_left, df_right, left_on, right_on)

Або якщо ви просто хочете пов’язати найближчий збіг:

fuzzymatcher.fuzzy_left_join(df_left, df_right, left_on, right_on)

1
Було б чудово, якби у нього не було стільки залежностей, чесно кажучи, спочатку мені довелося встановити інструмент побудови Visual Studio, тепер я отримую помилку:no such module: fts4
Ерфан

1
name 'fuzzymatcher' is not defined
Фатіма

@RobinL, чи можете ви роз'яснити, як виправити no such module: fts4проблему: Я намагався зробити це з нульовим успіхом.
TaL

11

Я б використав Jaro-Winkler, оскільки це один із найбільш ефективних та точних приблизних алгоритмів узгодження рядків, які доступні в даний час [ Cohen, et al. ], [ Вінклер ].

Ось як я це зробив би з Яро-Вінклером з пакету медуз :

def get_closest_match(x, list_strings):

  best_match = None
  highest_jw = 0

  for current_string in list_strings:
    current_score = jellyfish.jaro_winkler(x, current_string)

    if(current_score > highest_jw):
      highest_jw = current_score
      best_match = current_string

  return best_match

df1 = pandas.DataFrame([[1],[2],[3],[4],[5]], index=['one','two','three','four','five'], columns=['number'])
df2 = pandas.DataFrame([['a'],['b'],['c'],['d'],['e']], index=['one','too','three','fours','five'], columns=['letter'])

df2.index = df2.index.map(lambda x: get_closest_match(x, df1.index))

df1.join(df2)

Вихід:

    number  letter
one     1   a
two     2   b
three   3   c
four    4   d
five    5   e

як щодо def get_closest_match (x, list_strings): повернення відсортовано (list_strings, key = lambda y: jellyfish.jaro_winkler (x, y), reverse = True) [0]
andreabedini

3
Є якийсь спосіб пришвидшити це? Цей код погано масштабується.
citynorman

5

http://pandas.pydata.org/pandas-docs/dev/merging.html не має функції підключення, щоб робити це на льоту. Було б непогано ...

Я б просто зробив окремий крок і за допомогою difflib getclosest_matches створив новий стовпець в одному з 2 кадрів даних та об’єднав / об’єднав у нечіткому збіжному стовпці


4
Не могли б ви пояснити, як використовувати difflib.get_closest_matchesтакий стовпець, а потім об’єднати його?
Andy Hayden

3

Для загального підходу: fuzzy_merge

Для більш загального сценарію, в якому ми хочемо об'єднати стовпці з двох кадрів даних, які містять дещо різні рядки, наступна функція використовує difflib.get_close_matchesпоряд з merge, щоб імітувати функціональність pandas ', mergeале з нечіткими відповідностями:

import difflib 

def fuzzy_merge(df1, df2, left_on, right_on, how='inner', cutoff=0.6):
    df_other= df2.copy()
    df_other[left_on] = [get_closest_match(x, df1[left_on], cutoff) 
                         for x in df_other[right_on]]
    return df1.merge(df_other, on=left_on, how=how)

def get_closest_match(x, other, cutoff):
    matches = difflib.get_close_matches(x, other, cutoff=cutoff)
    return matches[0] if matches else None

Ось кілька випадків використання з двома зразками кадрів даних:

print(df1)

     key   number
0    one       1
1    two       2
2  three       3
3   four       4
4   five       5

print(df2)

                 key_close  letter
0                    three      c
1                      one      a
2                      too      b
3                    fours      d
4  a very different string      e

У наведеному вище прикладі ми отримаємо:

fuzzy_merge(df1, df2, left_on='key', right_on='key_close')

     key  number key_close letter
0    one       1       one      a
1    two       2       too      b
2  three       3     three      c
3   four       4     fours      d

І ми могли б зробити ліве приєднання з:

fuzzy_merge(df1, df2, left_on='key', right_on='key_close', how='left')

     key  number key_close letter
0    one       1       one      a
1    two       2       too      b
2  three       3     three      c
3   four       4     fours      d
4   five       5       NaN    NaN

Для лівого приєднання ми мали б усі невідповідні ключі в лівому фреймі даних, щоб None:

fuzzy_merge(df1, df2, left_on='key', right_on='key_close', how='right')

     key  number                key_close letter
0    one     1.0                      one      a
1    two     2.0                      too      b
2  three     3.0                    three      c
3   four     4.0                    fours      d
4   None     NaN  a very different string      e

Також зверніть увагу, що поверне порожній список, якщо в межах відсікання не знайдено жодного елемента. У загальному прикладі, якщо ми змінимо останній індекс, щоб сказати:difflib.get_close_matches df2

print(df2)

                          letter
one                          a
too                          b
three                        c
fours                        d
a very different string      e

Ми отримаємо index out of rangeпомилку:

df2.index.map(lambda x: difflib.get_close_matches(x, df1.index)[0])

IndexError: індекс списку виходить за межі діапазону

Для того, щоб вирішити цю проблему, вищевказана функція get_closest_matchповерне найближче збіг шляхом індексації списку, що повертається, difflib.get_close_matches лише якщо він насправді містить будь-які збіги.


Я б запропонував використовувати, applyщоб зробити це швидше:df_other[left_on] = df_other[right_on].apply(lambda x: get_closest_match(x, df1[left_on], cutoff))
irene

застосовувати не швидше , ніж список привілеї @irene :) перевірити stackoverflow.com/questions/16476924 / ...
Yatu

Хм ... я щойно спробував той самий код, це було помітно швидше для даних, які я мав. Може це залежить від даних?
Айрін

Зазвичай для надійних хронометражів потрібен порівняльний аналіз великих розмірів вибірки. Але на моєму досвіді, компілятори списків, як правило, такі ж швидкі чи швидші @irene. Також зауважте, що apply - це, в основному, просто цикл по рядках
яту

1
Зрозумів, спробую зрозуміти список наступного разу apply- це для мене повільно. Дякую!
Ірен

2

Як заголовок, це в основному працює, за винятком випадків, коли збіг не знайдений, або якщо у вас є NaN в будь-якому стовпці. Замість безпосереднього застосування get_close_matchesмені було легше застосувати наступну функцію. Вибір замінників NaN багато в чому залежатиме від вашого набору даних.

def fuzzy_match(a, b):
    left = '1' if pd.isnull(a) else a
    right = b.fillna('2')
    out = difflib.get_close_matches(left, right)
    return out[0] if out else np.NaN

2

Я використовував пакет Fuzzymatcher, і це добре працювало для мене. Відвідайте це посилання, щоб дізнатися більше про це.

використовуйте наведену нижче команду для встановлення

pip install fuzzymatcher

Нижче наведено зразок коду (вже поданий RobinL вище)

from fuzzymatcher import link_table, fuzzy_left_join

# Columns to match on from df_left
left_on = ["fname", "mname", "lname",  "dob"]

# Columns to match on from df_right
right_on = ["name", "middlename", "surname", "date"]

# The link table potentially contains several matches for each record
fuzzymatcher.link_table(df_left, df_right, left_on, right_on)

Ви можете отримати помилки

  1. ZeroDivisionError: ділення з нулем на нуль ---> Зверніться до цього посилання, щоб вирішити це
  2. OperationalError: Немає такого модуля: fts4 -> завантажте звідси sqlite3.dll і замініть файл DLL у вашій папці DLL python або anaconda.

Плюси:

  1. Працює швидше. У моєму випадку я порівняв один фрейм даних з 3000 рядками з іншим фреймом даних із 170 000 записами . Тут також використовується пошук SQLite3 по тексту. Так швидше, ніж у багатьох
  2. Може перевіряти кілька стовпців та 2 кадри даних . У моєму випадку я шукав найближчий збіг на основі адреси та назви компанії. Іноді назва компанії може бути однаковою, але адресу - теж добре перевірити.
  3. Дає оцінку за всі найближчі матчі за один і той же запис. ви вибираєте, яка гранична оцінка.

мінуси:

  1. Встановлення оригінального пакета глючить
  2. Потрібні також встановлені C ++ та візуальні студії
  3. Не працюю для 64-бітної анаконди / Python

Дякуємо, Reddy ... На даний момент це працює на наборі даних із 6000 рядків, який відповідає набору даних з 3 мільйонами рядків, і молиться ... Ви думаєте, це буде працювати швидше, ніж fuzzywuzzy?
Parseltongue

1
Привіт, @Parseltongue: у вашому випадку ці дані величезні. Я не думаю, що будь-який нечіткий, здається, ефективний проти більш ніж мільйона, але ви точно можете спробувати це. Я пробіг 6000 рядків проти 0,8 мільйона рядків і був досить непоганим.
reddy

2

Існує пакет називається , fuzzy_pandasякий можна використовувати levenshtein, jaro, metaphoneі bilencoметоду. З деякими великими прикладами тут

import pandas as pd
import fuzzy_pandas as fpd

df1 = pd.DataFrame({'Key':['Apple', 'Banana', 'Orange', 'Strawberry']})
df2 = pd.DataFrame({'Key':['Aple', 'Mango', 'Orag', 'Straw', 'Bannanna', 'Berry']})

results = fpd.fuzzy_merge(df1, df2,
            left_on='Key',
            right_on='Key',
            method='levenshtein',
            threshold=0.6)

results.head()

  Key    Key
0 Apple  Aple
1 Banana Bannanna
2 Orange Orag

1

Ви можете використовувати d6tjoin для цього

import d6tjoin.top1
d6tjoin.top1.MergeTop1(df1.reset_index(),df2.reset_index(),
       fuzzy_left_on=['index'],fuzzy_right_on=['index']).merge()['merged']

index number index_right letter 0 one 1 one a 1 two 2 too b 2 three 3 three c 3 four 4 fours d 4 five 5 five e

Він має безліч додаткових функцій, таких як:

  • перевірити якість приєднання, попередньо та після приєднання
  • налаштувати функцію подібності, наприклад, відредагувати відстань проти відстані Хеммінга
  • вкажіть максимальну відстань
  • багатоядерні обчислення

Детальніше див


Просто перевірив це, він дає мені дивні результати назад, наприклад , він узгоджений governmentз business, чи є спосіб налаштувати граничне значення для оцінки збіги?
Ерфан

Так см довідкових документів ви можете пройти top_limitі , можливо , також хоче , щоб зміни fun_diffв fun_diff=[affinegap.affineGapDistance]який прагне дати кращі матчі.
citynorman

0

Для більш складних випадків використання, щоб зіставити рядки з багатьма стовпцями, ви можете використовувати recordlinkageпакет. recordlinkageнадає всі інструменти для нечіткого збігу рядків між pandasфреймами даних, що допомагає дедуплікації даних під час злиття. Я написав докладну статтю про пакет тут

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.