Ist es möglich, Fuzzy Match Merge mit Python Pandas durchzuführen?

76

Ich habe zwei DataFrames, die ich basierend auf einer Spalte zusammenführen möchte. Aufgrund alternativer Schreibweisen, unterschiedlicher Anzahl von Leerzeichen, Fehlen / Vorhandensein diakritischer Zeichen möchte ich jedoch in der Lage sein, zusammenzuführen, solange sie einander ähnlich sind.

Jeder Ähnlichkeitsalgorithmus reicht aus (Soundex, Levenshtein, Difflib).

Angenommen, ein DataFrame enthält die folgenden Daten:

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

Dann möchte ich den resultierenden DataFrame erhalten

       number letter
one         1      a
two         2      b
three       3      c
four        4      d
five        5      e
Pocketfullofcheese
quelle
Ja mit d6tjoin siehe MergeTop1 Notizbuch
citynorman
1
Die akzeptierte Lösung schlägt fehl, wenn keine engen Übereinstimmungen gefunden werden. Eine einfache Möglichkeit finden Sie in dieser Alternative
yatu

Antworten:

85

Ähnlich wie bei @locojay können Sie den Index von difflib's get_close_matchesauf df2' anwenden und dann Folgendes anwenden 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

.

Wenn dies Spalten wären, könnten Sie auf die gleiche Weise auf die Spalte anwenden 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)
Andy Hayden
quelle
1
Weiß jemand, ob es eine Möglichkeit gibt, dies zwischen Zeilen einer Spalte zu tun? Ich versuche, Duplikate zu finden, die Tippfehler haben könnten
As3adTintin
2
Sie können n = 1 verwenden, um die Ergebnisse auf 1 zu beschränken. docs.python.org/3/library/…
Bastian
2
Wie gehe ich vor, wenn die beiden Datenrahmen unterschiedlich lang sind?
Famargar
Diese Lösung schlägt an vielen Stellen fehl und ich bevorzuge github.com/d6t/d6tjoin . Sie können die Ähnlichkeitsfunktion anpassen, affinegap ist eine bessere Ähnlichkeitsmetrik, es ist mehrkernig für schnelleres Rechnen, befasst sich mit doppelten Übereinstimmungen usw.
citynorman
Diese Lösung schlägt in den Fällen fehl, in denen keine engen Übereinstimmungen gefunden wurden. Hier ist ein einfacher Weg
yatu
32

Verwenden von fuzzywuzzy

Antwort 2019

Da das fuzzywuzzyPaket keine Beispiele enthält , habe ich eine Funktion geschrieben, die alle Übereinstimmungen basierend auf einem Schwellenwert zurückgibt, den Sie als Benutzer festlegen können:


Beispiel für einen Datenrahmen

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

Funktion für Fuzzy Matching

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

Verwenden unserer Funktion für die Datenrahmen: # 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

Verwenden unserer Funktion für die Datenrahmen: # 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         

Installation:

Pip

pip install fuzzywuzzy

Anakonda

conda install -c conda-forge fuzzywuzzy
Erfan
quelle
4
Gibt es eine Möglichkeit, alle Spalten von df2 auf das Match zu übertragen? Nehmen wir an, c ist ein Primär- oder Fremdschlüssel, den Sie in Tabelle 2 (df2) behalten möchten
Tinkinc
@Tinkinc hast du herausgefunden, wie es geht?
Fatima
Hey Erfan, wenn du einen Mo bekommst, denkst du, du könntest dies aktualisieren, um es mit Pandas 1.0 zu verwenden? Ich frage mich, welche Art von Leistungssteigerung es bekommen würde, wenn Sie den Motor für Cython oder Numba
Manakin
Diese Lösung sieht auch für mein Problem sehr vielversprechend aus. Aber können Sie erklären, wie dies funktioniert, wenn ich in beiden Datensätzen keine gemeinsame Spalte habe? Wie kann ich eine Übereinstimmungsspalte in einem der beiden Datensätze erstellen, die mir die Punktzahl gibt? Ich habe Ihre Lösung Nr. 2 verwendet. Ich bin mir nicht sicher, warum das Laufen so lange dauert.
Django0602
1
Wenn Sie auch die passenden Schlüssel benötigen, können Sies = df_2.to_dict()[key2]
suricactus
16

Ich habe ein Python-Paket geschrieben, das dieses Problem lösen soll:

