上市公司地址相似度计算&构建关系图谱

作 者: 凌岸 

小伍哥聊风控整理 

来源1: https://zhuanlan.zhihu.com/p/111203086

来源2: https://zhuanlan.zhihu.com/p/459309174

文末点击阅读原文跳转知乎,阅读作者更多精彩文章

今天和各位小伙伴分析一个在搭建知识图谱的时候遇到的一个麻烦的问题。在构建知识图谱的图关系,基础的原始数据来自很多不同的数据源。比如在金融风控领域,我们要构建的知识图谱中,包含地址、公司等出现频率比较高,并且名称一模一样的可能性很低的词汇。

比如下面两个是同一个地址么?

深圳市京基100大厦写字楼

深圳市罗湖区深南东路5016号京基100大厦

那在图关系的构建中,如果把上地址作为两个地址进行处理的话,那么就会创建两个实体,并且这两个实体之间并没有什么关联关系,这种处理方法,肯定是有问题的。此时,我们衍生出了两个问题:

1、第一,对于以上的问题做地址消歧,把两个地址经过处理后,变成同一个地址。

2、或者说,我们做一个两个实体之间的地址相似度,如在图谱中表现为 A节点与B节点的相似度为0.9以上。

这个时候我们需要做的是进行相似度计算。

我们在深圳证券交易所找到了公开的数据集。发现深交所数据已经失效了,给出百度链接在这里。链接: https://pan.baidu.com/wap/init?surl=qAOb17tGPg_HDKsaZ8w5xw 密码: 7ojt

本文目录如下:

一、地址数据预处理

二、数据地址标准化处理

三、相似度计算

四、构造节点表和关系表

五、导入节点表和关系表构建图谱

一、地址数据预处理

我们先导入一些后续用的上的python包,如下:

import numpy as np
import pandas as pd
from csv import reader
import time
import cpca
import jieba
from gensim import corpora, models, similarities

下面,我们导入public_company这个数据集,可以预览一下,该部分数据有哪些字段。

df.columns
Index(['公司代码', '公司简称', '公司全称', '英文名称', '注册地址', 'A股代码', 'A股简称', 'A股上市日期',
'A股总股本', 'A股流通股本', '地 区', '省 份', '城 市', '所属行业', '公司网址'],
dtype='object')

我们选取公司代码、公司简称、公司地址三个字段,并将字段中地址进行去空处理。

对数据做一个预览,所幸我们本次下载的公开数据质量很高,地址中并未出现一些质量差的数据,省去了多余的要做的一些处理。

10a094db3a8fd14d3495883fb6f88d0b.png
def read_data():
    #导入数据,并对数据做一些处理
    df = pd.read_excel("../public_company.xlsx", dtype={'公司代码': 'str'})
    addr_df = df[["公司代码", "公司简称", "注册地址"]]
    addr_df["注册地址"]= addr_df["注册地址"].apply(lambda x: str(x).strip())
    
    return addr_df

二、地址数据标准化处理

地址中有很多存在不标准的情况存在,所以使用capa的包进行标准化处理。写到这里,提一句,本身我是想自己去开发这个一个功能,但是发现了有开源工具,全称为:chinese_province_city_area_mapper。他也提供了全文模式和分词模式两种。

那他的功能也很强大,可以将地址数据补全:

0004cdede76108c240c1b5eb38f7c84d.png

我也附出这个cpca包的github链接:

https://github.com/DQinYuan/chinese_province_city_area_mapper

当然这个包也不是万能的,我们发现还是有一些会处理错误,比如讲 杭州湾 识别为 杭州市。

处理完成以后,我们再将每次地址的省市区拼接为,那么后续我们计算地址相似度的时候就可以,在同一个省市区下面进行计算。

当然,还有一些地址没有写区,我们分两种情况:

1.将使用cpca之后按照省市区拼接

2.将使用cpca之后按照省市拼接

处理结束的样例如下:

