工具&技巧 | 经济学圈特供 小刘帮你画专业社会网络图(二)
数据预处理
# 取出所有的节点备用
node_list = list(df.index)
# 对数据做初始分类
data_bins = [-float('inf'), -1, 30000, 2000000, 10000000, 20000000, float('inf')]
data_labels = [-1, 0, 1, 2, 3, 4]
df = df.apply(lambda row: pd.cut(row, data_bins, labels=data_labels),axis=1)
我这边使用了pd.cut来进行分类,data_bins是规定好所有的分类区间,默认是左开右闭,float('inf')是无穷大的意思,而data_labels是每个区间对应的标签。由于我们的处理方式中,标签也要进行数值运算,所以data_labels里面全是数字类型的元素。
至于为什么这么分类,是由研发部的神仙姐姐根据一些统计指标不断调优得出的。
分完类之后,我们的数据表变成了图二的样子:
绘制一模社会网络图
# 画图准备
"""
图的类型
Graph类是无向图的基类,无向图能有自己的属性或参数,不包含重边,允许有回路,节点可以是任何hash的python对象,节点和边可以保存key/value属性对。该类的构造函数为Graph(data=None,**attr),其中data可以是边列表,或任意一个Networkx的图对象,默认为none;attr是关键字参数,例如key=value对形式的属性。
MultiGraph是可以有重边的无向图,其它和Graph类似。其构造函数MultiGraph(data=None, *attr)。
DiGraph是有向图的基类,有向图可以有数自己的属性或参数,不包含重边,允许有回路;节点可以是任何hash的python对象,边和节点可含key/value属性对。该类的构造函数DiGraph(data=None,**attr),其中data可以是边列表,或任意一个Networkx的图对象,默认为none;attr是关键字参数,例如key=value对形式的属性。
MultiDiGraph是可以有重边的有向图,其它和DiGraph类似。其构造函数MultiDiGraph(data=None, *attr)。
"""
G = nx.Graph()
plt.figure(1,figsize=(60,60)) # 设置“一模”图片大小
值得一提的是,我是按照上述代码完成初始化之后,然后对节点数值进行一些运算,根据运算结果,再做一次节点属性上的分类。
# 计算节点属性
# 节点数值的分类
node_bins = [-float('inf'), 0, 25, 41, 50.5, 58, float('inf')]
# 第一个是节点的大小,第二个需要填入RBG颜色值
node_args = {-1: (50, (187,255,255), )
, 0: (100, (187,255,255), )
, 1: (200, (152,245,255), )
, 2: (400, (142,229,238), )
, 3: (550, (0,238,238), )
, 4: (800, (0,197,205), )
}
node_dict = {}
# 为所有节点做分类,目的是避免为每一个节点都去设置颜色,大小
for node in node_list:
a1n = df.loc[node,:]
an1 = df.loc[:,node]
a11 = df.loc[node,node]
node_sum = a1n.sum()+an1.sum()-a11 # 横向量加总+纵向量加总-对角线
node_type = pd.cut([node_sum], node_bins, labels=node_args.keys())[0]
print(node, '=》 节点的数值计算结果为', node_sum, '分类为', node_type)
G.add_node(node)
if node_type not in node_dict:
node_dict[node_type] = []
node_dict[node_type].append(node)
计算出node_sum后,再根据node_bins中的区间、node_args中的key来分类。计算出node_type后,然后将节点加入图对象G中,同时将其保存在node_dict中对应的节点属性分类下(方便画图时直接使用)。
注意:图四中的分类,我们是对原始矩阵,做数值上的分类。图五中的计算,是根据之前的数值分类,进行二次分类。目的是对节点的颜色、大小等属性进行分类。这个必须区分清楚。
计算结果如图四所示:
1、其实使用字符串来作为key会更好,用来区别于第一次的数值分类。
2、有可能大家会觉得使用字典来存放大小、颜色这类属性会更加直观一些,这个就是仁者见仁智者见智了,我觉得使用元组或列表会更简洁。
# 计算连线属性
# 连线数值的分类
edge_bins = [-float('inf'), 0, 1, 2, 3, float('inf')]
# 第一个是线的粗细,第二个需要填入RBG颜色值,第三个为连线的种类
edge_args = {
0: (0, (255,255,255), 'dotted')
, 1: (0, (255,255,255), 'dotted')
, 2: (0.4, (135,206,250), 'dotted')
, 3: (0.6, (100,149,237), 'solid')
, 4: (1.5, (65,105,225), 'solid')
}
hidden_list = (1,)
edge_dict = {edge_type:[] for edge_type in edge_args.keys()}
# 此处的连线为所有节点的组合,例如[1,2,3] => [(1, 2), (1, 3), (2, 3)]
for edge0 in itertools.combinations(node_list,2):
edge_num = df.loc[edge0] + df.loc[edge0[::-1]] # a12+a21
edge_type = pd.cut([edge_num], edge_bins, labels=edge_args.keys())[0]
print(edge0, '=》 连线的数值计算结果为', edge_num, '分类为', edge_type)
G.add_edge(*edge0, weight=edge_args[edge_type][0]) # 此处权重跟图中哪个区在中央有关
edge_dict[edge_type].append(edge0)
# 为节点分类排序,分类数值越大,位置越可能靠近同心圆圆心
nlist = sorted(list(node_dict.values()), key=lambda tup:tup[0])
"""
由于shell_layout有个BUG,如果一个同心圆上只有一个节点
那么算法就会把他放在这组同心圆的圆心处
无论多少个这种节点,都会重叠在一起
我的解决方法,暂定为:添加假的节点,然后在生成pos之后删掉,避免真的画进图中
所以,最好别用FakeNode来命名节点
"""
nlist = copy.deepcopy(nlist)
fakenode_list = []
for i, l0 in enumerate(nlist):
if i == 0:
continue
if len(l0) == 1:
fake_node = 'FakeNode' + str(random.randint(0,100))
l0.append(fake_node)
fakenode_list.append(fake_node)
# 节点排列为同心圆排列方式
pos = nx.shell_layout(G
, nlist=nlist
)
# 删除假的节点
pos = {node:arr for node,arr in pos.items() if node not in fakenode_list}
# 每一类“节点”单独设置一个样式
for node_type, nodelist in node_dict.items():
nx.draw_networkx_nodes(G
, pos
, alpha=1.0 # 节点颜色的透明度
, nodelist=nodelist # 需要加入的节点列表
, node_size=node_args[node_type][0] # 节点大小
, node_color=rgb_to_hex(node_args[node_type][1]) # 节点颜色,将RBG转为十六进制颜色数 # range(len(nodelist))
# , cmap=plt.cm.Greens
)
# 每一类“连线”单独设置一个样式
for edge_type, edgelist in edge_dict.items():
# 略过不要的分类
if edge_type in hidden_list:
continue
nx.draw_networkx_edges(G
, pos
, edgelist=edgelist # 需要加入的连线列表
, width=edge_args[edge_type][0] # 连线的粗细
, edge_color=rgb_to_hex(edge_args[edge_type][1]) # 连线的颜色
, alpha=0.9 # 连线颜色的透明度
, style=edge_args[edge_type][2]
)
# 设置标签的样式
nx.draw_networkx_labels(G
, pos
, font_family='sans-serif' # 字体
, font_size=4 # 字体大小
, alpha=0.8) # 透明度
plt.savefig("2019一模.png"
, dpi=600 # 输出图片的分辨路
)
plt.show()
左右滑动查看更多
绘制二模社会网络图
G.clear()
左右滑动查看更多
G = nx.DiGraph()
plt.figure(1,figsize=(60,60)) # 设置“二模”图片大小
左右滑动查看更多
# 计算节点大小
# 节点数值的分类
node_bins = [-float('inf'), 0, 10, 18.5, 27.5, 34.5,float('inf')]
# 将“出发”(设为0)、“到达”(设为1)节点分开设置参数, 第一个是节点的大小,第二个是节点形状,第三个是节点颜色
node_args = {-1: {0:(100, 'o', (255,255,255),)
, 1:(100, 'h', (255,255,255),)}
, 0: {0:(100, 'o', (255,248,220),)
, 1:(100, 'h', (255,240,245),)}
, 1: {0:(200, 'o', (255,250,205),)
, 1:(200, 'h', (255,228,225),)}
, 2: {0:(300, 'o', (255,246,143),)
, 1:(300, 'h', (255,181,197),)}
, 3: {0:(400, 'o', (255,236,139),)
, 1:(400, 'h', (255,160,122),)}
, 4: {0:(500, 'o', (255,215,0),)
, 1:(500, 'h', (255,114,86),)}
}
# 为所有节点做“位置”分类
node_dict = {}
for node in node_list:
# 计算横向量,即出发类节点
a1n = df.loc[node,:]
node_sum = a1n.sum() # 横向量总和
node_type = pd.cut([node_sum], node_bins, labels=node_args.keys())[0]
if (node_type, 0) not in node_dict:
node_dict[(node_type, 0)] = []
node_dict[(node_type, 0)].append(node)
print(node, '分类', (node_type, 0))
G.add_node(node)
# 计算纵向量,即到达类节点
an1 = df.loc[:,node]
node_sum = an1.sum()
# “到达”类节点前后都加一个空格,用来区分“出发”类节点,以免覆盖“出发”类节点
node = ' ' + node + ' '
node_type = pd.cut([node_sum], node_bins, labels=node_args.keys())[0]
if (node_type, 1) not in node_dict:
node_dict[(node_type, 1)] = []
node_dict[(node_type, 1)].append(node)
print(node, '分类', (node_type, 1))
G.add_node(node)
左右滑动查看更多
1、每个属性分类下再添加了一个位置类的属性,即“出发”(设为0)、“到达”(设为1)。同样的,容易看晕的同学可以改成字符串。
2、在向node_dict中存入node_type的时候,我加上位置属性0、1,。由于元组是不可变变量,自然能作为字典的key。
3、“出发”类节点与“到达”类节点必然会重名,重名的节点在使用add_node()函数的时候会直接覆盖已有节点,这个从pos的对象类型(字典)也看得出来。针对这种情况,我想出来的办法是,在“到达”类节点的名字前后都加一个空格。加两个空格的原因,是防止图中的标签因为空格的存在而发生位置偏移,例如在最后加一个空格,视觉上标签就会往左偏。
# 计算连线属性
# 连线数值的分类
edge_bins = [-float('inf'), 0, 1, 2, 3, float('inf')]
# 第一个是线的粗细,第二个需要填入RBG颜色值
edge_args = {
0: (0.1, (255,255,255), )
, 1: (0.2, (255,255,255), )
, 2: (0.4, (255,160,122), )
, 3: (0.6, (238,149,114), )
, 4: (1.5, (255,114,86), )
}
hidden_list = (1,)
edge_dict = {edge_type:[] for edge_type in edge_args.keys()}
# 此处的连线为所有节点的排列,例如[1,2,3] => [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)]
for edge0 in itertools.product(node_list,repeat=2):
edge_num = df.loc[edge0] # 取出“二模”数组的对应值
edge_type = pd.cut([edge_num], edge_bins, labels=edge_args.keys())[0]
G.add_edge(*edge0, weight=edge_args[edge_type][0]) # 此处权重跟图中哪个区在中央有关
edge_dict[edge_type].append((edge0[0],' ' + edge0[1] + ' '))
左右滑动查看更多
1、我在做这个项目的时候,有向的连线可以修改箭头样式,但永远都会是实线,怎么修改都画不出虚线来,设置draw_networkx_edges中的style参数没有任何效果,有画出来的同学可以留言教教我。
2、在遍历所有连线的时候,我这里是使用了itertools库中的product函数,即笛卡尔积函数。这是为了确保遍历矩阵中所有的点,我们是在node_dict中对“到达”类节点的名称进行了修改,node_list中还是原来矩阵中的节点个数。一模中我们使用的是combinations函数,因为两个节点之间只会有一种连线。
merge_list = lambda l0: functools.reduce(lambda l1,l2:l1+l2, l0)
# 这里的nlist中,越靠前的分类,在图中的位置越可能靠近同心圆中心
# 此分类元祖中,例如(4,0),第一个值4,代表4这个node分类;第二个数字,代表“出发”或者“到达”,0即“出发”,1即“到达”
circle_classify = [
((4,0),)
, ((3,0), (3,1))
, ((2,0), (2,1))
, ((1,0), (1,1))
, ((0,0), (0,1))
]
nlist = []
node_type_set = set(node_dict.keys())
for type_tup in circle_classify:
circle_list = []
for node_type_tup in type_tup:
# print(node_type_tup)
if node_type_tup in node_type_set:
circle_list.extend(node_dict[node_type_tup])
node_type_set.remove(node_type_tup)
else:
print('Warning: circle_classify中的分类', node_type_tup, '没有数据')
nlist.append(circle_list)
# 将circle_classify中没有的涉及的分类添加至最大的同心圆
if node_type_set:
rest_types = merge_list([node_dict[node_type] for node_type in node_type_set])
nlist.append(rest_types)
print('添加',rest_types,'到最外围')
我只贴出了有变化的代码。在一模中,我是根据节点的属性分类来排同心圆内的位置的。而在二模中,我们需要根据图形主题的需要,突出某几个特定节点的重要性,所以我这边就在circle_classify中允许自定义节点位置。
同时为了减少不必要的麻烦,未加入circle_classify的分类我都会放在半径最大的同心圆上,并打印出来以做提示,主要是为了避免分类太多时的一些不必要的麻烦。
nlist调整完毕之后,就可以按照类似于一模中的代码来画图了,最终图形也就是大家在上期文章中看到的第二幅图:
►往期推荐
回复【Python】👉简单有用易上手
回复【学术前沿】👉机器学习丨大数据
回复【数据资源】👉公开数据
回复【可视化】👉你心心念念的数据呈现
回复【老姚专栏】👉老姚趣谈值得一看
►一周热文
工具&技巧 | 经济学圈特供 小刘帮你画专业社会网络图(一)
特别推荐丨老姚专栏:“读书无用论”有道理吗?从比较的视角看“识别”问题
数据呈现丨装X利器来袭,Python可视化库Bokeh助你俘获小姐姐的心
工具&方法 | 六步法,用Python进行机器学习项目可以如此明了
数据Seminar
这里是大数据、分析技术与学术研究的三叉路口
欢迎扫描👇二维码添加关注