【问题标题】:Vectorizing or Speeding up Fuzzywuzzy String Matching on PANDAS Column向量化或加速 PANDAS 列上的模糊字符串匹配
【发布时间】:2019-03-08 22:26:32
【问题描述】:

我正在尝试在充满组织名称的 PANDAS 列中查找潜在匹配项。我目前正在使用 iterrows() 但它在大约 70,000 行的数据帧上非常慢。在查看了 StackOverflow 之后,我尝试实现一个 lambda 行(应用)方法,但这似乎几乎没有加快速度,如果有的话。

数据框的前四行如下所示:

index  org_name
0   cliftonlarsonallen llp minneapolis MN
1   loeb and troper llp newyork NY
2   dauby o'connor and zaleski llc carmel IN
3   wegner cpas llp madison WI

以下代码块有效,但需要大约五天的时间来处理:

org_list = df['org_name']
from fuzzywuzzy import process
for index, row in df.iterrows():
    x = process.extract(row['org_name'], org_list, limit=2)[1]
    if x[1]>93:
        df.loc[index, 'fuzzy_match'] = x[0]
        df.loc[index, 'fuzzy_match_score'] = x[1]

实际上,对于每一行,我将组织名称与所有组织名称的列表进行比较,取前两个匹配项,然后选择第二好的匹配项(因为顶部匹配项将是相同的名称),然后设置分数必须高于 93 才能创建新列的条件。我创建附加列的原因是我不想简单地替换值——我想先仔细检查结果。

有没有办法加快速度?我阅读了几篇关于“矢量化”此代码的博客文章和 StackOverflow 问题,但我的尝试失败了。我还考虑过简单地创建一个 70,000 x 70,000 Levenshtein 距离矩阵,然后从那里提取信息。是否有更快的方法为列表或 PANDAS 列中的每个元素生成最佳匹配?

【问题讨论】:

  • 所以对于每一行,您想使用该行的org_name 列作为您的查询,然后使用完整的org_name 列中的整个组织名称列表作为您的匹配选项?
  • 是的,没错。

标签: python pandas performance fuzzywuzzy


【解决方案1】:

考虑到您的任务,您使用 fuzz.WRatio 将 70k 字符串相互比较,因此您总共有 4,900,000,000 次比较,其中每一次比较都使用fuzzywuzzy 中的 levenshtein 距离,这是一个 O(N*M) 操作。 fuzz.WRatio 是具有不同权重的多个不同字符串匹配比率的组合。然后它选择其中最好的比率。因此,它甚至必须多次计算 Levenshtein 距离。所以一个目标应该是通过使用一种更快的匹配算法排除一些可能性来减少搜索空间。另一个问题是字符串被预处理以删除标点符号并将字符串小写。虽然这是匹配所必需的(例如,一个大写的单词变成了一个小写的单词),但我们可以提前做到这一点。所以我们只需要对这 70k 个字符串进行一次预处理。我将在这里使用RapidFuzz 而不是 FuzzyWuzzy,因为它更快(我是作者)。

在我的实验中,以下版本的执行速度是您之前解决方案的 10 倍以上,并应用了以下改进:

  1. 它提前对字符串进行预处理

  2. 它将 score_cutoff 传递给 extractOne,因此它可以跳过已经知道无法达到此比率的计算

import pandas as pd, numpy as np
from rapidfuzz import process, utils

org_list = df['org_name']
processed_orgs = [utils.default_process(org) for org in org_list]

for (i, processed_query) in enumerate(processed_orgs):
    # None is skipped by extractOne, so we set the current element to None an
    # revert this change after the comparision
    processed_orgs[i] = None
    match = process.extractOne(processed_query, processed_orgs, processor=None, score_cutoff=93)
    processed_orgs[i] = processed_query
    if match:
        df.loc[i, 'fuzzy_match'] = org_list[match[2]]
        df.loc[i, 'fuzzy_match_score'] = match[1]

