109 votes

est-il possible de faire une fusion par correspondance floue avec python pandas ?

J'ai deux DataFrames que je veux fusionner sur la base d'une colonne. Cependant, en raison d'orthographes différentes, d'un nombre d'espaces différent, de l'absence/présence de signes diacritiques, j'aimerais pouvoir les fusionner tant qu'ils sont similaires l'un à l'autre.

Tout algorithme de similarité fera l'affaire (soundex, Levenshtein, difflib's).

Disons qu'un DataFrame contient les données suivantes :

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

Ensuite, je veux obtenir le DataFrame résultant.

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

101voto

Andy Hayden Points 38010

Comme pour la suggestion de @locojay, vous pouvez appliquer difflib 's get_close_matches à df2 et ensuite appliquer un 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

.

S'il s'agissait de colonnes, dans la même veine, vous pourriez appliquer à la colonne alors 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)

61voto

Erfan Points 19682

Utilisation de fuzzywuzzy

Comme il n'y a pas d'exemples avec le fuzzywuzzy Voici une fonction que j'ai écrite et qui renvoie toutes les correspondances en fonction d'un seuil que vous pouvez définir en tant qu'utilisateur :


Exemple de cadre de données

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

Fonction pour la correspondance floue

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

En utilisant notre fonction sur les cadres de données : #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

En utilisant notre fonction sur les cadres de données : #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

Anaconda

conda install -c conda-forge fuzzywuzzy

22voto

RobinL Points 849

J'ai écrit un paquet Python qui vise à résoudre ce problème :

pip install fuzzymatcher

Vous pouvez trouver le repo aquí et docs aquí .

Utilisation de base :

Étant donné deux cadres de données df_left y df_right que vous voulez joindre de manière floue, vous pouvez écrire ce qui suit :

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)

Ou si vous voulez simplement vous lier à la correspondance la plus proche :

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

13voto

lostsoul29 Points 344

J'utiliserais Jaro-Winkler, car il s'agit de l'un des algorithmes de correspondance approximative de chaînes de caractères les plus performants et les plus précis actuellement disponibles. Cohen, et al. ], [ Winkler ].

C'est comme ça que je ferais avec Jaro-Winkler du méduse paquet :

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)

Sortie :

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

8voto

yatu Points 39897

Pour une approche générale : fuzzy_merge

Pour un scénario plus général dans lequel nous voulons fusionner les colonnes de deux dataframes qui contiennent des chaînes de caractères légèrement différentes, la fonction suivante utilise difflib.get_close_matches ainsi que merge afin d'imiter la fonctionnalité de la fonction merge mais avec une correspondance floue :

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

Voici quelques cas d'utilisation avec deux exemples de cadres de données :

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

Avec l'exemple ci-dessus, on obtiendrait :

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

Et on pourrait faire une jointure gauche avec :

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

Pour une jointure droite, nous aurions toutes les clés non correspondantes dans le cadre de données de gauche pour 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

Notez également que difflib.get_close_matches retournera une liste vide si aucun élément ne correspond à la limite fixée. Dans l'exemple partagé, si nous changeons le dernier indice dans df2 pour dire :

print(df2)

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

Nous aurions un index out of range erreur :

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

IndexError : l'index de la liste est hors de portée

Afin de résoudre ce problème, la fonction ci-dessus get_closest_match retournera la correspondance la plus proche en indexant la liste retournée par difflib.get_close_matches seulement si il contient effectivement des correspondances.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X