数据可视化领域,桑基图(Sankey Diagram)与和弦图(Chord Diagram)是两类专注于展示 “关联关系” 与 “流量流转” 的经典图表。它们能够将复杂的多维度数据转化为直观的图形,帮助用户快速洞察数据背后的路径、损耗、互动强度等核心信息。本文将详细介绍两类图表的定义、构成、应用场景与优势,并结合电商用户行为分析、城市交通流量分析两大场景,提供Matplotlib实现代码及解释,最后通过对比总结其适用边界。

一、桑基图:聚焦单向流转与路径损耗

📌什么是桑基图

桑基图是一种展示数据从一个节点到另一个节点单向流转过程的可视化图表,核心价值在于清晰呈现 “源 - 路径 - 目标” 的流转逻辑,同时通过线条宽度直观反映流转流量的大小,以及各环节的流量损耗(流失量)。

📈桑基图的构成

  • 节点(Node):代表流转过程中的关键环节或分类(如 “加购”“下单”“郊区 A”“市区 B”),通常以矩形或圆形呈现,位置按流转顺序排列。

  • 流(Flow):连接两个节点的线条,线条宽度与流转的流量(或数量)成正比,是桑基图的核心元素。

  • 层级(Level):节点按流转阶段划分的横向层级(如 “初始环节”“中间环节”“目标环节”),确保流转路径的有序性。

 💼桑基图的应用场景

  • 用户行为路径分析(如 “浏览→加购→下单→支付” 的流转及流失);

  • 资源分配与流转追踪(如资金、物资从源头到终端的分配路径);

  • 交通流量单向传递(如早高峰郊区到市区再到 CBD 的车流拆分);

  • 能源流向分析(如电力从发电厂到各区域用户的输送过程)。

🌟桑基图的优势

  • 直观展示多环节的单向流转路径,复杂路径一目了然;

  • 线条宽度量化流量大小,各环节损耗(流失量)可视化;

  • 支持多层级节点嵌套,适配长流程、多分支的流转场景;

  • 图形简洁易懂,非专业人员也能快速捕捉核心流转逻辑。

