茎叶图这个东西第一次见的人多半会觉得它有点“土”——一堆数字竖着排中间一条竖线隔开左边一位右边一串看起来既不像正经图表也不像能直接拿去汇报的东西。但我在实际做数据探查和分布诊断的时候用它的频率反而比直方图还高。原因很简单它把“形状”和“原始数值”同时保留下来了。直方图告诉你数据大概长什么样但分箱一划具体值就没了箱线图告诉你四分位和异常值但中间那坨数据怎么堆的它说不清楚。茎叶图刚好卡在中间既能一眼看出分布形态、集中趋势、离散程度又能把每一个原始数据点原封不动地摆在你面前。这篇内容适合谁看如果你是刚接触数据分析、统计基础还在补课阶段的人茎叶图是理解“分布”这个概念最便宜的工具纸笔就能画如果你已经天天跟数据打交道但习惯性只丢给绘图库出图那茎叶图在小样本快速诊断场景下能帮你省掉不少来回折腾的时间。全文我会从设计思路、构造细节、手工与代码两条实操路径、常见坑位排查几个角度拆开讲尽量把每个“为什么这么设计”说透而不是只给你一个画图公式。1. 茎叶图到底解决什么问题设计思路与方案取舍1.1 从“看形状”和“看数值”这对矛盾说起做数据分布诊断本质上要回答两个问题数据整体长什么样以及具体每个值落在哪。这两件事在传统图表里往往是矛盾的。直方图为了画出形状必须把连续区间切成若干箱箱宽一旦定下来箱内的个体差异就被抹平了。你看到某根柱子很高只知道“这个区间里数据多”但到底是集中在区间左端还是右端、有没有双峰被合并进同一个箱光看柱子是看不出来的。反过来如果你把原始数据一条条列出来数值是全了但几十上百个数排在一起人眼根本抓不住形态。茎叶图的设计思路就是用“茎”承担分箱的职责用“叶”保留箱内的原始精度。举个最直白的例子数据 23、25、27、31、34以十位为茎、个位为叶画出来是2 | 3 5 7 3 | 1 4左边“2”代表 20 到 29 这个区间右边“3 5 7”就是落在这个区间里的个位数。你既能看到 20 段有 3 个数据、30 段有 2 个数据形状又能精确读到 23、25、27 这些原始值数值。这就是它区别于直方图的核心价值——分箱不丢信息。这个特性决定了它的适用边界样本量小的时候它是神器样本量大的时候它会“爆行”。你想想如果有一千个数据叶子那一列能排到天边去可读性直接归零。所以业内比较一致的经验是样本量在 20 到 200 之间是茎叶图最舒服的区间超过这个范围就该考虑直方图或者密度图了。这不是硬规定但你在实际用的时候会发现超过两三百个数据光是数叶子就够你受的。1.2 为什么不用直方图或箱线图替代有人会问现在绘图库这么方便直方图一行代码就出来了为什么还要折腾茎叶图我的回答是它们回答的问题不一样不是替代关系。直方图的核心参数是箱宽bin width这个参数选得好不好直接决定你看到的形状是不是真的。箱宽太宽双峰会被糊成一个胖峰箱宽太窄噪声会被放大成一堆毛刺。而箱宽怎么选本身就是一个需要经验判断的事新手很容易在这里翻车。茎叶图没有这个烦恼因为它的“箱”是由数据的数量级天然决定的——十位、百位、十分位这些是数据自带的刻度不需要你人为设定。换句话说茎叶图把“分箱”这个主观决策变成了客观的进位规则这对刚上手的人来说友好得多。箱线图则是另一个极端它把数据压缩成五个数最小值、Q1、中位数、Q3、最大值异常值单独标出来。它擅长的是快速比较多个组的分布位置和离散程度但中间那 50% 的数据到底怎么堆的它完全不告诉你。我遇到过不少情况两组数据的箱线图看起来几乎一样但茎叶图一画一组是单峰对称另一组是明显的双峰后续的分析方向完全不同。所以我的习惯是箱线图用来横向对比多组茎叶图用来纵向深挖单组两者配合着看比单用任何一个都靠谱。1.1 补充一句关于“手工友好”的价值还有一点容易被忽略茎叶图是少数不依赖任何工具就能画出来的统计图。纸笔、白板、甚至聊天窗口里打几行字都能快速画一个出来。这在很多场景下非常实用——开会时临时要看一组数怎么分布你不可能当场打开电脑跑代码但你可以直接在白板上把茎叶图列出来边列边讲。这种“低门槛即时可视化”的能力是它至今没有被完全淘汰的重要原因。2. 构造细节拆解茎怎么定、叶怎么排、缩放怎么调2.1 茎的选取数量级决定一切茎叶图构造的第一步也是最关键的一步是确定“茎”取哪一位。规则说起来简单茎是数据的高位部分叶是紧接着的那一位。但实际操作中取哪一位需要看数据的分布范围。假设数据范围是 12 到 89那十位做茎、个位做叶是最自然的茎从 1 到 8一共 8 行清爽。但如果数据范围是 112 到 189十位做茎就不行了因为所有数据的十位都是 1 到 8 里的某几个茎的行数太少叶子会挤成一团。这时候应该用百位和十位组合做茎11、12、…、18个位做叶。再极端一点如果数据范围是 1.2 到 8.9那就得用个位做茎、十分位做叶。这里有个实操中很常见的坑茎的位数不统一。比如数据里有 5、15、105如果你机械地“取最高位做茎”5 的茎是 5、15 的茎是 1、105 的茎是 1画出来完全乱套。正确做法是先把所有数据对齐到同一数量级该补零的补零让茎的位数一致。5 要写成 005茎取前两位“00”叶取“5”15 写成 015茎“01”叶“5”105 茎“10”叶“5”。这样画出来才是可比的。这个细节很多教程不讲但你自己动手画一次就会立刻撞上。2.2 叶的排序升序是默认但别盲目排叶子在每一行里怎么排标准做法是升序排列这样每一行的最小值在最左、最大值在最右配合茎的有序性整个图从左上到右下就是全局有序的。这个有序性带来一个很实用的副产品中位数可以直接从图上数出来。总共 n 个数据从最小的叶子开始数到第 (n1)/2 个n 为奇数或第 n/2 与 n/21 个的平均n 为偶数不用另外排序计算。不过有一种情况我会故意不排序当我想看数据进入的原始顺序时。比如做数据采集质量检查我想知道后采集的数据是不是系统性偏大或偏小这时候保持叶子按采集顺序排列反而能看出时间趋势。这算是茎叶图的一个非主流用法但确实好用。当然正式出图给别人的时候还是老老实实升序不然读者会看得很别扭。2.3 茎叶缩放一行不够就拆成多行当某个茎下面的叶子太多一行排不下或者挤得看不清时就需要做茎叶缩放stem-and-leaf scaling。常见做法是把一个茎拆成两行或五行。拆两行的规则是第一行放叶子 0 到 4第二行放叶子 5 到 9两行共用同一个茎值通常在第一行后面标个标记区分。拆五行的规则更细分别放叶子 0-1、2-3、4-5、6-7、8-9。拆得越细形状的分辨率越高但行数也越多。什么时候该拆我的经验判断是如果某一行叶子超过 15 个或者整个图的行数少于 5 行就该考虑拆了。行数太少意味着分辨率不够形状看不出来叶子太多意味着单行信息过载。这两个信号出现任何一个缩放就是必要的。但也要注意别过度拆拆到五行以上图会变得很长反而不好读。一般两行拆分能解决大部分问题。2.4 一个完整的构造流程示例拿一组具体数据走一遍。假设有 30 个观测值23, 25, 27, 31, 34, 35, 38, 41, 42, 44, 45, 47, 48, 51, 52, 53, 55, 56, 58, 61, 62, 64, 65, 67, 71, 73, 75, 78, 82, 85第一步定茎范围 23 到 85十位做茎茎从 2 到 8。第二步分叶把每个数的个位挂到对应茎后面。第三步排序每行叶子升序。结果2 | 3 5 7 3 | 1 4 5 8 4 | 1 2 4 5 7 8 5 | 1 2 3 5 6 8 6 | 1 2 4 5 7 7 | 1 3 5 8 8 | 2 5看这个图能读出什么中间 4、5、6 三行叶子最密说明数据集中在 40 到 69 之间两端 2 和 8 行叶子稀疏尾部较轻整体大致对称略微右偏高端拖得稍长一点。中位数30 个数据第 15 和 16 个的平均从图上数第 15 个是 51、第 16 个是 52中位数 51.5。这些信息在三十秒内就能读出来这就是茎叶图在中小样本上的效率优势。3. 实操落地手工绘制与代码实现两条路径3.1 手工绘制纸笔场景下的标准动作手工画茎叶图有一套固定动作按顺序走不容易出错。扫描数据范围确定最大值和最小值据此决定茎取哪一位。列出所有可能的茎值从最小茎到最大茎竖着写一列后面跟一条竖线。注意即使某个茎下面没有数据也要保留这一行留空因为空缺本身也是分布信息——它告诉你这个区间是断档的。逐个读取数据把叶挂到对应茎后面。这一步不要排序先挂上去再说避免边挂边排导致遗漏。每行叶子升序排列。排的时候用手指或笔尖点着数防止漏排。检查总数。把所有叶子数一遍必须等于原始数据个数。这一步是硬性校验我见过太多人画完发现少了一两个数回头找半天。提示手工绘制时建议用方格纸或者等宽字体环境比如代码块、记事本因为茎叶图的对齐极其重要。比例字体下叶子宽度不一致图会歪得没法看。3.2 代码实现Python 手工构造版现成的统计库对茎叶图的支持其实不算好很多库要么没有、要么输出格式不理想。我的做法是自己写一个几十行的构造函数逻辑透明、格式可控。下面这个版本处理了前面提到的位数对齐和缩放问题def stem_leaf(data, stem_digits1, scale1): data: 数值列表 stem_digits: 茎占的位数从最高位算起 scale: 1 表示不拆行2 表示拆两行5 表示拆五行 # 统一数量级找到最大绝对值确定需要补几位 max_abs max(abs(x) for x in data) int_digits len(str(int(max_abs))) if max_abs 1 else 1 # 计算叶的位数 leaf_digits int_digits - stem_digits factor 10 ** leaf_digits if leaf_digits 0 else 1 buckets {} for x in data: scaled int(round(x / factor)) if factor 1 else int(round(x)) stem scaled // 10 if leaf_digits 0 else scaled // 10 leaf abs(scaled) % 10 buckets.setdefault(stem, []).append(leaf) lines [] for stem in sorted(buckets): leaves sorted(buckets[stem]) if scale 1: lines.append(f{stem:{stem_digits}} | .join(map(str, leaves))) elif scale 2: low [l for l in leaves if l 4] high [l for l in leaves if l 5] if low: lines.append(f{stem:{stem_digits}} | .join(map(str, low))) if high: lines.append(f{stem:{stem_digits}} | .join(map(str, high))) elif scale 5: groups [(0,1),(2,3),(4,5),(6,7),(8,9)] for lo, hi in groups: part [l for l in leaves if lo l hi] if part: lines.append(f{stem:{stem_digits}} | .join(map(str, part))) return \n.join(lines) sample [23,25,27,31,34,35,38,41,42,44,45,47,48, 51,52,53,55,56,58,61,62,64,65,67,71,73,75,78,82,85] print(stem_leaf(sample, stem_digits1, scale1))这段代码的核心逻辑就三步缩放对齐、分桶、排序输出。factor那一段处理的是小数和不同数量级混排的情况scale参数控制拆行。实测下来这个函数在几百个数据以内跑得飞快输出格式也干净直接贴进文档就能用。3.3 代码实现借助成熟库的快捷路径如果不想自己写stemgraphic这个库可以直接出图支持缩放和美化# pip install stemgraphic import stemgraphic data [23,25,27,31,34,35,38,41,42,44,45,47,48, 51,52,53,55,56,58,61,62,64,65,67,71,73,75,78,82,85] fig, ax stemgraphic.stem_graphic(data, scale1)它的好处是能直接生成带坐标轴的图形版本适合放进报告坏处是定制化程度不如自己写函数输出格式受库的约束。我的建议是快速探查用库正式产出用自写函数各取所需。3.4 参数选择的经验法则关于stem_digits和scale这两个参数怎么定我总结了一个简单的判断表数据范围建议茎位数建议缩放理由0-991十位1 或 2行数 10 行以内叶子密度适中100-9992百位十位1 或 2保证行数在 10 行左右0-9含小数1个位2 或 5行数太少必须拆行提分辨率样本量 200不建议用茎叶图-改用直方图或密度图这张表不是金科玉律但它覆盖了八成以上的日常场景。真正拿不准的时候先按表画一版看看行数和叶子密度再决定要不要调。4. 常见问题与排查技巧实录4.1 叶子数对不上原始数据个数这是最高频的问题几乎每个手工画茎叶图的人都踩过。原因通常有三个一是数据里有负数取叶的时候符号处理错了二是小数点的位置没对齐某个数被挂到了错误的茎上三是纯粹的数漏了。排查顺序建议这样走先数总叶子数和原始数据个数比对对不上就按茎逐行核对看哪一行的叶子数和该区间的数据个数不符锁定行之后把该区间的原始数据单独列出来和叶子逐个对。我自己的习惯是画之前先把数据排序排序后挂叶挂完再核对这样漏数一眼就能看出来——排序后相邻的两个数如果茎相同但叶不连续中间大概率漏了。4.2 图形严重右偏或左偏是不是数据有问题不一定。偏态本身是数据分布的合法形态收入、房价、响应时间这类数据天然右偏。但如果你预期数据应该对称画出来却严重偏斜那就要查两件事是不是有极端值没处理以及茎的位数是不是选错了。极端值的影响很直接一个 999 混在 20 到 80 的数据里会把茎的范围硬生生拉到 99中间一大片空行图变得又长又稀。这时候要么单独标注极端值要么先做截尾再画。茎位数选错的影响更隐蔽如果数据实际范围是 100 到 180你却用个位做茎那所有数据的茎都是 0 到 8 里的某几个叶子全是十位和百位图会完全失去意义。判断方法很简单画出来的茎如果只有个位数几行、叶子却密密麻麻基本就是茎位数选少了。4.3 双峰被误判成单峰茎叶图在识别双峰上比直方图有优势但前提是缩放要够细。如果两个峰靠得很近不拆行的情况下可能看起来就是一个宽峰。我的经验是当你在图上看到某一行叶子特别多、而且分布明显不均匀比如左边密右边疏或者中间有个明显的空档时就该拆行再看一遍。拆成两行或五行之后原本糊在一起的峰往往会分开。举个我实际遇到的例子一组响应时间数据不拆行时 3 字头那一行有 18 个叶子看起来是个单峰。拆成两行后3|0 1 1 2 2 3 3 4 和 3|5 6 7 8 8 9 9 9 明显是两个不同的聚集前者是正常响应后者是慢请求后续排查方向完全不同。这个案例说明缩放不是可选项而是诊断深度的调节旋钮。4.4 常见问题速查表现象可能原因处理办法叶子总数少于数据个数漏挂、负数符号处理错排序后逐行核对图特别长、中间大量空行存在极端值单独标注或截尾茎只有几行、叶子极密茎位数选少增加茎位数或拆行形状糊成一团看不出峰缩放不足拆两行或五行小数数据挂错位置小数点未对齐统一换算成整数再画负数茎叶混乱符号未单独处理正负分开画两张图注意负数处理是茎叶图的一个经典难点。比较稳妥的做法是正数和负数分开画两张图各自按绝对值构造然后在图标题里标明正负。混在一张图里处理符号出错概率极高。4.5 一个容易被忽略的校验技巧画完茎叶图后除了数叶子总数还有一个快速校验法用茎叶图反算中位数和极差和原始数据直接算的结果比对。中位数从图上数极差看最小茎最小叶和最大茎最大叶。两个数都对得上基本可以确认图没画错。这个方法比逐个核对快得多适合数据量中等50 到 100的场景。我现在的习惯是画完必做这一步三十秒的事能挡掉大部分低级错误。5. 茎叶图在实际分析中的组合用法5.1 和数据清洗流程的配合茎叶图在数据清洗阶段的价值被严重低估了。清洗前先画一张能快速暴露几类问题异常值孤零零挂在最上或最下的叶子、数据断档中间空行、精度不一致有的叶子是一位有的两位说明数据来源不统一。这些问题在表格里翻半天未必看得出来但茎叶图一画几秒钟就现形。我通常的做法是拿到原始数据先画茎叶图把可疑区间记下来再回到原始记录里核对。这比上来就跑描述统计更直观因为描述统计只给你几个汇总数而茎叶图给你的是全貌。5.2 和直方图对照使用前面说过两者不是替代关系这里补充一个具体的对照用法先用茎叶图定形状再用直方图做呈现。茎叶图帮你判断分布是单峰还是双峰、偏态方向、尾部厚度这些判断做完之后你再决定直方图的箱宽怎么设——单峰对称可以设宽一点双峰就必须设窄一点把两个峰分开。反过来如果先画直方图箱宽选错了你可能根本意识不到有双峰后面的判断全跟着错。这个顺序上的讲究是我踩过坑之后才养成的习惯。5.3 小样本场景下的效率优势最后说一个很实际的场景样本量在 20 到 50 之间的时候茎叶图几乎是效率最高的分布诊断工具。这个量级下直方图的箱宽怎么设都别扭——设宽了只有三四根柱子形状出不来设窄了每根柱子就一两个数噪声比信号大。而茎叶图在这个量级下刚好每行三五个叶子形状清晰、数值完整画起来也就一两分钟。我做过一个粗略的对比同样一组 40 个数据从拿到数据到形成“分布是否对称、有无异常、集中区间在哪”这三个判断茎叶图平均比直方图快将近一半因为省掉了调箱宽和反复出图的时间。这个效率优势在探索性分析的早期阶段特别值钱。那时候你对数据一无所知需要的是快速试错、快速形成假设而不是精雕细琢的成品图。茎叶图的“糙”在这个阶段反而是优点——它逼着你直接看数而不是看经过平滑处理的图形很多细节就是在看数的过程中被发现的。我个人在实际操作中的体会是工具没有高低之分关键是搞清楚它擅长什么、不擅长什么。茎叶图不擅长大数据量、不擅长精美呈现、不擅长多组对比但在中小样本的快速诊断上它至今没有对手。把它放进你的工具箱在合适的时候拿出来用比盲目追新工具实在得多。
阅读完成 · 觉得有帮助?