最近在一堆数据图表里折腾,发现不少人还在用直方图、饼图硬啃流量来源和转化路径的账。其实这种看“流向”的数据,桑基图才是真正的杀手锏。但问题来了,网上一搜桑基图教程,十篇里有八篇教你用在线网页工具,数据一多就要付费,要不就是需要复杂的前端环境配置。今天我就分享一下我一直在用的路子:纯Python加Excel,本地一键生成可交互的桑基图,全程不需要联网,也不用装什么重型软件,有些基础的人十分钟就能跑通。
1. 为什么偏偏是桑基图:这类图表的真实价值与适用场景
在我开始讲代码之前,先花两分钟把桑基图的定位说清楚。很多人第一次看到桑基图,第一反应是“这玩意儿不就是几条彩带在那儿扭来扭去吗”。这种想法就大错特错了。桑基图的核心价值在于,它能够把“总量从哪来、到哪去、中间经过什么路径、损耗了多少”一条线讲明白,而且是可视化的方式讲明白。
饼图告诉你A占百分之三十,柱状图告诉你B的值是八十,但桑基图告诉你的是:总量一千,其中有四百去了A路径,A路径里又有一百五最终转化成了目标结果,两百五在半路流失了。这种“流动+损耗+层级转化”的表达力,是任何一种图表都不具备的。所以你会看到,能源行业用桑基图看能量流向,电商行业用它看用户从落地页到支付的转化漏斗,制造业用它看原料到成品的物料损耗,就连游戏行业都在用桑基图分析玩家的关卡流失分布。
这也就意味着,如果你需要做任何形式的漏斗分析、流向分析、路径拆解,桑基图就不是一个“图表的备选项”,而是应该优先考虑的方案。比如自媒体运营,你想看清楚粉丝到底是从哪个平台来的,进来之后又看了哪些内容,最后多少人关注了你 —— 这一条完整的链路,用桑基图放出来,比写十页报告都直观。而Python+Excel的组合,刚好击中了两个最普遍的痛点:Excel是几乎所有人都在用的数据容器,Python则负责把这容器里的数据转换成视觉结果。不需要额外去学什么花哨的可视化工具,这两个配合就够了。
我在给某项目做用户路径分析的时候,就是把埋点导出的CSV用Excel整理了一下,再用Python脚本几秒钟画出了一张完整的事件流向图。当时团队里的人看完第一反应都是“这图是怎么做出来的,快教教我”。能落地、能复用、能在职场里产生实际反馈,这就是我推荐这个方案的根本原因。所以在往下看代码之前,你先想清楚自己的数据适不适合用桑基图表达——只要回答“我的数据里存不存在从A到B的流动”,答案如果是肯定的,那你就来对地方了。
2. 数据处理前置:从Excel混乱表格到标准三层结构
很多初学者一上来就急着写Python代码,结果卡在第一步:Excel里的数据长得乱七八糟的,根本不知道从哪列开始拖。这里我必须把桑基图的数据结构先给你讲透。所谓桑基图,底层其实就两个核心要素:节点和连线。节点就是那些你看到的长条矩形,代表不同的实体状态;连线就是节点之间的那些带有宽度的带状物,代表从上一个状态流向下一个状态的数量。所以你在Excel里整理数据时,本质上只需要管三列:起点列、终点列、数值列。
以最常见的用户路径分析为例,第一列叫“来源渠道”,里面填的是“搜索引擎”“社交媒体”“直接访问”这类入口;第二列叫“用户行为”,填的是“注册”“点击商品”“下单”这类动作;第三列叫“人数”,就是你统计出来的具体数值。整理的时候有几个坑,我必须提前给你踩平。第一个坑是表头命名不统一。Excel里经常出现“来源”“渠道来源”“流量来源”几个名字反复换着用,这对人眼来说无所谓,但对Python脚本来说就可能匹配不上列名。我的建议是,无论你后面用什么方法读取,先统一列名,这是花两分钟就能避免半小时debug的习惯。
第二个坑是空单元格。如果起点列有空值,说明这条数据本身缺失了流向源头,绘图出来之后会出现莫名其妙的悬空节点。第三个坑是文本型数字,很多人在Excel里输入的数值其实是文本格式,单元格左上角有个绿色小三角,这种数据读进Python之后,一旦做加法运算就会直接报错或者被跳过。我一般的处理思路是,先把Excel里的数据用筛选功能自查一遍,把明显缺失的行删掉,然后让数字列统一格式。
说回Excel读取,整个绘图流程我用的库是Pandas加Plotly,Excel读取就直接用Pandas的read_excel函数。这里有一个我特别建议你做的事:读完表之后,先不要急着画图,先打印前几行数据看看类型对不对。这一步虽然蠢,但真的能拦住大量低级问题。具体代码如下:
import pandas as pd df = pd.read_excel("sankey_data.xlsx", sheet_name="Sheet1") print(df.head()) print(df.dtypes)输出里如果显示人数那一列是object而不是int64,那就是文本型数字的问题。解决方法是在Excel里选中那一列,数据-分列-完成,或者直接在Python里用pd.to_numeric强制转换。很多时候所谓的“代码报错”,根源根本不在代码,而在数据本身没有洗干净。用Python做可视化的第一步永远是处理数据,这一步走稳了,后面全是坦途。
还有一个细节,就是数据透视。如果你拿到的Excel表是流水账的明细,比如每一行是一次访问事件,那你画桑基图之前得先聚合出各条路径的总量。我最常用的方法是:
flow_data = df.groupby(["来源渠道", "用户行为"]).size().reset_index(name="人数")这样你就能得到一张干净的三列聚合表,后面喂给桑基图的link数据就长这个样子。你记住一个核心逻辑:图里每一条色带,对应你聚合表里的每一行。想清楚这个映射关系,你就不再是“套代码”,而是真正理解了这个图是怎么画出来的。
3. 核心绘制工具对比:三款Python图表库的选型实测
在画代码之前,我还得花点篇幅说一说工具的选型。因为市面上的Python可视化库太多了,直接上来讲Plotly可能会有人问,为什么不选Matplotlib、不选Pyecharts,不选Graphviz之类的老牌工具。我三个阶段都经历过,这里就摆出我的实测结论,供你少走弯路。
先说Matplotlib加Sankey库。它的年份最老,属于Matplotlib的一个插件模块,能画出静态桑基图,样式是那种传统的平面线条感,色彩也不够丰富。缺点是交互能力差,图表做出来是一张死图,鼠标悬停看不到数值,也没法折叠节点数据。另外它的坐标系模式沿用的是Matplotlib的网格逻辑,数据一复杂,图层叠在一起就会显得很乱。适合的场景是什么?论文插图、打印报告,那种只要一张静态PDF的场合。但如果你做的是需要动态沟通分析的图表,它就不是首选了。
Plotly Express,这个是我今天的绝对主角。它画桑基图的核心函数是px.sankey或者底层一点的go.Sankey。跟Matplotlib相比,它最大的优势就是交互性——图表生成之后是HTML格式,浏览器打开,鼠标放到色带上能直接看到数据标签,可以拖拽节点查看不同组合下的数值变化,还能一键缩放。更香的是它的代码量极小,准备好三列数据,十几行脚本就能出图,不需要后面再花时间调细节。我在多个项目里都用它交付图表,给到需求方,对方打开HTML就能自己玩,满意度比PDF高了一个层级。
还有一类库比如ECharts,通过Pyecharts的封装可以在Python里调用。它的可视化效果相当花哨,适合做动态效果非常华丽的大屏项目,但配置项太多,光是一个标签位置遮蔽的处理就要折腾半天。除非你是专业的前端工程师,否则我是劝退的。至于Graphviz这种图数据库的强项工具,它更擅长画有向关系图而非流量宽度图,在桑基图这个赛道上血统不正,我也就不用它了。
从学习成本和效果回报来综合评估,Plotly是一个完胜的选择。单文件HTML交付,别人不需要Python环境就能查看,这种交付方式极大降低了你推广一个分析结果的门槛。在数据可视化这个领域,好东西做出来要让别人轻而易举能看到,工具选型这个决定其实直接决定了你的成果传播范围。所以接下来的实操部分,我直接以Plotly为主线,把每一行代码讲明白。
4. 手把手拆解Python代码:从Excel到桑基图的完整链路
好,现在进入真正的技术环节。我先给你看一段完整可运行的代码,然后逐行拆解它的含义和注意事项。这一段代码是基于Plotly的go.Sankey接口写的,因为相比px.sankey来说,go.Sankey暴露的参数更底层、可控性更强。对普通使用场景来说,两者的核心逻辑完全一样,但go接口让你能看懂“节点”和“连线”这两个概念是怎么映射到数据上的。
在我粘贴代码之前,你要先理解go.Sankey的核心机制:它需要你提供两个列表。第一个是节点列表,把所有出现的状态都搜集进去,形成一个不重复的索引列表,对应图上的矩形块。第二个是连线列表,每一行要存放的是起点下标、终点下标、数值大小。也就是说,你要把Excel里的文字状态转成数字编号,这一步是新手最容易犯迷糊的。这里我顺手写一个小函数,用Python的字典自动完成状态到索引的映射。
import pandas as pd import plotly.graph_objects as go df = pd.read_excel("sankey_data.xlsx") labels = list(dict.fromkeys( list(df["来源渠道"]) + list(df["用户行为"]) )) label_to_id = {label: idx for idx, label in enumerate(labels)} source_ids = [label_to_id[x] for x in df["来源渠道"]] target_ids = [label_to_id[x] for x in df["用户行为"]] values = df["人数"] fig = go.Figure(data=[go.Sankey( node=dict( pad=15, thickness=20, line=dict(color="black", width=0.5), label=labels, ), link=dict( source=source_ids, target=target_ids, value=values, ) )]) fig.update_layout(title_text="用户路径桑基图", font_size=12) fig.write_html("sankey_output.html")其中list(dict.fromkeys(...))的写法,既能把两个来源列和去向列的所有元素取并集,又能保证不重复且保持原始顺序,这是我比较推荐的做法。如果你直接用set去重,顺序会乱掉,后期查找节点时就容易晕头转向。再往下,source和target两个列表是数字索引,分别对应每一条流量的起点和终点在labels里的位置。用这种方式构建数据结构,图表引擎在渲染时就不需要再去解析字符串,性能也会好很多。
生成之后,直接fig.write_html输出成一个独立文件。双击那个HTML文件,浏览器打开就能看到图,鼠标放上去还能带着数字显示。这一步完成,你的桑基图就算画出来了。我不建议在Jupyter里画完就关掉,建议一定输出成HTML存好,因为后期总结汇报时这个文件可以直接发给别人,对方无需安装任何东西就能查看和交互,这个优势是静态图片无论如何比不了的。
5. 多层级看板:Excel里多Sheet数据怎么扩展成更复杂的桑基图
很多人的实际数据不会像我上面写的那么标准,更多时候是多个Sheet里面放了不同维度的数据。比如Sheet1是渠道到落地页的流量,Sheet2是落地页到注册成功的转化,Sheet3是注册到下单的流失。如果你辛辛苦苦画了三张分散的图,那就完全浪费了桑基图能讲叙事的潜力了。正确的姿势是:把三张表拼在一起,画成一张完整的、有跨层级路径的桑基图。
这种多层级结构的核心难点在于节点列表的整合。你想,每一层都有各自的起点和终点,第二层的起点其实等于第一层的终点,那么这个交集节点在整个图中只能占据唯一一个索引位置。做法很简单,把三个Sheet读出来之后,统一做一个纵向拼接:把每一张表的起点列和终点列进行“账号统一化”,比如Sheet1的“落地页”叫“产品页”,Sheet2的“落地页”叫“产品页”,那它们就是同一个节点;但商品ID等字段可能在不同Sheet里叫法不同,这时候你不能盲目拼接,而是要梳理好业务上的同义关系表。
我处理的步骤是:先定义一个总标签序列,把所有Sheet中的起点、终点全部塞进同一个列表并去重,然后写一个自动映射函数。映射函数本质上就是查表:对每一行数据,找出起点和终点在总标签序列中的下标。这一步只需要用两层for循环加一个字典,十几行代码搞定。
多层级示意图的另一个关键问题是“数值跨越层级”的准确性。比如第二层与第三层的连接,你不能直接用原始人数,而是要确保流动到第三层的人数比第二层小,否则逻辑就不通。这时候你需要检查数据表,看是否为多级转化表,每一级是不是上一级的子集。说实话这个检查Excel表外做最方便,直接在Excel里做几次筛选,把各环节的人数总数算出来,确保递减。如果人数反而增加,那说明数据追踪里有问题,可能是某个路径不该存在、或者埋点数据有重复记录。画图前把数据校正,比画完图再改要节省太多时间。
下面的代码展示的是多Sheet合并的核心思路,我会把重点放在整合逻辑上:
import pandas as pd sheet1 = pd.read_excel("multi.xlsx", sheet_name="渠道落地页") sheet2 = pd.read_excel("multi.xlsx", sheet_name="落地页到注册") sheet3 = pd.read_excel("multi.xlsx", sheet_name="注册到下单") sheets = [sheet1, sheet2, sheet3] all_labels = [] for df in sheets: for col in [df.columns[0], df.columns[1]]: all_labels.extend(df[col].tolist()) all_labels = list(dict.fromkeys(all_labels)) label_to_id = {label: idx for idx, label in enumerate(all_labels)} source_ids = [] target_ids = [] values = [] for df in sheets: source_ids.extend([label_to_id[x] for x in df[df.columns[0]]]) target_ids.extend([label_to_id[x] for x in df[df.columns[1]]]) values.extend(df[df.columns[2]].tolist())三个Sheet拼成了一个统一索引的映射关系,画出来的图就是一条完整的端到端链条。这种图在老板面前展示的时候,不需要你说一句话,对方的眼睛就会自动地沿着色带从左边移动到右边,在流失最集中的位置停留。数据的叙事能力,在这一刻比任何文字报告都要强。所以如果你的数据本身就横跨多个层级,千万不要拆开画,一定要做拼接整合,发挥桑基图的完整毁灭性魅力。
6. 颜值调优与交互体验强化:这些细节让图表上一个档次
画出来之后,很多人会觉得“为什么别人的桑基图这么好看,我的就这么土”。这里面的核心差距其实不在代码框架,而是几个可以快速调整的细节参数。我先说节点间距和粗细。node里的pad参数控制节点之间的空隙,一般数值在15到20之间看起来比较舒适。太挤了色带会搅成一团,太疏了又显得细长。节点矩形的厚度thickness我习惯设在20左右,太薄了不显眼,太厚了又会挤压色带的可视区域。
然后是配色。Plotly默认的配色是蓝绿色系,看久了有点审美疲劳。桑基图最怕的是颜色冲击太强,隔壁两条带子的色相撞在一起,看起来就像霓虹灯。我给出的建议是用一组低饱和度的渐变色系,或者直接用分档的colorscale按数值自动分配深浅。尤其重要的是,节点和与之相连的色带颜色最好同源,这样视线能很自然地顺着颜色追踪流动路径。
再来说交互功能的增强。go.Sankey里的link对象可以开启一个非常有用的属性叫hovertemplate,自定义悬停时弹出的文本。比如你可以让它显示“从搜索引擎来了500人,其中转化成了120个注册用户”这种带业务语义的描述,而不是默认的杂乱数字。代码可以这样写:
hover_text = [ f"{df['来源渠道'][i]} → {df['用户行为'][i]}: {df['人数'][i]}人" for i in range(len(df)) ] link=dict( source=source_ids, target=target_ids, value=values, customdata=hover_text, hovertemplate="%{customdata}<extra></extra>" )这个改动会让图表在演示的时候显得特别“聪明”——别人看到的不是密密麻麻的数字,而是清晰的一句话。每次汇报时,我只要打开这个HTML,再把鼠标悬停到对应的色带上,讲解的过程就变得非常顺滑。另外还有一个实用细节是配置arrangement="snap"参数,让节点在拖拽时自动吸附对齐,这个参数在节点特别多的时候能帮助维持图面整洁。还有一点,字体的大小如果看不清,直接在fig.update_layout(font_size=12)里调整,简单直接。总之花不超过十分钟去微调这几个细节,图表的专业度立刻提升一个档次,别人看了会以为你花了一个下午做的。
7. 数据处理实操:真实Excel里常见的脏数据与对策
这一节我专门讲实战中遇到的脏数据情况,说实话这部分才是桑基图能不能画成的分水岭。很多人在网上复制了代码,发现跑不起来,或者画出来的图完全不对,九成原因就出在数据没有处理干净,而这一步恰恰没有被教程重视。
第一种脏数据是“别名”。比如Excel里“来源渠道”列同时存在“百度”和“搜索引擎”这两种写法,画图的时候它们会被识别成两个独立节点,而实际上它们是同一个东西。这种错误在图上是最隐蔽的,你不会一下子看出有问题,只会疑惑“为什么多出一条断头路”。解决方案是画图前做一个同义词映射字典:
alias_map = {"百度": "搜索引擎", "Google": "搜索引擎", "知乎": "社交媒体"} df["来源渠道"] = df["来源渠道"].replace(alias_map)你先把业务里的同义词整个盘一遍,明确哪些词只是一个称呼的不同,然后统一成正式命名。这是业务语言和代码语言之间的翻译层,千万别跳过去。
第二种脏数据是Excel文件格式的“内嵌差异”。比如你用了read_excel却因为文件版本是xls还是xlsx而报错,或者明明文件里只有一个Sheet但你忘了指定sheet_name导致读出来是空表。这些问题的排查思路其实很简单:先把文件路径引对,然后再看看是不是文件里有合并单元格导致读到的DataFrame出现NaN值。合并单元格是Excel可视化里的老牌杀手,建议在读取之前先把数据取消合并,或者用fillna(method="ffill")把合并的数值向下填充。
第三种是高发问题:列名带空格或中文特殊字符。比如“来源渠道 ”后面多了一个空格,打印列名时根本看不出来,但代码里写df["来源渠道"]就报KeyError了。对付这个问题,我习惯在读取后立刻打印列名列表:
print(df.columns.tolist())把列名原封不动地看一遍,你就知道有没有空格或者隐藏字符了。许多人画图失败,问题根本不在于绘图本身,而是数据入门的这一个坎没迈过去。等踩过几次这种坑,你就会领悟:先花两分钟检查数据,比对着报错信息瞎改半天代码要效率高得多。
8. 进阶思路:自动生成多月份对比桑基图的小工具封装
等你会画单张桑基图之后,下一个自然而然的需求就是“我怎么批量地为每个月的数据分别生成一张图”。比如你手上有十二个月的Excel文件,每个月一张Sheet,如果每次都手动改文件名再运行一次脚本,那种低级重复劳动干一次就会让你怀疑人生。这时候就应该把绘制过程封装成函数,然后循环调用。
封装的时候有几个设计点可以注意。函数入口接收文件路径和月份参数,内部完成读取、清洗、映射、绘制、输出这一整套流程。返回值为HTML文件的路径,方便以后做批处理时记录产出。这里我给出一个按日期自动保存输出的实现思路:
import os def create_sankey(excel_path, month): df = pd.read_excel(excel_path, sheet_name=month) # 假设这里已经做好清洗与聚合 labels = ... # 构建节点和连线 fig = go.Figure(data=[go.Sankey(node=..., link=...)]) output_dir = "output" os.makedirs(output_dir, exist_ok=True) out_file = os.path.join(output_dir, f"{month}_sankey.html") fig.write_html(out_file) return out_file for month in ["1月", "2月", "3月", "4月"]: create_sankey("monthly_data.xlsx", month)这种做法特别适合那些需要周期性产出图表的场景。比如你每个月要给管理层交一份用户流动报告,那么这套脚本可以直接做成一个定时任务,到点自动跑,输出链接发给别人就行。我自己就曾把一个手工两小时的流程缩减成一条命令跑完,把精力解放出来去做更深的分析和解读。
9. 踩坑实录:Plotly版本不匹配和与Excel缺失值引发的连锁问题
讲真,跑通桑基图之后我原以为就完事了,结果后来遇到两个极其隐蔽的坑,我想把它们单独拎出来,免得你在同样的问题上卡着好几天。
第一个坑是Plotly的版本差异。有些教程使用的是很老版本的Plotly,代码里写了go.Sankey(node=dict(label=..., pad=...))这样的写法,而新版Plotly的go.Sankey对参数的默认值或类型检查做了变动,导致在老版本能跑的代码到新版本直接抛异常。这就提醒我写代码时不要直接复制别人的代码,最好先在本地跑通一个最小的demo,再替换成自己的数据。如果报错信息里提示Invalid property之类的,优先去查一下Plotly官方文档该对象的参数列表,通常是因为参数名拼写不标准或者参数类型不对。版本问题困住了很多人,但实际上把Plotly升级到最新版,绝大多数旧代码都能稳定运行——除非你是逆向兼容地运行旧脚本。
第二个坑,也是最让我崩溃的一个:Excel表里的“可见空值”。有些表格看起来有数据,但单元格中其实是空格字符串" ",而不是真正的空值。Pandas读进来之后,这个列显示为包含空格的文本,既不等于NaN,也不等于空字符串,导致你做的任何清洗逻辑都失效。这种空格字符串在go.Sankey里会导致节点列表中存在空节点,画出来的图在首尾位置出现莫名其妙的空白带。解决办法是在读取后立刻对全表做一个去除首尾空格的处理:
df = df.apply(lambda x: x.str.strip() if x.dtype == "object" else x)然后再用replace("", pd.NA)把真正的空字符串替换掉。这个细节花十分钟加上去,省去你后面排查几个小时。数据可视化这个活儿,最大的快乐大概就是把一堆乱糟糟的表格变成一张一眼就能看懂的趋势图表。但前提是把数据清洗这些看似枯燥的基础工作做到位,这里偷懒,后面一定会在更多地方还回来。
10. 使用场景再思考:当桑基图出现在你的汇报PPT里
最后我再多聊一层。代码和图表本身讲完了,但真正让桑基图产生价值的场景还是在汇报里。我见过很多人做了非常漂亮的交互HTML,结果到了PPT的环节,却不知道该怎么把它塞进一页静态幻灯片中。其实办法很简单,用浏览器打开HTML,然后截图,高分辨率导出为PNG即可。截图时把浏览器窗口拉大一点,色带舒展,字也清晰,效果比直接在电脑上截屏好很多。
如果想让PPT里的静态图也具备一点动态感,可以试着只截取重点路径的局部放大图。第二张图重点展示最粗的那条色带,配一句结论“本季度80%的流量集中在搜索渠道,但转化率只有A渠道的40%”。这种“比例感+差异感”的呈现,能让阅读者快速抓住核心洞察。桑基图本身是极佳的结论承载工具,它的宽度变化就是在讲故事,你只需要把故事里最重要的情节截下来,放大给观众看。
另外,做成HTML参数报告也很有价值。把最终生成的sankey_output.html嵌入企业内部数据平台,或者通过工作群发给同事,别人打开后自己拖拽、悬停、探索,这种自服务式的数据分析体验,比你发一张静态图片再配上三百字解读效率高出太多。我一直觉得,做数据分析的人,最需要修炼的不是代码技巧,而是“让别人轻松看懂你想表达什么”的能力。桑基图加Python加Excel这套组合,恰好就是帮你去掉数据噪音,专注于核心故事线的最佳利器。
我自己的日常工作流里,这是出镜率最高的工具组合之一。只要你准备好了Excel数据表和Python环境,不管你是做运营复盘、市场分析、产品迭代还是其他任何领域,这套方案都能让你在数据表达这块从容很多。别犹豫,拿真实数据跑一次,你就能体会到什么是“原来一个下午的整理,一笔代码解决”的快感。