分享Matplotlib官方gallery中关于桑基图的示例(https://matplotlib.org/stable/api/sankey_api.html#matplotlib.sankey.Sankey),可直接参考其中的代码进行使用和修改。

二、和弦图:聚焦双向关联与互动强度

📌什么是和弦图

和弦图是一种展示多个类别之间双向关联关系的可视化图表,核心价值在于呈现 “类别间的互动强度”,通过弧形线条的粗细和颜色区分关联的方向与强度,适用于分析无明确流转顺序的双向互动场景。

📈和弦图的构成

  • 圆弧(Arc):围绕在图表外围的环形部分,每个圆弧代表一个数据类别,圆弧的长度通常与该类别的数据总量成正比。可以结合具体数据案例,比如某电商平台各商品类别的销售额数据,展示圆弧长度如何反映销售额多少。​

  • 弦(Chord):连接不同圆弧的曲线,弦的粗细代表两个类别数据之间关联程度的强弱。例如在用户购买行为分析中,弦的粗细体现不同商品类别之间被同时购买的频次。​

  • 颜色编码:不同的颜色用于区分不同的圆弧和对应的弦,帮助读者快速识别不同类别及它们之间的关联。说明颜色选择的原则,如避免使用过于相近的颜色导致混淆,重要类别可使用更醒目的颜色。

💼和弦图的应用场景

  • 社交网络分析:用于展示用户之间的好友关系、互动频率等。例如分析某社交平台上不同兴趣小组用户之间的互动情况,通过和弦图可以清晰看到哪些小组之间的用户互动最为频繁,为平台优化推荐机制提供依据。​

  • 市场调研数据呈现:展示不同消费群体对各类产品的偏好程度。比如某家电企业进行市场调研,通过和弦图可以直观呈现年轻群体、中年群体、老年群体对冰箱、彩电、洗衣机等不同家电产品的偏好关联,帮助企业制定针对性的营销策略。​

  • 交通流量分析:呈现不同区域之间的交通流量往来情况。例如分析城市各区域之间的通勤流量,用圆弧代表各个区域,弦的粗细表示区域间的通勤人数,助力交通部门合理规划交通线路和基础设施建设。​

  • 内容推荐系统数据展示:体现用户对不同类型内容的浏览和互动关联。比如视频平台分析用户观看记录,通过和弦图展示用户在喜剧、动作、爱情等不同类型视频之间的跳转和关联,为平台优化内容推荐算法提供数据支撑。

🌟和弦图的优势

  • 直观展示关联关系:相比表格等传统数据呈现方式,和弦图能将复杂的多对多关联关系以图形化形式呈现,让读者一眼看出数据间的连接情况,快速捕捉关键关联信息。​

  • 凸显数据比例:外围圆弧的长度清晰反映各类别数据的总量占比,结合弦的粗细,能同时展现数据的整体分布和局部关联,便于读者全面把握数据特征。​

  • 视觉吸引力强:独特的环形结构和色彩搭配,使和弦图具有较强的视觉冲击力,容易吸引读者注意力,适合在报告、演示文稿和博客中使用,提升内容的可读性和吸引力。​

分享一个Github上的一个优秀的基于matplotlib绘制和弦图的方法(https://github.com/fengwangPhysics/matplotlib-chord-diagram/blob/master/matplotlib-chord.py

图像展示如下:

三、应用场景可视化实现——电商用户行为分析

1. 桑基图:用户从 “加购→下单” 的流转路径及流失量

数据说明:假设电商平台用户行为环节为 “加购→结算→下单→支付”,各环节流转数据如下(单位:万人):

  • 加购→结算:80 万人(加购环节流失 20 万人);

  • 结算→下单:60 万人(结算环节流失 20 万人);

  • 下单→支付:45 万人(下单环节流失 15 万人)。

import numpy as np
import plotly.graph_objects as go

# 1. 定义节点与流量数据
nodes = ['加购', '结算', '下单', '支付', '流失']
# 流量矩阵:rows为源节点,cols为目标节点,值为流转人数(万人)
flowMatrix = np.array([
    [0, 80, 0, 0, 20],    # 加购→结算80万,流失20万
    [0, 0, 60, 0, 20],    # 结算→下单60万,流失20万
    [0, 0, 0, 45, 15],    # 下单→支付45万,流失15万
    [0, 0, 0, 0, 0],      # 支付无后续流转
    [0, 0, 0, 0, 0]       # 流失节点无输出
])

# 2. 整理桑基图数据(源、目标、流量)
source = []
target = []
value = []
for i in range(len(nodes)):
    for j in range(len(nodes)):
        if flowMatrix[i, j] > 0:
            source.append(i)  # 源节点索引
            target.append(j)  # 目标节点索引
            value.append(flowMatrix[i, j])  # 流量值

# 3. 创建桑基图
fig = go.Figure(data=[go.Sankey(
    node=dict(
        pad=15,  # 节点间距
        thickness=20,  # 节点厚度
        line=dict(color="black", width=0.5),
        label=nodes  # 节点标签
    ),
    link=dict(
        source=source,  # 源节点索引列表
        target=target,  # 目标节点索引列表
        value=value,    # 流量值列表
        color=['rgba(52, 152, 219, 0.6)',  # 加购相关流量颜色
               'rgba(52, 152, 219, 0.6)',
               'rgba(46, 204, 113, 0.6)',  # 结算相关流量颜色
               'rgba(46, 204, 113, 0.6)',
               'rgba(243, 156, 18, 0.6)',  # 下单相关流量颜色
               'rgba(243, 156, 18, 0.6)']
    )
)])

# 4. 图表美化
fig.update_layout(
    title_text='电商用户加购→下单流转路径及流失量分析',
    font_size=12,
    width=800,
    height=500
)

fig.show()

代码解释

  • 节点与流量数据定义nodes明确 “加购、结算、下单、支付、流失”5 个核心节点,包含流失节点直接可视化各环节损耗,无信息隐藏;flowMatrix为源 - 目标流量矩阵,行索引对应源节点、列索引对应目标节点,值为流转人数(万人),清晰定义各环节流转关系(如加购→结算 80 万、加购→流失 20 万);

  • 桑基图数据整理:通过双重循环遍历流量矩阵,筛选出流量大于 0 的记录,分别用source(源节点索引)、target(目标节点索引)、value(流量值)列表存储,适配 Plotly 桑基图的数据输入格式;

  • 桑基图核心配置go.Sankey为 Plotly 内置桑基图函数,node参数设置节点样式(间距pad=15、厚度thickness=20、黑色边框),label绑定节点名称;link参数关联源目标索引与流量值,通过不同透明度的 RGB 颜色区分 “加购、结算、下单” 相关流量,增强视觉区分度

图像展示

2. 和弦图:不同商品品类的交叉购买关联强度

数据说明:选取 5 类核心商品(服装、家电、食品、配饰、美妆),交叉购买数据为双向关联强度(单位:万次,矩阵为对称矩阵):

品类

服装

家电

食品

配饰

美妆

服装

0

3.2

5.1

8.7

6.3

家电

3.2

0

2.8

1.5

1.2

食品

5.1

2.8

0

2.1

1.8

配饰

8.7

1.5

2.1

0

7.2

美妆

6.3

1.2

1.8

7.2

0

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.path import Path
import matplotlib.patches as patches

# --------------------------
# 新增:设置中文字体支持
# --------------------------
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC", "Microsoft YaHei"]
plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示问题

# 保留原弦图绘制工具函数
LW = 0.3

def polar2xy(r, theta):
    return np.array([r*np.cos(theta), r*np.sin(theta)])

def hex2rgb(c):
    return tuple(int(c[i:i+2], 16)/256.0 for i in (1, 3 ,5))

def IdeogramArc(start=0, end=60, radius=1.0, width=0.2, ax=None, color=(1,0,0)):
    if start > end:
        start, end = end, start
    start *= np.pi/180.
    end *= np.pi/180.
    opt = 4./3. * np.tan((end-start)/ 4.) * radius
    inner = radius*(1-width)
    verts = [
        polar2xy(radius, start),
        polar2xy(radius, start) + polar2xy(opt, start+0.5*np.pi),
        polar2xy(radius, end) + polar2xy(opt, end-0.5*np.pi),
        polar2xy(radius, end),
        polar2xy(inner, end),
        polar2xy(inner, end) + polar2xy(opt*(1-width), end-0.5*np.pi),
        polar2xy(inner, start) + polar2xy(opt*(1-width), start+0.5*np.pi),
        polar2xy(inner, start),
        polar2xy(radius, start),
    ]
    codes = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4,
             Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CLOSEPOLY]

    if ax is None:
        return verts, codes
    else:
        path = Path(verts, codes)
        patch = patches.PathPatch(path, facecolor=color+(0.5,), edgecolor=color+(0.4,), lw=LW)
        ax.add_patch(patch)

def ChordArc(start1=0, end1=60, start2=180, end2=240, radius=1.0, chordwidth=0.7, ax=None, color=(1,0,0)):
    if start1 > end1:
        start1, end1 = end1, start1
    if start2 > end2:
        start2, end2 = end2, start2
    start1 *= np.pi/180.
    end1 *= np.pi/180.
    start2 *= np.pi/180.
    end2 *= np.pi/180.
    opt1 = 4./3. * np.tan((end1-start1)/ 4.) * radius
    opt2 = 4./3. * np.tan((end2-start2)/ 4.) * radius
    rchord = radius * (1-chordwidth)
    verts = [
        polar2xy(radius, start1),
        polar2xy(radius, start1) + polar2xy(opt1, start1+0.5*np.pi),
        polar2xy(radius, end1) + polar2xy(opt1, end1-0.5*np.pi),
        polar2xy(radius, end1),
        polar2xy(rchord, end1),
        polar2xy(rchord, start2),
        polar2xy(radius, start2),
        polar2xy(radius, start2) + polar2xy(opt2, start2+0.5*np.pi),
        polar2xy(radius, end2) + polar2xy(opt2, end2-0.5*np.pi),
        polar2xy(radius, end2),
        polar2xy(rchord, end2),
        polar2xy(rchord, start1),
        polar2xy(radius, start1),
    ]
    codes = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CURVE4,
             Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CURVE4,
             Path.CURVE4, Path.CURVE4, Path.CURVE4]

    if ax is None:
        return verts, codes
    else:
        path = Path(verts, codes)
        patch = patches.PathPatch(path, facecolor=color+(0.5,), edgecolor=color+(0.4,), lw=LW)
        ax.add_patch(patch)

