☰
从条形图到棒棒糖图:用matplotlib实现极简数据可视化
2026/9/26 7:16:55 网站建设 项目流程

做数据分析这几年,我最大的一个感触是:很多人不是不会做图,而是被软件里的默认图表绑住了思路。打开Excel,插入图表,翻来覆去就是柱状图、饼图、折线图那几样。但你有没有想过,条形图其实可以换个形态——把粗壮的柱子抽成一根细线,顶端加一个圆点,就成了棒棒糖图(lollipop chart)。第一次见这种图的时候,我脑子里冒出来的话就是:这玩意儿不就是条形图穿了一件极简主义的衣服吗?但真正用起来才发现,它不只是好看,在信息传达效率上有很多实打实的优势。这篇文章不是教科书式的图表介绍,而是我从实际报表、汇报、发文中总结出来的经验,会用Python的matplotlib手把手带你画出能直接用的图,再讲清楚排序、分组、哑铃图这些进阶玩法,最后把实际踩过的坑都列出来。做数据分析、写报告、做PPT汇报的朋友都适合参考,不管你是刚入门的新手,还是已经画了几年条形图的老手,应该都能找到有用的东西。

1. 什么是棒棒糖图:从条形图到极简视觉的进化

1.1 棒棒糖图的核心构成与起源

棒棒糖图的构成非常朴素:一根从基线出发的细线,加上顶端的一个圆点。线是棒,点是糖,合在一起就是一根棒棒糖。听起来像开玩笑,但它的信息编码逻辑是经过验证的:数据的数值大小,由圆点所在的位置来承载,细线的作用是引导视线从基线平滑地移动到圆点。

这种图最早出现在哪里已经很难考证,但它的设计思路明显受到Edward Tufte“数据墨水比”理论的影响。Tufte在《The Visual Display of Quantitative Information》里提出过一个尖锐的问题:图表上每一点墨水,都应该承载数据信息;凡是不能承载信息的墨水,都属于可以删掉的噪音。按照这个标准,条形图其实有点“浪费”——一根粗柱子的绝大部分面积,都是重复编码同一个数值。你真正需要读取的,只是柱子的末端位置而已。棒棒糖图把柱体压缩成线条,只保留信息最关键的部分,从根源上把“噪音墨水”减掉了大半。

我第一次在项目里用棒棒糖图,是在给客户做门店销售排名的dashboard。当时数据有将近30家门店,用条形图画出来,标签密密麻麻,视觉上非常沉重。抱着试试看的心态换成棒棒糖图,第一版的观感变化就让我很惊讶:同样的数据,画面忽然“透”了,排名的层次感比之前清楚得多。从那以后,凡是排名类、对比类的数据,我都会优先考虑棒棒糖图,而不是默认条形图。

1.2 棒棒糖图与条形图的本质区别

很多人以为棒棒糖图只是条形图的“瘦身版”,换了个皮肤而已。但严格来说,两者的编码方式并不相同:

对比维度条形图棒棒糖图
视觉载体柱体/条形的面积细线(茎)+ 圆点(糖)
数据编码长度与面积双重编码端点位置编码
视觉重量较重,容易占据注意力较轻,留白更多
精确比较更依赖目测长度差更依赖端点位置对齐
信息密度类别多时容易拥挤类别多时依然较清爽
适用氛围正式、数据感强的场景简洁、现代的展示场景

这里有个关键点:人的视觉系统对“位置”的感知精度,要高于对“长度”的感知精度。想象一下,你不看刻度,直接目测两根柱子的高度差,和目测两个圆点的高度差,哪个更准?答案是圆点。因为圆点是明确的视觉锚点,你的眼睛可以精确地捕捉它的坐标位置,而柱子的高度差要受顶部边缘、柱体宽度、背景网格等多重因素干扰。这正是棒棒糖图不“花哨”但“好读”的原因。

当然,编码方式的改变也带来一些代价。条形图的面积本身传递着“量级感”,一眼看去,柱体粗壮、面积大,数据的分量是能直观感受到的。棒棒糖图弱化了这种量级感,更适合“比排名、看趋势”的场景,不太适合“展示总量、强调体量”的场景。选图之前,想清楚你想让读者感受到什么,比记住任何图表规范都重要。

