查看原文
其他

特别推荐 | “正则表达式”在工业企业数据库匹配中的运用(二)

Dyson 数据Seminar 2021-06-03

上期,我们主要给大家介绍了“正则表达式”具体用法(点此回顾)。本期,我们将继续探索利用“正则表达式”对工业企业数据库中企业名称进行预处理的应用环节。




统一“企业名称”中英文符号


在进行工业企业数据库匹配之前,清理企业名称中的中英文符号是非常有必要的。因为企业名称中夹带的符号,会严重影响匹配效果——由于不同观察期或不同样本来源登记的同一家企业,其名称夹有的中英文符号可能不一致,导致无法正确识别。
我们需要统一企业名称的中英文符号,这里,将中文符号转为英文符号。代码如下:
def punctuation2std(df, col_name, **kwargs): # 将中文符号转英文符号 # 必要参数 {'col_name':****} # 可选参数 {'add':{'\s':''}} punctuation_dict = { '—': '-' # 8212 -> 45 , ';': ';' # 65307 -> 59 , ':': ':' # 65306 -> 58 , '(': '(' # 65288 -> 40 , ')': ')' # 65289 -> 41 , ',': ',' # 65292 -> 44 , '!': '!' # 65281 -> 33 , '【': '[' # 12304 -> 91 , '】': ']' # 12305 -> 93 , '“': '"' # 8220 -> 34 , '”': '"' # 8221 -> 34 , '‘': '\'' # 8216 -> 39 , '’': '\'' # 8217 -> 39 , '%': '%' # 65285 -> 37 , '﹪': '%' # 65130 -> 37 , '1': '1' # 65297 -> 49 , '2': '2' # 65298-> 50 , '3': '3' # 65299 -> 51 , '4': '4' # 65300 -> 52 , '5': '5' # 65301 -> 53 , '6': '6' # 65302 -> 54 , '7': '7' # 65303 -> 55 , '8': '8' # 65304 -> 56 , '9': '9' # 65305 -> 57 , '0': '0' # 65296 -> 48 }
if 'add' in kwargs: punctuation_dict.update(kwargs['add'])
for key in punctuation_dict: df[col_name] = df[col_name].str.replace(key, punctuation_dict[key], regex=True)
return df
左右滑动查看更多
我这边传入的df是一个DataFrame数据表,col_name代表的是需要统一符号的列名。punctuation_dict是我的中英文符号对应字典,每一个key后面我都有备注了中英文符号对应的ASCII码。可以看到的是,稍有疏忽,我们就有可能漏掉几个符号没有统一,从而导致一小部分企业名称无法匹配上。例如65130和37,真的非常像,在有些字体中,不查ASCII码根本看不出区别。
下面是Python中字符串与ASCII码互相转换的函数:
In [1]: ord('﹪')Out[1]: 65130
In [2]: ord('%')Out[2]: 37
In [3]: chr(65130)Out[3]: '﹪'
左右滑动查看更多
最后,我用for循环遍历所有的中文字符,全部替换一遍(当然也可以用apply)。
注意:在一般的数据清洗中,我们会去除空格(由于企业名称可能是英文,这里并不能随便去空格)。空格是属于空白符的一种。空白符在正则表达式中用“\s”表示。仔细看了正则表达式元字符表的同学可以发现,“\s”表示了很多种空白符。例如“\t”(在记事本下按tab键输出的制表符)给我们直观的感受,就是很多个空格,但是他并不是空格,所以用replace(' ', '')是删不掉的。切记切记,“透过现象看本质”,所以我推荐在不是非常了解数据源的情况下,用正则表达式清理空白符会更加保险。
这里我用到了pandas.Series.str对象。千万不能小看这个str,她会直接影响replace函数的使用逻辑。其官方文档中介绍如——

Vectorized string functions for Series and Index. NAs stay NA unless handled otherwise by a particular method. Patterned after Python’s string methods, with some inspiration from R’s stringr package.


