☰
Python网络图可视化:nx.draw_networkx参数详解与实战避坑指南
2026/10/15 1:35:37 网站建设 项目流程

1. 从一次社交网络可视化需求说起

前阵子帮一个做社群运营的朋友处理一份数据,大概两千多个用户节点、一万多条关注关系,他想直观地看看这个社群里谁是核心人物、哪些小圈子比较封闭、整体结构是松散还是紧密。我第一反应就是用 NetworkX 配合 Matplotlib 来画,毕竟这套组合在 Python 圈子里几乎是图论可视化的默认选项。结果真正动手的时候才发现,nx.draw_networkx这个函数看着简单,参数一多就容易懵——节点标签重叠、箭头方向看不清、布局挤成一团、中文显示成方块,这些问题一个接一个冒出来。

我把这次踩坑的过程和最后跑通的方案整理出来,重点就是把nx.draw_networkx这个函数彻底讲透。它到底和nx.draw有什么区别,十几个参数分别控制什么,什么场景该用哪个布局,怎么让图从"能看"变成"好看且信息准确"。如果你也在用 Python 做网络关系图、知识图谱可视化、流程依赖分析,或者只是单纯想把手头那堆节点边数据画出来,这篇内容应该能帮你省下不少翻文档和试错的时间。

需要先说明一点:nx.draw_networkx是 NetworkX 提供的一个"一站式"绘图函数,它内部其实调用了好几个更细粒度的绘制函数(画节点、画边、画标签、画箭头),把这些步骤打包成一个调用。理解这一点很关键,因为后面很多参数的行为逻辑,都要从这个"打包"的本质去理解。

2. nx.draw_networkx 与 nx.draw 到底该选哪个

很多人第一次接触 NetworkX 绘图,看到的示例代码里既有nx.draw(G),也有nx.draw_networkx(G),两者画出来的图乍看差不多,于是就开始混用。实际上这两个函数定位不同,选错了会在后期调样式时非常难受。

2.1 两者的调用关系与职责边界

nx.draw是一个更上层的封装,它内部会根据图的类型自动判断要不要画箭头、要不要画标签,然后调用一系列底层函数完成绘制。它的参数相对少,适合快速出图、看个大概。而nx.draw_networkx更接近"手动挡",它把节点、边、标签、箭头这些元素的绘制拆得更清楚,参数也更细,适合需要精细控制每一个视觉元素的场景。

我个人的判断标准很简单:如果只是调试阶段想快速确认图的结构对不对,用nx.draw一行搞定;如果这张图最终要放进报告、要给别人看、要调整颜色大小标签位置,那就直接用nx.draw_networkx,别等到后期再改,因为两者的参数体系不完全兼容,中途换函数等于重写。

2.2 一个容易被忽略的差异:标签默认行为

nx.draw默认是不画节点标签的(除非你显式传with_labels=True),而nx.draw_networkx默认就会把节点标签画出来。这个差异看起来小,但在节点数量多的时候影响很大——两千个节点全打上标签,图会糊成一片黑。所以用nx.draw_networkx时,第一件要养成的习惯就是:节点多的时候主动关掉标签,或者只给关键节点打标签。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() # 快速看结构,用 nx.draw plt.figure(figsize=(8, 6)) nx.draw(G, with_labels=False, node_size=50) plt.title("nx.draw 快速预览") plt.show() # 精细控制,用 nx.draw_networkx plt.figure(figsize=(8, 6)) nx.draw_networkx(G, with_labels=False, node_size=50, node_color="steelblue") plt.title("nx.draw_networkx 精细控制") plt.axis("off") plt.show()

上面这段代码用的是 NetworkX 内置的空手道俱乐部图,节点少、结构清晰,适合拿来练手。注意nx.draw_networkx画完之后我加了plt.axis("off"),因为它的默认坐标轴是打开的,会显示一圈边框和刻度,看起来不够干净。这个细节文档里不会特意强调,但实际出图时几乎每次都要处理。

2.3 什么时候必须用 nx.draw_networkx