6ce41d4059c681d96f9990eb1884e13e.png
def get_dataset(addr_df):
    '''
    #对地址做一个标准化处理,其中导入cpca的包进行处理
    '''
    start = time.clock()
    
    location_str = []
    for i in addr_df['注册地址']:
        location_str.append(i.strip())
        
    addr_cp = cpca.transform(location_str,cut=False,open_warning=False)
    
    #给结果表拼接唯一识别代码
    e_data = addr_df[["公司代码", "公司简称"]]
    addr_cpca = pd.concat([e_data, addr_cp], axis=1)
    
    #1.区不为空
    addr_cpca_1 = addr_cpca[(addr_cpca['省']!= '')&(addr_cpca['市']!= '') & (addr_cpca['区']!= '')]
    addr_cpca_1= addr_cpca_1.dropna()
    
    addr_cpca_11= addr_cpca_1[(addr_cpca['地址']!='')]
    addr_cpca_12= addr_cpca_11. dropna(subset=['地址'])
    
    #将前三个字段完全拼接在一起进行分组然后组内进行相似度分析
    addr_cpca_12['省市区'] = addr_cpca_12['省'] + addr_cpca_12['市'] + addr_cpca_12['区']
    
    addr_cpca_12['省市区长度']=addr_cpca_12['省市区'].apply(lambda x: len(x))
    count_1 = addr_cpca_12['省市区'].value_counts().reset_index()
    count_1= count_1.rename(columns={'index':'省市区', '省市区':'个数'})
    
    count_delete_1= count_1[count_1['个数']==1]
    dataset_1 = pd.merge(addr_cpca_12, count_delete_1, on = '省市区', how = 'left')
    dataset_1= dataset_1[dataset_1['个数']!=1]
    
    #2.区为空  
    addr_cpca_2 = addr_cpca[(addr_cpca['省']!= '')&(addr_cpca['市']!= '') & (addr_cpca['区']== '')]
    addr_cpca_2 = addr_cpca_2.dropna()
    
    addr_cpca_21= addr_cpca_2[(addr_cpca['地址']!='')]
    addr_cpca_22= addr_cpca_21. dropna(subset=['地址'])
    
    #将前三个字段完全拼接在一起进行分组然后组内进行相似度分析
    addr_cpca_22['省市区'] = addr_cpca_22['省'] + addr_cpca_22['市']
    
    addr_cpca_22['省市区长度']=addr_cpca_22['省市区'].apply(lambda x: len(x))
    count_2 = addr_cpca_22['省市区'].value_counts().reset_index()
    count_2= count_2.rename(columns={'index':'省市区', '省市区':'个数'})
    
    count_delete_2 = count_2[count_2['个数']==1]
    dataset_2 = pd.merge(addr_cpca_22, count_delete_2, on = '省市区', how = 'left')
    dataset_2 = dataset_2[dataset_2['个数']!=1]
    
    print("Time used:", (time. clock()-start), "s")
    
    return dataset_1, dataset_2

三、相似度计算

在开始计算相似度时候,较为复杂,就是要比较表里面所有数据之间的相似度,那就是两两之间都要比较,

如果有N表数据,就要比较(N-1)! 次。

但是我们只是不同省份或者不同城市不同区之间的地址其实是不会相似的,我们接着上面的思路,按照区与区地址的地址进行循环比较即可。

我们套用两层循环函数即可,

第一层循环,函数为get_collect(),我们获取字段省市区,的单个省市区的文档,

然后再调用调用第二层循环函数,函数为cycle_first(),

第二层循环函数的思想是:

第1个地址与n-1个地址比较
第2个地址与n-2个地址比较
第3个地址与n-3个地址比较
第n-1个地址与n个地址比较
第n个不执行

第三层函数

计算目标文档与被比较文档之间的地址相似度,其中按照上面的函数举例即为,目标文档为第一个地址,被比较文档为,n-1个所有的地址,

这里我们是有使用TF-DF模型对料库建模,如此即将同一个省市区下面的地址循环完成。

54e3e3a532d349e02edd9f90bfdb4956.png

在算法里面,我们将第一个地址即为,佛山照明的地址”汾江北路64号“同时与剩下的5个地址进行比较相似度,