def selfChordArc(start=0, end=60, radius=1.0, chordwidth=0.7, ax=None, color=(1,0,0)):
    if start > end:
        start, end = end, start
    start *= np.pi/180.
    end *= np.pi/180.
    opt = 4./3. * np.tan((end-start)/ 4.) * radius
    rchord = radius * (1-chordwidth)
    verts = [
        polar2xy(radius, start),
        polar2xy(radius, start) + polar2xy(opt, start+0.5*np.pi),
        polar2xy(radius, end) + polar2xy(opt, end-0.5*np.pi),
        polar2xy(radius, end),
        polar2xy(rchord, end),
        polar2xy(rchord, start),
        polar2xy(radius, start),
    ]
    codes = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CURVE4]

    if ax is None:
        return verts, codes
    else:
        path = Path(verts, codes)
        patch = patches.PathPatch(path, facecolor=color+(0.5,), edgecolor=color+(0.4,), lw=LW)
        ax.add_patch(patch)

def chordDiagram(X, ax, colors=None, width=0.1, pad=2, chordwidth=0.7):
    x = X.sum(axis=1)
    ax.set_xlim(-1.1, 1.1)
    ax.set_ylim(-1.1, 1.1)

    if colors is None:
        colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
                  '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
        if len(x) > 10:
            print('x is too large! Use x smaller than 10')
        colors = [hex2rgb(colors[i]) for i in range(len(x))]

    y = x / np.sum(x).astype(float) * (360 - pad * len(x))
    pos = {}
    arc = []
    nodePos = []
    start = 0

    for i in range(len(x)):
        end = start + y[i]
        arc.append((start, end))
        angle = 0.5 * (start + end)
        if -30 <= angle <= 210:
            angle -= 90
        else:
            angle -= 270
        nodePos.append(tuple(polar2xy(1.1, 0.5 * (start + end) * np.pi / 180.)) + (angle,))
        z = (X[i, :] / x[i].astype(float)) * (end - start)
        ids = np.argsort(z)
        z0 = start
        for j in ids:
            pos[(i, j)] = (z0, z0 + z[j])
            z0 += z[j]
        start = end + pad

    for i in range(len(x)):
        start, end = arc[i]
        IdeogramArc(start=start, end=end, radius=1.0, ax=ax, color=colors[i], width=width)
        start, end = pos[(i, i)]
        selfChordArc(start, end, radius=1. - width, color=colors[i], chordwidth=chordwidth * 0.7, ax=ax)
        for j in range(i):
            color = colors[i]
            if X[i, j] > X[j, i]:
                color = colors[j]
            start1, end1 = pos[(i, j)]
            start2, end2 = pos[(j, i)]
            ChordArc(start1, end1, start2, end2, radius=1. - width, color=color, chordwidth=chordwidth, ax=ax)

    return nodePos