有三类场景我强烈建议直接用nx.draw_networkx:第一是需要分别控制节点和边的样式,比如节点按类别上色、边按权重调粗细;第二是需要精确控制标签的字体、大小、背景框;第三是需要在同一张图上叠加多次绘制(比如先画一层底图,再高亮某个子图)。这几种需求用nx.draw都会很别扭,而nx.draw_networkx配合它的兄弟函数能轻松实现。

3. 核心参数逐个拆解:从节点到边的视觉控制

nx.draw_networkx的参数大概有二十来个,但真正高频使用的就那么十来个。我把它们按"控制什么"分成几组来讲,这样比按字母顺序背参数表要清楚得多。

3.1 节点相关参数:大小、颜色、形状、透明度

节点是图的主体,控制节点的参数主要有node_size、node_color、node_shape、alpha、linewidths和edgecolors。

node_size默认是 300,单位是"点面积的平方根"级别,实际感受就是节点直径大概十几个像素。节点多的时候要调小,比如 20 到 50;节点少想突出显示,可以调到 500 甚至 1000。这里有个坑:node_size可以传单个数值,也可以传一个列表,给每个节点单独指定大小。传列表时长度必须和节点数完全一致,否则会报错,而且报错信息不太直观,新手容易卡在这里。

node_color同样支持单值和列表。传列表时,可以传颜色字符串列表,也可以传数值列表配合cmap参数做颜色映射。后者在做"按某个指标给节点上色"时特别有用,比如按节点的度中心性从低到高映射成从浅到深的蓝色。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() # 计算每个节点的度中心性 centrality = nx.degree_centrality(G) node_sizes = [v * 2000 + 100 for v in centrality.values()] node_colors = list(centrality.values()) plt.figure(figsize=(10, 8)) nx.draw_networkx( G, node_size=node_sizes, node_color=node_colors, cmap=plt.cm.Blues, with_labels=False, edgecolors="gray", linewidths=0.5, ) plt.axis("off") plt.show()

这段代码里,节点大小和颜色都跟度中心性挂钩,一眼就能看出哪些节点是"枢纽"。edgecolors给节点加了一圈灰色描边,linewidths控制描边粗细,这两个参数在节点颜色较浅时特别重要,否则节点边界会和背景糊在一起。

node_shape支持几种固定形状,常用的有圆形'o'、方形's'、三角形'^'。注意它只接受单个字符,不支持每个节点不同形状。如果你真的需要不同形状,得用更底层的matplotlib散点图自己画,这就超出nx.draw_networkx的能力范围了。

3.2 边相关参数:宽度、颜色、样式、箭头

边的参数主要有width、edge_color、style、arrows、arrowsize、arrowstyle。

width默认是 1.0,可以传单值也可以传列表。传列表时按边的顺序对应,这个顺序就是G.edges()的迭代顺序。这里有个实操经验:如果你要给边按权重设粗细,最好先确认一下边的顺序,或者干脆用G.edges(data=True)显式遍历一遍构造列表,避免顺序错乱导致粗细对不上。

edge_color同理,支持单值和列表。做加权图时,把权重归一化后映射成颜色深浅,比单纯用粗细表达更直观,因为细线的粗细差异人眼不容易分辨。

style控制线的样式,'solid'、'dashed'、'dotted'、'dashdot'四种。这个参数在区分不同类型的边时很有用,比如实线表示强关系、虚线表示弱关系。

arrows是个布尔值,控制有向图是否画箭头。默认情况下,如果图是有向图,nx.draw_networkx会自动画箭头。但这里有个大坑:箭头默认画在边的中点位置,当边比较短或者节点比较大时,箭头会被节点盖住,看起来就像没有箭头。解决办法是调小node_size,或者用arrowsize把箭头调大,再或者用nx.draw_networkx_edges单独画边并调整min_target_margin参数。

import networkx as nx import matplotlib.pyplot as plt DG = nx.DiGraph() DG.add_weighted_edges_from([ ("A", "B", 3), ("B", "C", 1), ("C", "A", 2), ("A", "D", 5), ("D", "C", 4), ]) pos = nx.circular_layout(DG) weights = [DG[u][v]["weight"] for u, v in DG.edges()] widths = [w * 0.8 for w in weights] plt.figure(figsize=(8, 8)) nx.draw_networkx( DG, pos=pos, width=widths, node_size=800, node_color="lightyellow", edgecolors="black", arrows=True, arrowsize=20, with_labels=True, font_size=12, ) plt.axis("off") plt.show()