def cal_similar(doc_goal, document, ssim = 0.1):
#def cal_similar(doc_goai, document):
    '''
    分词;计算文本相似度
    doc_goal,短文本,目标文档
    document,多个文本,被比较的多个文档
    '''
    all_doc_list=[]
    for doc in document:
        doc= "".join(doc)
        doc_list=[word for word in jieba.cut(doc)]
        all_doc_list.append(doc_list)


    #目标文档
    doc_goal = "".join(doc_goal)
    doc_goal_list = [word for word in jieba.cut(doc_goal)]
    
    #被比较的多个文档
    dictionary = corpora.Dictionary(all_doc_list)  #先用dictionary方法获词袋
    corpus = [dictionary.doc2bow(doc) for doc in all_doc_list]  #使用doc2bow制作预料库
    
    #目标文档
    doc_goal_vec = dictionary.doc2bow(doc_goal_list)
    tfidf = models.TfidfModel(corpus)#使用TF-DF模型对料库建模
    index = similarities.SparseMatrixSimilarity(tfidf[corpus], num_features = len(dictionary.keys()))#对每个目标文档,分析测文档的相似度
    
    #开始比较
    sims = index[tfidf[doc_goal_vec]]
    #similary= sorted(enumerate(sims),key=lambda item: -item[1])#根据相似度排序
    
    addr_dict={"被比较地址": document, "相似度": list(sims)}
    similary = pd.DataFrame(addr_dict)
    similary["目标地址"] = doc_goal
    similary_data = similary[["目标地址", "被比较地址", "相似度"]]
    similary_data= similary_data[similary_data["相似度"]>=ssim]
    
    return similary_data


def cycle_first(single_data):
    
    single_value = single_data.loc[:,["公司代码","地址"]].values #提取地址
    
    cycle_data = pd. DataFrame([])
    for key, value in enumerate(single_value):
        if key < len(single_data)-1:
            doc_goal=list(value)[1:]
            document=list(single_data["地址"])[key+1:]
            cycle = cal_similar(doc_goal, document, ssim=0)
            cycle['目标地址代码'] = list(single_data["公司代码"])[key]
            cycle['被比较地址代码'] = list(single_data["公司代码"])[key+1:]
            cycle = cycle[["目标地址代码","目标地址", "被比较地址代码", "被比较地址", "相似度"]]
            #print("循环第",key,"个地址,得到表的行数,",len(cycle),",当前子循环计算进度,",key/len(cycle))
        cycle_data = cycle_data.append(cycle)
        cycle_data = cycle_data.drop_duplicates()
   
    return cycle_data




def get_collect(dataset):


    start = time. clock()
    
    #获取单个省市区的文档
    collect_data = pd.DataFrame([])
    ssq=list(set(dataset['省市区']))
    for v, word in enumerate(ssq):
        single_data = dataset[dataset['省市区'] == word]
        print("循环第",v,"个省市区地址为:",word,",当前此区地址有:",len(single_data),",当前计算进度为:{:.1f}%" .format(v*100/len(ssq)))
        cycle_data = cycle_first(single_data)
        
        collect_data = collect_data.append(cycle_data)#将每个市区得到的结果放入一张表
        
        print("Time: %s" %time.ctime())
        print("-----------------------------------------------------------------------")
    
    print("Time used:",(time.clock() - start), "s")
    
    return collect_data
def run_(par = 0):
    #调用上述函数
    addr_df = read_data()
    dataset_1, dataset_2 = get_dataset(addr_df)
    #dataset. to_csv("../data/addr_data/document_address.csv", index =False)
    #dataset. to_csv( ". /data/addr_data/document_address. csv", index False)
    collect_data_1 = get_collect(dataset_1)
    collect_data_2 = get_collect(dataset_2)
    collect_data = pd.concat([collect_data_1, collect_data_2], axis=0)
    collect_data = collect_data[collect_data["相似度"]>=par].sort_values(by=["相似度"], ascending=[False])
    
    collect_data["相似度"] = collect_data["相似度"].apply(lambda x: ('%.2f' % x))


    return collect_data

我们最终只需要调用总函数,就可以运行上述代码,