if __name__ == "__main__":
    # 1. 定义品类与关联强度矩阵
    categories = ['服装', '家电', '食品', '配饰', '美妆']
    correlationMatrix = np.array([
        [0, 3.2, 5.1, 8.7, 6.3],
        [3.2, 0, 2.8, 1.5, 1.2],
        [5.1, 2.8, 0, 2.1, 1.8],
        [8.7, 1.5, 2.1, 0, 7.2],
        [6.3, 1.2, 1.8, 7.2, 0]
    ])

    # 2. 创建和弦图
    fig = plt.figure(figsize=(7, 7))
    ax = plt.axes([0, 0, 1, 1])

    # 使用viridis颜色映射,并转换为RGB格式(去掉透明度通道)
    from matplotlib.cm import viridis
    colors = [viridis(i / len(categories))[:3] for i in range(len(categories))]
    
    # 绘制弦图
    nodePos = chordDiagram(
        correlationMatrix, 
        ax, 
        colors=colors,
        width=0.1,
        pad=2,
        chordwidth=0.7
    )

    # 3. 图表美化与标注
    ax.axis('off')
    plt.title('商品品类交叉购买关联强度分析', fontsize=14, fontweight='bold', pad=20)

    # 添加节点标签
    prop = dict(fontsize=12, ha='center', va='center')
    for i in range(len(categories)):
        ax.text(nodePos[i][0], nodePos[i][1], categories[i], rotation=nodePos[i][2], **prop)

    # 保存图片
    plt.savefig("category_correlation.png", dpi=600,
                transparent=True,
                bbox_inches='tight', pad_inches=0.1)