这段代码画了一个带权有向图,边的粗细跟权重成正比。circular_layout把节点排成一个圆,适合节点不多、需要看清每条边的情况。注意arrowsize=20比默认值大,就是为了让箭头在节点较大的情况下依然可见。

3.3 标签相关参数:字体、大小、颜色、背景框

标签参数包括with_labels、font_size、font_color、font_family、font_weight、labels、bbox。

with_labels是总开关,设为False就完全不画标签。labels参数可以传入一个字典,自定义每个节点显示的文字,比如节点 ID 是数字,但你想显示成用户名,就用这个参数做映射。

font_family在中文字体场景下是关键。Matplotlib 默认字体不支持中文,直接画中文标签会显示成方块。解决办法是设置font_family为系统里已有的中文字体名,比如'SimHei'、'Microsoft YaHei'等。但更稳妥的做法是全局设置 Matplotlib 的字体,因为nx.draw_networkx的font_family参数在某些版本上传递不太稳定。

import matplotlib.pyplot as plt import networkx as nx # 全局设置中文字体,避免方块 plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False G = nx.Graph() G.add_edges_from([("张三", "李四"), ("李四", "王五"), ("王五", "张三"), ("张三", "赵六")]) pos = nx.spring_layout(G, seed=42) plt.figure(figsize=(8, 6)) nx.draw_networkx( G, pos=pos, node_size=1500, node_color="#AED6F1", edgecolors="gray", font_size=14, font_color="black", font_family="SimHei", bbox=dict(facecolor="white", edgecolor="none", alpha=0.7, pad=0.3), ) plt.axis("off") plt.show()

bbox参数给标签加背景框,在节点密集、标签容易和边重叠的场景下非常实用。alpha设成 0.7 让背景框半透明,既遮住了底下的线,又不会显得太突兀。这个技巧是我在画知识图谱时被逼出来的,不加背景框的话,标签和边线混在一起根本读不清。

3.4 布局与坐标参数:pos 的传入与生成

pos参数决定每个节点的坐标,是影响出图效果最大的因素之一。如果不传pos,nx.draw_networkx会默认调用spring_layout生成一个布局,但每次运行结果可能不同(除非设了随机种子),而且默认参数下的布局往往不够理想。

常用的布局函数有这几个:spring_layout基于力导向,适合大多数无向图,节点会自然聚成簇;circular_layout把节点排成圆,适合展示环形结构或节点数较少的有向图;kamada_kawai_layout也是力导向,但更注重保持节点间的距离比例,适合需要精确反映距离关系的图;shell_layout按层排列,适合有层次结构的图;spectral_layout基于拉普拉斯矩阵特征向量,速度快但布局有时不太直观。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() layouts = { "spring": nx.spring_layout(G, seed=42), "circular": nx.circular_layout(G), "kamada_kawai": nx.kamada_kawai_layout(G), "shell": nx.shell_layout(G), } fig, axes = plt.subplots(2, 2, figsize=(14, 12)) for ax, (name, pos) in zip(axes.flat, layouts.items()): nx.draw_networkx( G, pos=pos, ax=ax, node_size=80, node_color="steelblue", with_labels=False, edge_color="lightgray", ) ax.set_title(name) ax.axis("off") plt.tight_layout() plt.show()

这段代码把四种布局放在一张图里对比,能直观看出差异。spring_layout的seed参数一定要设,否则每次跑出来的图都不一样,做报告时很麻烦。ax参数允许把图绘制到指定的子图上,这是做多图对比的关键,很多人不知道nx.draw_networkx支持ax参数,结果只能一张一张单独画。

4. 布局算法的选择逻辑与调参经验

布局选得好不好,直接决定图能不能看。我见过太多人用默认布局画出来的图,节点挤成一团、边交叉得像毛线球,然后抱怨 NetworkX 不好用。其实问题不在库,在于没根据图的特性选对布局、调对参数。

