我为什么要做这个

我本人就很讨厌看到这种标题——我为什么xxx?喵喵喵?你为什么xxx关我什么事?唉,人总是会变成自己讨厌的人嘛。

换个说法,说说做这个的初衷,其实没有什么初衷,被逼的。

为了尽早完成毕业设计可以出去打工,我选择提前联系导师。于是就被提供了两个题目,我决定做水利相关的知识图谱。你看,多么河海特色!水水水,离开水就不能活了,没毛病。

于是先从图数据库入手,neo4j是个好东西,就决定是你了。在构建复杂的水水水相关实体前,我先拿中国行政区划数据试个水,逻辑相对简单,就一种隶属关系。

献上美图:

Screen Shot 2017-11-10 at 16.08.23.png

起步

首先,准备一下原料,中国行政区划元数据,不求完全结构化,起码半结构化,不然会很痛苦,毕竟有70W+数据。

Neo4j一套,python或者什么脚本语言一款,脑子一坨,足够了。

首先我拿到70W+的XML文件,哎哟我去,ls一下都卡半天,我都害怕python会不会应付不来,跑一遍几小时可能就太惨了。

首先用python列出几个文件:

1
2
files = os.listdir(datas_path)
print(files[0])

哈哈哈,我先搞个文件名,然后直接打开这个文件,避免了「我不能ls就不知道文件名,不知道文件名就不能看内容」的问题。可以看到里面的大致结构,这里取一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<mdExtInfo>
<obj_att>
<AD_GRAD>
<key>行政区划级别</key>
<value></value>
</AD_GRAD>
<AD_CODE>
<key>行政区划代码</key>
<value>441323121207</value>
</AD_CODE>
<UP_AD_NAME>
<key>上级行政区划名称</key>
<value>白盆珠镇</value>
</UP_AD_NAME>
<AD_FULL_NAME>
<key>行政区划全称</key>
<value>广东省-惠州市-惠东县-白盆珠镇-布心村民委员会</value>
</AD_FULL_NAME>
<AD_NAME>
<key>行政区划名称</key>
<value>布心村民委员会</value>
</AD_NAME>
</obj_att>
</mdExtInfo>

枚举行政区划级别

唉,几十万文件,我都不知道有几种行政区,先跑一遍看看有哪些区划级别。

核心代码:

1
2
3
4
5
6
7
cates = set()
for file in files:
doc = parse(datas_path + file)
for item in doc.iterfind('mdExtInfo/obj_att/AD_GRAD'):
title = item.findtext('value')
cates.add(title)
print(title, file, cates)

大概不到十分钟就能跑完,最后得到一共有6种级别:县级、村、国家级、乡级、地市级、省级。

给这些文件分类

每次都跑十分钟太过分了,而且我发现,不同的级别,XML里的结构不同,比如省级有十几个字段,乡级只有五个。所以分类是必须的。

按照不同级别,将同级别的文件名放在一个文件里,方便以后遍历。

到这里,我突然明白了一些道理,来来来,给自己加点戏!

本来以为都是非结构化数据,刚好有在看机器学习,哇可以学以致用了,把数据扔进去,自动聚类,开心的当上调参男孩…… 然而我发现数据格式化的挺不错,就开始自己去找他们的特征,这特喵不就是——人肉学习嘛!然而再反过来想,机器学习到底是什么呢?

构造 Neo4j 需要的 CSV

抽取属性

这么一大坨数据要导入的,用CQL实在太慢,生成指定格式的CSV文件就OK啦~真简单~

1
2
3
4
5
6
AD_CODE,AD_NAME,AD_GRAD,AD_AREA,AD_FULL_NAME,AD_ABBR_NAME,LOW_LEFT_LONG,UP_RIGHT_LONG,UP_RIGHT_LAT,AD_STAT_LAT,AD_STAT,LOW_LEFT_LAT,AD_STAT_LONG
610000000000,陕西省,省级,197025.84,陕西省,陕西,105.4872,111.2422,39.58533,34.26645358,西安市新城区新城大院,31.70674,108.94952476
650000000000,新疆维吾尔自治区,省级,1660000,新疆维吾尔自治区,新疆,73.49989,96.38728,49.18006,43.79179105,乌鲁木齐市中山路479号,34.33374,87.62484437
130000000000,河北省,省级,187700,河北省,河北,113.4551,119.8485,42.61558,38.03705206,石家庄市长安区裕华东路113号,36.04881,114.52429283

...

哈哈哈哈,先把它们有用的属性都拿出来,按级别分类放到6个CSV文件中,结构如上,从此就可以舒服很多了,毕竟不用遍历几十万个文件了。

构建行政单位实体

等…… 等一下……

其实在这之前,我发现了很多坑爹的地方,比如地市级单位有14个属性,但有个别数据只有几个属性,所以遍历的时候一定要判断,不存在的话要用空字符串替代,不然CSV对不齐后面就会更坑。

