图形可视化:从简单到高级的 7 个步骤

原文: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 中轻松创建一个简单的图:

import networkx as nx

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 个社交活动的观察结果。让我们加载这个图并绘制它:

import networkx as nx
import matplotlib.pyplot as plt

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”名称;让我们改变它们的颜色以获得更好的视觉效果。

首先,我将创建一个辅助方法来检测节点是否是事件:

def is_event_node(node: str) -> bool:
    """ Check if events starts with Exx """
    return re.match("^Ed", node) is not None

在这里,我使用正则表达式来确定节点模式(我的第一次尝试是使用node.startswith("E")方法,但一些女性的名字也可以以"E"开头)。现在,我们可以轻松地为每个节点创建一个颜色数组,并使用它来绘制图表:

def get_node_color(node: str) -> str:
    """ Get color of the individual node """
    return "#00AA00" if is_event_node(node) else "#00AAEE"

node_colors = [get_node_color(node) for node in G.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)) for node in G.nodes()}

def node_size(node: str) -> int:
    """ Get size of the individual node """
    k = 4 if is_event_node(node) else 1
    return 100*k + 100 + 50*edges[node]

node_sizes = [node_size(node) for node in G.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"

def get_node_color(node: str) -> str:
    """ Get color of the individual node """
    if is_event_node(node):
        if G.has_edge(node, highlighted_node):
            return "#00AA00"
    elif node == highlighted_node:
        return "#00AAEE"
    return "#AAAAAA"

def edge_color(node1: str, node2: str) -> str:
    """ Get color of the individual edge """
    if node1 == highlighted_node or node2 == highlighted_node:
        return "#992222"
    return "#999999"

def edge_weight(node1: str, node2: str) -> str:
    """ Get width of the individual edge """
    if node1 == highlighted_node or node2 == highlighted_node:
        return 3
    return 1

在这里,我为 Theresa Anderson 的节点使用了单独的颜色。我还改变了她访问的所有事件的颜色;has_edge方法是一个简单的方法来查找两个节点是否有共同边。我还改变了边的权重。

现在,我们可以绘制图表:

edge_colors = [edge_color(n1, n2) for n1, n2 in G.edges()]
edge_weights = [edge_weight(n1, n2) for n1, n2 in G.edges()]
node_colors = [get_node_color(node) for node in G.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 = [node for node in G.nodes() if is_event_node(node)]
node_people = [node for node in G.nodes() if not is_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) for node in G.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] for node in G.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 的方法;然而,我们可以用几行代码做到这一点:

def convert_to_d3(graph: nx.Graph) -> dict:
    """ Convert nx.Graph to D3 data """
    nodes, edges = [], []
    for node_name in graph.nodes:
        nodes.append({"id": node_name,
                      "color": get_node_color(node_name),
                      "radius": 0.01*node_size(node_name)})
    for node1, node2 in graph.edges:
        edges.append({"source": node1, "target": node2})
    return {"nodes": nodes, "links": edges}

# Save in D3 format
d3 = convert_to_d3(G)
with open('d3_graph.json', 'w', encoding='utf-8') as f_out:
    json.dump(d3, f_out, ensure_ascii=False, indent=2)

之后,我们可以将 JSON 数据加载到一个 JavaScript 页面中:

<script type="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 = await d3.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 simulation with several 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 库来分析从维基百科收集的现代艺术家的数据。对社交数据分析感兴趣的人也可以阅读其他帖子:

如果你喜欢这个故事,请随意订阅Medium,你将在我新文章发布时收到通知,以及访问成千上万其他作者故事的完全权限。你还可以通过LinkedIn与我建立联系。如果你想获取这篇文章和其他文章的完整源代码,请随意访问我的Patreon 页面

感谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值