
做图这件事很多人卡住的点其实不是“会不会写建图的代码”而是“图建出来之后别人根本看不出重点”。点和线全都长一个样颜色没有区分边的粗细也看不出差异一张图密密麻麻全是同样的圆点和细线信息量等于零。这一篇就是专门解决这个问题的。作为系列第三篇这里不再重复基础的数据结构而是直接用案例讲透四件事无向图和有向图怎么画、权重怎么映射到边的粗细、点和线怎么按分类信息区分颜色与形状以及大小、尺寸这些视觉参数到底该怎么传。配套代码基于networkx加matplotlib全部是可直接复制的写法。1. 案例背景与设计思路拆解1.1 一个真实的“网络长什么样”的场景这次我用的案例是一个模拟的部门协作网络图中每个节点代表一个部门节点之间存在协作关系就用边连接。为了让案例覆盖所有要讲的技术点我给数据预设了三类属性节点属性kind部门类型比如“核心部门”“支撑部门”“外部合作方”三类用来做点线的分类映射。边属性weight协作强度取 1 到 10 之间的整数用来控制边的粗细。图类型同一套数据分别构建无向图和有向图用来对比展示差异。这里最核心的认知是networkx 的draw系列函数本质上只是把数据属性翻译成视觉属性。你不需要在画图前手工算每个点的大小只需要把节点列表、颜色列表、尺寸列表按对应关系传进去渲染引擎会自己处理。但这恰恰是新手最容易搞混的地方——不知道每个参数到底接收的是“标量”还是“列表”也不知道这些列表的顺序必须和G.nodes()的顺序一致。1.2 为什么选 networkx matplotlib不用多解释networkx是 Python 生态里最成熟的图论与网络分析库内置了常见的布局算法、图生成器、中心性计算而且和matplotlib深度绑定一套代码就能完成建图和渲染。对大多数场景来说没有理由再去用别的高门槛可视化框架。具体到这个案例我的选择逻辑是用nx.Graph()处理无向图用nx.DiGraph()处理有向图API 完全一致切换成本几乎为零。用nx.draw_networkx_nodes、nx.draw_networkx_edges、nx.draw_networkx_labels分开绘制保证每个视觉元素都可以单独控制。布局固定用spring_layout配合随机种子保证每次运行结果一致方便复现。1.3 方案选型用“分开画”代替“一把梭”networkx里有一个nx.draw()函数能把节点、边、标签一把画完但它的问题是参数粒度太粗。你没法单独把某些节点放大也没法让一部分边变粗而另一部分保持细线。所以本案例所有示例都采用“三层拆分”的写法先画边层draw_networkx_edges处理粗细、颜色、箭头。再画节点层draw_networkx_nodes处理大小、颜色、形状。最后画标签层draw_networkx_labels处理文字位置和大小。这样的好处一是有向图的箭头不会被节点盖住二是当点和边的数量变大时你可以灵活调整每一层的zorder值控制遮挡关系。实际项目里我几乎都是用这种拆分写法很少直接用nx.draw()。2. 核心细节与绘图参数解析2.1 “列表传给谁”才是关键这是整个案例里最值得记的一句话node_size、node_color、width、edge_color这些参数都可以接收一个列表列表长度必须和对应元素数量相同。按照惯例nx.Graph在插入边时边的顺序以add_edge的调用顺序为准G.nodes()的返回顺序则和节点插入顺序一致。所以你在构建数据时最好把节点列表先固定下来再按同样的顺序去生成属性列表。举一个错误示范import networkx as nx G nx.Graph() G.add_edges_from([(A, B), (B, C)]) # 错误size_list 的顺序没有对齐节点顺序 size_list [800, 1200] # 本意是 C 最大 nx.draw_networkx_nodes(G, pos, node_sizesize_list)上面这段代码执行时size_list会按G.nodes()的顺序A, B, C分配导致 A 是 800B 是 1200而 C 拿到默认值。正确的做法是先建立节点顺序再生成列表。2.2 权重到底怎么影响边的粗细width参数直接接收数字或数字列表。权重是业务值粗细是视觉值这两者之间通常不能直接画等号。如果原始权重是 1 到 10那么把width直接设成[float(w) for w in weights]会发现权重 1 的边几乎看不见权重 10 的边又粗得像一条带子。实操里我更推荐做一个线性或非线性映射import numpy as np weights [3, 7, 5, 9, 2] min_w, max_w min(weights), max(weights) # 线性映射到 0.5 ~ 6.0 widths [0.5 5.5 * (w - min_w) / (max_w - min_w) for w in weights]如果数据里存在极端离群值比如大多数权重是 1 和 2偶尔跳出个 100那就适合先取对数再映射log_w np.log(weights) min_lw, max_lw log_w.min(), log_w.max() widths [0.5 5.5 * (x - min_lw) / (max_lw - min_lw) for x in log_w]这么做的原因很朴素眼睛对粗细的感知不是线性的一条 6 像素的边并不会让人觉得是 3 像素边的两倍。先用公式压一压极值再映射到合适的像素区间整体画面会干净得多。2.3 颜色分类区分“连续值”和“类别值”点线颜色分为两种用法。像weight这种连续数值可以直接把数值列表传给node_color或edge_color并指定一个cmap颜色映射例如cmapcoolwarm数值大小会自动对应颜色深浅。像kind这种类别值不能直接传给颜色参数必须先把类别转成数值。常见做法是用一个字典给每个类别分配一个数字编号再传编号列表。但这里有个坑不同类别之间应该用离散色而不是连续渐变色。用coolwarm给三个类别编号 0、1、2会让人误以为“类别 1 比类别 0 更冷”其实它们之间没有大小关系。所以更稳妥的做法是自己定义一个类别到颜色的映射字典kind_color_map { 核心部门: #d62728, 支撑部门: #1f77b4, 外部合作方: #2ca02c, } colors [kind_color_map[k] for k in kinds]这样颜色完全由类别决定不受顺序干扰图表语义也更准确。2.4 有向图的箭头和连接方式DiGraph和Graph在画图上的最大区别就是边带箭头。draw_networkx_edges有几个参数直接影响箭头效果arrowstyle-|表示实心三角箭头常用样式还有-、-|、|-。arrowsize箭头缩放系数一般在 10 到 20 之间太大会盖住节点太小又看不清楚。connectionstylearc3,rad0.1让边变成弧线防止双向边完全重叠。尤其当两个节点之间存在双向边时如果都用直线两条箭头会完全重合画面看起来只有一条边。我的常用方案是给双向边分别设置connectionstyle的rad为正负值或者在构建图时人为偏移节点坐标。这个方法后面实操部分会给出完整代码。3. 完整实操从数据到成图的四个案例3.1 准备模拟数据为了保证示例完整我用一个模拟部门协作关系来演示。需要说明的是这个数据集完全是虚构的仅用于展示绘图逻辑。节点为 A、B、C、D、E代表某组织体系内五个参与方每条边有weight属性每个节点有kind属性。import networkx as nx import matplotlib.pyplot as plt import numpy as np # 节点信息 node_kind { A: 核心部门, B: 核心部门, C: 支撑部门, D: 支撑部门, E: 外部合作方, } # 无向边默认 edge_list_undirected [ (A, B, 8), (B, C, 5), (C, D, 3), (A, D, 6), (D, E, 4), (A, E, 7), ] # 有向边用于演示有向图 edge_list_directed [ (A, B, 8), (B, A, 3), (B, C, 5), (C, D, 3), (A, D, 6), (D, E, 4), (A, E, 7), ]这里我把两类边分开准备方便后续直接构建不同类型的图。如果你有现成的DataFrame也可以用nx.from_pandas_edgelist直接读入但需要单独设置节点属性。3.2 案例一无向图 节点大小与颜色分类先做一个最基础的无向图展示节点属性怎么映射到视觉上。G_undirected nx.Graph() G_undirected.add_nodes_from(node_kind.keys()) for u, v, w in edge_list_undirected: G_undirected.add_edge(u, v, weightw) # 固定布局 pos nx.spring_layout(G_undirected, seed42, k1.2) # 按节点顺序生成属性列表 node_order list(G_undirected.nodes()) kinds [node_kind[n] for n in node_order] degrees dict(G_undirected.degree()) # 大小用度中心性作为参考映射到 800 ~ 2000 sizes [800 1200 * (degrees[n] - min(degrees.values())) / (max(degrees.values()) - min(degrees.values()) 1e-9) for n in node_order] # 颜色按类别映射 kind_color_map { 核心部门: #d62728, 支撑部门: #1f77b4, 外部合作方: #2ca02c, } colors [kind_color_map[k] for k in kinds] plt.figure(figsize(8, 6)) # 先画边 nx.draw_networkx_edges(G_undirected, pos, width2, edge_color#aaaaaa) # 再画节点 nodes nx.draw_networkx_nodes( G_undirected, pos, node_sizesizes, node_colorcolors, alpha0.9, ) # 最后画标签 nx.draw_networkx_labels( G_undirected, pos, font_size11, font_colorwhite, font_weightbold, ) # 图例 import matplotlib.patches as mpatches legend_handles [ mpatches.Patch(color#d62728, label核心部门), mpatches.Patch(color#1f77b4, label支撑部门), mpatches.Patch(color#2ca02c, label外部合作方), ] plt.legend(handleslegend_handles, locupper right, frameonTrue) plt.axis(off) plt.tight_layout() plt.show()这里有几个细节值得单独说明。第一sizes的计算里我加了1e-9防止除零这是通用做法。实际项目中如果所有节点度数都一样分母会是 0导致整个式子出错。加一个极小值一劳永逸。第二alpha0.9不是随便加的。当节点有重叠时半透明节点能透出被压住的边和点方便观察底层结构。但透明度不能太低否则节点颜色显得很“脏”。第三图例用的是matplotlib.patches.Patch因为draw_networkx_nodes返回的PathCollection不太适合做离散分类图例。这种做法把“业务分类”和“视觉颜色”的对应关系明确展示出来看图的人不用猜。3.3 案例二无向图 边的权重映射粗细接着让边的粗细反映协作强度。基础的写法已经够了关键在width参数的构造。G_weight nx.Graph() G_weight.add_nodes_from(node_kind.keys()) for u, v, w in edge_list_undirected: G_weight.add_edge(u, v, weightw) pos nx.spring_layout(G_weight, seed42, k1.2) edges list(G_weight.edges(dataTrue)) weights [e[2][weight] for e in edges] min_w, max_w min(weights), max(weights) widths [0.8 4.2 * (w - min_w) / (max_w - min_w) for w in weights] edge_colors weights # 直接用权重映射颜色 # 画边粗细和颜色都从权重来 nx.draw_networkx_edges( G_weight, pos, widthwidths, edge_coloredge_colors, edge_cmapplt.cm.YlOrRd, ) # 画节点这里用固定大小避免干扰 nx.draw_networkx_nodes(G_weight, pos, node_size1200, node_colorwhite, edgecolors#333333, linewidths1.5) nx.draw_networkx_labels(G_weight, pos, font_size11) # 颜色条显示权重与颜色的对应关系 plt.colorbar(plt.cm.ScalarMappable(cmapYlOrRd), axplt.gca(), labelweight, shrink0.8) plt.axis(off) plt.tight_layout() plt.show()用plt.colorbar加一个颜色条是让权重信息真正“可读”的关键一步。没有颜色条时深红色和浅黄色只代表不同但没人知道具体差多少有了颜色条读者就能反查数值区间。这里我也遇到过实际项目中的典型案例某个需求里权重全部集中在 1 和 2只有一条边是 9结果最小宽度 0.8、最大宽度 5 的映射导致除了那条边其他边全是细线。这种情况我会改成对数映射或者干脆用np.percentile做分段映射。后面常见问题会再细说。3.4 案例三有向图 双向箭头与弯曲连接有向图最大的难点是双向边的可视化。两节点之间如果有 A→B 和 B→A 两条边默认画法会把它们画成完全重叠的两条直线看起来很乱。下面代码用弧线区分。G_directed nx.DiGraph() G_directed.add_nodes_from(node_kind.keys()) for u, v, w in edge_list_directed: G_directed.add_edge(u, v, weightw) pos nx.spring_layout(G_directed, seed42, k1.2) # 按边遍历标记双向边 edge_pairs {} for u, v in G_directed.edges(): if (v, u) in G_directed.edges(): edge_pairs[(u, v)] 0.15 edge_pairs[(v, u)] -0.15 else: edge_pairs[(u, v)] 0 # 逐条画边因为每条边连接弧度不同 edges G_directed.edges(dataTrue) for u, v, data in edges: weight data[weight] width 0.8 4.2 * (weight - min_w) / (max_w - min_w) nx.draw_networkx_edges( G_directed, pos, edgelist[(u, v)], widthwidth, edge_colordata[weight], edge_cmapplt.cm.YlOrRd, arrowsTrue, arrowstyle-|, arrowsize14, connectionstylefarc3,rad{edge_pairs[(u, v)]}, ) nx.draw_networkx_nodes( G_directed, pos, node_size1300, node_color#4C72B0, edgecolorswhite, linewidths2, ) nx.draw_networkx_labels(G_directed, pos, font_size11, font_colorwhite) plt.axis(off) plt.tight_layout() plt.show()我把每条边单独绘制而不是一次性传全部edgelist原因是为了给不同边设置不同的connectionstyle。如果一次性传所有边connectionstyle只能共用一个参数双向边还是无法分开。需要注意rad的正负号。正数代表曲线向顺时针方向偏移负数代表逆时针。双向边必须一个正一个负否则还是重叠只是方向相反。对于多对双向边偏移量可以根据边的序号微调比如 0.1、0.2、0.3避免弧线互相交叉太厉害。3.5 案例四全局属性映射的自动化封装实际项目中不太可能每次都手写一遍widths和colors。我会把这种“属性转视觉参数”的逻辑封装成一个函数后续所有网络图都复用。下面给一个精简版def map_weight_to_width(weights, min_width0.6, max_width5.0, log_scaleFalse): import numpy as np weights np.asarray(weights, dtypefloat) if log_scale: weights np.log1p(weights) w_min, w_max weights.min(), weights.max() if w_max w_min: return [min_width] * len(weights) return [min_width (max_width - min_width) * (x - w_min) / (w_max - w_min) for x in weights] def map_kind_to_color(kinds, color_map): return [color_map[k] for k in kinds]这个封装就俩功能但足够应付大部分画图需求。第一次用新版数据集时只需要保证weights和kinds的顺序跟节点/边一致即可。后期再要调整粗细范围直接改min_width、max_width两个参数不用动绘图代码。4. 常见坑点与调试经验4.1 明明传了列表图片还是全部一样大出现这种问题九成是列表顺序没对齐。nx.draw_networkx_nodes会把node_size和node_color按G.nodes()的顺序分配而不是按你脑子里的顺序。我之前排查过一个案例数据集的DataFrame按“类型”排过序但建图时用add_edges_from插入节点顺序完全是另一套结果颜色全错。排查方法是打印一下顺序print(list(G.nodes())) print(sizes) print(colors)肉眼对照一下前五个值就能定位问题。更稳妥的做法是建图后立刻用一个字典或列表推导式根据G.nodes()重新生成属性列表。4.2 节点重叠得没法看当图比较大时spring_layout的默认参数容易让一堆节点挤在一起。优先调整布局参数而不是硬调节点坐标增大k比如k1.5或k2.0让节点间斥力变大。先画一遍图观察重叠区域再手动用pos[node] (x, y)微调个别节点。用nx.random_layout、nx.circular_layout等不同布局对比哪个更适合当前数据。如果节点数量上千那就别用spring_layout了基本跑不动。此时改用nx.kamada_kawai_layout或直接用nx.spectral_layout或者做子图拆分展示。4.3 权重差异太大细边全看不见线性映射遇到离群值就是一场灾难。一个简单的排查方法是先看权重的分布import numpy as np print(np.percentile(weights, [25, 50, 75, 90]))如果 75 分位和最大值差了好几倍说明分布偏斜严重优先用对数映射。实测中我通常先尝试log1p映射再决定是否需要线性。如果你希望保留真实数量关系可以改成“按分位数分段”比如把权重分成低、中、高三档每档给一个固定的宽度区间。4.4 有向图箭头被节点遮住箭头被遮住是因为边的起点和终点都在节点中心箭头尖正好落在节点内部。解决办法有两个把arrowsize调大比如 15 到 18让箭头尖超出节点边缘。使用nx.draw_networkx_edges里的arrowstyle-配合connectionstylearc3,rad0.1让弧线的端点稍微偏离圆心。如果你的matplotlib版本支持draw_networkx_edges里的node_distance参数也可以直接设置这个值来调节箭头离节点的距离。4.5 中文标签显示成方块这个问题几乎每个用 matplotlib 画图的人都遇到过。networkx自己不负责字体渲染最终显示由 matplotlib 决定。先检查系统是否有中文字体然后设置plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False注意axes.unicode_minus要单独设成False否则坐标轴上的负号会显示为方框。如果是在某些服务器环境下没有这些字体可以改标签为英文或者下载可用的字体文件放入 matplotlib 字体目录并重建缓存。4.6 一次性传入大量边导致程序卡死画图卡死大多数不是因为渲染慢而是因为draw_networkx_edges默认会为每条边单独创建对象。节点数量到几百、边到几千时画面对象数量巨大内存暴涨。两个优化方向减少视觉复杂度适当地把边合并、去重或者只画权重高于阈值的边。使用matplotlib的LineCollection或PatchCollection一次性批量绘制。networkx内部其实也是这么做的但你自己控制集合时能更好地限制对象数量。对日常几十个节点的图这些优化用不上但如果你要画知识图谱节点数量通常几百起步建议一开始就按批量绘制的方式写。5. 这套技术能往哪些场景延伸画图本身不是目的目的是让图里的结构信息被快速读懂。这个案例用到的“把属性映射成视觉参数”的思路可以迁移到很多领域。知识图谱可视化节点代表实体颜色代表实体类型边的粗细代表关系强度或共现频次。供应链网络节点代表供应商或仓库节点大小代表库存量边宽度代表物流量。社交网络分析节点颜色代表社群划分节点大小代表影响力边粗细代表互动频率。交通与通信网络节点是车站或基站权重是客流量或带宽占用有向边代表流向。在这些场景里真正有用的往往不是“画得多漂亮”而是“一眼能看到异常”。权重高的边如果明显更粗就可能是一个瓶颈链路某个节点如果又大又红则可能是关键枢纽。视觉编码做对了数据分析结论会非常直观。关于后续扩展方向我自己常用的进阶路线有三个一是用nx.community做社群检测再按社群给节点上色二是用pyvis或plotly生成交互式 HTML 图鼠标悬停就能看属性三是结合中心性指标把degree_centrality、betweenness_centrality映射到节点大小上让“关键节点”自己跳出来。每一步都是在这个案例基础上加一层映射逻辑。最后分享一个小经验画图前先在纸上列出“哪个属性映射到哪个视觉通道”再动手写代码。比如“协作强度→边粗细部门类型→节点颜色连接数→节点大小”。这个一页纸的映射表能帮你省掉大量调参时间也方便团队其他人理解你的图。我在这上面踩过不少次反复改参数的坑每次都是因为一开始没把映射关系想清楚。