原文:
towardsdatascience.com/graph-visualization-7-steps-from-easy-to-advanced-4f5d24e18056
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f11219b4d9364394a30b8524c3756a4f.png
Davis 的南方俱乐部图,图片由作者提供
一些数据类型,如社交网络或知识图谱,可以“原生”地以图形形式表示。这种数据的可视化可能具有挑战性,而且没有通用的配方。在这篇文章中,我将展示使用开源NetworkX库进行图形可视化的几个步骤。
让我们开始吧!
基本示例
如果我们想在 Python 中使用图表,NetworkX 可能是最受欢迎的选择。它是一个用于网络分析的开放源代码 Python 包,包括不同的算法和强大的功能。正如我们所知,每个图都包含节点(顶点)和它们之间的关系;我们可以在 NetworkX 中轻松创建一个简单的图:
importnetworkxasnx G=nx.Graph()G.add_node("A")G.add_node("B")G.add_edge("A","B")...然而,以这种方式创建一个大型图可能会很累人,在这篇文章中,我将使用 NetworkX 库中包含的“Davis 的南方俱乐部女性”图(3-clause BSD 许可)。这些数据由 A. Davis 等人于 20 世纪 30 年代收集(A. Davis,1941 年,《深南》,芝加哥:芝加哥大学出版社)。它代表了 18 位南方女性参加 14 个社交活动的观察结果。让我们加载这个图并绘制它:
importnetworkxasnximportmatplotlib.pyplotasplt G=nx.davis_southern_women_graph()fig1=plt.figure(figsize=(12,8))nx.draw(G,with_labels=True)plt.show()结果看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b5c7fb53c0265292a3e9bed3a1e88c1e.png
Davis 的南方俱乐部女性图,图片由作者提供
它是可行的,但这张图片肯定可以改进。让我们看看不同的方法来实现它。
1. 布局
根据定义,一个图本身只包含节点和它们之间的关系;它没有任何坐标。同一个图可以用许多不同的方式显示,NetworkX 中也有不同的布局可供选择。没有一种通用的解决方案适合所有情况,视觉印象也可能具有主观性。最好的方法是尝试不同的选项,找到更适合特定数据集的图像。
螺旋布局这种布局可以通过使用spiral_layout方法生成:
pos=nx.spiral_layout(G)print(pos)#> {'Evelyn Jefferson': array([-0.51048124, 0.00953613]),# 'Laura Mandeville': array([-0.59223481, -0.08317364]), ... }nx.draw(G,pos=pos,with_labels=True)如我们从print输出中可以看到,布局本身只是一个包含坐标的字典。这个布局可以作为draw方法的可选参数指定。结果看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c612617ac081ef4f435ee54b2e0ff81e.png
螺旋布局,图片由作者提供
对于这种类型的图,这并不是最好的选择;让我们尝试其他方法。
圆形布局在这里,代码逻辑是相同的。首先,我们创建一个布局,然后我们在代码中使用它:
pos=nx.circular_layout(G)nx.draw(G,pos=pos,with_labels=True)结果:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/eba20af5a3705254d27b49cda49c9b52.png
循环布局,图片由作者提供
就像上一个例子一样,圆形布局对于这个图表来说并不是最好的。
Kamada-Kawai 布局此方法使用 Kamada-Kawai 路径长度成本函数:
pos=nx.kamada_kawai_layout(G)nx.draw(G,pos=pos,with_labels=True)结果看起来更好:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3ec1cb1a5a17fbaa89e815c4eb223588.png
Kamada-Kawai 布局,图片由作者提供
弹簧布局此方法使用 Fruchterman-Reingold 力导向算法,它作为一种“反重力力”,将节点彼此拉远,除非系统达到平衡。
pos=nx.spring_layout(G,seed=42)nx.draw(G,pos=pos,with_labels=True)主观上,结果看起来最好:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0418362d12fc27548587d65fc97b029b.png
弹簧布局,图片由作者提供
这里的seed参数在我们要得到相同结果时很有用,否则,每次重绘都会产生另一个看起来不同的图表。
其他图形布局类型在 NetworkX 中可用;读者可以自己尝试它们。
2. 节点颜色
作为提醒,我们的图表代表了 18 位参与 14 个社交活动的女性。图表中的所有事件都有“Exx”名称;让我们改变它们的颜色以获得更好的视觉效果。
首先,我将创建一个辅助方法来检测节点是否是事件:
defis_event_node(node:str)->bool:""" Check if events starts with Exx """returnre.match("^Ed",node)isnotNone在这里,我使用正则表达式来确定节点模式(我的第一次尝试是使用node.startswith("E")方法,但一些女性的名字也可以以"E"开头)。现在,我们可以轻松地为每个节点创建一个颜色数组,并使用它来绘制图表:
defget_node_color(node:str)->str:""" Get color of the individual node """return"#00AA00"ifis_event_node(node)else"#00AAEE"node_colors=[get_node_color(node)fornodeinG.nodes()]nx.draw(G,pos=pos,node_color=node_colors,with_labels=True)结果看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cb5bf4a4243256dda16511a47be1eed5.png
节点颜色,图片由作者提供
3. 节点大小
就像颜色一样,我们可以指定每个节点的大小。让我们使“事件”节点更大;节点大小也可以与它的连接数成比例:
edges={node:len(G.edges(node))fornodeinG.nodes()}defnode_size(node:str)->int:""" Get size of the individual node """k=4ifis_event_node(node)else1return100*k+100+50*edges[node]node_sizes=[node_size(node)fornodeinG.nodes()]nx.draw(G,pos=pos,node_color=node_colors,node_size=node_sizes,with_labels=True)在这里,我将每个节点的边数保存到一个单独的字典中。我还使用了之前相同的is_event_node方法来使“事件”节点更大。
结果看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5e2845cbfd390133ce9d548c3f1794a7.png
不同的节点大小,图片由作者提供
4. 边颜色
我们不仅可以指定节点颜色,还可以指定边颜色。作为一个例子,让我们突出显示 Theresa Anderson 访问的所有事件。为此,我需要三个辅助方法:
highlighted_node="Theresa Anderson"defget_node_color(node:str)->str:""" Get color of the individual node """ifis_event_node(node):ifG.has_edge(node,highlighted_node):return"#00AA00"elifnode==highlighted_node:return"#00AAEE"return"#AAAAAA"defedge_color(node1:str,node2:str)->str:""" Get color of the individual edge """ifnode1==highlighted_nodeornode2==highlighted_node:return"#992222"return"#999999"defedge_weight(node1:str,node2:str)->str:""" Get width of the individual edge """ifnode1==highlighted_nodeornode2==highlighted_node:return3return1在这里,我为 Theresa Anderson 的节点使用了单独的颜色。我还改变了她访问的所有事件的颜色;has_edge方法是一个简单的方法来查找两个节点是否有共同边。我还改变了边的权重。
现在,我们可以绘制图表:
edge_colors=[edge_color(n1,n2)forn1,n2inG.edges()]edge_weights=[edge_weight(n1,n2)forn1,n2inG.edges()]node_colors=[get_node_color(node)fornodeinG.nodes()]nx.draw(G,pos=pos,node_color=node_colors,node_size=node_sizes,edge_color=edge_colors,width=edge_weights,with_labels=True)结果看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/00f8d128b2628847c7f1f4db8f2295ed.png
突出显示节点和边的图表,图片由作者提供
5. 节点标签
当我们有一个具有不同节点类型的图时,我们可以为不同的节点使用不同的字体。然而,令我惊讶的是,在 NetworkX 中,没有简单的方法来指定字体,就像我们为颜色所做的那样。为了绘制“事件”和“人物”节点,我们可以将图分成子图并分别绘制:
node_events=[nodefornodeinG.nodes()ifis_event_node(node)]node_people=[nodefornodeinG.nodes()ifnotis_event_node(node)]nx.draw(G,pos=pos,node_color=node_colors,node_size=node_sizes,with_labels=False)nx.draw_networkx_labels(G.subgraph(node_events),pos=pos,font_weight="bold")nx.draw_networkx_labels(G.subgraph(node_people),pos=pos,font_weight="normal",font_size=11)在这里,我首先像以前一样绘制节点,但将with_labels参数设置为 False。然后我使用了两次draw_networkx_labels方法,并设置了不同的字体设置。
输出看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d1f171ea9877f381990f7d3574a724b6.png
具有不同标签的图,图片由作者提供
6. 节点属性
我们能够使用辅助 Python 方法设置节点颜色和大小。然而,使用节点属性,我们也可以将此信息保存到图中:
colors_dict={node:get_node_color(node)fornodeinG.nodes()}nx.set_node_attributes(G,colors_dict,"color")我们也可以为一些节点手动指定属性:
custom_colors_dict={"Frances Anderson":"orange","Theresa Anderson":"orange","E3":"darkgreen","E5":"darkgreen","E6":"darkgreen"}nx.set_node_attributes(G,custom_colors_dict,"color")然后,我们可以将图保存到文件中,所有信息都将被保留:
nx.write_gml(G,"davis_southern_women.gml")然后,我们可以加载图并从节点属性中提取所有颜色;我们不再需要任何辅助方法:
attributes=nx.get_node_attributes(G,"color")node_color_attrs=[attributes[node]fornodeinG.nodes()]nx.draw(G,pos=pos,node_color=node_color_attrs,node_size=node_sizes,with_labels=False)nx.draw_networkx_labels(G.subgraph(node_events),pos=pos,font_weight="bold")nx.draw_networkx_labels(G.subgraph(node_people),pos=pos,font_weight="normal",font_size=11)结果看起来像这样:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/14dd3991dd62f0424c6d41e4cda9eb68.png
具有节点属性的图,图片由作者提供
这里,我们看到与之前相同的颜色和四个具有自定义颜色的节点;所有信息都保存到了 GML 文件中。
7. 奖励:使用 D3.JS 绘制图
当我们绘制图时,NetworkX 在底层使用 Matplotlib。这对于像这样的小图来说是可以的,但如果图中包含 1000+个节点,Matplotlib 就会变得非常慢。使用D3.JS可以获得更好的结果。D3.JS 是一个开源的 JavaScript 库,可以更有效地进行图可视化。D3 是一个成熟的数据可视化项目(第一个版本于 2011 年发布),它不仅适用于图;在示例画廊中可以找到许多美丽的图像。
我没有找到将 NetworkX 图“原生”导出到 D3 的方法;然而,我们可以用几行代码做到这一点:
defconvert_to_d3(graph:nx.Graph)->dict:""" Convert nx.Graph to D3 data """nodes,edges=[],[]fornode_nameingraph.nodes:nodes.append({"id":node_name,"color":get_node_color(node_name),"radius":0.01*node_size(node_name)})fornode1,node2ingraph.edges:edges.append({"source":node1,"target":node2})return{"nodes":nodes,"links":edges}# Save in D3 formatd3=convert_to_d3(G)withopen('d3_graph.json','w',encoding='utf-8')asf_out:json.dump(d3,f_out,ensure_ascii=False,indent=2)之后,我们可以将 JSON 数据加载到一个 JavaScript 页面中:
<scripttype="module">//Specify the dimensions of the chart.const width=window.innerWidth;const height=window.innerHeight;//Specify the color scale.const color=d3.scaleOrdinal(d3.schemeTableau10);const data=awaitd3.json("./d3_graph.json");const links=data.links.map(d=>({...d}));const nodes=data.nodes.map(d=>({...d}));//Create the SVG container.const svg=d3.create("svg").attr("width",width).attr("height",height).attr("viewBox",[0,0,width,height]).attr("style","max-width: 100%; height: auto;");...//Create a simulationwithseveral forces.const simulation=d3.forceSimulation(nodes).force("link",d3.forceLink(links).id(d=>d.id)).force("charge",d3.forceManyBody()).force("center",d3.forceCenter(width/2,height/2)).on("tick",ticked);//Append the SVG element.container.append(svg.node());</script>本文不专注于 JavaScript 本身;在 D3.JS 中绘制图的代码示例很容易在网上找到(这个可以作为一个好的开始;完整源代码的链接也位于本页末尾)。
最后,我们可以运行本地服务器并在浏览器中打开一个页面。作为 JavaScript 的一个优点,我们可以使图交互式,甚至可以通过拖放移动节点:
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3ae276fe9f6341972bac8284700e5cec.png
图可视化,图片由作者提供
作为缺点,几乎每次在 D3 渲染中的更改都需要深入到 JavaScript、CSS 和 HTML 样式。我不是前端网页开发者,即使是微小的调整也需要花费太多时间(然而,学习新事物总是令人愉快的:)。例如,我示例中的默认图形大小太小,我没有找到一种简单的方法来设置它的默认“缩放”值。然而,对于复杂的图形,别无选择,因为 Matplotlib 渲染太慢。我使用了 D3 库来可视化现代艺术家的图形,结果很好:
Python 数据分析:我们了解现代艺术家什么?
结论
在这篇文章中,我展示了使用 NetworkX 制作图形可视化的不同方法。正如我们所见,这个过程主要是直截了当的,我们可以轻松调整许多参数,如节点大小或颜色。更复杂的图形可以导出为 JSON,并与 JavaScript 一起使用。之后,我们可以使用强大的 D3.JS 库——在网页浏览器中的渲染过程可能是硬件加速的,并且速度更快。
在我之前的文章Python 数据分析:我们了解现代艺术家什么?中,我使用了 NetworkX 库来分析从维基百科收集的现代艺术家的数据。对社交数据分析感兴趣的人也可以阅读其他帖子:
探索性数据分析:我们了解 YouTube 频道什么?
德国住房租赁市场:使用 Python 进行探索性数据分析
人们关于气候写了什么:使用 Python 进行 Twitter 数据聚类
在 Twitter 帖子中寻找时间模式:使用 Python 进行探索性数据分析
Python 数据分析:我们了解流行歌曲什么?
如果你喜欢这个故事,请随意订阅Medium,你将在我新文章发布时收到通知,以及访问成千上万其他作者故事的完全权限。你还可以通过LinkedIn与我建立联系。如果你想获取这篇文章和其他文章的完整源代码,请随意访问我的Patreon 页面。
感谢阅读。