是否可以与python pandas进行模糊匹配合并?


76

我有两个要基于列合并的DataFrame。但是,由于拼写不同,空格数不同,不存在变音符,只要它们彼此相似,我希望能够合并。

任何相似性算法都可以使用(soundex,Levenshtein,difflib)。

假设一个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


1
在找不到紧密匹配的情况下,接受的解决方案将失败。有关简单的方法,请参阅此替代方法
yatu

Answers:


85

类似@locojay建议,你可以申请difflibget_close_matchesdf2的指标,然后应用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

在找不到紧密匹配的情况下,此解决方案将失败。这里是围绕一个简单的方法
雅图

32

使用 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您知道怎么做吗?
法蒂玛

嘿,Erfan,当您想起您可以将其更新为可与pandas 1.0一起使用时吗?我不知道什么样的性能提升,如果你改变了适用于用Cython或Numba发动机它会得到
侏儒鸟

对于我的问题,该解决方案看起来也确实很有希望。但是,如果我在两个数据集中都没有一个公共列,您能解释一下这将如何工作吗?如何在两个提供得分的数据集中之一中创建匹配列?我已经使用了您的#2解决方案。我不确定为什么要花这么多时间来运行。
Django0602

1
如果您也需要匹配的钥匙,则可以使用s = df_2.to_dict()[key2]
suricactus

16

我已经编写了一个Python软件包,旨在解决这个问题:

pip install fuzzymatcher

你可以找到回购这里和文档在这里

基本用法:

给定两个要模糊连接的数据帧df_leftdf_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
Erfan

1
name 'fuzzymatcher' is not defined
法蒂玛

您可以请@RobinL详细说明如何解决:no such module: fts4问题吗?我一直在努力使这项工作取得零成功。
TaL

11

我将使用Jaro-Winkler,因为它是目前可用的最高效,最精确的近似字符串匹配算法之一[ Cohen等。],[ Winkler ]。

这就是我如何使用水母包中的Jaro-Winkler做到的:

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,键= 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创建此类列,然后在该列上进行合并吗?
安迪·海登

3

对于一般方法: fuzzy_merge

对于更常见的情况,我们要合并包含稍有不同的字符串的两个数据帧中的列,下面的函数difflib.get_close_matches与一起使用merge,以便模仿大熊猫的功能,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_matchdifflib.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/...
雅图

嗯...我只是尝试了相同的代码,所以对于我拥有的数据,它显然要快得多。也许是数据相关的?
irene

通常,为了获得可靠的时序,您需要对大样本量进行基准测试。但是根据我的经验,list-comps通常会更快或更快速@irene还请注意,apply基本上也只是遍历行
yatu

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,并替换python或anaconda DLLs文件夹中的DLL文件。

优点:

  1. 工作更快。 在我的情况下,我将一个具有3000行的数据框与具有170,000条记录的另一个数据框进行了比较。这也使用跨文本的SQLite3搜索。如此之快
  2. 可以检查多个列和2个数据框就我而言,我正在根据地址和公司名称寻找最接近的匹配项。有时,公司名称可能相同,但地址也是要检查的好东西。
  3. 为您提供同一记录的所有最接近匹配项的分数。您可以选择截止分数。

缺点:

  1. 原始套件安装有问题
  2. 所需的C ++和Visual Studio也已安装
  3. 无法用于64位anaconda / Python

谢谢reddy ...目前正在具有6000行的数据集与具有300万行的数据集匹配上运行,并祈祷...您认为这样做的速度会比Fuzzywuzzy快吗?
蛇佬腔

1
@Parseltongue,您好:在您的情况下,此数据非常庞大。我不认为任何模糊不清的方法似乎对100万以上的代码都是有效的,但是您绝对可以为此尝试一下。我运行了6000行,而运行了80万行,还不错。
reddy

2

有一个叫包fuzzy_pandas,可以使用levenshteinjarometaphonebilenco方法。这里有一些很好的例子

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

它具有各种其他功能,例如:

  • 检查加入质量,加入前和加入后
  • 自定义相似度函数,例如编辑距离与汉明距离
  • 指定最大距离
  • 多核计算

有关详细信息,请参见


刚刚测试这一点,让我奇怪的结果反馈,例如,它匹配governmentbusiness,有没有配置匹配得分阈值的方法吗?
艾凡

有见参考文档,你可以传递top_limit,也可能要变fun_difffun_diff=[affinegap.affineGapDistance]趋向于提供更好的匹配。
citynorman
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.