入门图论及NetworkX的使用.
图(Graph)是一种表示对象之间关系的抽象数据结构。图由节点(Vertex)和边(Edge)组成,节点表示对象,边表示对象之间的关系。图可以用于建模各种实际问题,如社交网络、交通网络、电力网络等。
NetworkX是一个用Python编写的库,专门用于创建、操作和研究复杂网络的结构、动态和功能。它提供了简单易用的接口来处理图论和网络结构。NetworkX适用于处理大型网络结构,并提供了许多内置的图算法,如路径寻找、图的构建和修改、节点属性操作等。
pip install networkx
并回车。import networkx as nx
# 创建一个无向图
G = nx.Graph()
# 添加节点
G.add_node(1)
G.add_nodes_from([2, 3])
# 添加边
G.add_edge(1, 2)
G.add_edges_from([(2, 3), (1, 3)])
# 查看图的节点和边
print("图的节点: ", G.nodes(), "; 图的边: ", G.edges(), '.')
# 可视化
nx.draw(G, node_size=500, with_labels=True)
控制台输出结果 - 无向图
有向图的创建方式很简单,只需要把上面无向图的对象:
# 创建一个无向图
G = nx.Graph()
换成:
# 创建一个有向图
G = nx.DiGraph()
即可。
控制台输出结果 - 有向图
创建有权图时需要添加权重信息,且可视化的代码略有不同:
import networkx as nx
import matplotlib.pyplot as plt
# 创建一个带权重的无向图
G = nx.Graph()
# 添加带权重的边
G.add_edges_from([
(2, 3, {'diameter': 1.0,'length': 12.0}),
(1, 3, {'diameter': 2.0,'length': 10.0}),
(2, 1, {'diameter': 3.0,'length': 8.0})
])
# 提取权重信息
edge_weights = nx.get_edge_attributes(G, 'length')
# 可视化图
pos = nx.spring_layout(G) # 选择布局算法,这里使用弹簧布局
nx.draw(G, pos, with_labels=True, font_weight='bold', node_size=700, node_color='skyblue', font_color='black')
# 绘制权重标签
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_weights)
# 显示图形
plt.show()
# 查看图的节点和边
print("图的节点: ", G.nodes(), "; 图的边: ", G.edges(data=True), '.')
控制台输出结果 - 有权图
邻接矩阵(Adjacency Matrix): 邻接矩阵是一个二维矩阵,其中的行和列分别对应图中的节点。矩阵的元素表示节点之间是否存在边。对于无向图,邻接矩阵是对称的;对于有向图,邻接矩阵不一定对称。
# 获取邻接矩阵(默认是稀疏矩阵格式)
adj_matrix = nx.adjacency_matrix(G)
# 将稀疏矩阵转换为密集矩阵(如果需要)
dense_adj_matrix = adj_matrix.todense()
请注意,如果你的图是有向图,你可以使用 nx.adjacency_matrix(G, directed=True)
来获取有向图的邻接矩阵。如果你想要自定义矩阵的表示方式,你可以使用 toarray()
方法将稀疏矩阵转换为 NumPy 数组。
下面的演示均以:
G.add_edges_from([ # Fig. 7
(0, 1), (0, 2), (0, 7),
(1, 0), (1, 2), (1, 3), (1, 4), (1, 7),
(2, 0), (2, 1), (2, 3),
(3, 1), (3, 2), (3, 4), (3, 5), (3, 7),
(4, 1), (4, 3), (4, 5), (4, 6), (4, 7),
(5, 3), (5, 4), (5, 6),
(6, 4), (6, 5),
(7, 0), (7, 1), (7, 3), (7, 4)
])
演示图结构
为示例。
度(Degree)的定义:
NetworkX
求度:
# 获取节点的度
degrees = dict(G.degree())
print("节点的度:", degrees)
节点的度: {0: 3, 1: 5, 2: 3, 7: 4, 3: 5, 4: 5, 5: 3, 6: 2}
平均度(Average Degree)是图中所有节点的度的平均值。
# 计算平均度
average_degree = sum(dict(G.degree()).values()) / len(G)
print("平均度:", average_degree)
平均度: 3.75
度分布(Degree Distribution)描述了图中节点的度的分布情况,即度为 k 的节点有多少个。度分布是图结构的一个重要特征,它可以帮助我们了解网络中节点的连接模式。
# 获取度分布
degree_distribution = nx.degree_histogram(G)
# 绘制度分布直方图
plt.bar(range(len(degree_distribution)), degree_distribution, tick_label=range(len(degree_distribution)))
plt.xlabel("Degree")
plt.ylabel("Frequency")
plt.title("Degree Distribution")
plt.show()
度矩阵(Degree Matrix)是一个表示图中节点度信息的对角矩阵。对于一个无向图,度矩阵的定义如下:
# 获取度矩阵
np.diag(list(dict(G.degree()).values()))
控制台输出结果 - 度矩阵
拉普拉斯矩阵(Laplacian Matrix)有多种定义方式,其中最常见的计算方法是使用度矩阵减去邻接矩阵。假设无向图 G 有 n 个节点,其邻接矩阵为 A,度矩阵为 D。标准拉普拉斯矩阵 L 的计算如下:
# 获取邻接矩阵和度矩阵
adj_matrix = nx.adjacency_matrix(G).toarray()
degree_matrix = np.diag(list(dict(G.degree()).values()))
# 计算标准拉普拉斯矩阵
laplacian_matrix = degree_matrix - adj_matrix
请注意,对于有向图,可以使用
nx.directed_laplacian_matrix
函数来计算有向图的拉普拉斯矩阵。同样,还有对称归一化拉普拉斯矩阵和随机游走拉普拉斯矩阵等不同定义方式。
在图论中,路径和距离是描述图中节点之间连接关系和位置关系的重要概念。
获取图中的所有最短路径和距离:
# 获取所有节点对之间的最短路径和距离
all_shortest_paths = dict(nx.all_pairs_shortest_path(G))
all_shortest_distances = dict(nx.all_pairs_shortest_path_length(G))
# 打印最短路径和距离
all_info = []
for source in all_shortest_paths:
for target in all_shortest_paths[source]:
paths = all_shortest_paths[source][target]
distance = all_shortest_distances[source][target]
print(f"最短路径从节点 {source} 到节点 {target}: {paths}, 距离: {distance}")
演示实例:
G = nx.Graph()
G.add_edges_from([(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)])
nx.draw(G, node_size=500, with_labels=True)
在图论中,集聚系数(Clustering Coefficient)是用于度量节点周围邻居之间连接紧密程度的指标。它可以帮助我们了解图中的局部连接性。有三种主要的集聚系数:节点的集聚系数、平均集聚系数和全局集聚系数。
节点的集聚系数是一个节点邻居之间实际存在的边数与可能存在的最大边数之比。对于一个无向图中的节点 i,其集聚系数 C_i 的计算方式如下:
这个值在 [0, 1] 范围内,表示节点的邻居之间连接紧密的程度。
# 计算节点的集聚系数
node_clustering = nx.clustering(G)
print("节点的集聚系数:", node_clustering)
节点的集聚系数: {1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0, 5: 1.0}
以节点2为例,节点2的邻居节点是1、3、4、5。节点1、3、4、5可能存在的最大边数组合为C_4^2 = \frac{4 \times 3}{2 \times 1} = 6.
节点1、3、4、5实际存在的连接情况是:
1: [1, 3], [1, 4], [1, 5]
3: [3, 4], [3, 5]
4: [4, 5]
共有6种。因此,节点2的聚集系数为1.
平均集聚系数是图中所有节点的集聚系数的平均值。对于一个无向图 G,其平均集聚系数 C 的计算方式如下:
其中,n 是图中的节点数。
# 计算平均集聚系数
average_clustering = nx.average_clustering(G)
print("平均集聚系数:", average_clustering)
全局集聚系数是图中所有闭合三角形的数量与所有可能的闭合三角形的数量的比值。闭合三角形是由三个节点和它们之间的边组成的子图。对于一个无向图 G,其全局集聚系数 C_{\text{global}} 的计算方式如下:
连接三元组(Open Triplet)是图论中的一个概念,它指的是在图中任选三个节点,其中至少有两个节点是相互连接的。这种三元组被称为“开放的”,因为它们不一定构成一个闭合的三角形。连接三元组是用来计算图的集聚系数的一个重要概念。 在具体的定义中,连接三元组通常包含以下两种情况:
在计算图的全局集聚系数时,会考虑图中所有可能的连接三元组。全局集聚系数是闭合三元组数量与连接三元组总数量的比例。这个比例说明了在所有可能形成三角形的节点组合中,有多少实际形成了闭合的三角形。 例如,在社交网络分析中,闭合三元组可能表示一种更强的社会关系,因为如果A认识B,B认识C,且A也认识C,这可能意味着这三个人之间有更紧密的社交联系。而非闭合的三元组则可能表示潜在的、未完全形成的社交联系。
平均集聚系数关注的是每个节点的局部连接性,而全局集聚系数关注的是整个图中的全局连接性。
# 计算全局集聚系数
global_clustering = nx.transitivity(G)
print("全局集聚系数:", global_clustering)
全局集聚系数: 1.0
上面的示例图是一个完全图(任意两个顶点都是相连的),在完全图中,每一组三个节点都会形成一个闭合三角形,所以闭合三元组的数量等于连接三元组的数量。因此,全局集聚系数是 1.0。
实际存在的闭合三角形的数量(闭合三元组的数量):
from itertools import combinations
# 获取图中所有可能的闭合三角形数量
all_triangles_count = 0
for node in G.nodes(): # 遍历所有节点
neighbors = set(G.neighbors(node)) # 获取该节点的所有邻居节点
for pair in combinations(neighbors, 2): # 获取所有邻居节点的两两组合
if G.has_edge(pair[0], pair[1]): # 检查这个两两组合之间是否有边(因为它俩都是node的邻居节点,因此它俩肯定与node相连),如果它俩相连,那么就说明node与pair[0]、pair[1]构成了三角形.
all_triangles_count += 1
上面的代码等同于:
# 获取图中实际存在的闭合三角形数量(也需要除以3)
actual_triangles_count = sum(nx.triangles(G).values())
需要注意的是:
all_triangles_count
需要除以3得到的才是闭合三元组的数量,因为闭合三元组中的三个顶点会在不同的组合中进行重复计算。
连接三元组的数量:
open_triplets_count = 0
for node in G.nodes():
neighbors_count = len(list(G.neighbors(node)))
open_triplets_count += (neighbors_count * (neighbors_count - 1)) // 2
# open_triplets_count 现在就是图中连接三元组的总数量
从 n 个数中随机挑选出 2 个的组合数可以用组合数学中的公式来计算,这个公式是 C(n, 2) 或者 \binom{n}{2} ,可以表示为:
连通性描述的是图中节点之间是否存在路径相连的性质。一个图是连通的,意味着从图中的任意一个节点到另一个节点都存在路径。
G = nx.Graph()
G.add_nodes_from([i for i in range(1, 8)])
G.add_edges_from([(1, 2), (1, 3), (2, 3), (4, 7), (5, 6), (5, 7), (6, 7)])
nx.draw(G, node_size=500, with_labels=True)
# 查看图的连通性
nx.is_connected(G)
什么叫做图的连通性?在无向图中,如果对于每一对不同的顶点 u 和 v,都存在至少一条由边连接的路径从 u 到 v,则该图是连通的。请注意这个概念并不等同于完全图的概念,完全图的概念是每一对不同的顶点 u 和 v都直接相连,而连通图的每一对不同的顶点 u 都可以达到 v,两者可以不直接相连。
通过创建两个图来展示不同的连通性:
# 创建具有较强连通性的图(低Fiedler值)
G_strong = nx.Graph()
G_strong.add_edges_from([(1, 2), (2, 3), (3, 4), (4, 1), (1, 3), (2, 4)])
# 创建具有较弱连通性的图(高Fiedler值)
G_weak = nx.Graph()
G_weak.add_edges_from([(1, 2), (2, 3), (3, 4)])
# 绘制这两个图
plt.figure(figsize=(12, 6))
# 绘制具有较强连通性的图
plt.subplot(1, 2, 1)
nx.draw(G_strong, with_labels=True, node_color='lightblue', node_size=700)
plt.title("Strongly Connected Graph")
# 绘制具有较弱连通性的图
plt.subplot(1, 2, 2)
nx.draw(G_weak, with_labels=True, node_color='lightgreen', node_size=700)
plt.title("Weakly Connected Graph")
plt.show()
可以看出,左边的图具有更高的连通性,其应该具有更高的高Fiedler值,表明要将图分割成孤立的子图,需要切断更多的边。这通常表示图在整体上更加紧密连接,没有明显的弱连接点。右边的图具有更低的连通性,表明图可以通过切断较少的边来分割成不同的部分。这通常发生在图中存在一个或多个"瓶颈"区域,这些区域的边相对较少,是连接大的图区域的桥梁。
G_strong
G_weak
通过对两个图结构的拉普拉斯矩阵进行特征值分解发现,左边图结构的Fiedler值为4.0,而右边的为0.586左右。因此,说明左边图结构的连通性更强。
Thinking/ 思考 两点自己的思考:
下面的讲解以[STANKOVIC L, DAKOVIC M, SEJDIC E. Introduction to Graph Signal Processing[C]//,2019:3-108. 10.1007/978-3-030-03574-7_1.](https://www.researchgate.net/publication/329350163_Introduction_to_Graph_Signal_Processing)中P19页的Fig. 10(图1,连通图)、P14页的Fig. 7(图2,非连通图)为例。
G_strong = nx.Graph()
G_strong.add_edges_from([
(0, 1), (0, 2), (0, 7),
(1, 2), (1, 3), (1, 4), (1, 7),
(2, 3),
(3, 4), (3, 5), (3, 7),
(4, 5), (4, 6), (4, 7),
(5, 6)
])
G_weak = nx.Graph()
G_weak.add_edges_from([
(0, 1), (0, 2),
(1, 2),
(3, 4), (3, 5), (3, 7),
(4, 5), (4, 6), (4, 7),
(5, 6)
])
# 绘制这两个图
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
nx.draw(G_strong, with_labels=True, node_color='lightblue', node_size=700)
plt.title("Strongly Connected Graph")
plt.subplot(1, 2, 2)
nx.draw(G_weak, with_labels=True, node_color='lightgreen', node_size=700)
plt.title("Weakly Connected Graph")
plt.show()
邻接矩阵更倾向于揭示图的整体连通性,而拉普拉斯矩阵的特征值和特征向量则更多地用于研究图的局部结构特征,如社区结构或者图的连通分量。
为什么拉普拉斯矩阵的第一个特征值总是0?
拉普拉斯矩阵的第一个特征值总是0,其对应的特征向量是一个所有元素都相同的向量。这代表图中所有节点的均匀分布。
拉普拉斯矩阵 L 的第一个特征值总是 0 的原因与拉普拉斯矩阵的定义和图的性质有关。拉普拉斯矩阵 L 定义为度矩阵 D 减去邻接矩阵 A ,即 L = D - A 。
这里是为什么其第一个特征值总是 0:
简而言之,拉普拉斯矩阵的每一行和每一列的和为零这个属性保证了第一个特征值必定是 0。这与图的基本性质密切相关,特别是与图的连通性有关。
其他特征值的意义:
下面对上面的示例图进行拉普拉斯矩阵的特征值分解。
首先,获取图结构的拉普拉斯矩阵:
# 获取拉普拉斯矩阵
L = nx.laplacian_matrix(G).toarray()
L
$$ \begin{bmatrix} 3 & -1 & -1 & -1 & 0 & 0 & 0 & 0 \\ -1 & 5 & -1 & -1 & -1 & -1 & 0 & 0 \\ -1 & -1 & 3 & 0 & -1 & 0 & 0 & 0 \\ -1 & -1 & 0 & 4 & -1 & -1 & 0 & 0 \\ 0 & -1 & -1 & -1 & 5 & -1 & -1 & 0 \\ 0 & -1 & 0 & -1 & -1 & 5 & -1 & -1 \\ 0 & 0 & 0 & 0 & -1 & -1 & 3 & -1 \\ 0 & 0 & 0 & 0 & 0 & -1 & -1 & 2 \\ \end{bmatrix} VS \begin{bmatrix} 2 & -1 & -1 & 0 & 0 & 0 & 0 & 0 \\ -1 & 2 & -1 & 0 & 0 & 0 & 0 & 0 \\ -1 & -1 & 2 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 3 & -1 & -1 & -1 & 0 \\ 0 & 0 & 0 & -1 & 4 & -1 & -1 & -1 \\ 0 & 0 & 0 & -1 & -1 & 3 & 0 & -1 \\ 0 & 0 & 0 & -1 & -1 & 0 & 2 & 0 \\ 0 & 0 & 0 & 0 & -1 & -1 & 0 & 2 \\ \end{bmatrix} $$
需要注意的是,返回的拉普拉斯矩阵的行列顺序并不与图中的顺序相同,矩阵中的行列数据是按照节点的添加顺序来的。如何查看节点的顺序:
list(G.nodes())
# [0, 1, 2, 7, 3, 4, 5, 6]
对于图1来说,因为节点7添加的早,所以排在节点3之前。也就是说,拉普拉斯矩阵中第4行代表的是第7个元素的连接情况。
#求矩阵特征值以及特征向量
eig_value, eig_vector = np.linalg.eig(L)
eig_value
特征值及其可视化:
def autolabel(rects):
for rect in rects:
height = rect.get_height()
height_pos = height + .1 if height >= -0.1 else height - .1
plt.text(
rect.get_x() + rect.get_width() / 2,
height_pos,
'{:.5f}'.format(height),
size=10,
family="Times new roman",
horizontalalignment='center',
verticalalignment='center'
)
fig = plt.bar([i for i in range(len(eig_value))], np.sort(eig_value))
autolabel(fig)
plt.title('Eigenvalues')
plt.show()
图1
图2
图1有一个接近零的特征值,表明图是连通的。图2特征值有两个接近于零的值,这与图中的两个连通分量相对应。特征值为0的数量恰好等于图的连通分量的数量。
图2的 Fiedler 值(最小非0值)为1.58,该 Fiedler 值对应的特征向量是:
$$ \begin{bmatrix} 0. \\ 0. \\ 0. \\ -0.4472136 \\ -0.4472136 \\ -0.4472136 \\ -0.4472136 \\ -0.4472136 \\ \end{bmatrix} $$
说明,该 Fiedler 值代表的是3、4、5、6、7节点对应的连通分量的连通性。
总结:图1的连通性更强,因为其特征值中仅有一个为0;图2包含两个连通分量,因为其特征值中包含两个0。图2中3、4、5、6、7节点组成的连通分量的连通性要高于图1整体的连通性。因为图2中3、4、5、6、7节点组成的连通分量的 Fiedler 值为1.58,大于图1整体的连通分量1.13。
特征向量(图1):
$$ \begin{bmatrix} 0.35355339 & 0.40809973 & -0.38534069 & -0.32322928 & 0.61237244 & 0.10317586 & -0.24182446 & 0.10660991 \\ 0.35355339 & 0.21821011 & 0.07059821 & -0.06640201 & -0.20412415 & 0.58748394 & 0.64631494 & 0.11603433 \\ 0.35355339 & 0.36261755 & -0.3221172 & 0.67753284 & -0.20412415 & -0.23709192 & -0.00834996 & -0.28766179 \\ 0.35355339 & 0.18086105 & 0.27243317 & -0.5085369 & -0.20412415 & -0.62680632 & 0.20197088 & -0.18470141 \\ 0.35355339 & 0.05048967 & 0.33222523 & 0.17458035 & -0.20412415 & -0.05547633 & -0.37548832 & 0.7388255 \\ 0.35355339 & -0.15837433 & 0.24016423 & -0.13207484 & -0.20412415 & 0.41726191 & -0.52854257 & -0.52883224 \\ 0.35355339 & -0.40809973 & 0.38534069 & 0.32322928 & 0.61237244 & -0.10317586 & 0.24182446 & -0.10660991 \\ 0.35355339 & -0.65380405 & -0.59330365 & -0.14509945 & -0.20412415 & -0.08537128 & 0.06409502 & 0.14633561 \\ \end{bmatrix} $$
将特征值从小到大排序,然后可视化特征向量:
def plot_eigenvectors(value, vector):
vector = vector[:, np.argsort(value)]
value = np.sort(value)
# plt.figure(dpi=100)
fig, ax_arr = plt.subplots(len(value), 1, sharex='col', sharey='row', figsize=(5, 2 * len(value)))
fig.subplots_adjust(hspace=0.05, wspace=0.03)
for i in range(len(value)):
ax_arr[i].stem([i for i in range(len(value))], vector[:, i])
ax_arr[i].set_ylabel(f'$u_{{{i}}}(n)$')
plot_eigenvectors(eig_value, eig_vector)
图1
图2
----- END -----