4、NetworkX入门:创建图、添加节点和边、图的基本属性查看、图的绘制基础
好,咱们正式开始动手了。
前面聊了那么多图论的概念,什么节点、边、度、路径……说实话,光看理论容易晕。我自己的经验是,上手敲代码才是最快的学习方式。今天我们就用Python里最经典的NetworkX库,把那些抽象的概念变成实实在在能跑、能看的东西。
4.1 安装与导入:准备工作
NetworkX是Python的第三方库,安装很简单。我个人习惯用pip,一行命令搞定:
pip install networkx matplotlib
这里顺便把matplotlib也装上,因为后面画图要用到它。嗯,注意一下,如果你用的是Anaconda环境,可能已经自带了这两个库,可以跳过安装步骤。
导入的时候,我一般这样写:
import networkx as nx
import matplotlib.pyplot as plt
为什么要起别名?说白了就是图个方便。后面每次调用nx.Graph()比敲networkx.Graph()省事多了,你试试就知道。
4.2 创建图:空图起步
创建一个空图,就像准备一张白纸。代码极其简单:
G = nx.Graph() # 创建一个无向图
print(type(G)) # <class 'networkx.classes.graph.Graph'>
这里创建的是无向图。如果你需要处理有向关系(比如资金流向、A给B转账),那就用nx.DiGraph()。我在做反欺诈项目时,经常用有向图来追踪资金链路,这个后面会细讲。
另外还有nx.MultiGraph()和nx.MultiDiGraph(),支持两个节点之间有多条边。比如两个人之间既有微信转账又有支付宝转账,就可以用多重图来表示。不过刚开始学,咱们先聚焦在基础的无向图上。
4.3 添加节点和边:给图注入灵魂
图建好了,接下来就是往里塞东西。节点和边是图的两个基本元素,咱们一个一个来。
4.3.1 添加节点
添加单个节点:
G.add_node(1) # 添加一个节点,标签为1
G.add_node("Alice") # 节点可以是字符串
G.add_node((1, 2)) # 甚至可以是元组
添加多个节点:
G.add_nodes_from([2, 3, 4, "Bob"])
节点可以是任何可哈希的对象。整数、字符串、元组都行,但列表不行。为什么?因为列表是可变的,不能作为字典的键。NetworkX底层就是用字典来存节点的,这个细节你了解一下就好,不用死记。
4.3.2 添加边
添加一条边:
G.add_edge(1, 2) # 在节点1和2之间连一条边
G.add_edge("Alice", "Bob")
添加多条边:
edges = [(1, 2), (2, 3), (3, 4), (1, 4)]
G.add_edges_from(edges)
这里有个小细节:如果添加边时,两端的节点还不存在,NetworkX会自动帮你创建这些节点。比如:
G.add_edge(5, 6) # 节点5和6会自动添加到图中
这个特性挺方便的,但有时候也会带来隐患。我曾经在项目中因为拼写错误,不小心多了一个节点,排查了半天才发现。所以建议你养成好习惯:先加节点,再加边,或者至少确认节点名称是正确的。
4.3.3 带属性的节点和边
现实中的金融网络,节点和边往往带有额外信息。比如一个节点代表一家公司,它可能有“市值”、“行业”等属性;一条边代表一笔交易,可能有“金额”、“时间”等属性。NetworkX支持给节点和边添加属性:
# 添加带属性的节点
G.add_node("腾讯", 市值=45000, 行业="科技")
G.add_node("阿里", 市值=40000, 行业="科技")
# 添加带属性的边
G.add_edge("腾讯", "阿里", 金额=5000, 时间="2024-01-15")
属性可以随时查看和修改。我个人觉得,属性是让图变得“有血有肉”的关键。没有属性的图只是一堆点和线,有了属性,它才能反映真实世界的复杂关系。
4.4 图的基本属性查看:你的图长什么样?
图建好了,怎么知道它里面有什么?NetworkX提供了一系列查看属性的方法。我挑几个最常用的:
| 方法 | 作用 | 示例输出 |
|---|---|---|
G.nodes() |
查看所有节点 | [1, 2, 3, 4, 'Alice', 'Bob'] |
G.edges() |
查看所有边 | [(1, 2), (2, 3), (3, 4), (1, 4)] |
G.number_of_nodes() |
节点数量 | 6 |
G.number_of_edges() |
边的数量 | 4 |
G.degree() |
查看每个节点的度 | [(1, 2), (2, 2), (3, 2), (4, 2)] |
G.neighbors(1) |
查看节点1的邻居 | [2, 4] |
G.has_edge(1, 2) |
判断边是否存在 | True |
举个例子,咱们把刚才创建的图拿出来看看:
print("节点列表:", list(G.nodes()))
print("边列表:", list(G.edges()))
print("节点数量:", G.number_of_nodes())
print("边的数量:", G.number_of_edges())
print("每个节点的度:", dict(G.degree()))
输出结果:
节点列表: [1, 2, 3, 4, 'Alice', 'Bob', '腾讯', '阿里']
边列表: [(1, 2), (2, 3), (3, 4), (1, 4), ('Alice', 'Bob'), ('腾讯', '阿里')]
节点数量: 8
边的数量: 6
每个节点的度: {1: 2, 2: 2, 3: 2, 4: 2, 'Alice': 1, 'Bob': 1, '腾讯': 1, '阿里': 1}
看到没?degree()返回的是一个字典,键是节点,值是该节点的度。度这个概念,咱们前面讲过,就是节点连接的边的数量。这里节点1的度是2,因为它连接了节点2和节点4。
G.degree(1)。如果想看所有节点的度排序,可以用 sorted(G.degree(), key=lambda x: x[1], reverse=True)。这在分析“谁是最重要的节点”时非常有用。
4.5 图的绘制基础:一图胜千言
数据看完了,但说实话,光看数字不够直观。尤其是当图比较大的时候,你很难从数字中看出结构。这时候就需要画图了。
NetworkX本身不负责绘图,它把绘图的任务交给了matplotlib。不过它提供了一套非常方便的封装,让你几行代码就能出图。
4.5.1 最简单的画法
nx.draw(G, with_labels=True)
plt.show()
就这么两行,一张图就出来了。with_labels=True表示在节点上显示标签(也就是节点的名称)。如果不加这个参数,节点就是一个个小圆点,你分不清谁是谁。
4.5.2 自定义布局
默认的布局是spring_layout,它模拟了物理弹簧系统,让相互连接的节点靠得更近。效果通常不错,但有时候节点会叠在一起。你可以试试其他布局:
# 圆形布局
pos = nx.circular_layout(G)
nx.draw(G, pos, with_labels=True)
plt.show()
# 分层布局(适合树状结构)
pos = nx.shell_layout(G)
nx.draw(G, pos, with_labels=True)
plt.show()
# 随机布局(不推荐,但了解一下)
pos = nx.random_layout(G)
nx.draw(G, pos, with_labels=True)
plt.show()
我个人最常用的是spring_layout和kamada_kawai_layout。后者在节点数量不多时,布局效果非常漂亮,节点分布均匀,很少重叠。
4.5.3 美化你的图
默认的图是黑白的,有点单调。咱们可以加点颜色和样式:
pos = nx.spring_layout(G, seed=42) # seed固定随机种子,让每次布局一致
nx.draw_networkx_nodes(G, pos, node_color='skyblue', node_size=500)
nx.draw_networkx_edges(G, pos, edge_color='gray', width=2)
nx.draw_networkx_labels(G, pos, font_size=12, font_color='black')
plt.axis('off') # 关闭坐标轴
plt.show()
这里我把节点画成天蓝色,边画成灰色,还调整了节点大小和字体。看起来专业多了吧?
plt.axis('off') 是个好习惯。否则图上会显示坐标轴刻度,对于网络图来说毫无意义,反而显得杂乱。
4.5.4 带属性的图怎么画?
如果节点有属性,比如“市值”,你可以根据市值大小来调整节点颜色或大小:
# 假设节点有市值属性
node_sizes = [G.nodes[n].get('市值', 100) / 100 for n in G.nodes()]
node_colors = ['red' if G.nodes[n].get('行业') == '科技' else 'blue' for n in G.nodes()]
nx.draw(G, pos, node_size=node_sizes, node_color=node_colors, with_labels=True)
plt.show()
这样,市值大的节点就大,科技行业的节点是红色,其他行业是蓝色。一眼就能看出哪些是“大块头”,哪些是“科技公司”。
4.6 本章知识体系总览
为了让你对本章内容有个整体把握,我画了一张结构图:
4.7 避坑指南:我踩过的几个坑
最后,分享几个我实际项目中遇到的坑,希望能帮你省点时间。
我曾经在一个项目中,有的节点用整数(比如1、2、3),有的节点用字符串(比如"1"、"2"、"3")。结果在查询边的时候,
G.has_edge(1, "1") 返回False,但实际上它们应该是同一个节点。因为NetworkX把整数1和字符串"1"当成两个不同的节点。所以,统一节点名称的数据类型,要么全用整数,要么全用字符串。
每次运行
nx.draw(),如果不指定seed参数,布局都会变化。这在调试时很烦人——你刚记住某个节点的位置,重新运行一次它就跑了。我的习惯是:pos = nx.spring_layout(G, seed=42),固定种子,布局不变。
如果你有几千个节点,直接用
nx.draw()可能会卡死。因为matplotlib要渲染那么多元素,很吃内存。这时候我建议用nx.draw_networkx_nodes()和nx.draw_networkx_edges()分开绘制,并且关闭标签显示(with_labels=False)。如果图实在太大,可以考虑用nx.write_gexf()导出到Gephi等专业工具中可视化。
好了,这一章的内容就到这里。你现在应该能自己创建一个图,往里面加节点和边,查看它的基本属性,并且把它画出来了。别光看,打开你的Python环境,敲几行代码试试。动手才是硬道理。