弄清楚企业名称是否与另一个企业名称非常相似 - Python

时间:2011-06-19 03:52:48

标签: python normalization matching similarity edit-distance

我正在使用大型企业数据库。

我希望能够比较两个商业名称的相似性,看看它们是否可能是重复的。

下面是一个应该测试的商业名称列表,因为它很有可能是重复的,有什么好办法可以解决这个问题?

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

10 个答案:

答案 0 :(得分:39)

我最近做了类似的任务,虽然我将新数据与数据库中的现有名称进行匹配,而不是在一组中查找重复项。名称匹配实际上是一项经过充分研究的任务,除了您考虑匹配通用字符串之外,还有许多因素。

首先,我建议看看一篇论文,如何玩“名称游戏”:Raffo和Lhuillery的专利检索比较不同的启发式。发布的版本为here,PDF可免费提供here。作者提供了一个很好的总结,比较了许多不同的匹配策略。他们考虑三个阶段,他们称之为解析,匹配和过滤。

解析包括应用各种清洁技术。一些例子:

  • 标准化字母(例如,全部小写)
  • 标准化标点符号(例如,逗号必须后跟空格)
  • 标准化空格(例如,将所有空格转换为单个空格)
  • 标准化重音和特殊字符(例如,将重音字母转换为ASCII等效字符)
  • 标准化法律控制条款(例如,将“公司”转换为“公司”)

在我的情况下,我将所有字母折叠成小写,用空格替换所有标点符号,用非重音字符替换重音字符,删除所有其他特殊字符,并从列表后面的名称的开头和结尾删除法律控制术语。

匹配是已解析名称的比较。这可以是简单的字符串匹配,编辑距离,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字符串,以下代码改为工作(sourceregex):

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距离,该距离可用于测量两个序列之间的差异(基本上是编辑距离)。

Python中的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")