《数据分析实战-托马兹.卓巴斯》读书笔记第8章--图(NetworkX、Gephi)修订版

python数据分析个人学习读书笔记-目录索引

 第8章介绍了如何使用NetworkX和Gephi来对图数据进行处理、理解、可视化和分析。

本章中,会学习以下技巧:
·使用NetworkX在Python中处理图对象
·使用Gephi将图可视化
·识别信用卡信息被盗的用户
·识别谁盗窃了信用卡

8.1导论

图无处不在;当你开GPS驾车时,你可能还没意识到这是在解决一个图问题,以最短路径或最短时间从A点到达B点。
图论起源于18世纪,Leonard Euler提出了七桥问题的解法。(关于这个话题,可以参考http://www2.gsu.edu/~matgtc/origin%20of%20graph%20theory.pdf)从那时起,有些看上去不能解决的问题得到解决了;互联网(或者你的本地网络)可以看成一个图,航线调度问题可以建模为一个图,或者(我们后面会看到)一个社交网络在我们意识到这是一个图之后就更容易处理了。
图是由节点和连接两个节点的边组成:
邀月工作室

前面的例子可能是最简单的网络,只有两个节点,和连接它们的一条边。这是一个无向图:节点之间的连接没有方向的差别。比如,如果节点是人,边可以代表人与人之间相互认识。这种情况下,我们总不能指定一个方向,毕竟关系是双向的:A认识B,B也认识A。
图中也可以带方向。这可以是一个建筑的工程计划,这里节点是特定的任务,而边代表计划的行进。

8.2使用NetworkX在Python中处理图对象

社交网络的爆发,比如Facebook、Twitter或LinkedIn等,带来了很多问题。比如:谁是谁的朋友,我能经由朋友圈接触到中意公司的招聘者么,我和Obama总统是相连的么,谁是我的网络里最有影响力的人?
这些问题在当今社会很常见,作为数据科学家,应该知道怎么解决。
本技巧中,我们会用一个假造的20人的Twitter网络。你会学到如何创建一个图,加入节点和边(以及附加的元数据),分析图,导出图以便用Gephi阅读。
准备:需装好NetworkX、collections和Matplotlib。

步骤:NetworkX提供了一个构建和分析图的框架。可以处理无向图,也能处理有向图。另外,还可以建模多重图:两个节点之间可以有多条边以及带有闭环的图。

/*
pip install Networkx
*/

本技巧中要用到的程序在本章文件夹(Codes/Chapter08)下的graph_handling.py文件中:

1 import networkx as nx
2 import networkx.algorithms as alg
3 import numpy as np
4 import matplotlib.pyplot as plt
5 
6 # create graph object
7 twitter = nx.Graph()

原理:首先导入需要的模块;networkx.algorithms提供了后面要用到的各种图算法。
然后,我们创建无向图的框架,两个节点,一条边。
要了解所有图类型,可查看NetworkX的文档:http://networkx.github.io/documentation/networkx-1.10/reference/classes.html
现在,我们加一些节点。前面提过,我们的网络中有20个人。用.add_node(...)方法加入:

 1 # add users
 2 twitter.add_node('Tom', {'age': 34})
 3 twitter.add_node('Rachel', {'age': 33})
 4 twitter.add_node('Skye', {'age': 29})
 5 twitter.add_node('Bob', {'age': 45})
 6 twitter.add_node('Mike', {'age': 23})
 7 twitter.add_node('Peter', {'age': 46})
 8 twitter.add_node('Matt', {'age': 58})
 9 twitter.add_node('Lester', {'age': 65})
10 twitter.add_node('Jack', {'age': 32})
11 twitter.add_node('Max', {'age': 75})
12 twitter.add_node('Linda', {'age': 23})
13 twitter.add_node('Rory', {'age': 18})
14 twitter.add_node('Richard', {'age': 24})
15 twitter.add_node('Jackie', {'age': 25})
16 twitter.add_node('Alex', {'age': 24})
17 twitter.add_node('Bart', {'age': 33})
18 twitter.add_node('Greg', {'age': 45})
19 twitter.add_node('Rob', {'age': 19})
20 twitter.add_node('Markus', {'age': 21})
21 twitter.add_node('Glenn', {'age': 24})

 Tips:
 

/*    
File "D:\Java2018\practicalDataAnalysis\Codes\Chapter08\graph_handling_org.py", line 10
    twitter.add_node('Tom', {'age': 34}))
                                        ^
SyntaxError: invalid syntax 
*/

 

解决方案:查阅官方资料,注意版本差异,使用UTM属性赋值

 1 /*  
 2 #Use keywords set/change node attributes:
 3 >>>
 4 G.add_node(1, size=10)
 5 G.add_node(3, weight=0.4, UTM=('13S', 382871, 3972649))
 6 
 7 import networkx as nx
 8 import numpy as np
 9 import matplotlib.pyplot as plt
10 
11 # create graph object
12 twitter = nx.Graph()
13 twitter.add_node('Tom', UTM={'age',33})
14 
15 print(twitter.nodes['Tom']['UTM'])
16 #------------------------------------
17 {33, 'age'}
18 
19 # add users
20 
21 twitter.add_node('Rachel')
22 twitter.nodes['Rachel']['age'] = 33
23 
24 twitter.add_node('Skye')
25 twitter.nodes['Skye']['age'] = 29
26 
27 twitter.add_node('Bob')
28 twitter.nodes['Bob']['age'] = 45
29 
30 twitter.add_node('Mike')
31 twitter.nodes['Mike']['age'] = 23
32 
33 twitter.add_node('Peter')
34 twitter.nodes['Peter']['age'] = 46
35 
36 twitter.add_node('Matt')
37 twitter.nodes['Matt']['age'] = 58
38 
39 twitter.add_node('Lester')
40 twitter.nodes['Lester']['age'] = 65
41 
42 twitter.add_node('Jack')
43 twitter.nodes['Jack']['age'] = 32
44 
45 twitter.add_node('Max')
46 twitter.nodes['Max']['age'] = 75
47 
48 twitter.add_node('Linda')
49 twitter.nodes['Linda']['age'] = 23
50 
51 twitter.add_node('Rory')
52 twitter.nodes['Rory']['age'] = 18
53 
54 twitter.add_node('Richard')
55 twitter.nodes['Richard']['age'] = 24
56 
57 twitter.add_node('Jackie')
58 twitter.nodes['Jackie']['age'] = 25
59 
60 twitter.add_node('Alex')
61 twitter.nodes['Alex']['age'] = 24
62 
63 twitter.add_node('Bart')
64 twitter.nodes['Bart']['age'] = 33
65 
66 twitter.add_node('Greg')
67 twitter.nodes['Greg']['age'] = 45
68 
69 twitter.add_node('Rob')
70 twitter.nodes['Rob']['age'] = 19
71 
72 twitter.add_node('Markus')
73 twitter.nodes['Markus']['age'] = 21
74 
75 twitter.add_node('Glenn')
76 twitter.nodes['Glenn']['age'] = 24
77 
78 */

方法以节点ID作为第一个参数,第二个参数是可选的;参数二是一个修饰节点的元数据字典。
节点需要是去重的,也就是说,如果有两个Peter,你得通过某种方式区分他们(比如用姓,或者序列号)。
你也能用列表添加节点。查看这里的.add_nodes_from(...)方法:

http://networkx.github.io/documentation/networkx-1.10/reference/generated/networkx.Graph.add_nodes_from.html#networkx.Graph.add_nodes_from
https://networkx.github.io/documentation/networkx-2.4/reference/classes/generated/networkx.Graph.add_node.html#networkx.Graph.add_nodes_from

也可以通过直接访问节点的方式添加元数据。既然这是一个代表Twitter社交网络的图,那我们就加一下发帖数吧:

 1 # add posts
 2 twitter.node['Rory']['posts'] = 182
 3 twitter.node['Rob']['posts'] = 111
 4 twitter.node['Markus']['posts'] = 159
 5 twitter.node['Linda']['posts'] = 128
 6 twitter.node['Mike']['posts'] = 289
 7 twitter.node['Alex']['posts'] = 188
 8 twitter.node['Glenn']['posts'] = 252
 9 twitter.node['Richard']['posts'] = 106
10 twitter.node['Jackie']['posts'] = 138
11 twitter.node['Skye']['posts'] = 78
12 twitter.node['Jack']['posts'] = 62
13 twitter.node['Bart']['posts'] = 38
14 twitter.node['Rachel']['posts'] = 89
15 twitter.node['Tom']['posts'] = 23
16 twitter.node['Bob']['posts'] = 21
17 twitter.node['Greg']['posts'] = 41
18 twitter.node['Peter']['posts'] = 64
19 twitter.node['Matt']['posts'] = 8
20 twitter.node['Lester']['posts'] = 4
21 twitter.node['Max']['posts'] = 2

如你所见,你可以通过ID访问节点并设置元数据;当你想在创建节点时就设置一些固定参数(比如前面指定的年龄)并及时更新元数据(比如用户发推)时,这是很有用的。
现在,看下谁认识谁:

 1 # add followers
 2 twitter.add_edge('Rob', 'Rory', {'Weight': 1})
 3 twitter.add_edge('Markus', 'Rory', {'Weight': 1})
 4 twitter.add_edge('Markus', 'Rob', {'Weight': 5})
 5 twitter.add_edge('Mike', 'Rory', {'Weight': 1})
 6 twitter.add_edge('Mike', 'Rob', {'Weight': 1})
 7 twitter.add_edge('Mike', 'Markus', {'Weight': 1})
 8 twitter.add_edge('Mike', 'Linda', {'Weight': 5})
 9 twitter.add_edge('Alex', 'Rob', {'Weight': 1})
10 twitter.add_edge('Alex', 'Markus', {'Weight': 1})
11 twitter.add_edge('Alex', 'Mike', {'Weight': 1})
12 twitter.add_edge('Glenn', 'Rory', {'Weight': 1})
13 twitter.add_edge('Glenn', 'Rob', {'Weight': 1})
14 twitter.add_edge('Glenn', 'Markus', {'Weight': 1})
15 twitter.add_edge('Glenn', 'Linda', {'Weight': 2})
16 twitter.add_edge('Glenn', 'Mike', {'Weight': 1})
17 twitter.add_edge('Glenn', 'Alex', {'Weight': 1})
18 twitter.add_edge('Richard', 'Rob', {'Weight': 1})
19 twitter.add_edge('Richard', 'Linda', {'Weight': 1})
20 twitter.add_edge('Richard', 'Mike', {'Weight': 1})
21 twitter.add_edge('Richard', 'Alex', {'Weight': 1})
22 twitter.add_edge('Richard', 'Glenn', {'Weight': 1})
23 twitter.add_edge('Jackie', 'Linda', {'Weight': 1})
24 twitter.add_edge('Jackie', 'Mike', {'Weight': 1})
25 twitter.add_edge('Jackie', 'Glenn', {'Weight': 1})
26 twitter.add_edge('Jackie', 'Skye', {'Weight': 1})
27 twitter.add_edge('Tom', 'Rachel', {'Weight': 5})
28 twitter.add_edge('Rachel', 'Bart', {'Weight': 1})
29 twitter.add_edge('Tom', 'Bart', {'Weight': 2})
30 twitter.add_edge('Jack', 'Skye', {'Weight': 1})
31 twitter.add_edge('Bart', 'Skye', {'Weight': 1})
32 twitter.add_edge('Rachel', 'Skye', {'Weight': 1})
33 twitter.add_edge('Greg', 'Bob', {'Weight': 1})
34 twitter.add_edge('Peter', 'Greg', {'Weight': 1})
35 twitter.add_edge('Lester', 'Matt', {'Weight': 1})
36 twitter.add_edge('Max', 'Matt', {'Weight': 1})
37 twitter.add_edge('Rachel', 'Linda', {'Weight': 1})
38 twitter.add_edge('Tom', 'Linda', {'Weight': 1})
39 twitter.add_edge('Bart', 'Greg', {'Weight': 2})
40 twitter.add_edge('Tom', 'Greg', {'Weight': 2})
41 twitter.add_edge('Peter', 'Lester', {'Weight': 2})
42 twitter.add_edge('Tom', 'Mike', {'Weight': 1})
43 twitter.add_edge('Rachel', 'Mike', {'Weight': 1})
44 twitter.add_edge('Rachel', 'Glenn', {'Weight': 1})
45 twitter.add_edge('Lester', 'Max', {'Weight': 1})
46 twitter.add_edge('Matt', 'Peter', {'Weight': 1})

.add_edge(...)方法以源节点为第一个参数,以目标节点作为第二个参数;在我们这个图中,顺序并不重要,因为是无向图。你可以只提供这两个参数;元数据字典是可选的(和节点的情况类似)。我们使用Weight参数区分某些连接(下面会用到)。让我们加上描述这些区别的关系:

 1 # add relationship
 2 twitter['Rob']['Rory']['relationship'] = 'friend'
 3 twitter['Markus']['Rory']['relationship'] = 'friend'
 4 twitter['Markus']['Rob']['relationship'] = 'spouse'
 5 twitter['Mike']['Rory']['relationship'] = 'friend'
 6 twitter['Mike']['Rob']['relationship'] = 'friend'
 7 twitter['Mike']['Markus']['relationship'] = 'friend'
 8 twitter['Mike']['Linda']['relationship'] = 'spouse'
 9 twitter['Alex']['Rob']['relationship'] = 'friend'
10 twitter['Alex']['Markus']['relationship'] = 'friend'
11 twitter['Alex']['Mike']['relationship'] = 'friend'
12 twitter['Glenn']['Rory']['relationship'] = 'friend'
13 twitter['Glenn']['Rob']['relationship'] = 'friend'
14 twitter['Glenn']['Markus']['relationship'] = 'friend'
15 twitter['Glenn']['Linda']['relationship'] = 'sibling'
16 twitter['Glenn']['Mike']['relationship'] = 'friend'
17 twitter['Glenn']['Alex']['relationship'] = 'friend'
18 twitter['Richard']['Rob']['relationship'] = 'friend'
19 twitter['Richard']['Linda']['relationship'] = 'friend'
20 twitter['Richard']['Mike']['relationship'] = 'friend'
21 twitter['Richard']['Alex']['relationship'] = 'friend'
22 twitter['Richard']['Glenn']['relationship'] = 'friend'
23 twitter['Jackie']['Linda']['relationship'] = 'friend'
24 twitter['Jackie']['Mike']['relationship'] = 'friend'
25 twitter['Jackie']['Glenn']['relationship'] = 'friend'
26 twitter['Jackie']['Skye']['relationship'] = 'friend'
27 twitter['Tom']['Rachel']['relationship'] = 'spouse'
28 twitter['Rachel']['Bart']['relationship'] = 'friend'
29 twitter['Tom']['Bart']['relationship'] = 'sibling'
30 twitter['Jack']['Skye']['relationship'] = 'friend'
31 twitter['Bart']['Skye']['relationship'] = 'friend'
32 twitter['Rachel']['Skye']['relationship'] = 'friend'
33 twitter['Greg']['Bob']['relationship'] = 'friend'
34 twitter['Peter']['Greg']['relationship'] = 'friend'
35 twitter['Lester']['Matt']['relationship'] = 'friend'
36 twitter['Max']['Matt']['relationship'] = 'friend'
37 twitter['Rachel']['Linda']['relationship'] = 'friend'
38 twitter['Tom']['Linda']['relationship'] = 'friend'
39 twitter['Bart']['Greg']['relationship'] = 'sibling'
40 twitter['Tom']['Greg']['relationship'] = 'sibling'
41 twitter['Peter']['Lester']['relationship'] = 'generation'
42 twitter['Tom']['Mike']['relationship'] = 'friend'
43 twitter['Rachel']['Mike']['relationship'] = 'friend'
44 twitter['Rachel']['Glenn']['relationship'] = 'friend'
45 twitter['Lester']['Max']['relationship'] = 'friend'
46 twitter['Matt']['Peter']['relationship'] = 'friend'

我们的网络中有四种关系:friend、spouse、sibling和generation。最后一个是父子关系。相应的Weight值是1、5、2和2。
注意我们是如何访问边并设置元数据的,例如,twitter['Rachel']['Tom'],然后设置interest属性。
更多:NetworkX提供了多种辅助访问、操作以及分析图的有用方法。
要获得图中所有节点的列表,可以不带任何参数调用.nodes(...)方法,即.nodes()。打印出来,类似这样:

邀月工作室

.nodes(...)方法也接受data参数;.nodes(data=True)会返回每个节点的元数据:

邀月工作室
可以通过类似的方式访问边;调用.edges(...)方法;调用.edges(Data=True)会返回下面的列表(已简化):
邀月工作室

创建了图,我们来分析其结构。我们看的第一个指标就是图的密度:

1 # graph's density and centrality
2 print('\nDensity of the graph: ', nx.density(twitter)) 

.density(...)参数度量图中节点之间的连通度;一个图中,所有节点都与其他所有节点相连(没有环)时密度是1。简单来说,图的密度就是图中边的数目与可能的边的数目的比例。对于我们的图,我们有如下结果:

/* 
Density of the graph:  0.23684210526315788
 */

这说明我们的图是稀疏的:可能的边的数目用等式n*(n-1)/2计算,所以我们得到20*19/2=190。我们图中边的总数是45。即,图中呈现的可能连接只占23.7%。
如果你看不明白为什么是n*(n-1)/2,查看这里:http://jwilson.coe.uga.edu/EMAT6680 Fa2013/Hendricks/Essay%202/Essay2.html
另一个有用的指标是度数。节点的度数是其所有邻居的数目。节点相邻指的是有边直接相连。也就是说,节点的度数不过就是邻接的节点的总数。
.centrality.degree_centrality(...)方法计算的是节点的度数与图中最大可能度数(即节点数减1)的比例:

1 centrality = sorted(
2     alg.centrality.degree_centrality(twitter).items(),
3     key=lambda e: e[1], reverse=True)

计算出来的结果根据中心度降序排列;这样我们可以看出图中谁联系的最多:
邀月工作室
看来Mike和Glenn是图中连接最多的人(下面我们会从图像上推断这一点)。
.assortativity.average_neighbor_degree(...)方法,对每一个节点,计算邻居的平均度数。这个指标能让我们找出谁是网络中连接最多的人的好友——如果你要和某个你不知道的人建立联系,这个指标就很有用了:

1 average_degree = sorted(
2     alg.assortativity.average_neighbor_degree(twitter)\
3     .items(), key=lambda e: e[1], reverse=True)

我们看看最有影响力的人:
邀月工作室

可见,当你想扩展网络时,Rory、Jackie、Richard和Alex是你的最优选择,比如他们都认识Mike和Glenn。
其他还有一些有用的指标。作为新手,可以查看这个网站,http://webwhompers.com/graph-theory.html

以及学习NetworkX的文档,http://networkx.github.io/documentation/networkx-1.10/reference/algorithms.html
NetworkX框架内置有绘图功能。
NetworkX可调用Graphviz和pydot,但这两个模块只能在Python 2.7下使用,还没有导入到Python 3.4中,我们没法利用它们。
要绘制我们创建的网络,我们调用.draw_networkx(...)方法:

1  # draw the graph
2 nx.draw_networkx(twitter)
3 plt.savefig('../../Data/Chapter08/twitter_networkx.png')

得到的图看上去不太有吸引力,也不太有信息性,因为内容有重叠,很难阅读。不过还是显示了图的结构。注意,你的图即使有完全相同的连接,也很有可能看上去有个不同的布局。
邀月工作室
幸运的是,我们可以将图导出成Gephi能处理的GraphML格式,这是理解我们社交网络的下一站:

1 # save graph
2 nx.write_graphml(twitter,
3     '../../Data/Chapter08/twitter.graphml')

Tips:

某些情况下会报这个

/*
Traceback (most recent call last):
  File "D:\Java2018\practicalDataAnalysis\Codes\Chapter08\graph_handling.py", line 182, in <module>
    '../../Data/Chapter08/twitter.graphml')
  File "<D:\tools\Python37\lib\site-packages\decorator.py:decorator-gen-658>", line 2, in write_graphml_lxml
  File "D:\tools\Python37\lib\site-packages\networkx\utils\decorators.py", line 240, in _open_file
    result = func_to_be_decorated(*new_args, **kwargs)
  File "D:\tools\Python37\lib\site-packages\networkx\readwrite\graphml.py", line 149, in write_graphml_lxml
    infer_numeric_types=infer_numeric_types)
  File "D:\tools\Python37\lib\site-packages\networkx\readwrite\graphml.py", line 613, in __init__
    self.add_graph_element(graph)
  File "D:\tools\Python37\lib\site-packages\networkx\readwrite\graphml.py", line 652, in add_graph_element
    T = self.xml_type[self.attr_type(k, "node", v)]
KeyError: <class 'set'>
 */


参考:这里有NetworkX的另一个快速介绍:http://www.python-course.eu/networkx.php

8.3使用Gephi将图可视化

Gephi是一个用于分析和可视化复杂网络的开源应用。可在任何运行Java的平台上运行,所以在Windows、Linux或Mac环境下都可以使用。
要获得Gephi,访问https://gephi.org/users/download/,下载适合你的系统的包。下载后,根据弹窗安装程序。

/*
我们在Mac OS X El Capitan上运行0.8.2-beta版的Gephi时遇到了很多问题。
Gephi的最新版本使用了与移植到Mac OS X上的Java不兼容的库(比如,https://github.com/gephi/gephi/issues/1141)。
即使安装Java 6历史版本也没法运行Gephi 0.8.2-beta;只有降到0.8.1-beta才可行。本技巧中所有的可视化都是用0.8.1-beta版本实现的。
*/

邀月安装的是0.9.2版本。多语言版本,如果你觉得不爽,可以切换到中文。
准备:需装好Gephi。
步骤:用你的平台的特定方式打开软件包。窗口的顶部是视图控制(如下图所示);程序默认视图是Overview,Data Laboratory视图让你可以访问并编辑图的基础数据(节点和边),而Preview视图是打印前的预览:

邀月工作室


视图控制让你可以控制节点和边的展现;你可以更改节点和边的颜色,以及大小和标签。

布局控制让你可以控制图的布局;我们会简略地看下如何使用包括的算法。

统计与过滤部分让你可以计算图表的统计数据(比如前一技巧里介绍过的平均度数或图密度)。这里也允许你过滤某些节点或边,让你聚焦于图中感兴趣的那一小块。

图窗口展示了图。访问File|Open,导航至Data/Chapter08文件夹,选择twitter.graphml。打开图时,你看到的应该类似这样:
邀月工作室
不是很信息性。作者喜欢做的第一件事就是给节点染色以知道人群的年龄,以及更改节点的大小以体现发帖的数目(原谅我切换到中文界面了。^_^)
邀月工作室
导航至图控制部分,并选择Ranking页卡。在Nodes页,从下拉菜单中选择age,你看到的应该类似这样:


邀月工作室
新版本Gephi也许和这里长得不一样,Color选项也可能有不同的选项。要熟悉Gephi,这就不该是个问题,跟着本技巧中的例子走应该不难。
我们保持原来的颜色,只改颜色范围的界限点(或转换函数)。单击Spline...,将曲线调整成上图。
单击Apply,节点的颜色应该变了,不过你可能注意不到,因为节点的大小也变了;我们现在修正这一点。
仍然是Nodes页,首先点击图控制部分右上角的钻石图标;鼠标悬浮其上,它会显示Size/Weight。现在,从下拉菜单中选择posts。你看到的应该类似这样:
邀月工作室
单击Apply,这时节点的大小应该反映发帖数,如上图。
现在颜色可以辨识。然而,要认出节点代表哪个人,我们要给每个节点加上标签。
导航至图窗口。底部有一排图标。点击T(下面的截图中高亮部分):
邀月工作室
好了,我们知道谁是谁了。
图还不能展示任何特殊的形状。所以,我们要发掘更有意义的结构。导航至Layout控制页卡。从下拉菜单中选择Force Atlas——一个可以帮我们发现图的隐藏形式的算法。
这个算法对图的树进行平衡,方式是连接的节点以引力的作用聚在一起,而未连接的节点受斥力作用。你可以控制这两种作用的强度(如下图所示):
邀月工作室
Force Atlas算法在指定下列参数后,会循环作用,达到最佳布局:
·Inertia参数控制每次处理时一个节点保留多少速度;0.5意味着节点几乎是静止的。
·Repulsion strength定义了斥力的强度;5000.0意味着图是分散的。对比右边值为15000的图。
·Attraction strength定义了相连节点之间引力的强度;吸引和排斥之间的差别在于,排斥作用于所有节点,而吸引只作用于相连的节点。
·Maximum displacement限制了节点偏离初始位置的最大距离。
·Auto stabilize function固定了给定排斥和吸引参数后会振动的点。
·Autostab Strength参数控制自动稳定函数的强度;较高的值意味着摆动的节点会较快地稳定。
·Autostab sensibility定义了算法执行过程中,Inertia参数变化的程度。
·Gravity参数指定了每个节点向图中心的引力强度。
·Attraction Distrib参数控制引力中心的分布,以使得图看起来平衡。这会是一个轴辐式分布。
·Adjust by Sizes控制节点的交叠;选上后,节点不会重叠。
·Speed控制算法的速度;高的值(必须大于0)会加速算法的收敛,代价是精度的损失。
下面的图展示了不同斥力强度下我们的图:
你可以设置对比5000与15000,略去。
可以看出,两边几乎是同样的形状,但右边的更分散——我们都认不出名字。
Weight参数控制边的大小。然而,细线不明显。你可以使用左下角的滑动按钮控制厚薄:
见上图
我们根据关系的类型给边染上不同的颜色。到图控制面板上,选择Partition。再前往Edges页卡,从下拉菜单中选择relationship:

注意新版本中,Ranking和Partition已经合并到了Appearance页卡。
应用这些变动后,你可以看到图中颜色的变化。注意关系不仅用颜色表示,也用权重表示。最后图看上去是这样:
邀月工作室
可以明显看到轴,准确标出了社交网络中年龄的差异。
更多:现在看看Gephi是否和之前NetworkX得到的结果相同。前往Statistics和Filter面板。比较图密度:
邀月工作室
单击Graph Density旁边的Run;在我的例子中,得到同NetworkX完全相同的结果:0.237。
最后,让我们探索数据中的联系。我们使用过滤器。首先,看看谁结婚了:
邀月工作室
从Library,导航至Attributes|Equal(我们只选择等于spouse的边),选择relationship,拖拽至Queries。应该会出现Equal(relationship)Settings窗口。在Pattern中输入spouse,单击OK。根据单击的是Select还是Filter,你会看到不同的图:
邀月工作室
也可以加上过滤器。我们来过滤年龄在18和32之间的已婚人士:
邀月工作室
我们以在年龄过滤器中使用Range开始。选出年龄在18和32之间的节点。在Range过滤器的底部,你会看到(不是前一张图里,那里位置已经被Equal过滤器占了)有个地方显示Drag Subfilter;拖一个关系的Equal过滤器到那里,指定为spouse。现在,如果单击Range(age)过滤器并选中Select,你会看到这样的图:
如上图合并。
可以看到,我们选出了没到32的人,并将其中结了婚的突出显示了。
参考:如果要了解更多,我强烈推荐探索Gephi网站上的资源:https://gephi.org/users/
参看这本书:https://www.packtpub.com/big-data-and-business-intelligence/network-graph-analysis-and-visualization-gephi

8.4识别信用卡信息被盗的用户

当今社会,要做一个诈骗犯,不像以前人们想的那么遥远了。在网上使用双重加密和强密码的你可能觉得很安全,然而传统的信用卡信息盗窃起来还是相对容易的。信用卡诈骗正以惊人的速率增长(http://www.economist.com/news/finance-and-economics/21596547-why-america-has-such-high-rate-payment-card-fraud-skimming-top),2012年便达到了55亿美元的规模,这可不是一件好玩的事。
本技巧将聚焦于一种特殊的信用卡诈骗形式——网上购物。我们假设部分(大额)交易,有些卖家会要求买家通话并确认信用卡信息。
为了使用本技巧,我们生成一个数据集,有1000个买家和20个卖家。50多天里,我们的买家进行了22.5万多笔交易,总额超过5700万美元。我们也知道有一个卖家(卖家4)不诚实,时不时地从买家偷取信用卡信息。然后将信息卖到网上,有人用了这张信用卡,卡的所有者便会报告有未授权的付款。
本技巧中,会学到如何从图中抽取数据,并找到诈骗的受害者。
准备:需装好NetworkX、collections和NumPy。
步骤:我们用(压缩后的)GraphML格式将数据保存到Data/Chapter08文件夹。NetworkX让读取GraphML数据变得方便,即便是压缩到了文档中( graph_fraudTransactions.py文件):

1 import networkx as nx
2 import numpy as np
3 import collections as c
4 
5 # import the graph
6 graph_file = '../../Data/Chapter08/fraud.gz'
7 fraud = nx.read_graphml(graph_file)

原理:首先,读入数据后,我们看看处理的图是什么类型:

 print('\nType of the graph: ', type(fraud))

由于任何人都可以与任何卖家进行多笔交易,所以我们处理的是一个有并行边的有向图,每条边代表一个交易。NetworkX确认了这一点:

/*
Type of the graph:  <class 'networkx.classes.multidigraph.MultiDiGraph'>

*/

我们确认节点和边的数目:

 1 # population and merchants
 2 nodes = fraud.nodes()
 3 
 4 nodes_population = [n for n in nodes if 'p_' in n]
 5 nodes_merchants  = [n for n in nodes if 'm_' in n]
 6 
 7 n_population = len(nodes_population)
 8 n_merchants  = len(nodes_merchants)
 9 
10 print('\nTotal population: {0}, number of merchants: {1}' \
11     .format(n_population, n_merchants))
12 
13 # number of transactions
14 n_transactions = fraud.number_of_edges()
15 print('Total number of transactions: {0}' \
16     .format(n_transactions))
17 
18 # what do we know about a transaction
19 p_1_transactions = fraud.out_edges('p_1', data=True)
20 print('\nMetadata for a transaction: ',
21     list(p_1_transactions[0][2].keys()))
22 
23 print('Total value of all transactions: {0}' \
24     .format(np.sum([t[2]['amount']
25         for t in fraud.edges(data=True)])))
26 
27 # identify customers with stolen credit cards
28 all_disputed_transactions = \
29     [dt for dt in fraud.edges(data=True) if dt[2]['disputed']]
30 
31 print('\nDISPUTED TRANSACTIONS')
32 print('Total number of disputed transactions: {0}' \
33     .format(len(all_disputed_transactions)))
34 print('Total value of disputed transactions: {0}' \
35     .format(np.sum([dt[2]['amount']
36         for dt in all_disputed_transactions])))

首先,我们从图中调出所有的节点。我们知道买家节点的前缀是p_而卖家节点的前缀是m_;我们创建双方的列表,查看列表长度:

/*
Total population: 1000, number of merchants: 20
 */

.number_of_edges()方法返回图中边的总数,即交易总数:

/*
Total number of transactions: 225037
*/

然后,我们查看交易有哪些元数据:我们使用.out_edges(...)方法获取p_1的全部交易。这个方法返回p_1出发的所有边的列表,(指定data=True参数)带上所有的元数据。如同8.2节中展示的,这个列表的元素是三元组:(起点,终点,元数据);元数据元素是一个字典,所以我们提取出所有的键:

/*
Metadata for a transaction:['type'  'time', 'amount', 'disputed', 'key' ]


#===============================================================================
# print('\nMetadata for a transaction: ',
#     list(p_1_transactions[0][2].keys()))
#===============================================================================
#===============================================================================
#
# [('p_1', 'm_1', {'type': 'purchase', 'time': 0, 'amount': 410, 'disputed': False, 'key': 0}),
#('p_1', 'm_1', {'type': 'purchase', 'time': 1, 'amount': 386, 'disputed': False, 'key': 1}) ]
#                                                                                                
#===============================================================================

  File "D:\Java2018\practicalDataAnalysis\Codes\Chapter08\graph_fraudTransactions.py", line 45, in <module>
    list(p_1_transactions[0][2].keys()))
TypeError: 'OutMultiEdgeDataView' object is not subscriptable


 */

 


在我们的图中,type总是购买,time是交易发生的时间,amount是交易的额度。disputed标明交易是否有争议。
我们看看50天内,人们在网上消费了多少。我们遍历所有边,用交易额度创建一个列表。最后,NumPy的.sum(...)方法将列表中的元素加总,得到最终值:

/*
-- Total value of all transactions: 57273724
 */
/*
Type of the graph:  <class 'networkx.classes.multidigraph.MultiDiGraph'>

Total population: 1000, number of merchants: 20
Total number of transactions: 225037
Total value of all transactions: 57273724

DISPUTED TRANSACTIONS
Total number of disputed transactions: 49
Total value of disputed transactions: 14277
Total number of people scammed: 33
 */

更多:既然我们了解了图的基本情况,那么要辨别出诈骗的源头,则先要辨别出受害的消费者:

 1 # identify customers with stolen credit cards 辨别出被信用卡被盗的消费者
 2 all_disputed_transactions = \
 3     [dt for dt in fraud.edges(data=True) if dt[2]['disputed']]
 4 
 5 print('\nDISPUTED TRANSACTIONS')
 6 print('Total number of disputed transactions: {0}' \
 7     .format(len(all_disputed_transactions)))
 8 print('Total value of disputed transactions: {0}' \
 9     .format(np.sum([dt[2]['amount']
10         for dt in all_disputed_transactions])))
11 
12 # a list of people scammed受害者列表
13 people_scammed = list(set(
14     [p[0] for p in all_disputed_transactions]))
15 
16 print('Total number of people scammed: {0}' \
17     .format(len(people_scammed)))
18 
19 # a list of all disputed transactions所有有争议交易的列表
20 print('All disputed transactions:')
21 
22 for dt in sorted(all_disputed_transactions,
23     key=lambda e: e[0]):
24     print('({0}, {1}: {{time:{2}, amount:{3}}})'\
25         .format(dt[0], dt[1],
26          dt[2]['amount'], dt[2]['amount']))      

我们先检查所有边的disputed标志位,找出所有的争议交易:

/*
Total number of disputed transactions: 49
*/

225037笔交易中,49笔是欺诈。这个数字并不大,不过,如果我们放任不管,不找出欺诈的源头,这是在给未来埋雷。另外,这并没有算无争议的交易,也许真实的数字要高得多。
然后看看盗刷金额。如同计算总额一样,我们遍历所有争议交易并加总:

/*
Total value of disputed transactions: 14277
 */

超过14000千美元被盗;大约每笔290美元。
我们还不知道有多少受害者。遍历所有交易,取出所有被盗用户。set(...)方法生成一个去重的列表(和数学上的集合一样,不能有重复元素)。将集合变回列表:

/*
Total number of people scammed: 33
 */

总共,有33名受害者。平均到每人,1.48笔交易,金额约430美元。
看下这些交易(简化过了)。
.format(...)方法使用{.}代表模板字符串中要放置数字的位置,所以你要使用双重花括号{{}}来打印出{}(花括号)。
前10笔争议交易是:

/*
All disputed transactions:
(p_114, m_12: {time:290, amount:290})
(p_123, m_12: {time:273, amount:273})
(p_154, m_2: {time:448, amount:448})
(p_164, m_3: {time:98, amount:98})
(p_224, m_2: {time:162, amount:162})
(p_272, m_2: {time:489, amount:489})
(p_276, m_3: {time:122, amount:122})
(p_325, m_2: {time:409, amount:409})
(p_389, m_2: {time:262, amount:262})
(p_389, m_2: {time:247, amount:247})
(p_389, m_3: {time:233, amount:233})
(p_389, m_3: {time:251, amount:251})
(p_389, m_12: {time:460, amount:460})
(p_392, m_2: {time:117, amount:117})
(p_415, m_12: {time:410, amount:410})
...
 */

最后看看每个人的损失:

 1 # how much each person lost 每个人的损失
 2 transactions = c.defaultdict(list)
 3 
 4 for p in all_disputed_transactions:
 5     transactions[p[0]].append(p[2]['amount'])
 6 
 7 for p in sorted(transactions.items(),
 8     key=lambda e: np.sum(e[1]), reverse=True):
 9     print('Value lost by {0}: \t{1}'\
10         .format(p[0], np.sum(p[1])))

我们从创建.defaultdict(...)开始。.defaultdict(...)是一个类似字典的对象。不过,在正常的字典中,如果键值是列表而键名不存在,此时你不能用.append(...)加值。(如果这么做,Python会抛出一个异常。)使用.defaultdict(...)的话,不会抛出异常,这个数据结构先插入一个新键,键值是一个空列表,然后将值附加到新创建的列表中。所以我们可以用transactions[p[0]].append(p[2][‘amount’]),不用像下面这样:

1 for p in all_disputed_transactions:
2     try:
3     transactions[p[0]].append(p[2]['amount'])
4     except:
5     transactions[p[0]]=[p[2]['amount']]

将所有交易拆开后,我们可以打印出受害者的列表(指定reverse=True,从受影响最严重开始排序):

/*
Value lost by p_389:     1453
Value lost by p_721:     1383
Value lost by p_583:     878
Value lost by p_607:     750
Value lost by p_471:     675
Value lost by p_504:     581
Value lost by p_70:     519
Value lost by p_272:     489
Value lost by p_8:     486
Value lost by p_684:     484
Value lost by p_545:     477
Value lost by p_514:     463
Value lost by p_154:     448
Value lost by p_415:     410
Value lost by p_325:     409
Value lost by p_637:     365
Value lost by p_865:     361
Value lost by p_54:     356
Value lost by p_540:     343
Value lost by p_709:     342
Value lost by p_590:     328
Value lost by p_114:     290
Value lost by p_542:     282
Value lost by p_123:     273
Value lost by p_577:     224
Value lost by p_482:     215
Value lost by p_734:     197
Value lost by p_418:     163
Value lost by p_224:     162
Value lost by p_908:     134
Value lost by p_276:     122
Value lost by p_392:     117
Value lost by p_164:     98
 */

可以看出,分布并不平均;有些人的损失超过了1000美金,有7人的损失超过了500美元。这可不是个小数目,应该调查。

8.5识别谁盗窃了信用卡

识别出了信用卡信息被盗的用户,也知道了他们的损失,让我们找出谁该为此负责。
准备:需装好NetworkX、collections和NumPy。

步骤:
本技巧将试着找出所有受害者在第一笔欺诈交易发生前都消费过的卖家(graph_fraudOrigin.py文件):

 1 import networkx as nx
 2 import numpy as np
 3 import collections as c
 4 
 5 # import the graph
 6 graph_file = '../../Data/Chapter08/fraud.gz'
 7 fraud = nx.read_graphml(graph_file)
 8 
 9 # identify customers with stolen credit cards
10 people_scammed = c.defaultdict(list)
11 
12 for (person, merchant, data) in fraud.edges(data=True):
13     if data['disputed']:
14         people_scammed[person].append(data['time'])
15 
16 print('\nTotal number of people scammed: {0}' \
17     .format(len(people_scammed)))
18 
19 # what was the time of the first disputed transaction for each
20 # scammed person
21 stolen_time = {}
22 
23 for person in people_scammed:
24     stolen_time[person] = \
25         np.min(people_scammed[person])
26 
27 # let's find the common merchants for all those scammed
28 merchants = c.defaultdict(list)
29 for person in people_scammed:
30     edges = fraud.out_edges(person, data=True)
31     
32     for (person, merchant, data) in edges:
33         if  stolen_time[person] - data['time'] <= 1 and \
34             stolen_time[person] - data['time'] >= 0:
35 
36             merchants[merchant].append(person)
37 
38 merchants = [(merch, len(set(merchants[merch])))
39     for merch in merchants]
40 
41 print('\nTop 5 merchants where people made purchases')
42 print('shortly before their credit cards were stolen')
43 print(sorted(merchants, key=lambda e: e[1], reverse=True)[:5])

原理:我们先用与之前类似的风格读入数据。然后,创建scammed_people列表,但方式与之前稍有不同。我们要找出第一笔争议交易的时间。所以我们再次使用.defaultdict(...),遍历所有的争议交易,并创建一个字典,字典中对每个人都获取报告争议的时间列表。
这样做是为了检查先于争议交易的所有交易;诈骗犯先得偷取信用卡信息,然后才能进行欺诈。
对于所有的受害者,我们遍历列表中的scammed_people元素,找到争议交易的最小时间。存到stolen_time字典。
现在是时候检查第一次争议之前的交易了。在for循环中,我们遍历每个受害者在第一次争议之前的所有交易。
代码中,我们只检查前一天的交易,stolen_time[person]-data['time']<=1和stolen_time[person]-data['time']>=0。
但时间窗口可以调整,看你什么时候找到所有受害者的共同卖家。在我们的例子中,我们回溯一天就找到了:33个受害者都在同一个卖家消费过,然后一天之内就发生了第一笔争议。
有了交易列表,我们找出卖家。我们再次使用set(...)操作,对每个买家选出去重后的卖家(毕竟这段时间内可能与同一个卖家产生多笔交易),统计个数。
最后看看谁是赢家:

/* Total number of people scammed: 33

Top 5 merchants where people made purchases
shortly before their credit cards were stolen
[('m_4', 33), ('m_2', 16), ('m_3', 14), ('m_12', 9), ('m_6', 8)]
 */

可见,所有受害者在信用卡被盗之前都在卖家m_4处消费过。这里的数据是我们生成的,我们知道这就是答案。然而在现实世界,这五个卖家可能有关联,或者职员中藏着一匹害群之马,要展开调查。
参考:我们适配了图数据库Neo4j的方法。关于Neo4j以及用图数据库侦查欺诈,你可以在这里了解更多:https://linkurio.us/stolen-credit-cards-and-fraud-detection-with-neo4j/

 

第8章完。

python数据分析个人学习读书笔记-目录索引

 

随书源码官方下载:
http://www.hzcourse.com/web/refbook/detail/7821/92

 

posted @ 2020-03-28 23:01  邀月  阅读(561)  评论(0编辑  收藏  举报