pip install fuzzymatcher

Das Repo finden Sie hier und die Dokumente hier .

Grundlegende Verwendung:

Bei zwei Datenrahmen df_leftund df_right, die Sie unscharf verbinden möchten, können Sie Folgendes schreiben:

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)

Oder wenn Sie nur auf die nächstgelegene Übereinstimmung verlinken möchten:

fuzzymatcher.fuzzy_left_join(df_left, df_right, left_on, right_on)
RobinL
quelle
1
Wäre großartig gewesen, wenn es nicht so viele Abhängigkeiten gegeben hätte, zuerst musste ich das Visual Studio Build Tool installieren, jetzt bekomme ich den Fehler:no such module: fts4
Erfan
1
name 'fuzzymatcher' is not defined
Fatima
@RobinL Können Sie bitte erläutern, wie das no such module: fts4Problem behoben werden kann ? Ich habe versucht, dies ohne Erfolg zu arbeiten.
TaL
11

Ich würde Jaro-Winkler verwenden, da es einer der leistungsstärksten und genauesten derzeit verfügbaren Algorithmen für die ungefähre Zeichenfolgenanpassung ist [ Cohen et al. ], [ Winkler ].

So würde ich es mit Jaro-Winkler aus dem Quallenpaket machen :

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)

Ausgabe:

    number  letter
one     1   a
two     2   b
three   3   c
four    4   d
five    5   e
lostsoul29
quelle
Wie wäre es mit def get_closest_match (x, list_strings): return sortiert (list_strings, key = lambda y: jellyfish.jaro_winkler (x, y), reverse = True) [0]
andreabedini
3
Gibt es eine Möglichkeit, dies zu beschleunigen? Dieser Code lässt sich nicht gut skalieren.
Citynorman
5

http://pandas.pydata.org/pandas-docs/dev/merging.html verfügt nicht über eine Hook-Funktion, um dies im laufenden Betrieb zu tun. Wäre aber schön ...

Ich würde nur einen separaten Schritt ausführen und difflib getclosest_matches verwenden, um eine neue Spalte in einem der beiden Datenrahmen und die Zusammenführung / Verknüpfung in der Fuzzy-Matched-Spalte zu erstellen

locojay
quelle
4
Können Sie erklären, wie Sie difflib.get_closest_matcheseine solche Spalte erstellen und dann zusammenführen?
Andy Hayden
3

Für einen allgemeinen Ansatz: fuzzy_merge

Für ein allgemeineres Szenario, in dem Spalten aus zwei Datenrahmen zusammengeführt werden sollen, die leicht unterschiedliche Zeichenfolgen enthalten, wird die folgende Funktion difflib.get_close_matcheszusammen mit mergeverwendet, um die Funktionalität von Pandas nachzuahmen, mergejedoch mit Fuzzy-Matching:

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

Hier sind einige Anwendungsfälle mit zwei Beispieldatenrahmen:

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

Mit dem obigen Beispiel würden wir erhalten:

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

Und wir könnten einen Linken machen mit:

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

Für einen linken Join hätten wir alle nicht übereinstimmenden Schlüssel im linken Datenrahmen, um 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

Beachten Sie auch, dass eine leere Liste zurückgegeben wird, wenn innerhalb des Cutoffs kein Element übereinstimmt. Wenn wir im gemeinsamen Beispiel den letzten Index ändern, um zu sagen:difflib.get_close_matches df2

print(df2)

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

Wir würden eine index out of rangeFehlermeldung erhalten:

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

IndexError: Listenindex außerhalb des Bereichs

Um dies zu lösen, gibt die obige Funktion get_closest_matchdie engste Übereinstimmung zurück, indem sie die zurückgegebene Liste difflib.get_close_matches nur indiziert, wenn sie tatsächlich Übereinstimmungen enthält.

Yatu
quelle
Ich würde vorschlagen apply, es schneller zu machen:df_other[left_on] = df_other[right_on].apply(lambda x: get_closest_match(x, df1[left_on], cutoff))
irene
bewerben ist nicht schneller als Liste comps @irene :) check stackoverflow.com/questions/16476924/…
yatu
Hmm ... Ich habe gerade den gleichen Code ausprobiert, er war für die Daten, die ich hatte, sichtbar schneller. Vielleicht ist es datenabhängig?
Irene
Normalerweise benötigen Sie für zuverlässige Timings ein Benchmarking bei großen Stichproben. Aber meiner Erfahrung nach sind Listen-Comps normalerweise genauso schnell oder schneller @irene Beachten Sie auch, dass apply im Grunde genommen auch nur die Zeilen
durchläuft
1
Verstanden, werde versuchen, das Listenverständnis beim nächsten Mal applyfür mich zu verlangsamen. Vielen Dank!
Irene
2