2. 为什么用棒棒糖图:适用场景、审美逻辑与取舍边界

2.1 数据墨水比与负空间的优势

套用Tufte的理论,棒棒糖图的“数据墨水比”几乎是所有常见图表里最高的那一档。图表里剩下的每一笔——细线、圆点、坐标轴、网格——都在为同一个目标服务:让你快速读出谁大谁小。那些被省掉的柱体填充色、边框线、阴影,全部转化成了干净的负空间。别小看负空间,图表里的留白不是浪费,它是引导视线流动的缓冲带。就像一间屋子摆满了家具会让人透不过气,一张图塞满了柱体也会让人找不到重点。

负空间的另一个好处是:当你的类别数量比较多时,比如15到30个类别,条形图会变成一个密不透风的“斑马线”,而棒棒糖图依然保持清爽。我画过一张包含28个国家的指标对比图,用条形图时,标签和柱体挤在一起,几乎需要放大才能看清;换成棒棒糖图后,每条线的间距拉开,标签可读性明显提升,整体效果干净利落。这个特性,让棒棒糖图特别适合“榜单类”数据。

2.2 哪些场景该用,哪些场景别硬用

从我的实践看,棒棒糖图的优势场景非常明确,主要有四类:

  • 排名类数据,比如销售排行、城市人口、评分排行,排序后一张图就能说清楚座次。
  • 类别数量较多的对比,尤其当类别名较长时,横向布局的棒棒糖图能留出足够空间给标签。
  • 两期或多期对比,做成哑铃图(两个圆点中间一条线),比并排的条形组更直观。
  • 追求简洁现代视觉风格的汇报场景,比如给管理层做PPT,少即是多的画面更受欢迎。

反过来,有几类情况我建议你还是老老实实用条形图。第一,数值需要精确读取时。圆点虽然位置精确,但人眼读取“点在哪一格”不如读取“柱子到哪一格”来得自然。如果读者需要对着图报出“第三个是785万”,条形图反而更友好。第二,数值非常接近时。比如一堆数据都集中在80到85之间,圆点会挤成一条线,难以区分;这时候放大纵轴又显得心虚,条形图至少还能看出柱高的细微差异。第三,数据存在较多负数时。负数会导致线条跨越基线,视觉上容易混乱,处理成本高。第四,你想传达“体量”而非“排名”时,条形图的面积优势就体现出来了。

我见过不少新手看完文章后,把手里所有图都改成棒棒糖图,结果反而不如原来清楚。图表选型不是追潮流,是匹配问题。你的数据长什么样、你想让读者关注什么,这两件事想明白了,选型基本不会出错。

3. 用Python手把手画第一张棒棒糖图:从数据到成品

3.1 环境准备与数据结构

画棒棒糖图的工具很多,Excel其实也能硬画,但只能用散点图加误差线模拟,操作繁琐且不灵活。我日常最顺手的方案是Python的matplotlib,搭配pandas做数据整理。要求不高,Python 3.8以上即可,matplotlib建议3.5以上版本,安装命令很简单:

pip install matplotlib pandas

数据结构上,棒棒糖图只需要两列:一列是类别名(比如门店名称、城市名、产品名),一列是数值(比如销售额、评分、人数)。下面这段代码模拟了一个常见场景:某连锁品牌10家门店的年度销售额,单位是万元。

import pandas as pd data = { 'store': ['门店A', '门店B', '门店C', '门店D', '门店E', '门店F', '门店G', '门店H', '门店I', '门店J'], 'sales': [820, 765, 720, 698, 651, 620, 588, 561, 530, 498] } df = pd.DataFrame(data) print(df)

有一点值得提醒:matplotlib默认不支持中文显示,如果你直接拿上面的“门店”标签去画图,很可能会出来一堆方框。解决方法是提前设置中文字体,我一般在代码开头加上这几行:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # Windows常用黑体 plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题

Mac用户可以把SimHei换成PingFang SC或者STHeiti,Linux服务器上如果没有中文字体,可以先装一个文泉驿或者思源黑体。这个坑我踩过太多次,提前处理能省很多事。

3.2 横向棒棒糖图:完整代码与逐行拆解

