正如题目中说的一样,这个程序的目的是实现公司名及公司地址的模糊匹配,也可以迁移到房产信息、电话号码之类的字段上。本来的应用场景是反团伙欺诈以及失联客户的修复,大概的意思就是说多个相同公司的同事都在我公司借贷的欺诈可能性要高于其他客户,以及造假的房产信息和电话号码可能不完全相同,但有一定的相似性,我们需要把这些客户找出来,但是又不能用精确匹配。因为存在问题的房产信息和电话可能只是相似,而不是完全相同;对于公司名和地址来说,就更糟糕一些,即使是真实的信息,但同一个公司的叫法可能会多种多样,如果麻烦一些,还要建一个同义词库。而我们做的工作还没有那么全面,只是提取出了公司名中的关键词。
而做评分卡模型也需要用到这个思想。因为评分卡的其中一个字段就是所在城市等级,这就需要从原始数据的地址中提取出城市信息,但地址的格式又不够标准,因为信息是客户人工填写的,举个例子,“山东省济南市”也有可能被写成“山东济南”,所以一个正则表达式就不足以解决这个字段提取的问题了。
总结一下,模糊匹配的两个应用场景:
1)构建反欺诈知识图谱
2)评分卡提取所需字段
地址处理的基本思路就是建立一个标准库,对地址逐个进行对比,再返回结果和置信度,所以词库的建立是地址处理的关键。
下面是解决问题的过程:
第一步,希望可以提取出公司名中的关键字。首先,要分析一下数据库中公司名的形式:“浙江杭州立多林贸易有限公司”,“汇川区万全兄弟购物中心”,“温州雪龙集团有限公司北京销售分公司”……
经过分析,可以看出,公司名大致分为3个部分:1、省市区名称;2、关键词+公司属性;3、分公司或分局等信息。
首先,需要使用python连接到数据库,取出公司名字段:
1、下载并安装psycopg2模块,下载链接如下:
http://www.stickpeople.com/projects/python/win-psycopg/
安装完毕后,输入下面语句,连接数据库:
import psycopg2
#录入数据
conn=psycopg2.connect(database="testdb", user="postgres", password="postgres", host="10.180.157.168", port="1975")
cur=conn.cursor()
needinfo=['brhs_unitname','dict_unit_province','dict_unit_city','dict_unit_arer','brhs_unit_address']
cur.execute("SELECT brhs_unitname,dict_unit_province,dict_unit_city,dict_unit_arer,brhs_unit_address FROM aaa_t_jk_dhzh_brhs limit 1000;")
selects=cur.fetchall()
enterprise_datas=pd.DataFrame(selects,columns=needinfo)
2、根据上面的分析,下一步就要先将省市区提取出来。提取省市区的常规做法是建立行政区划库,类似于jieba分词中的txt词典。在解决这个问题时,我和同事两个人的思路不尽相同。在使用jieba对公司名进行分词后,一种思路是建立一个最简词库,词库中的词都是行政区划的最简称,例如“宁夏”、“内蒙古”等,如果字典中的词在分词中,则取出该分词;另一种思路是,使用全称词库,例如“宁夏回族自治区”、“内蒙古自治区”等,如果分词后的词可以和其完全匹配,则取出该分词,否则,按照一定的规则,将字典中的词去掉“省”、“市”、“自治区”后,再和分词进行匹配。经过讨论,最后,行政区划库的形式如下图所示:
region_nosuffix | region_suffix | city_nosuffix | city_suffix | province_nosuffix | province_suffix |
---|---|---|---|---|---|
东城 | 东城区 | 北京直辖区 | 北京直辖区 | 北京 | 北京市 |
西城 | 西城区 | 北京直辖区 | 北京直辖区 | 北京 | 北京市 |
朝阳 | 朝阳区 | 北京直辖区 | 北京直辖区 | 北京 | 北京市 |
: | : | : | : | : | : |
3、使用python建立词典,分别建立以下几个词典:
#建立字典
district=pd.read_excel('~\district_new_nosuffix.xlsx',sheetname='slice').fillna('')
##建立pcr字典
district_dict_pcr=defaultdict(lambda:defaultdict(lambda:defaultdict(int)))
for num in range(len(district)):
district_dict_pcr[district.ix[num,'province_nosuffix']][district.ix[num,'city_nosuffix']][district.ix[num,'region_nosuffix']]=district.ix[num,'region_code']
district_dict_pcr=dict(district_dict_pcr)
##建立rcp字典
district_dict_rcp=defaultdict(list)
for num in range(len(district)):
district_dict_rcp[district.ix[num,'region_nosuffix']].extend([district.ix[num,'province_suffix'],district.ix[num,'city_suffix'],district.ix[num,'region_suffix']])
district_dict_rcp=dict(district_dict_rcp)
##建立cp字典
district_dict_cp=defaultdict(list)
for num in range(len(district)):
if district_dict_cp[district.ix[num,'city_nosuffix']]==[]:
district_dict_cp[district.ix[num,'city_nosuffix']].extend([district.ix[num,'province_suffix'],district.ix[num,'city_suffix']])
district_dict_cp=dict(district_dict_cp)
##建立pp字典
district_dict_pp=defaultdict(str)
for num in range(len(district)):
district_dict_pp[district.ix[num,'province_nosuffix']]=district.ix[num,'province_suffix']
district_dict_pp=dict(district_dict_pp)
4、使用分词包对公司名进行分词,并标出词性
显然,需要提取出行政区,我们需要词性为ns的词,但分词后发现,Jieba自带的词典中,我们需要的省市区的词性并非全部为ns,而其他词,如“大望路”等我们不需要的词却可能为”ns”,于是,需要创建专用分词词典,并标注其词性为ns,替换默认的词典。
def word_flag(words):
'''
提取flag是u'ns'的word
'''
result=defaultdict(list)
output=pseg.cut(words)
for word,flag in output:
result[flag].append(word)
return result[u'ns']
5、提取出三级行政区划
前面已经提到过,由于书写的不规范,导致同一个行政区划,写法可能不同。例如,“山西省太原市小店区”,既有可能被写成“山西太原小店区”,也有可能被写成“太原市小店区”,还可能直接被写成“小店区”,所以,提取时,要分多种情况:
def geo_checker(word):
'''
验证word
'''
if len(word)>2:
if (u'省' in word or u'自治区' in word):
word=word.replace(u'省','').replace(u'自治区','')
try:
district_dict_pcr[word]
return district_dict_pp[word]
except KeyError:
return u'未知省份'
elif (u'地区' in word or u'自治州' in word or u'盟' in word):
word=word.replace(u'地区','').replace(u'自治州','').replace(u'盟','')
try:
district_dict_cp[word]
return district_dict_cp[word]
except KeyError:
return u'未知城市'
elif (u'自治县' in word or u'矿区' in word or u'自治旗' in word):
word=word.replace(u'自治县','').replace(u'矿区','').replace(u'自治旗','')
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知区县'
elif u'县' in word :
word=word.replace(u'县','')
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知区县'
elif u'市' in word:
if (u'天津' in word or u'北京' in word or u'重庆' in word or u'上海' in word):
word=word.replace(u'市','')
try:
district_dict_pcr[word]
return district_dict_pp[word]
except KeyError:
return u'未知省份'
if u'区' in word:
word=word.replace(u'区','')
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知区县'
word=word.replace(u'市','')
try:
district_dict_cp[word]
return district_dict_cp[word]
except KeyError:
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知城市或区县'
elif u'区' in word:
word=word.replace(u'区','')
try:
district_dict_cp[word]
return district_dict_cp[word]
except KeyError:
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知城市或区县'
else:
try:
district_dict_pcr[word]
return district_dict_pp[word]
except KeyError:
try:
district_dict_cp[word]
return district_dict_cp[word]
except KeyError:
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知地理信息'
else:
if (u'市' in word or u'区' in word):
word=word.replace(u'市','').replace(u'区','')
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
try:
district_dict_cp[word]
return district_dict_cp[word]
except KeyError:
return u'未知城市或区县'
else:
try:
district_dict_pcr[word]
return district_dict_pp[word]
except KeyError:
try:
district_dict_cp[word]
return district_dict_cp[word]
except KeyError:
try:
district_dict_rcp[word]
return district_dict_rcp[word]
except KeyError:
return u'未知地理信息'
def type_checker(x):
if type(x)==list:return 1
else:return 0
def geo_standard(geo_list):
for i in range(len(geo_list)):
geo_list[i]=geo_checker(geo_list[i])
geo_result=[x for x in geo_list if u'未知' not in x]
geo_result_type=[type_checker(x) for x in geo_result]
if sum(geo_result_type)>0:
geo_result=geo_result[:geo_result_type.index(1)+1]
return geo_result
def geo_match(geo_result):
for info in range(1,len(geo_result)):
if not re.match(geo_result[info-1],geo_result[info]):
return ''
return 'Match'
def geo_validate(geo_result):
if geo_result==[]:
return u'缺失地理信息'
elif type(geo_result[-1])!=list:
if len(geo_result)>1:
for info in range(1,len(geo_result)):
if not geo_match(geo_result):
return u'无效地理信息'
return geo_result[-1]
else:
if len(geo_result)==1:
return ','.join(geo_result[-1])
else:
for choice in range(len(geo_result[-1])):
possible=geo_result[:-1]
possible.append(geo_result[-1][choice])
if not geo_match(possible):continue
return possible[-1]
return u'无效地理信息'
def geo_guarder(geo_result):
geo_result=geo_result.replace(u'北京直辖区','').replace(u'天津直辖区','').replace(u'上海直辖区','').replace(u'重庆直辖区','').replace(u'海南直辖区','').replace(u'湖北直辖区','')
return geo_result
#名称中的地理信息处理完毕
un_geo=unit_name.map(geo_substitute).map(word_flag).map(geo_standard).map(geo_validate).map(geo_guarder)
6、接下来,提取分公司信息
#划分名称中的组织架构
def branch(unit_name):
'''
检测分支机构信息
'''
pattern=[u'公司(.+?)(分公司)',u'局(.+?)(分局)',u'站(.+?)(分站)',u'社(.+?)(分社)',u'公司(.+?)(分行)',u'小学(.+?)(分校)',u'大学(.+?)(分校)',u'高中(.+?)(分校)']
#unit_name=unit_name.decode('utf8')
formation=defaultdict(str)
for choice in range(len(pattern)):
reg=re.search(pattern[choice],unit_name)
try:
formation['parent']=unit_name[:reg.start(1)]
formation['branch']=unit_name[reg.start(1):reg.end(2)]
formation['section']=unit_name[reg.end(2):]
return formation
except:
continue
formation['parent']=unit_name
return formation
7、接下来,就是提取关键字信息
这也是所有步骤中比较困难的一点,主要的问题在于,需要新建行业词典。
下面是去掉行政区划以及分公司后,几个公司名的例子:
后英经贸有限公司
卫强餐饮有限公司
品展装潢有限公司
泰广兴空分设备配件有限公司
宇楠贸易有限公司
金路新力商贸有限公司
旭攀贸易有限公司
元东刀具厂
湛江霞山海明炉料商行
北铭钢铁有限公司
三达化学有限公司
友祚木业有限公司
雨竹广告有限公司
蒂暖实业有限公司
传银五金厂
盛师傅藤器厂
经过分析,公司名称大致可分为2种情况,1、后缀为“有限公司”、“责任有限个公司”、“有限责任公司”、“公司”;2、后缀名为“玩具厂”、“小卖部”、“化工厂”等。
所以,需要新建两个行业词典,其数据来源为公司购买的千万级法人库公司名称与地址,由于数据量较大,需要使用kattle将其导入数据库中进行操作。从所有公司名称中选出后缀为“有限公司”的名称,去掉这四个字的后缀后,分别截取倒数2个,3个,4个……字符,并按照其出现频率排序,然后,通过人工,按照字符串由多到少的顺序选取行业属性,其原因为,例如,假设“文化传播”在四个字符中出现的频率较高,有可能“化传播”在三个字符的时候,频率也较高,所以要先选出较长的字符串,然后手动删除掉较短字符串中,虽然频率较高,但并非一个词的字符串。得到的行业属性如下:
安全防范技术
安全防范技术服务
安装
安装工程
百货
办公设备
包装
包装制品
保洁
保洁服务
泵业
宾馆
玻璃
材料
材料销售
财务顾问
财务咨询
财务咨询服务
餐饮
餐饮服务
餐饮管理
仓储
测绘
策划
茶叶
产品
产品销售
同理,构造企业属性词库:
技术服务部
汽车修理站
配件经营部
营销服务部
机械配件厂
汽车运输队
建筑工程队
证券营业部
汽车维护厂
工会委员会
金属结构厂
金属加工厂
物资经销处
安装工程处
汽车维修站
技术开发部
这样我们就从企业名中剔除了属性,留下关键词,可以进行公司名称的模糊匹配了。
当然,对于部分公司,可能还存在别称,这就需要建立同义词库,这里暂时不讨论。
第二部分,是公司地址的模糊匹配。相对于公司名,地址的模糊匹配较简单。
斗篷山路317号灵智广场
马家工业园C幢4号
马家工业园C幢4号
鸳鸯路8号海阔天空二期1-5-3
高坪镇宏发路30号
之江路934号
滨湖路明湖花园1-4号
花荄五路口文苑路东段203号
渝南大道19号铠恩国际A馆1022B
小吕乡刘坡村
马坊乡钦桥村
珍溪镇滴水村六社
万象城一期A座底商A-1002
朱槿路11号柬埔寨园区5号楼2单元602号
之江路934号
生产物资市场11栋33号
东肖镇东肖南路15号
经过分析,得出公司地址的全称通常使用四级、五级、六级行政区划、某某路+数字+号+具体门牌号,故可以使用正则表达式进行提取:
unit_name=enterprise_datas['brhs_unit_address']
df = unit_name.str.extract('(.*[镇|乡|街道|街|道])?(.*[村|委员会|委会])?(.*[组])?(.*路)?(\d+号)?([\u4e00-\u9fa5]*)?(.*)?',expand=True)
df.columns = ['乡镇街道', '村','组', '道路', '门牌号', '地标名称', '其他']
writer = pd.ExcelWriter('pandas_simple.xlsx', engine='xlsxwriter')
# Convert the dataframe to an XlsxWriter Excel object.
df.to_excel(writer, sheet_name='Sheet1')
writer.save()