以下是 RapidFuzz 最相关的改进列表,以使其在此示例中比 FuzzyWuzzy 更快:

  1. 它完全用 C++ 实现,而 FuzzyWuzzy 的很大一部分是用 Python 实现的

  2. 在计算 levenshtein 距离时,它会考虑 score_cutoff 以选择基于优化的实现。例如。当字符串之间的长度差异很大时,它可以在 O(1) 中退出。

  3. FuzzyWuzzy 使用 Python-Levenshtein 计算两个字符串之间的相似度,它使用权重为 2 的加权 Levenshtein 距离进行替换。这是使用 Wagner-Fischer 实现的。另一方面,RapidFuzz 使用基于BitPal 的位并行实现,速度更快

  4. fuzz.WRatio 结合了多个其他字符串匹配算法的结果,如fuzz.ratio、fuzz.token_sort_ratio 和fuzz.token_set_ratio,并在加权后取最大结果。因此,fuzz.ratio 的权重为 1,fuzz.token_sort_ratio 和 fuzz.token_set_ratio 的权重为 0.95 之一。当score_cutoff大于95时,fuzz.token_sort_ratio和fuzz.token_set_ratio不再计算,因为保证结果小于score_cutoff

  5. 在 process.extractOne 中,RapidFuzz 尽可能避免通过 Python 调用,并提前对查询进行一次预处理。例如。 BitPal 算法需要将要比较的两个字符串之一存储到一个位向量中,该位向量占算法运行时间的很大一部分。在 process.extractOne 中,查询只存储到这个位向量中一次,然后位向量被重用,使算法更快。

  6. 由于 extractOne 只搜索最佳匹配,它使用当前最佳匹配的比率作为score_cutoff 用于下一个元素。这样,在许多情况下,它可以通过使用 2) 中对 levenshtein 距离计算的改进来快速丢弃更多元素。当它找到一个相似度为 100 的元素时,它会提前退出,因为之后不可能有更好的匹配。

【讨论】:

  • 我喜欢图书馆!匹配时间从 66 小时缩短到约 90 分钟。
  • 这个库太棒了,我对上面的性能进行了类似的改进。
  • 很棒的图书馆。刚试了一个问题,它比fuzzywuzzy减少了16倍的时间。
  • 我同意你的观点,应该允许这种用法。我在此打开了github.com/maxbachmann/rapidfuzz/issues/39。
  • 是的..谢谢...看到你的其他答案并想通了!!! :)
【解决方案2】:

此解决方案利用 apply() 并应展示合理的性能改进。随意使用scorer 并更改threshold 以满足您的需求:

import pandas as pd, numpy as np
from fuzzywuzzy import process, fuzz

df = pd.DataFrame([['cliftonlarsonallen llp minneapolis MN'],
        ['loeb and troper llp newyork NY'],
        ["dauby o'connor and zaleski llc carmel IN"],
        ['wegner cpas llp madison WI']],
        columns=['org_name'])

org_list = df['org_name']

threshold = 40

def find_match(x):

  match = process.extract(x, org_list, limit=2, scorer=fuzz.partial_token_sort_ratio)[1]
  match = match if match[1]>threshold else np.nan
  return match

df['match found'] = df['org_name'].apply(find_match)

返回:

                                   org_name                                     match found
0     cliftonlarsonallen llp minneapolis MN             (wegner cpas llp madison WI, 50, 3)
1            loeb and troper llp newyork NY             (wegner cpas llp madison WI, 46, 3)
2  dauby o'connor and zaleski llc carmel IN                                             NaN
3                wegner cpas llp madison WI  (cliftonlarsonallen llp minneapolis MN, 50, 0)

如果你只想返回匹配的字符串本身,那么你可以修改如下:

match = match[0] if match[1]>threshold else np.nan

我在这里添加了@user3483203 的与列表理解有关的评论作为替代选项:

df['match found'] = [find_match(row) for row in df['org_name']]

请注意,process.extract() 旨在处理单个查询字符串并将传递的评分算法应用于该查询和提供的匹配选项。出于这个原因,您必须针对所有 70,000 个匹配选项(您当前设置代码的方式)评估该查询。因此,您将评估 len(match_options)**2(或 4,900,000,000)个字符串比较。因此,我认为通过find_match() 函数中更广泛的逻辑限制潜在的匹配选项可以实现最佳性能改进,例如强制匹配选项以与查询相同的字母开头,等等。

【讨论】:

  • 使用列表推导比使用应用可以获得更好的性能。类似[find_match(row) for row in df.org_name]
  • 谢谢,rahlf23。我很欣赏这个。但是,我提到我已经尝试过没有性能提升的行应用方法。将代码应用于前两行需要 11.7 秒,而使用 iterrows 方法需要 11.6 秒。
  • 您是否真的根据所有 70,000 个匹配选项检查每个查询?我相信您可以找到一种方法来限制每个查询的匹配选项
  • 谢谢@user3483203。这看起来很有希望,但是请您帮我填写一下我将如何使用它来生成包含匹配项的新列的空白吗?
  • @rahlf23 -- 是的,这就是我在这里寻求帮助的原因。有什么建议吗?
【解决方案3】:

不建议在数据帧上使用 iterrows(),您可以使用 apply() 代替。但这可能不会大大加快速度。缓慢的是fuzzywuzzy 的提取方法,其中您的输入与所有70k 行进行比较(字符串距离方法的计算成本很高)。因此,如果您打算坚持使用fuzzywuzzy,一个解决方案是将您的搜索限制为仅具有相同首字母的搜索。或者,如果您的数据中有另一列可用作提示(州、市、...)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-09
    • 2015-02-07
    • 1970-01-01
    • 2012-02-14
    • 2014-11-02
    • 1970-01-01
    • 2020-03-13
    • 2018-05-31
    相关资源
    最近更新 更多