横向布局是最经典、也最适合排名类数据的形态。完整代码不长,核心就两个绘图命令:

import matplotlib.pyplot as plt import numpy as np import pandas as pd plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 数据准备 stores = ['门店A', '门店B', '门店C', '门店D', '门店E', '门店F', '门店G', '门店H', '门店I', '门店J'] sales = [820, 765, 720, 698, 651, 620, 588, 561, 530, 498] # 创建画布 fig, ax = plt.subplots(figsize=(10, 6)) # 第一步:画茎干,也就是那些细线 ax.hlines(y=range(len(stores)), xmin=0, xmax=sales, color='#2E86AB', linewidth=1.5) # 第二步:画棒棒糖,也就是末端圆点 ax.scatter(sales, range(len(stores)), color='#A23B72', s=80, zorder=3) # 第三步:设置y轴类别标签 ax.set_yticks(range(len(stores))) ax.set_yticklabels(stores) ax.set_xlabel('销售额(万元)') ax.set_title('2024年各门店销售额排行榜') plt.tight_layout() plt.show()

这里三个关键点需要理解:

一是hlines的参数。y=range(len(stores))表示每条线的纵坐标,第一个门店对应y=0,第二个对应y=1,依此类推;xmin=0表示线从水平位置0出发;xmax=sales表示线延伸到该门店的销售额位置。一句话:这行命令画的是从0到数值的一条水平线。

二是scatter的参数。s=80控制圆点大小,zorder=3控制绘制顺序。matplotlib按zorder从低到高绘制,圆点的zorder设为3,保证它压在线条上面,否则细线的端点可能会冒出圆点边缘,看起来像糖棍扎穿了糖果,很丑。

三是坐标体系的对应关系。横向布局下,类别放在y轴,数值放在x轴,所以y=range(n)正好把10家门店从上到下排开。如果你想让门店A显示在最上面,需要留意数据顺序,后面第4节会专门讲排序。

跑完这段代码,你会得到一张最基础、但已经能用的棒棒糖图。剩下的工作就是打磨颜值:网格线、配色、数值标签,一个都别急,先把基本形态画对再说。

3.3 纵向变体、配色与细节定制

纵向布局的棒棒糖图语法几乎一致,只是hlines换成vlines,x轴放类别,y轴放数值:

fig, ax = plt.subplots(figsize=(12, 5)) ax.vlines(x=range(len(stores)), ymin=0, ymax=sales, color='#2E86AB', linewidth=1.5) ax.scatter(range(len(stores)), sales, color='#A23B72', s=80, zorder=3) ax.set_xticks(range(len(stores))) ax.set_xticklabels(stores, rotation=45) ax.set_ylabel('销售额(万元)') ax.set_title('2024年各门店销售额排行榜') plt.tight_layout() plt.show()

纵向适合类别名比较短(比如只有三五个字)的场景,类别多了会显得拥挤。我实际应用中,横向用得远比纵向多,因为门店、城市这类文本标签通常不短,横向能舒舒服服地放下。

做细节定制时,我比较常用的三个技巧:

第一,突出第一名。把最高值那个门店的圆点换一种颜色,或者放大一号,让读者第一眼就看到“冠军”。这是一种低成本但效果极佳的视觉引导。

# 找出最大值索引 top_idx = sales.index(max(sales)) ax.scatter(sales, range(len(stores)), color='#A23B72', s=80, zorder=3) ax.scatter(sales[top_idx], top_idx, color='#F18F01', s=120, zorder=4)

第二,加上数值标签。如果数据量不大,我倾向于在圆点右侧标注数值,方便读者精确读取。用ax.text()逐个标注即可,注意横向布局要加一点水平偏移,避免文字压住圆点:

for i, v in enumerate(sales): ax.text(v + 15, i, f'{v}', va='center', fontsize=9, color='#333333')

