判断一个商名是否与另一个非常相似 - Python

49 投票
10 回答
33148 浏览
提问于 2025-04-16 19:50

我正在处理一个包含很多商家的大数据库。

我想比较两个商家的名字,看看它们是否相似,以判断它们可能是重复的。

下面是一组商家名字,它们应该被测试为有很高的重复可能性,请问有什么好的方法来进行这个比较呢?

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 个回答

9

有一个很棒的库可以用来在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,它也支持模糊搜索。

22

我想给这个很棒的被接受的答案添加一些例子。测试是在 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 字符串,下面的代码可以替代使用 (来源, 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 那样)。所以像“公司”这样的常见词会和那些可能唯一识别公司名字的词一样影响得分。

为了改进这一点,你可以使用在这个 很棒的系列文章 中建议的名字相似度技巧(不是我写的)。这里有一个代码示例:

# 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),并使用一些标记数据训练一个二分类模型,这样在给定多个得分的情况下,它就能判断候选对是否匹配。更多内容可以在同一篇博客文章中找到。

59

我最近做过类似的事情,不过我是在数据库中把新数据和已有的名字进行匹配,而不是在同一组数据中寻找重复项。名字匹配其实是一个研究得很透彻的任务,涉及的因素比你想象的要复杂得多。

首先,我建议你看看一篇论文,如何玩“名字游戏”:比较不同启发式方法的专利检索,作者是Raffo和Lhuillery。你可以在这里找到发表的版本,PDF版本可以在这里免费下载。作者对几种不同的匹配策略进行了很好的总结,他们把这个过程分为三个阶段:解析、匹配和过滤。

解析阶段就是对数据进行各种清理。比如:

  • 统一字母大小写(例如,全部小写)
  • 统一标点符号(例如,逗号后面必须有空格)
  • 统一空格(例如,把连续的空格变成一个空格)
  • 统一带重音的特殊字符(例如,把带重音的字母转换成ASCII字符)
  • 统一法律控制术语(例如,把“Co.”转换成“Company”)

在我的案例中,我把所有字母都变成小写,把所有标点符号替换为空格,把带重音的字符换成不带重音的字符,去掉了其他特殊字符,并且根据一个列表去掉了名字开头和结尾的法律控制术语。

匹配阶段就是比较解析后的名字。这可以是简单的字符串匹配、编辑距离、Soundex或Metaphone,或者比较名字中单词的集合,甚至是比较字母集合或n-gram(长度为n的字母序列)。n-gram方法对于名字来说非常有效,因为它忽略了单词的顺序,这样在处理“examples的部门”和“部门的examples”时就会更容易。实际上,使用简单的Jaccard指数比较二元组(2-gram,即字符对)是非常有效的。与其他一些建议相比,Levenshtein距离在名字匹配方面效果较差。

在我的案例中,我分两步进行匹配,首先比较解析后的名字是否相等,然后对剩下的名字使用Jaccard指数来比较二元组。我并没有计算所有名字对的Jaccard指数,而是先设定了一个最大可能值,如果这个值足够高,才会计算Jaccard指数。大多数名字对之间的相似度还是不够,因此没有匹配成功,但这样大大减少了需要比较的次数。

过滤阶段是利用辅助数据来排除解析和匹配阶段的误匹配。一个简单的例子是检查匹配的名字是否对应于不同城市的公司,从而确认它们是不同的公司。这个例子可以在匹配之前作为一种预过滤来使用。更复杂或耗时的检查可以在之后进行。

我没有做太多的过滤。我只检查了公司的国家是否相同,仅此而已。数据中实际上没有太多可能性,时间限制也让我无法进行广泛的数据搜索来增强过滤,反正后面还有手动检查的计划。

撰写回答