字符串的相似性可以这样比较...

问题:数据库里长期积累的一些数据,由于经手的人太多,结果现在相同的客户出现了很多不同的名字,因此想做个相似度查询,把比较相似的名称查找出来,人工比对进行校对统一。

一、笨办法

想要比较两个字符串是否相似,这个应该有相应的算法,我先用了自己的办法,到时再到网上找找,有没有更好办法。
假设,有这样三个客户名:

s1 = "宁波市第一医院"
s2 = "宁波市一院"
s3 = "宁波李惠利医院"

我们先把他们变成集合,再比较两者的交集与并集,计算比值,原理是利用如果两个相似的名称的交集长度应该与并集长度相近,我们来看看:

# s1与s2相比
# 交集
set(s1)&set(s2)
# 输出: {'一', '宁', '市', '波', '院'}
# 并集
set(s1)|set(s2)
# 输出: {'一', '医', '宁', '市', '波', '第', '院'}
len(set(s1)&set(s2))/len(set(s1)|set(s2))
# 输出: 0.7142857142857143
# s1与s3相比
set(s1)&set(s3)
# 输出: {'医', '宁', '波', '院'}
set(s1)|set(s3)
# 输出: {'一', '利', '医', '宁', '市', '惠', '李', '波', '第', '院'}
len(set(s1)&set(s3))/len(set(s1)|set(s3))
# 输出: 0.4

利用这一特性,可以看出我们的策略是有效的。
不过这个算法还是存在一些问题,比如:

s1 = "宁波市一院"
s2 = "宁波市二院"
s3 = "宁波市三院"

这三个客户,人工识别是知道是不同的,可是用算法来看看:

len(set(s1)&set(s2))/len(set(s1)|set(s2))
# 输出:  0.6666666666666666
len(set(s1)&set(s3))/len(set(s1)|set(s3))
# 输出:  0.6666666666666666

他们输出值是一样的,都超过了0.5,这就需要我们在设置筛选条件时通过反复调试,找到最适合的阀值了,不然会出现,明明是不一样的,却筛选出来。

二、不要重复造轮子

有个伟大的程序员说过"不要重复造轮子",我们前面的分析其实就是重复造轮子,那现在我们来找找有没有别人造好的轮子。
网上找了下,原来python自带的库difflib就可以解决这个问题,python就是好用呀,再来试试:

import difflib
s1 = "宁波市第一医院"
s2 = "宁波市一院"
s3 = "宁波李惠利医院"
# 为了不用重复输入太长,我们先自定义一个函数

def string_similar(s1, s2):
    return difflib.SequenceMatcher(None, s1, s2).quick_ratio()

print(string_similar(s1,s1))
print(string_similar(s1,s2))
print(string_similar(s1,s3))
# 输出
# 1.0
# 0.8333333333333334
# 0.5714285714285714

可以很好的区分相似字符串,那么我们再来试试,前面第二个例子:

s1 = "宁波市一院"
s2 = "宁波市二院"
s3 = "宁波市三院"

print(string_similar(s1,s2))
print(string_similar(s1,s3))
# 输出
# 0.8
# 0.8

和我们自己做的算法类似,依然要调试阀值,避免筛选错误。这里虽然和我们自己造的轮子差不多,可是这个是python自带的库,通常运行会比我们自己造的轮子要快,所以还是推荐大家用这个库.

三、也试试第三方库

虽然前面已经用过我们自己造的轮子和python自带的库,都能解决我们的问题,但是我还是想看看有没有什么第三方库也能达到我们的目的。
经过一翻查找,我们找到了fuzzywuzzy,这是一个第三方库,需要我们自己安装,在查找的过程中我们还了解到一个新的知识点,我们前面的例子中都没有考虑一个问题,那就是字符串的位置关系。当然这个是在我们自己造的轮子里完全没有体现的,而系统自己带的库,我们等下会再测试一个例子,看看字符串位置会有影响吗?

pip install fuzzywuzzy

安装好,我们就可以开始使用了。

from fuzzywuzzy import fuzz

s1 = "宁波市第一医院"
s2 = "宁波市一院"
s3 = "宁波李惠利医院"
print(fuzz.ratio(s1,s1))
print(fuzz.ratio(s1,s2))
print(fuzz.ratio(s1,s3))
# 输出
# 100
# 83
# 57

