
做数据分析的人应该都遇到过这种场面老板盯着报表问“用户从注册到付费中间到底流失在哪一步”你手里只有一张Excel翻来覆去看到的都是死数字看不出名堂。这时候你最需要的是一张桑基图。桑基图也叫Sankey图天生就是干这个的——用一条条宽度不等的“河流”把数据从一个节点流向另一个节点的路径和体量画出来。河道宽的地方就是量大窄的地方就是量小哪里有损耗、哪里在汇聚图上一眼就能看清楚。我这次要分享的是我实际工作中一直在用的一套组合方案用Python读取Excel里的业务数据一键生成Sankey图。不需要手动去Excel里折腾形状也不用装一堆臃肿的BI工具数据留在Excel里不动写好的脚本一跑图就出来了。这篇内容适合谁适合手里只有Excel表格、又想把数据流向讲清楚的朋友。无论你是要画用户转化漏斗、订单流转路径还是做库存调拨、能源消耗分配的分析这套流程都能直接套用。我尽量把每一步都写完整零基础的朋友跟着操作也能跑通有基础的朋友可以直接跳到第4节看代码。1. 桑基图的核心价值与方案选型逻辑1.1 桑基图到底在画什么桑基图的原理其实特别朴素一句话就能说明白数值总和守恒通道宽度代表大小。它由两类元素组成一类是“节点”比如注册、登录、下单、支付这些是数据流经的站点另一类是“链路”也就是连接节点的带箭头的条带条带的宽度直接对应数值。比如从“注册”到“支付”的条带宽度就代表注册用户中实际付费的人数。这个原理是不是很像城市供水系统总水管进来按照不同分支分流每个分支的粗细就是供水量。桑基图的可贵之处就在这个“守恒”进到一个节点的总流量必须等于从这个节点流出的总流量。正是靠着这个约束数据里有没有漏算、重算图上一看便知根本不需要你去逐行核对数值。我日常用得最多的场景是用户转化漏斗。传统漏斗图只能按步骤给你看每一步还剩多少人但它回答不了“流失的人都从哪一步走了”。桑基图就能清楚地展示从“曝光”进来一万人5000人去了“点击”3000人直接“关闭页面”另2000人去了“浏览详情”——每个去向都是一条独立链路既能看到整体漏斗的剩余率也能看到用户在不同支路之间如何流动。能源分析是桑基图的传统强项。发电厂的煤、气、风、水几种能源进来经过发电、输电、配电几个环节最终被居民、工厂、交通各自消耗。中间哪个环节损耗最大一眼就能定位。物流领域的运费分配、人才招聘的渠道来源分析也都是桑基图的高频使用场景。1.2 为什么非要用PythonExcel这套组合很多人第一反应是Excel里能不能直接画桑基图很遗憾Excel原生图表类型里根本没有桑基图。你可能见过别人用堆积条形图去“伪装”桑基效果那种做法本质上就是把几条堆叠条在不同列错位摆放视觉上勉强有点流向的意思但数据格式稍有调整整个图就得重做而且根本无法准确表达多层级的分流汇聚关系。换个思路用Power BI或Tableau工具本身没问题但为了画一张图引入一套BI软件在大多数业务场景里属于小题大做。数据源是Excel最终交付也是Excel或者PPT里的图片中间绕这么大一圈不值得。Python在这个场景下的优势是刚好卡在需求和习惯之间。数据源是我们最熟悉的Excel用pandas读进来做简单清洗再用可视化库出图整个流程脚本化。这意味着一件事你这次做完的图表模板下次数据一更新重新跑一次脚本图就自动刷新了。手工在Excel里画图每换一批数据就得重新拖一次这个差距是根本性的。当然Python的选择不是因为“Python什么都行”这种空话而是它有成熟的Excel读取库和可视化库。pandas读Excel只需要一行代码plotly、pyecharts对桑基图的封装很完善节点、链路、颜色、标签都提供了直接可调的参数。数据规整和可视化这两个最花时间的环节都有现成的轮子可用。2. Excel里的数据要怎么准备2.1 桑基图的数据格式三列搞定一切不管是plotly还是pyecharts桑基图的数据输入结构都是同一个逻辑谁流向谁流量是多少。落到Excel表格里就是三列数据源节点目标节点数值曝光点击5000曝光关闭页面3000曝光浏览详情2000点击注册800点击流失4200“源节点”是流出的地方“目标节点”是流入的地方数值就是这条链路的流量大小。Python在画图时会自动把所有出现在这两列里的名字提取出来合并成一个节点列表再根据每行的对应关系去建立连接。这里有个很容易搞混的概念要提前说清楚桑基图的节点并不是预先定义好的而是由源节点和目标节点的并集自动生成的。比如上面这个表格里节点就是“曝光、点击、关闭页面、浏览详情、注册、流失”这六个。只要表格里出现了新名字图里就会多一个节点不需要你在代码里手动注册。这也是为什么用Excel维护数据特别方便——加一行数据图就跟着变。2.2 从业务明细表到三列结构的转换实际业务里很少直接给你排好的“源-目标-数值”三列更多的是一张行为明细表一列是用户ID一列是行为阶段一列是时间或顺序编号。这种表要转成桑基图需要的格式核心思路是把同一个用户相邻两次行为拼成一条记录。举个例子有一张用户行为表每行记录一个用户在一个阶段的状态用户张三依次经历了“注册、浏览、下单、支付”四个阶段。我们想把每一步的转化关系画出来就得构造“注册→浏览”“浏览→下单”“下单→支付”这样三条链路。用pandas操作的话只需要对每个用户按时间排序后把“阶段”这一列向下整体挪一行就能得到目标阶段然后合并成一张新的关系表。import pandas as pd # 原始行为表 df pd.read_excel(user_behavior.xlsx) # 排序保证阶段顺序正确 df df.sort_values([user_id, step_order]) # 构造相邻阶段 df[next_stage] df.groupby(user_id)[stage].shift(-1) # 删除最后一环没有下一步的 df df.dropna(subset[next_stage]) # 统计两两相邻阶段的次数 result ( df.groupby([stage, next_stage]) .size() .reset_index(namevalue) )这个做法有个细节容易出错直接shift会把上一个用户的第一条行为当作下一个用户的下一步所以必须先按用户分组再做shift否则流量统计会错得离谱。我在刚写这类脚本的时候在这个坑里栽过当时画出来的图里莫名其妙多了一条跨用户的链路排查了半天才发现是分组顺序的问题。如果是已经统计好的交叉表处理起来就更简单了。比如Excel里是一个透视表行是来源渠道列是转化去向单元格放人数这种格式用pandas的melt函数就能转成长表格式一步到位。2.3 数据清洗的几个细节数据准备阶段最影响成图质量的往往是Excel表格本身的小毛病。我每次处理数据前都会先做三件事去空、去重、去自环。去空针对的是源节点或目标节点为空的情况。Excel里有空行或者某些合并单元格导致某列读出来是NaN缺失值如果没有剔除画图时会直接报错或者出现一条指向“None”的莫名其妙的链路。去重针对的是同一对源目标和数值重复出现的行。这种数据在Excel里很常见看数据的人手工复制粘贴出的各种“备份行”不清理就直接求和然后再画图数值会虚高。去自环针对的是源节点和目标节点完全相同的情况。用户从浏览到浏览这种没有信息量的链路画到图上只会让节点之间多一条毫无意义的回路视觉上干扰非常大。关于读取Excel文件本身还有一个必须提前解决的问题文件格式和引擎的匹配。pandas读取xlsx后缀的文件需要openpyxl库读取xls后缀的老文件需要xlrd库。如果你发现pd.read_excel直接报错八成是这两个库没装。另外文件路径里有中文的时候代码一般在读取阶段不会有问题但某些老版本库在Windows上会报编码错误稳妥的做法是在脚本最前面声明文件路径时不要手动把中文路径粘死而是放到配置变量里一起维护。3. 三条实现路线按需选择3.1 Plotly交互体验最好的方案Plotly是我个人用下来最顺手的选择。它生成的桑基图是HTML文件可以直接在浏览器里打开图表上每个节点和链路都支持鼠标悬停查看具体数值节点还可以手动拖拽调整位置。对于要放入汇报场景的图这种交互能力能让你在现场展示时自由地“扒开”某条链路看细节。Plotly的缺点也不是没有。它默认输出的静态图片保存需要额外安装一个kaleido库而且生成的是网页形式如果你不习惯浏览器看图可能会觉得有点重。但对大多数场景来说node的交互拖拽和多层级钻取体验已经值回这个成本了。3.2 Pyecharts中文环境下的颜值担当Pyecharts是百度ECharts的Python封装在国内项目里用得非常多。它的优势一是文档和社区资料是中文的遇到问题搜起来效率高二是图表样式默认就比Plotly更精致颜色渐变、透明度、曲线的平滑度这些细节都处理得比较好看出来的图直接贴到公众号或PPT里都很体面。需要说明的是pyecharts同样生成HTML文件交互能力和Plotly不相上下。它的配置项是链式调用的风格刚开始接触时会觉得.options和.add这种写法和普通Python代码不太一样但照着官方示例抄一遍基本就能举一反三。3.3 Matplotlib零依赖的轻量兜底如果只是临时看一眼数据或者环境里不方便装重依赖matplotlib自带的sankey模块也能用。它跟前面两个方案的区别是输出的是静态PNG图片没有交互而且数据格式要求是节点和流量列表分开传使用起来略繁琐。我一般在什么情况下用它在服务器上快速排查数据的时候。服务器没有浏览器也没法指望HTML文件一张静态图片直接发到聊天窗口里就能看。作为兜底方案它足够用。三条路线的差异可以看这张表对比项PlotlyPyechartsMatplotlib输出格式HTMLHTMLPNG交互拖拽支持支持不支持中文字体需配置默认支持较好需配置数据输入方式DataFrame直接传需要node/link列表需要节点和流量列表适合场景数据分析汇报中文项目交付快速出图排查4. Plotly完整实操从Excel数据到可交互的桑基图4.1 环境准备先确认Python环境已经安装好了。没装的朋友直接去Python官网下载安装包安装时记得勾选“Add Python to PATH”将Python添加到系统路径这个选项不勾后面在命令行里输python会提示找不到命令。装好之后用pip安装两个核心库就够了pip install pandas openpyxl plotly kaleidopandas负责读取Excel数据openpyxl是xlsx文件的读取引擎plotly负责画图kaleido是plotly导出静态图片的辅助库。这四个库一起装就能覆盖后面全部流程。4.2 完整代码与逐行拆解下面这段代码是我在实际项目里一直使用的模板你只需要把Excel文件名和列名改成自己的就能直接运行import pandas as pd import plotly.graph_objects as go # 1. 读取Excel表格 df pd.read_excel(sankey_data.xlsx, sheet_nameSheet1) # 2. 汇总重复链路并按需排序 df ( df.groupby([source, target], as_indexFalse)[value] .sum() .sort_values(value, ascendingFalse) ) # 3. 提取全部节点 nodes list(dict.fromkeys(df[source].tolist() df[target].tolist())) node_index {node: i for i, node in enumerate(nodes)} # 4. 构建绘图数据 source_idx df[source].map(node_index) target_idx df[target].map(node_index) fig go.Figure(data[go.Sankey( nodedict( pad15, # 节点之间的间距 thickness20, # 节点条的粗细 labelnodes, color#4C78A8 ), linkdict( sourcesource_idx, targettarget_idx, valuedf[value], colorrgba(76, 120, 168, 0.4) # 半透明链路 ) )]) fig.update_layout( title_text业务流量流向桑基图, fontdict(size12), width1000, height600 ) # 5. 保存 fig.write_html(sankey_output.html) print(文件已生成请用浏览器打开 sankey_output.html)这段代码里有几个关键点需要解释一下。第2步的groupby和排序不是必需的但如果Excel里存在重复的链路行这一步可以把它们汇总成一条避免出现两条重名但宽度各不相同的链路。第3步使用dict.fromkeys去重是为了在维持节点首次出现顺序的同时去掉重复项保证节点顺序稳定。第4步把源节点和目标节点的名字映射成数字索引这是plotly绘图时的硬性要求。4.3 核心参数到底怎么调参数调节是这个环节最容易让人困惑的地方因为单个参数看起来都简单组合起来效果却千差万别。node.pad控制的是节点与节点之间的留白单位是像素。pad设太小节点会贴在一起一眼看去挤成一团pad设太大图会显得松散长链路的走向就会变得不明显。个人经验是节点少于10个的时候pad用15到20节点超过20个就降到8到10。这个值没有标准答案核心判据是节点标签能不能分清。node.thickness控制的是节点条的横向宽度。宽度太粗会遮挡链路宽度太细节点不醒目。20这个值在大多数情况下表现不错。link.color控制链路的颜色。这里有个极其实用的写法把颜色设置成带透明度的rgba值这样当多条链路叠在一起时你能清楚地看到重叠区域的深浅变化。透明度0.4左右是我常用的既能区分重叠又不会让颜色太浑浊。arrangement参数值得单独拿出来说。默认是“snap”模式即节点自动吸附对齐整体布局比较规整。如果你的数据层级非常明确可以改成“fixed”模式那么节点在纵向上就会保持链表里出现的先后顺序适合严格按漏斗步骤展示的场景。还有一种“freeform”模式允许所有节点自由拖拽交互演示时比较灵活。4.4 导出HTML之外还要PNGwrite_html保存的交互页面适合直接发给别人用浏览器查看。但如果要把图放进PPT或Word汇报就需要一张静态图片。这时候用write_image保存PNGfig.write_image(sankey_output.png, scale2)scale参数是把图片放大两倍输出得到的PNG分辨率更高插入文档里不会发虚。这里要特别提醒write_image依赖kaleido库如果运行时报错“ModuleNotFoundError: kaleido”回到命令行执行pip install kaleido即可。另外kaleido在离线内网环境下安装有时会失败实在装不上就把截图作为兜底方案吧。5. Pyecharts方案中文环境下的简洁实现5.1 安装与数据构造如果你偏向pyecharts路线安装只需要一条命令pip install pyechartspyecharts对数据的组织方式和plotly略有不同。它在add方法里需要两个参数nodes是节点列表links是链路列表。从Excel到这两个列表的转换也很直接import pandas as pd from pyecharts import options as opts from pyecharts.charts import Sankey df pd.read_excel(sankey_data.xlsx) # 构造节点列表每个节点是个字典 all_nodes list(set(df[source].tolist() df[target].tolist())) nodes [{name: name} for name in all_nodes] # 构造链路列表 links [] for _, row in df.iterrows(): links.append({ source: row[source], target: row[target], value: row[value] }) sankey ( Sankey() .add( 流向图, nodesnodes, linkslinks, node_width20, node_gap15, orienthorizontal, linestyle_optopts.LineStyleOpts( opacity0.3, curve0.5, colorsource ), label_optsopts.LabelOpts(positionright) ) .set_global_opts(title_optsopts.TitleOpts(titleSankey流向图)) ) sankey.render(sankey_pyecharts.html)5.2 几个值得留意的配置差异第一pyecharts里节点的顺序没有强制的排列逻辑所有节点默认会分散排布。如果你的链路是多层级的比如第一层只有1个源头节点第二层有5个中间节点第三层有3个终点建议在构造nodes列表时按层级顺序排列这样图会更接近自上而下的瀑布效果。第二linestyle_opt里的curve参数控制链路弯曲程度默认0.5是比较圆润的曲线。如果链路数量特别多curve可以降到0.2减少交叉时的视觉混乱。colorsource这个写法很贴心它会让链路颜色自动跟随源节点颜色整张图的颜色体系不会乱。第三pyecharts默认支持中文标签这是我在国内项目里更愿意用它而不是plotly的原因之一。plotly需要额外设置中文字体否则标签显示成方块口字pyecharts基本开箱即用。当然如果你要定制更复杂的主题颜色pyecharts的配置项也足够丰富只是需要花时间翻文档。6. 常见问题与排查技巧实录6.1 数据问题先检查Excel再检查代码桑基图出问题我九成的情况最后都发现是Excel数据不干净而不是代码写错了。最典型的症状是节点之间出现诡异的数倍关系——比如从“曝光”到“点击”的链路宽度比“曝光”的总宽度还大。这种问题几乎一定是链路汇总时没有去重同一对源目标在Excel里被记了多行绘制之前没有像第4节代码第2步那样做groupby汇总。另一个常见问题是流量对不上。进入某个节点的总流量总和跟流出该节点的总流量差了一截。检查方向是有没有目标节点为空的行有没有某个阶段只出现在“源”列而没有出现在“目标”列换句话说就是某个环节的数据断链了。补齐数据比改代码快得多。自环问题我再强调一次。如果用户行为记录里有“浏览→浏览”这样的记录会导致节点自己连自己。虽然绘图不会报错但图面上会多出一条毫无意义的回路既难看又容易误导人判读数据。在数据清洗阶段用df[df[source] ! df[target]]直接过滤掉这类行。6.2 代码报错按报错信息读问题比较常见的报错和处理方式我整理成了一张速查表报错信息原因解决方法ModuleNotFoundError: No module named openpyxl缺少xlsx读取引擎pip install openpyxlModuleNotFoundError: No module named kaleido缺少图片导出组件pip install kaleidoValueError: x is not in list节点映射时源节点或目标节点有缺失值清洗数据确保两列无空值UnicodeDecodeErrorExcel路径或文件编码异常用英文重命名Excel文件排除路径干扰中文标签显示为方块plotly默认字体不支持中文在fig.update_layout的font参数里指定中文字体路径关于最后一个问题再展开一下。plotly的HTML文件里如果中文显示为□是因为浏览器渲染时没有找到合适的中文字体。解决方案是在布局参数里指定一个你系统里存在的中文字体名fig.update_layout(fontdict(familyMicrosoft YaHei, SimHei, Arial))Windows系统一般都有“Microsoft YaHei”微软雅黑和“SimHei”黑体Mac系统则可以填“PingFang SC”。这个设置对HTML输出的效果立竿见影。6.3 渲染效果问题节点多、链路密当你数据里的节点超过40个链路超过100条任何方案都会面临图面拥挤的问题。这种时候最有效的处理不是研究某个参数而是先瘦身数据。我的做法是先把所有链路按数值排序保留累计占比达到80%的前N条链路把剩下那些细碎的链路合并成一个“其他”节点。这个思路和帕累托法则如出一辙——大部分流向都是由少数主干链路贡献的把次要支路压缩后图面干净主干信息反而更突出。如果实在不想丢数据也可以从布局上缓解。把图的高度调大一些比如高度设为800到1000像素给节点更多的纵向空间。同时把node.pad调小到8左右减少节点间距占用的空间。这样能改善但治标不治本数据太多的时候还是回归“其他”合并方案最实在。结尾我再分享一个实战小心得。这套PythonExcel的流程用顺手之后我养成了一个习惯在工作交接时把Excel数据模板和Python脚本一起交给同事而不是只交一张最终的截图。因为图是死的数据是活的。下次领导再要看这批数据的最新流向不需要重新去折腾画图工具打开Excel把上个月的数值换成这个月的双击运行脚本新图就出来了。这种“数据驱动出图”的方式才是PythonExcel做桑基图真正省时间的核心价值所在。