Als Heads-up funktioniert dies grundsätzlich, es sei denn, es wird keine Übereinstimmung gefunden oder Sie haben NaNs in einer der Spalten. Anstatt get_close_matchesmich direkt zu bewerben , fiel es mir leichter, die folgende Funktion anzuwenden. Die Wahl des NaN-Ersatzes hängt stark von Ihrem Datensatz ab.

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
Luke
quelle
2

Ich habe das Fuzzymatcher-Paket verwendet und das hat bei mir gut funktioniert. Besuchen Sie diesen Link für weitere Details.

Verwenden Sie zum Installieren den folgenden Befehl

pip install fuzzymatcher

Unten finden Sie den Beispielcode (bereits oben von RobinL eingereicht).

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)

Möglicherweise auftretende Fehler

  1. ZeroDivisionError: Float Division durch Null ---> Klicken Sie auf diesen Link , um das Problem zu beheben
  2. OperationalError: Kein solches Modul: fts4 -> Laden Sie die sqlite3.dll von hier herunter und ersetzen Sie die DLL-Datei in Ihrem Python- oder Anaconda-DLL-Ordner.

Vorteile:

  1. Arbeitet schneller. In meinem Fall habe ich einen Datenrahmen mit 3000 Zeilen mit einem anderen Datenrahmen mit 170.000 Datensätzen verglichen . Dies verwendet auch die SQLite3-Suche über Text hinweg. So schneller als viele
  2. Kann über mehrere Spalten und 2 Datenrahmen hinweg prüfen . In meinem Fall suchte ich nach der engsten Übereinstimmung basierend auf Adresse und Firmenname. Manchmal kann der Firmenname derselbe sein, aber die Adresse ist auch gut zu überprüfen.
  3. Gibt Ihnen Punkte für alle engsten Übereinstimmungen für denselben Datensatz. Sie wählen, was der Cutoff-Score ist.

Nachteile:

  1. Die ursprüngliche Paketinstallation ist fehlerhaft
  2. Erforderliche C ++ - und Visual Studios sind ebenfalls installiert
  3. Funktioniert nicht für 64-Bit-Anaconda / Python
reddy
quelle
Danke reddy ... derzeit läuft dies auf einem Datensatz mit 6000 Zeilen, die mit einem Datensatz mit 3 Millionen Zeilen verglichen werden, und betet ... Glaubst du, dass dies schneller als Fuzzywuzzy läuft?
Parseltongue
1
Hi @Parseltongue: Diese Daten sind in Ihrem Fall sehr groß. Ich glaube nicht, dass ein Fuzzywuzzy gegen mehr als eine Million effizient zu sein scheint, aber Sie können es definitiv versuchen. Ich lief 6000 Zeilen gegen 0,8 Millionen Zeilen und war ziemlich gut.
Reddy
2

Es gibt ein Paket namens fuzzy_pandasdass verwenden kann levenshtein, jaro, metaphoneund bilencoMethoden. Mit einigen guten Beispielen hier

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
Kamnesie
quelle
1

Sie können mit d6tjoin für das

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

Es verfügt über eine Vielzahl zusätzlicher Funktionen wie:

  • Überprüfen Sie die Join-Qualität, vor und nach dem Join
  • Passen Sie die Ähnlichkeitsfunktion an, z. B. bearbeiten Sie die Entfernung gegenüber der Hamming-Entfernung
  • Maximale Entfernung angeben
  • Multi-Core-Computing

Details siehe

Stadtmensch
quelle
Gerade diese getestet, es gibt mir seltsame Ergebnisse zurück, zum Beispiel sie abgestimmt governmentmit business, ist es eine Möglichkeit , den Schwellenwert für den Abgleichswert zu konfigurieren?
Erfan
Ja, siehe Referenzdokumente, die Sie übergeben können, top_limitund möchten möglicherweise auch ändern, fun_diffzu fun_diff=[affinegap.affineGapDistance]welchen tendenziell bessere Übereinstimmungen führen.
Citynorman