很好,可以正常区分,再测试一下位置关系:

s4 = "李惠利宁波医院"
print(fuzz.ratio(s3,s4))
# 输出: 71

可以看出,位置是有影响的,我们自己造的轮子根据我们的算法原理是对位置不敏感的,依然测试下:

len(set(s3)&set(s4))/len(set(s3)|set(s4))
# 输出: 1.0

如我们所想的那样,没有考虑到位置关系的影响。
fuzzywuzzy还有一个方法,可以只考虑包含关系,比如:

s1 = "宁波李惠利医院"
s2 = "宁波李惠利医院东部院区"
# 先看看我们自己的算法
len(set(s1)&set(s2))/len(set(s1)|set(s2))
# 再看看fuzzywuzzy的方法
print(fuzz.partial_ratio(s1,s2))
print(fuzz.ratio(s1,s2))
# 输出
# 0.7
# 100
# 78

可以看出,我们自己的算法没有考虑到这种情况,如果按阀值在0.8以上,这个相似性会忽略,而采用fuzzywuzzy这种方法就会识别出来。

四、实战

分析了这么多库后,我们会采用哪种方法,自己造的轮子不用说,已经出局,那么python自带的还是第三方库呢,其实经过查阅文档,官方库完全能解决我们的问题,而且速度还有保证,不过官方库一般的问题就是代码可能比较底层,也就是不太人性化,调用起来有点别扭。这时我们可以用第三方库,使用起来比较平滑。
我们的文件内容,名称是网上爬来的:

《字符串的相似性可以这样比较...》

# coding:utf-8
import pandas as pd
import fire

# 原来的方法
def check_similar(s1, s2):
    """比较两个字符串的相似度
    :param s1: string 字符串1
    :param s2: string 字符串2
    :return: float 返回相似度
    """
    if s1 is None:
        return
    if s2 is None:
        return
    s1 = set(list(s1))
    s2 = set(list(s2))
    return len(s1 & s2) / len(s1 | s2)

# 采用fuzz
def check_similar_with_fuzz(s1, s2):
    """比较两个字符串的相似度
    :param s1: string 字符串1
    :param s2: string 字符串2
    :return: float 返回相似度
    """
    return fuzz.partial_ratio(s1, s2)

def find_similar(df, threshold=0.8, id_col_num=0, name_col_num=3):
    """找出文档中的符合相似度的字符串
    :param df: pd.DataFrame 包含字符串的文档
    :param threshold: float 相似度
    :param id_col_num: int 参考列号
    :param name_col_num: int 需要对比字符串的列号
    :return list: 返回包含符合相似度的结果列表
    """
    ...

def main(target_path, save_path, id_col_num, name_col_num):
    """主函数,运行命令行程序
    :param target_path:str 目标路径
    :param save_path: str 保存路径
    :param id_col_num: int 参考列号
    :param name_col_num: int 需要对比字符串的列号
    """
    df = pd.read_excel(target_path)
    result = find_similar(
        df, threshold=80, id_col_num=id_col_num, name_col_num=name_col_num)
    out = pd.DataFrame(result)
    out.to_csv(save_path, index=False)

if __name__ == '__main__':
    fire.Fire(main)

运行测试

# 文件里给每个字符串做了一个编号,方便查找原文件里的位置
python similar.py "目标路径" "存储路径" 参考列号 比较列号

输出结果查看下:

《字符串的相似性可以这样比较...》

可以看到输出,确实把相似的都排出来了,不过话说这些公司取名可真没有创意呀,这样的只有两个字不一样的,怎么算呢?

五、提高

经过实战,我们发现能够成功的把相似的客户名放到一起供人工核对了,可是如果内容多的话还有公司名字只有极少数字不一样,依然会是比较麻烦。这时我们有两个方法,不断的调试阀值,再调高点。另外就是人工识别吧。
其实应该还可以设计那些词汇不需要比较,比如这里的有限公司之类的,还有各种地名都可以去除再比较。

后记

既然有这么多公司名,下次分析下,这些公司比较喜欢用什么字做公司名。最才是美容机构的心头好呢?

点赞
关注极客数据微信公众号

发表评论

电子邮件地址不会被公开。 必填项已用*标注