第三,控制配色的数量。整张图最好不超过三种颜色,一深一浅一强调色足矣。棒棒糖图本身就是极简风格,配色一旦花哨,极简优势瞬间归零。我常用的组合是:线条用低饱和度的蓝灰(比如#2E86AB),圆点用稳重的中性色或主题色,高亮项单独用暖色(比如橙色#F18F01)。这套逻辑放到灰度印刷场景也成立。

4. 进阶玩法:排序、分组与哑铃图

4.1 排序的艺术:排名图的灵魂

棒棒糖图最常见的用途就是排名,排序自然成了它的灵魂。很多人画完图发现“第一名”在底部,第二反应过来:为什么跟我想的不一样?原因在于matplotlib的y轴默认从下往上增长,y=0在第一行、显示在最底部。具体来说:

df_sorted = df.sort_values('sales', ascending=True).reset_index(drop=True) stores = df_sorted['store'].tolist() sales = df_sorted['sales'].tolist()

这个操作先把数据按销售额升序排列,让最小值排在索引0,显示在最底部;最大值排在最后,显示在最顶部。横向棒棒糖图不需要额外的invert_yaxis(),只要你按照升序传入数据,视觉上自然形成“从高到低”的排名榜。如果你偏好用降序排列再反转y轴,也可以这样:

df_sorted = df.sort_values('sales', ascending=False).reset_index(drop=True) fig, ax = plt.subplots(figsize=(10, 6)) ax.hlines(y=range(len(df_sorted)), xmin=0, xmax=df_sorted['sales'], color='#2E86AB', linewidth=1.5) ax.scatter(df_sorted['sales'], range(len(df_sorted)), color='#A23B72', s=80, zorder=3) ax.invert_yaxis() # 反转y轴,让索引0显示在顶部

两种方法结果一样,但我的习惯是第一种:直接在数据层面排好序,少一步坐标轴操作,也减少一处出错的可能。排序之后,棒棒糖图的“阶梯感”会非常明显,谁高谁低一清二楚,配合圆点位置,扫一眼就能完成排名认知。

4.2 哑铃图:两期对比的绝佳武器

棒棒糖图再往前跨一步,就成了哑铃图(dumbbell chart)。哑铃图的形态是两个圆点,中间连一条横线,非常适合做两期对比:去年vs今年、活动前vs活动后、方案A vs方案B。它本质上就是同时画两套棒棒糖的端点,再用线把同一个类别的两个点连起来。用matplotlib实现也很直接:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False stores = ['门店A', '门店B', '门店C', '门店D', '门店E', '门店F', '门店G', '门店H', '门店I', '门店J'] sales_2023 = [750, 710, 690, 660, 620, 600, 580, 550, 520, 500] sales_2024 = [820, 765, 720, 698, 651, 620, 588, 561, 530, 498] fig, ax = plt.subplots(figsize=(10, 6)) # 同一类别两个年份的端点连成一条横线 for i in range(len(stores)): ax.plot([sales_2023[i], sales_2024[i]], [i, i], color='#8D99AE', linewidth=1.5, zorder=1) # 分别画两年数据的圆点 ax.scatter(sales_2023, range(len(stores)), color='#2E86AB', s=70, label='2023年', zorder=3) ax.scatter(sales_2024, range(len(stores)), color='#A23B72', s=70, label='2024年', zorder=3) ax.set_yticks(range(len(stores))) ax.set_yticklabels(stores) ax.set_xlabel('销售额(万元)') ax.set_title('各门店销售额年度对比') ax.legend() plt.tight_layout() plt.show()

哑铃图的读图逻辑非常直观:每个门店看两个圆点的间距,间距越大,说明变化越大。为了进一步引导注意力,我通常会把增长的类别用暖色高亮,减少的用灰色弱化,让读者瞬间抓住“谁在涨、谁在跌”的结论。这张图在向管理层汇报年度复盘时效果尤其好,因为它把“变化”这件事可视化成了一目了然的几何关系。

4.3 其他工具的实现思路

Python之外,顺手提两句其他常用工具,避免你只会一个工具时被卡住。R语言里,ggplot2实现棒棒糖图非常优雅,核心是geom_segment加geom_point的组合:

library(ggplot2) ggplot(df, aes(x = reorder(store, sales), y = sales)) + geom_segment(aes(xend = store, yend = 0), color = "grey60") + geom_point(size = 3, color = "#A23B72") + coord_flip()

Excel实现起来比较绕,基本思路是用“散点图+水平误差线”模拟,误差线设置成“负偏差、无端帽、百分百”,再把线条调细。操作不算复杂,但每次调整数据都要重新检查误差线设置,适合偶尔用一次的场景,不适合常态化报表。Tableau和Power BI则直接提供了类似的原生图表类型,或者你可以在Tableau里用“双轴+圆图”组合快速实现,步骤也不复杂。

5. 常见问题与排查技巧实录

5.1 常见问题速查表

从我实际画图遇到的情况看,新手最容易卡在下面几个问题上,整理成一张速查表,方便你对照排查:

问题现象可能原因解决方案
中文标签显示为方框未设置中文字体设置plt.rcParams['font.sans-serif']=['SimHei']
圆点挡住了线头,糖棍像扎穿糖果绘制顺序不对给scatter设置zorder=3以上
最大值出现在图底部y轴方向与数据顺序不匹配数据按升序传入,或调用invert_yaxis()
类别标签重叠、显示不全画布太小或字体过大调大figsize,横向图适当加宽
线条密密麻麻像蜘蛛网类别太多或线条太粗精简类别,linewidth降到1-1.5
数值标签压住圆点文本位置偏移不够text里加水平偏移量,如v+15
负值无法正常显示基线不为0或数据范围理解错检查xmin/ymin设置,确认基线与0对齐
导出图片文字模糊保存分辨率太低plt.savefig('xxx.png', dpi=300)

这8个问题里,前三个占了实际求助的八成。尤其是中文字体和圆点遮挡这两个,属于画图五分钟、折腾半小时的典型问题,强烈建议写成一个固定模板,每次画图直接复用。

5.2 实操心得:我自己常用的三个加分技巧

排查完问题,再分享三个我常用的加分细节,不一定写在官方文档里,但实测下来对成图质量影响很大。

第一个细节是网格线的处理。棒棒糖图的信息核心在圆点位置,为了让人眼能快速对齐坐标,我建议把x轴方向的网格线调成浅灰、虚线样式,y轴方向的网格线直接关掉。横竖都画网格会让图变脏,只保留数值方向的浅网格,既辅助读数,又不抢圆点的风头。

ax.grid(axis='x', linestyle='--', alpha=0.5) ax.set_axisbelow(True) # 让网格线位于数据下层

第二个细节是“少就是多”的取舍。我见过有人给棒棒糖图的每个圆点都加了光晕、渐变、阴影,结果图面变得比条形图还杂乱。棒棒糖图的核心竞争力就是简洁,任何不能承载信息的装饰都应该删掉。一个稳妥的检查方法是:把图片缩小到手机屏幕大小,如果你还能一眼看清排名的先后顺序,说明信息层级是对的;如果看不清,要优先精简装饰,而不是放大字号。

第三个细节是标注“关键结论”。图是给人看的,不是给数字看的。排名图画完之后,我会在最高点、最低点、或变化最大的点旁边加一句话注释,比如“同比+9.3%”或“连续三季度第一”。这种注释把读者的注意力直接引导到图表想表达的结论上,比让读者自己对比数据更高效,也更符合汇报场景的需求。

另外还想提醒一点:棒棒糖图毕竟不是万能的。当你的类别数超过30个时,即使细线和圆点再克制,视觉上也会拥挤;当你要突出数据的“总量体量”时,条形图或堆叠图更合适;当数据分布跨度极大(比如最小值和最大值差了五十倍)时,对数坐标下棒棒糖图的表现尤其需要谨慎,线条长度会严重失真,不如直接改用其他形式。图表选型本质上是在跟读者的大脑对话,你希望在对方脑子里留下什么结论,就选择什么样的视觉语言。

我个人画了几年图表后,越来越认同一个观点:好看的图不是靠堆砌技巧画出来的,而是靠克制画出来的。棒棒糖图恰恰是这种“克制美学”的代表。它没有复杂的图层、没有华丽的效果,只是一根线加一个圆点,但正因为信息足够聚焦,反而比很多花哨的图表更能说服人。如果你还没在项目里尝试过棒棒糖图,下次遇到排名类数据时,不妨花十分钟画一张,和原来的条形图并排放着对比一下,你会直观感受到什么叫“当条形图遇上极简美学”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询