In [23]: collect_data = run_(par = 0.1)
Time used: 0.2949850000000005 s
循环第 0 个省市区地址为: 山东省青岛市崂山区 ,当前此区地址有: 5 ,当前计算进度为:0.0%
Loading model cost 0.633 seconds.
Prefix dict has been built succesfully.
Time: Thu Mar 5 21:52:31 2020
-----------------------------------------------------------------------
循环第 1 个省市区地址为: 广东省珠海市金湾区 ,当前此区地址有: 4 ,当前计算进度为:0.5%
Time: Thu Mar 5 21:52:31 2020
...
循环第 97 个省市区地址为: 四川省自贡市 ,当前此区地址有: 2 ,当前计算进度为:98.0%
Time: Wed Mar 4 16:19:32 2020
-----------------------------------------------------------------------
循环第 98 个省市区地址为: 山东省聊城市 ,当前此区地址有: 2 ,当前计算进度为:99.0%
Time: Wed Mar 4 16:19:32 2020
-----------------------------------------------------------------------
Time used: 5.461850999999999 s

可以看到,其实整个代码运行的速度很快,2000多条地址,5秒就通过我们自己组合起来的算法计算完成。

我们可以打印一下前5行结果,看一下相似度计算的结果,肉眼观察一下结果如何:

1cf48d9939ca9ab1172dd919eae034ae.png

每一行都在同一个省市区下面,那么最后的地址判断,我们看到还是准确的,基本上都识别出来了,特别是相似度为0.9的那个几组。

四、构造节点表和关系表

构造节点表和关系表、以及使用neo4j-admin工具导入数据,我们可以看下官网要求的数据格式,以及导入语句的要求:

官网网址如下:

https://neo4j.com/docs/operations-manual/4.0/tutorial/import-tool/

neo4j要求节点表,必须有:ID唯一的不重复的标识,可以加上属性,标签可以选。

关系表关系有三个必填字段, :START_ID,:END_ID和:TYPE,且其中开始和结束ID必须要可以在节点表找得到。

cb60e53bc536a40e9704499ef13ffe3c.png

添加图片注释,不超过 140 字(可选)

为了要将结果数据导入到neo4j中,我们要加工好节点表和关系,我们顺势采用这个结果。

#建表:一张节点表,一张关系表
df = pd.read_excel("../public_company.xlsx", dtype={'公司代码': 'str'})
df_node = df[['公司代码', '公司简称', '公司全称', '注册地址', '所属行业']]
df_node = df_node.rename(columns = {"公司代码": ":ID"})
df_node.to_csv("/../node.csv", index =False)


df_rela = collect_data[collect_data["相似度"]>= 0.6]
df_rela = df_rela[["目标地址代码", "被比较地址代码", "相似度"]]
df_relation = df_rela.rename(columns = {"目标地址代码": ":START_ID", "被比较地址代码": ":END_ID", "相似度":":TYPE"})
df_relation.to_csv("../relationship.csv", index = False)

接下来,我们需要在电脑上装neo4j,假设你已经部署了neo4j。

我们先看ne4j是否已经在开启状态,

bogon:bin mbp$ ./neo4j status
Neo4j is running at pid 11074

发现已经正在运行了,先停止掉,neo4j社区版,只有再关掉图数据库的时候,才能导入数据

bogon:bin mbp$ ./neo4j stop
Stopping Neo4j.. stopped

停掉图数据库以后,我们还要进去database,删掉之前存在graph.db,才能导入数据,否则,导入会报错。

bogon:bin mbp$ cd /Users/mbp/neo4j-community-3.5.4/data/databases
bogon:databases mbp$ rm -rf graph.db
bogon:databases mbp$ cd /Users/mbp/neo4j-community-3.5.4/bin
bogon:bin mbp$ ./neo4j-admin import --mode=csv --database=graph.db --nodes:公司 "/Users/mbp/neo4j-community-3.5.4/import/node.csv" --relationships:地址相似 "/Users/mbp/neo4j-community-3.5.4/import/relationship.csv" --ignore-duplicate-nodes=true --ignore-missing-nodes=true
Neo4j version: 3.5.4
Importing the contents of these files into /Users/mbp/neo4j-community-3.5.4/data/databases/graph.db:
Nodes:
  :公司
  /Users/mbp/neo4j-community-3.5.4/import/node.csv
Relationships:
  :地址相似
  /Users/mbp/neo4j-community-3.5.4/import/relationship.csv