4.1 spring_layout 的 k 值与 iterations

spring_layout是最常用的布局,它模拟物理弹簧系统:边像弹簧把节点拉近,节点之间又有斥力把它们推开。核心参数是k和iterations。

k控制节点间的理想距离,默认是1/sqrt(n),n 是节点数。k 越大,节点分布越松散;k 越小,节点越紧凑。节点多的时候适当调大 k,能减少重叠。iterations是迭代次数,默认 50,节点多的时候可以调到 100 甚至 200,让布局更稳定。但迭代次数不是越多越好,超过一定值后改善很小,反而浪费时间。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() fig, axes = plt.subplots(1, 3, figsize=(18, 6)) for ax, k in zip(axes, [0.1, 0.3, 0.8]): pos = nx.spring_layout(G, k=k, iterations=100, seed=42) nx.draw_networkx( G, pos=pos, ax=ax, node_size=60, with_labels=False, edge_color="lightgray" ) ax.set_title(f"k={k}") ax.axis("off") plt.tight_layout() plt.show()

实测下来,k 值在 0.2 到 0.5 之间对大多数中小型图比较合适。节点超过一千时,k 可以调到 0.5 以上,同时把iterations提到 100,否则布局会显得很挤。

4.2 大规模图怎么办:从布局到抽样的取舍

当节点数上万时,spring_layout会非常慢,因为它的复杂度大致是 O(n²)。这时候有几个应对策略。

第一是换用spectral_layout,它基于矩阵运算,速度快很多,但布局的直观性差一些。第二是先对图做抽样或聚合,比如只画度最高的前几百个节点,或者用社区发现算法把图聚合成若干个超节点再画。第三是干脆放弃在 Matplotlib 里画全图,改用专门的图可视化工具,但那就不在本文讨论范围内了。

我自己的经验是:超过三千个节点,nx.draw_networkx配合 Matplotlib 就已经很吃力了,出图质量也会明显下降。这时候与其硬画,不如先做数据层面的精简,把真正需要展示的结构提炼出来。

4.3 固定布局做对比:seed 与 pos 复用

做对比实验时,最忌讳每次布局都变。正确做法是先把pos算好存下来,后续所有绘制都复用同一个pos。这样节点位置固定,只有你想对比的那个变量(比如颜色、大小)在变,对比才有意义。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() pos = nx.spring_layout(G, seed=42) # 固定布局 fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # 左图:按度中心性上色 centrality = nx.degree_centrality(G) nx.draw_networkx( G, pos=pos, ax=axes[0], node_color=list(centrality.values()), cmap=plt.cm.Reds, node_size=100, with_labels=False, ) axes[0].set_title("按度中心性上色") axes[0].axis("off") # 右图:按聚类系数上色 clustering = nx.clustering(G) nx.draw_networkx( G, pos=pos, ax=axes[1], node_color=list(clustering.values()), cmap=plt.cm.Greens, node_size=100, with_labels=False, ) axes[1].set_title("按聚类系数上色") axes[1].axis("off") plt.tight_layout() plt.show()

这段代码里pos只算了一次,两张图共用,节点位置完全一致,读者能直接对比两种指标下的颜色分布差异。这个技巧在写分析报告时特别有用。

5. 那些文档里不会写的踩坑记录

参数讲完了,接下来是我在实际项目中踩过的坑。这些问题在官方文档里要么没提,要么一笔带过,但每一个都曾让我卡了半小时以上。

5.1 中文标签显示方块的三层排查

中文显示方块是最高频的问题。排查要分三层:第一层是字体是否安装,第二层是 Matplotlib 是否配置了中文字体,第三层是nx.draw_networkx的font_family参数是否覆盖了全局配置。

很多人只做了第二层,设置了plt.rcParams["font.sans-serif"],但nx.draw_networkx内部调用text时如果显式传了font_family,会覆盖全局设置。所以最稳妥的做法是全局配置和函数参数都设成同一个中文字体名。另外,axes.unicode_minus要设成False,否则负号也会显示异常。

