系列文章目录
参考
- 论文: Visualizing complex networks by leveraging community structures
背景
力导向图非常适合于渲染关系型信息图,其主要思想是将关系图的节点视为电荷、连边视作弹簧,在多次动态更新之后节点的位置将最终趋向于稳定状态。
对适用于一般网状结构数据绘图的算法来说,力导向算法是一种常被应用的方法。通过对每个节点的计算,算出引力和排斥力综合的合力,再由此合力来移动节点的位置。执行一次后根据节点新位置算出新的能量值,如同力学概念,能量值越小,表示整个网络越趋于稳定。一般来说能量值越小,网络图的配置显示就会越清晰,因此当能量值到达最小值的时候,网状图的配置状态就是我们想要的结果。
实现与改进
传统实现方法
- 随机生成节点位置
- 迭代循环调整点的位置
- 点与点之间均存在斥力
- 有连边的节点直接存在引力
- 引力和斥力产生合力改变节点位置
利用上述方法对有社区结构的网络图进行可视化,最终得到以下效果(这里就画出了节点位置,没有画出连边):
可以看到映射的节点对应的社区结构并不明显。
改进方法
为了体现出社区中节点的关联性,我们将节点间的作用力中的引力做以下修改:
将原来的有连边的节点直接存在引力修改为以下四级:无边相连且不在同一簇 < 有边相连但是不是同一簇的 < 同一簇但无边相连 < 有边相连且是同一簇的。这样子的布局既保证了社区内节点的关联性,也保证了社区间节点的关系。
最终得到的可视化效果:
只有节点位置映射的
加上连边的
代码实现
核心代码
import json
from turtle import position
import numpy as np
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
from tools import readGraphData, getClusterLabels, drawNodesMap
def sparse_fruchterman_reingold(A, k=None, pos=None, fixed=None, iterations=500, threshold=1e-5, dim=2):
# np.random.seed(1)
nodes_num = A.shape[0]
pos = np.asarray(np.random.rand(nodes_num, dim), dtype=A.dtype)
if fixed is None:
fixed = []
if k is None:
k = np.sqrt(1.0 / nodes_num) * 60.0 ## k作为斥力的系数
t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
dt = t / float(iterations + 1)
displacement = np.zeros((dim, nodes_num))
for iteration in range(iterations):
displacement *= 0
for i in range(A.shape[0]):
if i in fixed:
continue
delta = (pos[i] - pos).T
distance = np.sqrt((delta ** 2).sum(axis=0))
distance = np.where(distance < 0.001, 0.001, distance)
Ai = A[i,:] ## 相当于与每个节点的弹簧的吸引力
displacement[:, i] += \
(delta * (k * k / distance ** 2 - Ai * distance / k)).sum(axis=1)
length = np.sqrt((displacement ** 2).sum(axis=0))
length = np.where(length < 0.01, 0.1, length)
delta_pos = (displacement * t / length).T
pos += delta_pos
# cool temperature
t -= dt
err = np.linalg.norm(delta_pos) / nodes_num
if err < threshold:
break
return pos
class ClusterNodesLayout(object):
"""
## 用于布局聚类后的网络图
```
整体思想为:使用力导向图算法,计算每个节点的位置
斥力: 所有节点间都有
引力:无边相连且不在同一簇 < 有边相连但是不是同一簇的 < 同一簇但无边相连 < 有边相连且是同一簇的
```
"""
def __init__(self, graph, label2nodesArr) -> None:
"""
## 初始化
:param graph: 网络图
:param label2nodesArr: 聚类后的节点到簇的映射 -> {label:[node1, node2, ...], ...}
"""
self.graph = graph
self.label2nodesArr = label2nodesArr
self.__initNodesMatrix()
self.__initNodesLabels()
def __call__(self, iterations=300):
springMatrix = self.SpringLink()
self.nodesPosition = sparse_fruchterman_reingold(springMatrix, iterations=iterations)
nodesPositionInfo = self.getNodesPositionInfo()
return nodesPositionInfo
def __initNodesLabels(self):
"""
## 初始化节点到簇的映射
"""
nodes = self.nodes
label2nodesArr = self.label2nodesArr
if isinstance(label2nodesArr, dict):
node2label = {node:idx for idx, nodeItems in enumerate(label2nodesArr.values()) for node in nodeItems }
nodesLabels = [node2label[node] for node in nodes]
self.nodesLabels = nodesLabels
elif isinstance(label2nodesArr, list):
self.nodesLabels = label2nodesArr
def __initNodesMatrix(self):
nodes = sorted(self.graph.nodes())
linkMatrix = np.asarray(
nx.adjacency_matrix(self.graph, nodelist=nodes,weight='weight').todense(),
dtype='float32')
self.nodes = nodes
self.linkMatrix = linkMatrix
def SpringLink(self):
"""
## 获得弹簧长度对应的矩阵
在这里设置前面提到的四级引力关系
"""
linkMatrix = self.linkMatrix
nodesLabels = self.nodesLabels
k = 1.0
linkMatrix *= k
springMatrix = []
for i, line in enumerate(linkMatrix):
itemLinks = []
for j, item in enumerate(line):
if nodesLabels[i] == nodesLabels[j] and item != 0:
itemLinks.append(1.5*k)
elif nodesLabels[i] == nodesLabels[j]:
itemLinks.append(1.2*k)
else:
itemLinks.append(item)
springMatrix.append(itemLinks)
springMatrix = np.array(springMatrix)
springMatrix *= 0.8
return springMatrix
def getNodesPositionInfo(self):
"""
## 转化最终要保存的节点位置信息
"""
nodes = self.nodes
nodesPosition = self.nodesPosition
nodesPositionInfo = [{
"id": pointId, "x": point[0], "y": point[1]
} for pointId,point in zip(nodes,nodesPosition)]
self.nodesPositionInfo = nodesPositionInfo
return nodesPositionInfo
def test():
graphs, graphsMatrix, nodes = readGraphData(f'./data/CELE.txt')
labelGroups = getClusterLabels(f'./未融合的聚类结果/pta-CELE-类数-10-1.json')
task = ClusterNodesLayout(graphs[0], labelGroups)
positionInfo = task(iterations=300)
drawNodesMap(task.nodesPosition, task.nodesLabels)
plt.show()
if __name__ == '__main__':
test()
tools中的相关函数
import json
import numpy as np
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
Colors = ['red', 'green', 'blue', 'yellow', 'orange', 'purple', 'black', 'brown', 'pink', 'gray', 'cyan', 'indigo', 'magenta']
def readGraphData(filename):
"""
## 读取图数据
:params filename: 文件路径
"""
df = pd.read_csv(filename, header=None, sep=' ', names=['layerID', 'source', 'target', 'weight'], dtype='int32')
layers = list(set(df['layerID'].tolist()))
graphs = []
graphsMatrix = []
nodes = []
for i in layers:
g = nx.from_pandas_edgelist(df[df['layerID'] == i], source='source', target='target',edge_attr='weight')
nodelist=sorted(g.nodes())
L = np.asarray(nx.adjacency_matrix(g, nodelist=nodelist,weight='weight').todense(),dtype='float32')
graphs.append(g)
graphsMatrix.append(L)
nodes = nodelist
return graphs, graphsMatrix, nodes
def getClusterLabels(filePath):
"""
## 获取节点聚类的标签数据
"""
with open(filePath, 'r') as f:
data = json.load(f)
return data
def drawNodesMap(nodesPosition, labels):
"""
## 绘制布局出来的散点图并绘制连线
:param nodesPosition: 节点的坐标
:param labels: 节点对应的类标签
"""
plt.figure(figsize=(10,10))
plt.title('Graph')
for idx, node in enumerate(nodesPosition):
plt.scatter(node[0], node[1], c=Colors[labels[idx]], s=50)
def getCommunityData(filePath):
"""
## 读取保存的模块度得到的社区数据
"""
if filePath.endswith('.txt'):
with open(filePath, "r", encoding="utf-8" ) as file:
data = file.read().replace("frozenset({", "[").replace("})", "]")
data = json.loads(data)
community_data = {idx:nodes for idx, nodes in enumerate(data) }
return community_data
elif filePath.endswith('.json'):
with open(filePath, "r", encoding="utf-8" ) as file:
data = json.load(file)
community_data = {idx:nodes for idx, nodes in enumerate(data.values()) }
return community_data