需要注意的是,pandas.Series.str与pandas.Series对象都有replace方法。

In [1]: import pandas as pd
In [2]: ser = pd.Series(['AA', 'BBBBB', 'CCC'])
In [3]: ser.dtypeOut[3]: dtype('O')
In [4]: ser.replace('B', 'D')Out[4]: 0 AA1 BBBBB2 CCCdtype: object In [5]: ser.replace('B', 'D', regex=True)Out[5]: 0 AA1 DDDDD2 CCCdtype: object

In [6]: ser.str.replace('B', 'D')Out[6]: 0 AA1 DDDDD2 CCCdtype: object In [7]: ser.str.replace('B', 'D', regex=False)Out[7]: 0 AA1 DDDDD2 CCCdtype: object

左右滑动查看更多

我们先看第4行与第5行代码,翻阅官方文档可知,pandas.Series对象的replace函数,regex参数默认为False。当regex为FALSE的时候,那么是无法把第二个元素中的'B'替换成'D'的。为什么呢?因为此时pandas判断的是'BBBBB'=='B'。如果成立,才会替换;如果让regex参数为True,其实上pandas的逻辑已经是类似于re.sub(r'B', 'BBBBB', some_string)了。
再看第6行与第7行代码,我们将Series转化为了pandas.Series.str对象。这个对象下面,regex参数默认为True。如果regex为True,启用了正则表达式,那么则与第5行代码逻辑相同。若是False,其实pandas的逻辑就变成Python默认的字符串类数据的replace方法了。
In [8]: ser.str.replace(r'[B|C]', 'D')Out[8]: 0 AA1 DDDDD2 DDDdtype: object
In [9]: ser.str.replace(r'[B|C]', 'D', regex=False)Out[9]: 0 AA1 BBBBB2 CCCdtype: object In [10]: 'ABCDEFG'.replace('CD', '123')Out[10]: 'AB123EFG'

左右滑动查看更多

第10行代码就是Python默认的替换逻辑,聪明的你看出来这三种逻辑的区别了吗?灵活运用这三种不同的逻辑能让你在使用pandas做字符串类的数据时效率倍增。
值得一提的是,若要在pandas中使用类似于re.search的逻辑查询(或者说提取),pandas.Series并不提供,而pandas.Series.str对象则有extract函数与之对应。所以虽然将Series转化为pandas.Series.str对象时会稍微损耗一点点效率(因为对象转化需要花点时间),我还是建议要在pandas下使用正则表达式,都统一转化为pandas.Series.str来使用,以免弄混了徒增找BUG的时间。
既然提到了pandas.Series.str,我想顺带提一下这个对象的一些用途。
In [1]: ser.str[:3] # 切片Out[1]: 0 AA1 BBB2 CCCdtype: object
In [2]: ser.str.count('A')Out[2]: 0 21 02 0dtype: int64
左右滑动查看更多
pandas的人性化就在这里体现出来了。这个对象所包含的方法,跟Python默认的字符串处理方法非常类似。这样使用不仅在代码上更加简洁, 并且对比apply函数的话,能带来代码运行效率上的微量提升(主要因为正则表达式引擎本身比较耗资源),有兴趣的同学可以参考本公众号以前的文章(点此回顾)。




企业名称主体部分模糊匹配


统一完符号后,就开始对企业名称进行精确匹配了。使用merge函数来做表连接。
res_temp = pd.merge(test_df , parent_df , how='left' , left_on='lookup_name_x' , right_on='lookup_name_y' , suffixes=('','[step%s]' %data['i'])                    )
左右滑动查看更多
接着把精确匹配成功的样本筛选出来,放在校验数据集中。把没有成功匹配上的样本保留下来,再进行企业名称主体的模糊匹配。
在模糊匹配之前,提取工业企业数据库中未匹配上样本企业的名称主体部分。正如我前面说的,写正则表达式就是一个找规律的游戏。我们先来看看原始数据中企业名称的模样。