首先做个属性映射,我不想存到数据库里字段都是瞎眼的大写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
prop_maps = {
'AD_CODE': 'code:ID(AD)',
'AD_NAME': 'name',
# 'AD_GRAD': 'level',
'AD_AREA': 'area:double',
'AD_FULL_NAME': 'full_name',
'AD_ABBR_NAME': 'abbr_name',
'LOW_LEFT_LONG': 'low_left_longtitude:double',
'UP_RIGHT_LONG': 'up_right_longtitude:double',
'UP_RIGHT_LAT': 'up_right_latitude:double',
'AD_STAT_LAT': 'station_latitude:double',
'AD_STAT': 'station',
'LOW_LEFT_LAT': 'low_left_latitude:double',
'AD_STAT_LONG': 'station_longtitude:double'
# 'UP_AD_NAME': 'father_name'
}

根据 Neo4j Import Tool 上的格式,我们要注意ID字段,以及LABEL字段,我决定给它们两个标签,一个是级别,比如Province,一个是AD,表示Administrative Division 行政区划,毕竟这个数据库还会有很多水水水的数据进来。

1
2
3
4
5
6
code:ID(AD),name,area:double,full_name,abbr_name,low_left_longtitude:double,up_right_longtitude:double,up_right_latitude:double,station_latitude:double,station,low_left_latitude:double,station_longtitude:double,:LABEL
610000000000,陕西省,197025.84,陕西省,陕西,105.4872,111.2422,39.58533,34.26645358,西安市新城区新城大院,31.70674,108.94952476,AD;Province
650000000000,新疆维吾尔自治区,1660000,新疆维吾尔自治区,新疆,73.49989,96.38728,49.18006,43.79179105,乌鲁木齐市中山路479号,34.33374,87.62484437,AD;Province
130000000000,河北省,187700,河北省,河北,113.4551,119.8485,42.61558,38.03705206,石家庄市长安区裕华东路113号,36.04881,114.52429283,AD;Province

...

最后的数据如上所示(省级部分)。直接看一坨CSV体验很差对吧,其实不用看全,看到主要的几个字段就是可以了,比如code:ID(AD),表示code字段,行政代码,ID表示我要用于之后导入关系的主键,(AD)表示这个ID不是全局的,是一个叫AD的group,可以理解命名空间,详情请看官方文档。还有LABEL字段,表示标签……废话…… area:double表示用双精度,否则默认为字符串,你不希望数字都变成字符串吧。

构建行政区上下级关系

在这之前,必须要验证很多细节,比如真的所有数据都有上级这个属性嘛?所有数据都是完美的符合规则的嘛?果然不是。。。

甚至,数据还有一些错误的,比如有一条就是陕西省-嘉峪关市-市辖区,为什么我发现他是错误的呢?我是陕西人嘛?我地理及格了嘛?NONONO!因为我想验证一下所有地名的全称,去掉最后一段,即上级行政区全称,是否存在。哈哈哈不存在的,结果就是没找到甘肃省-嘉峪关市-市辖区这条数据,仿佛链条都断掉了。

于是我跑了一下元数据,看看嘉峪关市到底是哪里的,发现陕西和甘肃都有,见鬼了,去网上搜了一下,不存在的。要把这条数据的「陕西」替换为「甘肃」,不想动元数据,所以写在了清理数据的脚本中。

用 UP_AD_NAME 字段找上级

用几个字典,存下地名和代号,然后拿 UP_AD_NAME 去匹配上一级的字典。

我太天真了哈哈哈,测试一下有多少重名的,果然到了县级就没法看了,什么「西城区」,哪个城市都有。

所以干脆拿全名前缀来匹配

直接把所有行政区存到一个字典,全名为键,代号为值。找一个行政区的上级时,其实就是去掉最后一段。比如 江苏省-南京市-江宁区 的上级就是 江苏省-南京市,我验证了一下是不是字典里都存在。果然又㕛叒叕有几个不听话的数据。

比如 新疆生产建设兵团-农四师-兵团七十六团--三连 最后居然两个-,中间是空的,我发现有这样的 新疆生产建设兵团-农四师-兵团七十六团-兵团七十六团 数据,就尝试给他们都加上,但结果是有些又找不到了。所以我干脆删了,后果是某些村级直接属于县级,算了算了没毛病,毕竟数据没给全,至少这样没什么大问题。

最后!居然发现有相同全名的,无话可说,仔细观察可以发现,都是村级的,名字一样,代号却不同,只能认为是干扰数据,随缘选一个吧。

完成CSV

1
2
3
4
5
6
7
8
9
10
11
12
:START_ID(AD),:END_ID(AD),:TYPE
610000000000,000000000000,BELONGS_TO
650000000000,000000000000,BELONGS_TO
130000000000,000000000000,BELONGS_TO
660000000000,000000000000,BELONGS_TO
140000000000,000000000000,BELONGS_TO
520000000000,000000000000,BELONGS_TO
360000000000,000000000000,BELONGS_TO
630000000000,000000000000,BELONGS_TO
230000000000,000000000000,BELONGS_TO

...

最后就是生成这样的数据,再写个导入 neo4j 的脚本即可。

怎么玩 Neo4j

算了下一篇文章再写吧,累死了……