[英]Fuzzy string matching in Python
我有2个超过一百万个名称的列表,它们的命名约定略有不同。 目的是匹配具有95%置信度的逻辑的相似记录。
我知道有一些库可以利用,例如Python中的FuzzyWuzzy模块。
但是,就处理而言,将一个列表中的每个字符串与另一个列表进行比较似乎会占用太多资源,在这种情况下,这似乎需要将100万乘以另一百万迭代次数。
还有其他更有效的方法来解决此问题吗?
更新:
因此,我创建了一个存储桶功能并应用了一个简单的规范化功能,即删除空格,符号并将值转换为小写字母等。
for n in list(dftest['YM'].unique()):
n = str(n)
frame = dftest['Name'][dftest['YM'] == n]
print len(frame)
print n
for names in tqdm(frame):
closest = process.extractOne(names,frame)
通过使用pythons pandas,数据被加载到按年份分组的较小的存储桶中,然后使用FuzzyWuzzy模块,使用process.extractOne
获得最佳匹配。
结果仍然有些令人失望。 在测试过程中,上面的代码用于仅包含5,000个名称的测试数据帧,并且占用了几乎一个小时的时间。
测试数据被分割。
我正在按桶比较它们的YM在同一桶中。
问题可能是因为我正在使用FuzzyWuzzy模块吗? 感谢任何帮助。
这里有几种优化级别可以将这个问题从O(n ^ 2)转变为较小的时间复杂度。
预处理 :在第一遍中对列表进行排序,为每个字符串创建一个输出映射,它们的映射键可以归一化。 规范化可能包括:
这将导致"Andrew H Smith"
, "andrew h. smith"
, "ándréw h. smith"
生成相同的密钥"andrewhsmith"
,并且将您的万余名集缩小到一个较小的一套独特的/类似的分组名。
您可以使用此utlity方法来规范化您的字符串(尽管不包括unicode部分):
def process_str_for_similarity_cmp(input_str, normalized=False, ignore_list=[]):
""" Processes string for similarity comparisons , cleans special characters and extra whitespaces
if normalized is True and removes the substrings which are in ignore_list)
Args:
input_str (str) : input string to be processed
normalized (bool) : if True , method removes special characters and extra whitespace from string,
and converts to lowercase
ignore_list (list) : the substrings which need to be removed from the input string
Returns:
str : returns processed string
"""
for ignore_str in ignore_list:
input_str = re.sub(r'{0}'.format(ignore_str), "", input_str, flags=re.IGNORECASE)
if normalized is True:
input_str = input_str.strip().lower()
#clean special chars and extra whitespace
input_str = re.sub("\W", "", input_str).strip()
return input_str
现在,如果相似的字符串的规范化密钥相同,则它们将已经位于相同的存储桶中。
为了进行进一步的比较, 您将只需要比较键,而不是名称 。 例如andrewhsmith
和andrewhsmeeth
,因为除了上面完成的标准化比较之外,名称的这种相似性还需要模糊字符串匹配。
Bucketing : 您是否真的需要将5个字符的密钥与9个字符的密钥进行比较,看是否匹配率达到95% ? 你不可以。 因此,您可以创建与字符串匹配的存储桶。 例如,将5个字符名称与4-6个字符名称匹配,将6个字符名称与5-7个字符匹配,等等。对于大多数实际匹配而言,字符键的n + 1,n-1个字符限制是一个相当不错的选择。
开始比赛 :名称的大多数变体将以规范化格式具有相同的第一个字符(例如, Andrew H Smith
, ándréw h. smith
和Andrew H. Smeeth
生成andrewhsmith
, andrewhsmith
和andrewhsmeeth
密钥。通常,第一个andrewhsmeeth
不会有所不同字符,这样你就可以运行开始键,其对应a
至与启动其他键a
,而属于长度桶之内,这将大大减低你的匹配时间,无需匹配的关键andrewhsmith
到bndrewhsmith
与这样的名称变化首字母很少存在。
然后,您可以在此方法 (或FuzzyWuzzy模块)上使用某些东西来查找字符串相似度百分比,可以排除jaro_winkler或difflib中的一个来优化速度和结果质量:
def find_string_similarity(first_str, second_str, normalized=False, ignore_list=[]):
""" Calculates matching ratio between two strings
Args:
first_str (str) : First String
second_str (str) : Second String
normalized (bool) : if True ,method removes special characters and extra whitespace
from strings then calculates matching ratio
ignore_list (list) : list has some characters which has to be substituted with "" in string
Returns:
Float Value : Returns a matching ratio between 1.0 ( most matching ) and 0.0 ( not matching )
using difflib's SequenceMatcher and and jellyfish's jaro_winkler algorithms with
equal weightage to each
Examples:
>>> find_string_similarity("hello world","Hello,World!",normalized=True)
1.0
>>> find_string_similarity("entrepreneurship","entreprenaurship")
0.95625
>>> find_string_similarity("Taj-Mahal","The Taj Mahal",normalized= True,ignore_list=["the","of"])
1.0
"""
first_str = process_str_for_similarity_cmp(first_str, normalized=normalized, ignore_list=ignore_list)
second_str = process_str_for_similarity_cmp(second_str, normalized=normalized, ignore_list=ignore_list)
match_ratio = (difflib.SequenceMatcher(None, first_str, second_str).ratio() + jellyfish.jaro_winkler(unicode(first_str), unicode(second_str)))/2.0
return match_ratio
您必须索引或规范化字符串,以避免O(n ^ 2)运行。 基本上,您必须将每个字符串映射为普通形式,并使用所有与相应普通形式链接的单词构建反向字典。
让我们考虑一下“世界”和“单词”的正常形式是相同的。 因此,首先建立一个Normalized -> [word1, word2, word3],
的反向字典Normalized -> [word1, word2, word3],
例如:
"world" <-> Normalized('world')
"word" <-> Normalized('wrd')
to:
Normalized('world') -> ["world", "word"]
在那里-归类词典中所有具有多个值的项目(列表)都是匹配的单词。
归一化算法取决于数据,即单词。 考虑以下之一:
特定于Fuzzywuzzy,请注意,当前process.extractOne默认为WRatio,这是其算法中最慢的,处理器默认为utils.full_process。 如果您像记分员那样传递fuzz.QRatio,它将更快得多,但是根据您要匹配的内容而没有那么强大。 虽然可能对名字没问题。 我个人对token_set_ratio感到幸运,它至少比WRatio快一些。 您还可以事先对所有选择运行utils.full_process(),然后以fuzz.ratio作为记分器运行,并且Processor = None跳过该处理步骤。 (请参阅下文)如果您仅使用基本比率函数,则Fuzzywuzzy可能会显得过大。 首先,我有一个JavaScript端口(fuzzball.js),您也可以在其中预先计算令牌集,并使用它们而不是每次都重新计算。)
这不会减少绝对的比较数量,但会有所帮助。 (可能是BK树吗?我自己也在调查相同情况)
另外,请务必安装python-Levenshtein,以便您使用更快的计算速度。
**以下行为可能会更改,正在讨论的未解决问题等**
fuzz.ratio不会运行完整进程,并且token_set和token_sort函数接受full_process = False参数,并且如果未设置Processor = None,那么提取函数将始终尝试运行完整进程。 可以使用functools的局部语句来传递带有full_process = False的fuzz.token_set_ratio作为记分员,并事先对您的选择运行utils.full_process。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.