注意:不同操作系统可用的中文字体名不同。Windows 上常用SimHei、Microsoft YaHei,macOS 上常用Arial Unicode MS、PingFang SC,Linux 上则取决于安装了什么字体。写跨平台代码时,最好做一个字体回退列表。

5.2 箭头被节点盖住的定位问题

有向图的箭头被节点盖住,根本原因是箭头的绘制位置和节点的绘制顺序、大小有关。nx.draw_networkx默认先画节点再画边,但箭头是边的一部分,如果节点半径大于边的一半长度,箭头就落在节点内部了。

解决办法有三个:调小node_size、调大arrowsize、或者用nx.draw_networkx_edges单独画边并设置min_target_margin。第三个方案最灵活,但需要多写几行代码。

import networkx as nx import matplotlib.pyplot as plt DG = nx.DiGraph() DG.add_edges_from([("A", "B"), ("B", "C"), ("C", "A")]) pos = nx.circular_layout(DG) plt.figure(figsize=(8, 8)) nx.draw_networkx_nodes(DG, pos, node_size=2000, node_color="lightblue") nx.draw_networkx_labels(DG, pos, font_size=14) nx.draw_networkx_edges( DG, pos, arrows=True, arrowsize=25, min_target_margin=25, # 箭头离目标节点留出距离 edge_color="gray", ) plt.axis("off") plt.show()

min_target_margin的单位是像素,设成节点半径左右的值,箭头就会停在节点边缘而不是被盖住。这个参数在nx.draw_networkx里不能直接传,必须拆开用nx.draw_networkx_edges,这是很多人不知道的。

5.3 节点标签重叠的缓解思路

标签重叠没有完美解法,只能缓解。常用手段有:调大k让节点散开、只给重要节点打标签、给标签加bbox背景框、把标签字体调小。

如果节点实在太多,我的做法是只给度最高的前 10% 节点打标签,其余节点不打。实现方式是构造一个labels字典,只包含需要显示的节点。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() pos = nx.spring_layout(G, k=0.5, seed=42) # 只给度最高的 8 个节点打标签 top_nodes = sorted(G.degree, key=lambda x: x[1], reverse=True)[:8] labels = {n: str(n) for n, _ in top_nodes} plt.figure(figsize=(10, 8)) nx.draw_networkx( G, pos=pos, node_size=100, with_labels=True, labels=labels, font_size=12, font_color="darkred", bbox=dict(facecolor="white", edgecolor="none", alpha=0.8), ) plt.axis("off") plt.show()

这样图上只有关键节点的标签,既保留了信息量,又不会糊成一片。

5.4 保存图片时的白边与分辨率

plt.savefig保存出来的图经常带一圈白边,或者分辨率不够。解决办法是bbox_inches="tight"去掉白边,dpi设成 300 保证印刷质量。

plt.savefig("network.png", dpi=300, bbox_inches="tight", transparent=False)

如果背景要透明,把transparent设成True。但注意透明背景在有些文档里显示效果不好,除非明确需要,否则还是白底稳妥。

6. 从能画到好用:几个实战场景的完整方案

参数和坑都讲完了,最后给几个完整场景的方案,可以直接拿去改。

6.1 社交网络核心人物高亮

需求是画一个社交关系图,把度中心性最高的几个节点用不同颜色和大小突出显示。

import networkx as nx import matplotlib.pyplot as plt G = nx.karate_club_graph() pos = nx.spring_layout(G, k=0.4, iterations=100, seed=42) centrality = nx.degree_centrality(G) # 按中心性分三档 node_colors = [] node_sizes = [] for n in G.nodes(): c = centrality[n] if c > 0.3: node_colors.append("#E74C3C") node_sizes.append(600) elif c > 0.15: node_colors.append("#F39C12") node_sizes.append(300) else: node_colors.append("#85C1E9") node_sizes.append(100) plt.figure(figsize=(12, 10)) nx.draw_networkx_edges(G, pos, edge_color="#D5D8DC", width=0.8) nx.draw_networkx_nodes( G, pos, node_color=node_colors, node_size=node_sizes, edgecolors="white", linewidths=1.5, ) nx.draw_networkx_labels( G, pos, labels={n: str(n) for n in G.nodes() if centrality[n] > 0.3}, font_size=11, font_color="#2C3E50", ) plt.axis("off") plt.tight_layout() plt.savefig("social_network.png", dpi=300, bbox_inches="tight") plt.show()

这个方案的关键是把节点分成三档,用颜色和大小双重编码,视觉层次很清楚。边统一用浅灰色细线,不抢节点的视觉焦点。

6.2 有向依赖图的清晰呈现

需求是画一个任务依赖图,箭头方向表示依赖关系,需要保证箭头清晰可见。

import networkx as nx import matplotlib.pyplot as plt DG = nx.DiGraph() DG.add_edges_from([ ("需求", "设计"), ("设计", "开发"), ("开发", "测试"), ("测试", "上线"), ("设计", "评审"), ("评审", "开发"), ]) pos = nx.spring_layout(DG, seed=42, k=1.2) plt.figure(figsize=(10, 8)) nx.draw_networkx_nodes(DG, pos, node_size=2500, node_color="#D6EAF8", edgecolors="#2874A6", linewidths=2) nx.draw_networkx_labels(DG, pos, font_size=13, font_family="SimHei") nx.draw_networkx_edges( DG, pos, arrows=True, arrowsize=22, edge_color="#5D6D7E", width=1.5, min_target_margin=30, connectionstyle="arc3,rad=0.1", ) plt.axis("off") plt.tight_layout() plt.show()

connectionstyle参数让边带一点弧度,避免多条边重叠在一起。min_target_margin保证箭头不被节点盖住。这两个参数配合使用,有向图的可读性会大幅提升。

6.3 加权图的边粗细与颜色双编码

需求是画一个带权重的网络,权重既影响边的粗细也影响颜色。

import networkx as nx import matplotlib.pyplot as plt G = nx.Graph() G.add_weighted_edges_from([ ("A", "B", 5), ("B", "C", 2), ("C", "D", 8), ("D", "A", 3), ("A", "C", 6), ("B", "D", 1), ]) pos = nx.circular_layout(G) weights = [G[u][v]["weight"] for u, v in G.edges()] max_w = max(weights) widths = [1 + 4 * (w / max_w) for w in weights] edge_colors = [plt.cm.OrRd(w / max_w) for w in weights] plt.figure(figsize=(8, 8)) nx.draw_networkx_nodes(G, pos, node_size=1200, node_color="#FDEBD0", edgecolors="#B9770E", linewidths=2) nx.draw_networkx_labels(G, pos, font_size=14) nx.draw_networkx_edges(G, pos, width=widths, edge_color=edge_colors) plt.axis("off") plt.tight_layout() plt.show()

权重归一化后同时映射到粗细和颜色,双重编码让权重差异一目了然。plt.cm.OrRd是一个从浅橙到深红的色带,适合表达"强度"这类语义。

7. 我个人的几个使用习惯

用了这么久nx.draw_networkx,有几个习惯是踩坑踩出来的,分享出来供参考。

第一,永远先算pos再画图,不要依赖默认布局。默认布局每次跑都不一样,调试时很痛苦,出报告时更痛苦。

第二,节点超过 200 个就关标签,超过 500 个就考虑抽样。硬画全图的结果通常是既看不清结构又看不清标签,两头不讨好。

第三,边和节点分开画。虽然nx.draw_networkx能一次画完,但分开用nx.draw_networkx_nodes、nx.draw_networkx_edges、nx.draw_networkx_labels能获得更精细的控制,尤其是在需要调整绘制顺序(比如让节点盖住边)的时候。

第四,保存图片时dpi=300和bbox_inches="tight"是标配,这两个参数能让出图质量提升一个档次,而且几乎没有任何副作用。

第五,遇到中文问题先检查字体配置,再检查函数参数,最后检查系统字体是否真的存在。三层排查下来基本能解决所有中文显示问题。

这套组合拳打下来,nx.draw_networkx基本能满足日常所有的网络可视化需求。它不是什么高深的东西,但参数多、细节杂,把每个参数的行为和背后的逻辑搞清楚,出图效率和质量都会有明显提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询