图一
按照上篇介绍的企业名称命名规则,我们这里把“企业取名 + 行业属性”视为企业名称主体,即要想办法对“地区冠名”与“企业类型”进行模式化处理。
先看地区冠名部分,团队同事整理过民政局的县码全表,如下图:

图二

exist字段表示这个区的名字以及县码还有没有在被使用

不能使用最新的县码的原因,是因为一个撤销的区内的绝大部分企业都可能沿用之前的区名。

我们需要把这数千条数据转换为正则表达式才能用的。假设我们已经把图五中的数据读入了变量county_code,你第一想法有可能是这么来写正则表达式:
In [1]: '(' + county_code['prov'] + '?)?(' + county_code['city'] + '?)?(' + county_code['county'] + '?)?'Out[1]: 0 (北京市?)?(北京市?)?(东城区?)?1 (北京市?)?(北京市?)?(西城区?)?2 (北京市?)?(北京市?)?(崇文区?)?3 (北京市?)?(北京市?)?(宣武区?)?# 以下结果省略
左右滑动查看更多
然后再将其转为列表,用“|”连接,构成一个超级长的正则表达式(长度为144710!!!),来处理这些企业名称。

这种做法是不合理的,原因是我们要处理的企业名称量是非常大的!而正则表达式越长,程序处理难度是呈指数增长的。因此,我们只能对省、市、县分开处理,那么正则表达式可能是这样的(我这边先以处理“省”为例子展开):
In [2]: '|'.join(set(county_code['prov'].str.replace('(省|市|自治区)', '').tolist()))Out[2]: '江苏|重庆|广西壮族|安徽|福建|江西|湖北|宁夏回族|海南|山东|贵州|黑龙江|浙江|山西|云南|吉林|青海|河南|广东|内蒙古|西藏|河北|上海|辽宁|陕西|新疆维吾尔|天津|四川|湖南|甘肃|北京'
左右滑动查看更多
或者是这样的:
In [3]: '|'.join((county_code['prov'].drop_duplicates() + '?').tolist())Out[3]: '北京市?|天津市?|河北省?|山西省?|内蒙古自治区?|辽宁省?|吉林省?|黑龙江省?|上海市?|江苏省?|浙江省?|安徽省?|福建省?|江西省?|山东省?|河南省?|湖北省?|湖南省?|广东省?|广西壮族自治区?|海南省?|重庆市?|四川省?|贵州省?|云南省?|西藏自治区?|陕西省?|甘肃省?|青海省?|宁夏回族自治区?|新疆维吾尔自治区?'
左右滑动查看更多

天真!!!

👉省的关键字较少,还能勉强这么处理。但是市、县(区)的名称较多且具有混淆性。如,县码140402代表的是“山西省长治市城区”,县名为“城区”;再如“沁县”中的“县”字,其本身就是县名中的一部分,不能随意去掉。

👉“企业取名”部位,可能含某个市县的名字。例如,湖南长沙市芙蓉区,在工商库里检索企业名称中包含“芙蓉”两字且不属于长沙市内的企业,约有7000家,像“芙蓉酒家”、“芙蓉网吧”、“芙蓉丝绒”、“芙蓉乡”等使用“芙蓉”二字的企业不胜枚举,更别说“广东省广州市白云区”了。因此,我们只能从企业名称的前缀部分逐步剔掉地区冠名,即使用元字符“^”,先处理以省名开头的部分,接着再考虑去掉以县名开头的部分。

👉在处理县级名称的时候,我们还遇到更为复杂问题。即使是逐步剔除市县名,像“常德芙蓉王广告实业公司”这样的企业,若使用“芙蓉区?”这样的正则表达式,还是会出问题。经过再三考虑,我们决定保留县名。