Available resources:
  Total machine memory: 8.00 GB
  Free machine memory: 54.28 MB
  Max heap memory : 1.78 GB
  Processors: 4
  Configured max memory: 5.60 GB
  High-IO: true


...


(1/4) Node import 2020-03-03 12:43:15.718+0800
  Estimated number of nodes: 5.30 k
  Estimated disk space usage: 1.78 MB
  Estimated required memory usage: 1020.07 MB
-......... .......... .......... .......... ..........   5% ∆62ms
.......... .......... .......... .......... ..........  10% ∆0ms
...
.......... .......... .......... .......... ..........  95% ∆0ms
.......... .......... .......... .......... .......... 100% ∆1ms


(2/4) Relationship import 2020-03-03 12:43:16.205+0800
  Estimated number of relationships: 216.00 
  Estimated disk space usage: 7.17 kB
  Estimated required memory usage: 1.00 GB
.......... .......... .......... .......... ..........   5% ∆45ms
.......... .......... .......... .......... ..........  10% ∆0ms
...
.......... .......... .......... .......... ..........  95% ∆0ms
.......... .......... .......... .......... .......... 100% ∆0ms


(3/4) Relationship linking 2020-03-03 12:43:16.257+0800
  Estimated required memory usage: 1020.03 MB
-......... .......... .......... .......... ..........   5% ∆34ms
...
.......... .......... .......... .......... ..........  90% ∆0ms
.......... .......... .......... .......... ..........  95% ∆0ms
.......... .......... .......... .......... .........(4/4) Post processing 2020-03-03 12:43:16.398+0800
  Estimated required memory usage: 1020.01 MB
-......... .......... .......... .......... ..........   5% ∆55ms
.......... .......... .......... .......... ..........  10% ∆0ms
...
.......... .......... .......... .......... .......... 100% ∆0ms




IMPORT DONE in 1s 261ms. 
Imported:
  2214 nodes
  216 relationships
  8856 properties
Peak memory usage: 1.00 GB
bogon:bin mbp$

然后再次开启neo4j图数据库,

bogon:bin mbp$ ./neo4j start
Active database: graph.db
Directories in use:
  home:         /Users/mbp/neo4j-community-3.5.4
  config:       /Users/mbp/neo4j-community-3.5.4/conf
  logs:         /Users/mbp/neo4j-community-3.5.4/logs
  plugins:      /Users/mbp/neo4j-community-3.5.4/plugins
  import:       /Users/mbp/neo4j-community-3.5.4/import
  data:         /Users/mbp/neo4j-community-3.5.4/data
  certificates: /Users/mbp/neo4j-community-3.5.4/certificates
  run:          /Users/mbp/neo4j-community-3.5.4/run
Starting Neo4j.
Started neo4j (pid 11074). It is available at http://localhost:7474/
There may be a short delay until the server is ready.
See /Users/mbp/neo4j-community-3.5.4/logs/neo4j.log for current status.

我们打开浏览器,输入网址 localhost:7474/browser/

a3645a2c4cad510fe9a25a4a09761837.png

我们看到节点和关系以及属性都成功导入了,标签也导入了。

一共有2214个节点,219个关系,其中节点的属性为 公司全称、公司简称、所属行业、注册地址。

实际上关系做成这样,我们是不建议这样做的,因为目前我们的数字种类不多,neo4j不会报错,实际上他的官网有要求,关系总的种类大于6万多条导入时候会报错。

我们在网页端输入cypher语句,看一下节点与节点之间的效果:

MATCH p=()-->() RETURN p
ae500a3ae6eb47ad128afffa71c10727.png

我们选取其中一个小强连通闭环图看一下:

051e1534754d4bc99d85bcda65e3dbea.png b4de6096568631a53bcf4edec27dea52.png

我们看到安车检测和优博讯还有奥拓电子是直接同一栋楼上的。

虽然安车检测与芭田股份以及远望谷所填写的地址貌似看起来不在一起,但是发现他们全部都是在 联合总部大厦的。

题外话,如果这些节点是 申请小额贷款的客户,他们地址高度相似,集中在同一个小区,在关系图谱上面,他们被通过地址相似的方式计算出来,有一个强连通图,那么是否他们有团伙欺诈的嫌疑?我认为这是一个很好的应用场景。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值