plt.show()

代码解释

  • 坐标转换与颜色工具函数polar2xy实现极坐标转直角坐标,hex2rgb将十六进制颜色转为 RGB 格式,为绘图提供基础支持;

  • 图形绘制工具函数IdeogramArc绘制节点扇区,ChordArc绘制品类间关联弧,selfChordArc绘制品类内自关联弧,三者共同构成和弦图的 “节点” 与 “边”;

  • 和弦图核心函数chordDiagram:输入关联矩阵X,计算各品类的扇区角度、关联占比,自动绘制节点与关联弧,返回节点标签的定位信息;

  • 数据与图表渲染correlationMatrix定义 “服装、家电、食品、配饰、美妆”5 大品类的交叉购买强度,colors通过viridis颜色映射区分不同品类;

  • 图表美化与标注ax.axis('off')隐藏坐标轴,plt.title设置标题,ax.text根据nodePos的坐标与角度,精准添加品类标签。

图像展示

四、桑基图与和弦图对比

对比维度

桑基图

和弦图

核心用途

展示单向流转路径及流量损耗

展示多类别双向关联强度

数据结构

源 - 目标流量矩阵(可非对称)

双向关联矩阵(通常对称,也支持非对称)

布局方式

横向层级布局(按流转顺序排列节点)

环形布局(类别沿圆环排列)

关键元素

节点、流(线条宽度 = 流量)

扇区、弦(弧形宽度 = 关联强度)

优势场景

长流程、多环节的流转分析(如用户路径、交通流)

无明确顺序的双向互动分析(如交叉购买、通勤)

视觉重点

路径的连续性与损耗量

类别间的互动强度与双向性

数据规模适配

适合中低维度(节点数≤10),避免路径混乱

适合中高维度(类别数≤20),环形布局节省空间

局限性 不支持双向关联展示,多分支易拥挤 无法展示多环节流转,单向路径不直观

五、总结

桑基图与和弦图作为数据关联与流转的核心可视化工具,各自聚焦不同的业务场景:桑基图以 “单向流转” 为核心,通过层级布局和线条宽度,清晰呈现多环节的路径逻辑与流量损耗,适用于用户行为路径、交通流拆分等场景;和弦图以 “双向关联” 为核心,通过环形布局和弧形线条,高效展示多类别间的互动强度,适用于交叉购买、双向通勤等场景。

在实际应用中,需根据数据特性选择图表类型:若关注 “从 A 到 B 再到 C 的过程与损耗”,优先选择桑基图;若关注 “A 与 B、B 与 C 的双向互动强度”,优先选择和弦图。两者结合可实现 “流转过程 + 关联强度” 的全维度数据洞察,为业务决策提供更全面的可视化支持。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