于是,处理到市级名称的时候,正则表达式代码写成:
In [4]: '|'.join(('^' + county_code['city'].drop_duplicates() + '?').tolist())Out[4]: '^石家庄市?|^唐山市?......(省略)......^巴彦淖尔盟|^乌兰察布盟|......(省略)......
左右滑动查看更多
这样就大功告成了吗?
仍然没有!
例如“新疆维吾尔自治区?”,“自治区”是一个词语,如果去匹配“新疆维吾尔自治”的话,不仅会漏掉很多“新疆维吾尔”单独出现的情况,还可能在删除“新疆维吾尔自治区”的时候,把“区”字给留下了,给市级名称的匹配留下隐患。
所以,必须再次调整:
In [5]: '|'.join(('^' + county_code['city'].drop_duplicates() + '?').tolist())Out[5]: '^北京市?|^天津市?......(省略)......^巴彦淖尔盟|^乌兰察布盟|......(省略)......^大兴安岭(地区)?|^延边朝鲜族(自治州)?......(省略)......
左右滑动查看更多
经过反复的踩雷与测试,在优化个别代码之后,战战兢兢做出了比较符合预期的正则表达式。所以说,使用正则表达式没有其他窍门,就是要反复练,反复纠错,使其精度最大化。
结合上述代码,运行我们前面提到的pandas.Series.str下的replace函数,我们就可以把企业名称中省市县的关键词去掉。接着再将企业类型、括号中的内容等可能由于多一个字少一个字导致匹配出错的内容都删掉,得到企业名称主体。通过这种方法得到的结果,识别率会有很大提升。最后,我们再通过法人或县码进行二次检验与筛选,保证了正确性。结果如图所示:

图三
上图三,是我们对工业企业数据库进行纵向合并建立面板以及横向融合工商企业数据库的最终结果展示。
在我们的数据库中,QYID是经过匹配后,工业企业数据库新生成的企业唯一ID。
BRANDT_ID是我们参考brand方法进行纵向匹配时生成的ID,ENTID是我们融合工商企业数据库所产生ID。
当然,以上匹配,向大家展示的仅仅是企业名称的匹配。整个工业企业数据库匹配,我们还利用了较多其他的融合变量,如组织机构代码、法人名称、县码等等信息进行识别与校验。将来有机会再介绍。




后记


肯定有同学觉得使用正则表达式很麻烦,为什么不用现在比较流行的机器学习呢?
我当时的考虑是,机器学习其实是一个黑箱。我们将数据塞进去,等他出结果够拿来用。并不知道他在里面干了些什么事。“我们使用了隐马尔可夫模型做了企业名称分词。”这可能是我能做的为数不多的解释了。
如果解释数据匹配逻辑不是问题,若要使用机器学习来做文本处理,分词将会是最关键的一步。就拿前面的例子来说,你怎么告诉电脑,“芙蓉王”是一个品牌,而不是“芙蓉区”这个区名呢?我们会需要把所有的省市县名、所有的品牌名称等等全部做成词典放入程序。那就需要去网上把所有的品牌名称都爬一遍,非常耗时间。
所以,面对规律性这么强的企业名称,“杀鸡焉用牛刀“呢?






►往期推荐

回复【Python】👉简单有用易上手

回复【学术前沿】👉机器学习丨大数据

回复【数据资源】👉公开数据

回复【可视化】👉你心心念念的数据呈现

回复【老姚专栏】👉老姚趣谈值得一看


►一周热文

中秋特刊 | R还可以绘图:八月十五招友玩“月”

特别推荐丨老姚专栏:遗漏变量偏差中的高估与低估

工具&方法丨经济学圈特供 使用Jupyter Notebook的12个小技巧

数据呈现 | 用R绘制箱线、散点图,揭示地区企业进退规律




数据Seminar

这里是大数据、分析技术与学术研究的三叉路口


作者:Dyson(刘颖波)审阅、修订:杨奇明、简华(何年华)、江东(刘良东)编辑:青酱






    欢迎扫描👇二维码添加关注    


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存