我正在尝试在包含机构名称的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列中的每个元素生成最佳匹配?
答案 0 :(得分:9)
给出的任务是使用fuzz.WRatio比较70k个字符串,这样您总共有4,900,000,000个比较,而每个比较都使用Fuzzywuzzy内部的levenshtein距离,这是O(N * M)运算。 fuzz.WRatio是具有不同权重的多个不同字符串匹配率的组合。然后,在其中选择最佳比率。因此,它甚至必须多次计算Levenshtein距离。因此,一个目标应该是通过使用一种更快的匹配算法排除一些可能性来减少搜索空间。另一个问题是对字符串进行了预处理,以删除标点符号并小写字符串。虽然这是匹配所必需的(例如,大写单词等于小写单词),但我们基本上可以提前进行此操作。因此,我们只需要预处理一次70k字符串。在这里,我将使用RapidFuzz而不是FuzzyWuzzy,因为它要快得多(我是作者)。
以下版本在实验中的执行速度是您以前的解决方案的十倍以上,并进行了以下改进:
1)它会生成将组织映射到预处理组织的字典,因此不必在每次运行中都这样做
2)它将一个score_cutoff传递给extractOne,因此它可以跳过已经知道无法达到该比率的计算
import pandas as pd, numpy as np
from rapidfuzz import process, utils
org_list = df['org_name']
processed_orgs = {org: utils.default_process(org) for org in org_list}
for (i, (query, processed_query)) in enumerate(processed_orgs.items()):
choices = processed_orgs.copy()
del choices[query]
match = process.extractOne(processed_query, choices, processor=None, score_cutoff=93)
if match:
df.loc[i, 'fuzzy_match'] = match[2]
df.loc[i, 'fuzzy_match_score'] = match[1]
在此示例中,列出了RapidFuzz最相关的改进,以使其比FuzzyWuzzy更快:
1)它完全用C ++实现,而FuzzyWuzzy的很大一部分都用Python实现了
2)在计算勒芬施泰因距离时,考虑到score_cutoff
在无法达到分数时提早退出。这样,当字符串之间的长度差异较大时,它可以在O(1)中退出;当两个字符串之间存在许多不常见的字符时,它可以在计算Levenshtein距离时具有O(N * M)
3)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
4)由于extractOne仅搜索最佳匹配,因此它将当前最佳匹配的比率用作下一个元素的score_cutoff
。这样,在很多情况下,可以通过使用2)中的levenshtein距离计算的改进来快速丢弃更多元素
答案 1 :(得分:1)
此解决方案利用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()
函数中更广泛的逻辑(例如,通过强制匹配选项以与查询相同的字母开头,等等。
答案 2 :(得分:1)
不建议在数据帧上使用iterrows(),而可以使用apply()。但这可能不会大大加快速度。较慢的是Fuzzywuzzy的提取方法,其中将您的输入与所有70k行进行比较(字符串距离方法的计算量很大)。因此,如果您打算坚持模糊不清,则一种解决方案是将搜索限制在例如仅那些具有相同首字母的搜索。或者,如果数据中还有另一列可以用作提示(州,城市,...)