我正在使用大型企业数据库。
我希望能够比较两个商业名称的相似性,看看它们是否可能是重复的。
下面是一个应该测试的商业名称列表,因为它很有可能是重复的,有什么好办法可以解决这个问题?
George Washington Middle Schl George Washington School Santa Fe East Inc Santa Fe East Chop't Creative Salad Co Chop't Creative Salad Company Manny and Olga's Pizza Manny's & Olga's Pizza Ray's Hell Burger Too Ray's Hell Burgers El Sol El Sol de America Olney Theatre Center for the Arts Olney Theatre 21 M Lounge 21M Lounge Holiday Inn Hotel Washington Holiday Inn Washington-Georgetown Residence Inn Washington,DC/Dupont Circle Residence Inn Marriott Dupont Circle Jimmy John's Gourmet Sandwiches Jimmy John's Omni Shoreham Hotel at Washington D.C. Omni Shoreham Hotel
答案 0 :(得分:39)
我最近做了类似的任务,虽然我将新数据与数据库中的现有名称进行匹配,而不是在一组中查找重复项。名称匹配实际上是一项经过充分研究的任务,除了您考虑匹配通用字符串之外,还有许多因素。
首先,我建议看看一篇论文,如何玩“名称游戏”:Raffo和Lhuillery的专利检索比较不同的启发式。发布的版本为here,PDF可免费提供here。作者提供了一个很好的总结,比较了许多不同的匹配策略。他们考虑三个阶段,他们称之为解析,匹配和过滤。
解析包括应用各种清洁技术。一些例子:
在我的情况下,我将所有字母折叠成小写,用空格替换所有标点符号,用非重音字符替换重音字符,删除所有其他特殊字符,并从列表后面的名称的开头和结尾删除法律控制术语。
匹配是已解析名称的比较。这可以是简单的字符串匹配,编辑距离,Soundex或Metaphone,比较组成名称的单词组,或比较字母组或 n -grams(字母序列长度 ñ)。 n -gram方法实际上对于名称来说非常好,因为它忽略了单词顺序,对“例子部门”与“示例部门”之类的东西有很大帮助。事实上,使用像Jaccard index之类的简单比较双字母(2克,字符对)是非常有效的。与其他一些建议相反, Levenshtein距离是名称匹配方面较差的方法之一。
在我的例子中,我分两步进行匹配,首先比较解析后的名称是否相等,然后使用Jaccard索引来计算余下的bigrams。我没有实际计算所有名称对的所有Jaccard索引值,而是首先对两组给定大小的Jaccard索引的最大可能值进行约束,并且只有在该上限足够高时才计算Jaccard索引。可能有用。大多数名称对仍然不同,以至于它们不匹配,但它大大减少了比较的次数。
过滤是使用辅助数据来拒绝解析和匹配阶段的误报。一个简单的版本是查看匹配的名称是否与不同城市的企业相对应,从而与不同的企业相对应。该示例可以在匹配之前应用,作为一种预过滤。之后可能会应用更复杂或耗时的检查。
我没有做太多过滤。我检查了各个公司的国家,看看它们是否相同,就是这样。数据中没有那么多的可能性,一些时间限制排除了对增加过滤的额外数据的广泛搜索,并且无论如何都计划进行人工检查。
答案 1 :(得分:9)
我想在一些优秀的答案中添加一些例子。在Python 2.7中测试。
让我们用这个奇怪的名字作为例子。
name = "THE | big,- Pharma: LLC" # example of a company name
我们可以从删除法律控制条款(此处为LLC)开始。要做到这一点,有一个很棒的cleanco Python库,它正是这样做的:
from cleanco import cleanco
name = cleanco(name).clean_name() # 'THE | big,- Pharma'
删除所有标点符号:
name = name.translate(None, string.punctuation) # 'THE big Pharma'
(对于unicode字符串,以下代码改为工作(source,regex):
import regex
name = regex.sub(ur"[[:punct:]]+", "", name) # u'THE big Pharma'
使用NLTK将名称拆分为令牌:
import nltk
tokens = nltk.word_tokenize(name) # ['THE', 'big', 'Pharma']
小写所有标记:
tokens = [t.lower() for t in tokens] # ['the', 'big', 'pharma']
删除停用词。请注意,这可能会导致问题On Mars
等公司与Mars
错误匹配,因为On
是一个禁用词。
from nltk.corpus import stopwords
tokens = [t for t in tokens if t not in stopwords.words('english')] # ['big', 'pharma']
我在这里不包括重音和特殊字符(欢迎改进)。
现在,当我们将所有公司名称映射到令牌时,我们希望找到匹配对。可以说,Jaccard(或Jaro-Winkler)的相似性比Levenstein更好,但仍然不够好。原因是它没有考虑名称中单词的重要性(如TF-IDF那样)。因此,像“公司”这样的常用词会影响得分,就像可能唯一识别公司名称的词一样。
要改进这一点,您可以使用此awesome series of posts(不是我的)中建议的名称相似性技巧。这是一个代码示例:
# token2frequency is just a word counter of all words in all names
# in the dataset
def sequence_uniqueness(seq, token2frequency):
return sum(1/token2frequency(t)**0.5 for t in seq)
def name_similarity(a, b, token2frequency):
a_tokens = set(a.split())
b_tokens = set(b.split())
a_uniq = sequence_uniqueness(a_tokens)
b_uniq = sequence_uniqueness(b_tokens)
return sequence_uniqueness(a.intersection(b))/(a_uniq * b_uniq) ** 0.5
使用它,您可以匹配具有超过特定阈值的相似性的名称。作为一种更复杂的方法,您还可以使用几个分数(例如,这个唯一性分数,Jaccard和Jaro-Winkler)并使用一些标记数据训练二元分类模型,如果候选对,将给出多个分数输出是不匹配。有关这方面的更多信息可以在同一篇博文中找到。
答案 2 :(得分:7)
您可以使用Levenshtein距离,该距离可用于测量两个序列之间的差异(基本上是编辑距离)。
def levenshtein_distance(a,b):
n, m = len(a), len(b)
if n > m:
# Make sure n <= m, to use O(min(n,m)) space
a,b = b,a
n,m = m,n
current = range(n+1)
for i in range(1,m+1):
previous, current = current, [i]+[0]*n
for j in range(1,n+1):
add, delete = previous[j]+1, current[j-1]+1
change = previous[j-1]
if a[j-1] != b[i-1]:
change = change + 1
current[j] = min(add, delete, change)
return current[n]
if __name__=="__main__":
from sys import argv
print levenshtein_distance(argv[1],argv[2])
答案 3 :(得分:6)
有一个很棒的库可以搜索python:fuzzywuzzy的类似/模糊字符串。在提到的Levenshtein距离测量时,这是一个很好的包装库。 在这里如何分析您的名字:
#!/usr/bin/env python
from fuzzywuzzy import fuzz
names = [
("George Washington Middle Schl",
"George Washington School"),
("Santa Fe East Inc",
"Santa Fe East"),
("Chop't Creative Salad Co",
"Chop't Creative Salad Company"),
("Manny and Olga's Pizza",
"Manny's & Olga's Pizza"),
("Ray's Hell Burger Too",
"Ray's Hell Burgers"),
("El Sol",
"El Sol de America"),
("Olney Theatre Center for the Arts",
"Olney Theatre"),
("21 M Lounge",
"21M Lounge"),
("Holiday Inn Hotel Washington",
"Holiday Inn Washington-Georgetown"),
("Residence Inn Washington,DC/Dupont Circle",
"Residence Inn Marriott Dupont Circle"),
("Jimmy John's Gourmet Sandwiches",
"Jimmy John's"),
("Omni Shoreham Hotel at Washington D.C.",
"Omni Shoreham Hotel"),
]
if __name__ == '__main__':
for pair in names:
print "{:>3} :: {}".format(fuzz.partial_ratio(*pair), pair)
>>> 79 :: ('George Washington Middle Schl', 'George Washington School')
>>> 100 :: ('Santa Fe East Inc', 'Santa Fe East')
>>> 100 :: ("Chop't Creative Salad Co", "Chop't Creative Salad Company")
>>> 86 :: ("Manny and Olga's Pizza", "Manny's & Olga's Pizza")
>>> 94 :: ("Ray's Hell Burger Too", "Ray's Hell Burgers")
>>> 100 :: ('El Sol', 'El Sol de America')
>>> 100 :: ('Olney Theatre Center for the Arts', 'Olney Theatre')
>>> 90 :: ('21 M Lounge', '21M Lounge')
>>> 79 :: ('Holiday Inn Hotel Washington', 'Holiday Inn Washington-Georgetown')
>>> 69 :: ('Residence Inn Washington,DC/Dupont Circle', 'Residence Inn Marriott Dupont Circle')
>>> 100 :: ("Jimmy John's Gourmet Sandwiches", "Jimmy John's")
>>> 100 :: ('Omni Shoreham Hotel at Washington D.C.', 'Omni Shoreham Hotel')
解决此类问题的另一种方法可能是Elasticsearch,它也支持模糊搜索。
答案 4 :(得分:2)
我搜索了“python编辑距离”,这个库是第一个结果:http://www.mindrot.org/projects/py-editdist/
另一个执行相同工作的Python库在这里:http://pypi.python.org/pypi/python-Levenshtein/
edit distance表示只需执行简单(通常是基于字符)编辑操作即可将一个字符串转换为另一个字符串所需的工作量。每个操作(替换,删除,插入;有时转置)都有相关的成本,两个字符串之间的最小编辑距离可以衡量两者之间的差异。
在您的特定情况下,您可能需要对字符串进行排序,以便找到从较长到较短的距离,并减少对字符删除的惩罚(因为我发现在很多情况下,其中一个字符串几乎是一个子字符串另一个)。所以删除不应该受到很多惩罚。
您还可以使用此示例代码:http://norvig.com/spell-correct.html
答案 5 :(得分:2)
考虑使用Diff-Match-Patch library。您对Diff过程感兴趣 - 在文本上应用差异可以让您很好地了解差异,以及它们的程序化表示。
答案 6 :(得分:1)
你可以做的是用空格,逗号等分隔单词,然后你计算它与另一个名字共有的单词数量,然后在它被认为是“相似”之前添加一些单词thresold。
另一种方法是做同样的事情,但是为每个字符拿出单词并拼接它们。然后,对于每个单词,您需要比较是否以相同的顺序(从两侧)找到x个数量的字符(或百分比),然后您可以说该单词也是相似的。
Ex:你有sqre和square
然后你通过caracters检查并发现sqre都是正方形且顺序相同,那么它就是一个相似的单词。
答案 7 :(得分:1)
基于Levenshtein距离的算法不错(不是很完美),但是它们的主要缺点是每次比较都很慢,并且涉及必须比较每种可能的组合这一事实。
解决该问题的另一种方法是,使用嵌入词或词袋将每个公司的名称(经过清理和假定)转换为数字向量。然后,根据可用情况,应用无监督或受监督的ML方法。
答案 8 :(得分:1)
这是对丹尼斯评论的更新。这个答案和他发布的链接确实很有帮助,但我无法立即使它们正常工作。在尝试了模糊模糊搜索之后,我发现这给了我一系列更好的答案。我有很多商人,我只想将他们分组。最终,我将有一张桌子,可以用来尝试一些机器学习,但是现在这需要花费很多精力。
我只需要稍微更新一下他的代码,并添加一个函数来创建tokens2frequency字典。原始文章也没有,然后函数也没有正确引用它。
import pandas as pd
from collections import Counter
from cleanco import cleanco
import regex
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
# token2frequency is just a Counter of all words in all names
# in the dataset
def sequence_uniqueness(seq, token2frequency):
return sum(1/token2frequency[t]**0.5 for t in seq)
def name_similarity(a, b, token2frequency):
a_tokens = set(a)
b_tokens = set(b)
a_uniq = sequence_uniqueness(a, token2frequency)
b_uniq = sequence_uniqueness(b, token2frequency)
if a_uniq==0 or b_uniq == 0:
return 0
else:
return sequence_uniqueness(a_tokens.intersection(b_tokens), token2frequency)/(a_uniq * b_uniq) ** 0.5
def parse_name(name):
name = cleanco(name).clean_name()
#name = name.translate(None, string.punctuation)
name = regex.sub(r"[[:punct:]]+", "", name)
tokens = nltk.word_tokenize(name)
tokens = [t.lower() for t in tokens]
tokens = [t for t in tokens if t not in stopwords.words('english')]
return tokens
def build_token2frequency(names):
alltokens = []
for tokens in names.values():
alltokens += tokens
return Counter(alltokens)
with open('marchants.json') as merchantfile:
merchants = pd.read_json(merchantfile)
merchants = merchants.unique()
parsed_names = {merchant:parse_name(merchant) for merchant in merchants}
token2frequency = build_token2frequency(parsed_names)
grouping = {}
for merchant, tokens in parsed_names.items():
grouping[merchant] = {merchant2: name_similarity(tokens, tokens2, token2frequency) for merchant2, tokens2 in parsed_names.items()}
filtered_matches = {}
for merchant in pcard_merchants:
filtered_matches[merchant] = {merchant1: ratio for merchant1, ratio in grouping[merchant].items() if ratio >0.3 }
这将为您提供最终的过滤名称列表,以及与之匹配的其他名称。它是与其他文章相同的基本代码,只是缺少了一些内容。它也在Python 3.8中运行
答案 9 :(得分:0)
我创建了火柴卡夫 (https://github.com/MatchKraft/matchkraft-python)。它在模糊模糊的基础上工作,您可以在一个列表中模糊匹配公司名称。
它非常易于使用。这是python中的一个例子:
from matchkraft import MatchKraft
mk = MatchKraft('<YOUR API TOKEN HERE>')
job_id = mk.highlight_duplicates(name='Stackoverflow Job',
primary_list=[
'George Washington Middle Schl',
'George Washington School',
'Santa Fe East Inc',
'Santa Fe East',
'Rays Hell Burger Too',
'El Sol de America',
'microsoft',
'Olney Theatre',
'El Sol'
]
)
print (job_id)
mk.execute_job(job_id=job_id)
job = mk.get_job_information(job_id=job_id)
print (job.status)
while (job.status!='Completed'):
print (job.status)
time.sleep(10)
job = mk.get_job_information(job_id=job_id)
results = mk.get_results_information(job_id=job_id)
if isinstance(results, list):
for r in results:
print(r.master_record + ' --> ' + r.match_record)
else:
print("No Results Found")