很多人第一次接触茎叶图是在统计课的教材里看到一堆5|1234这样的数字排列觉得它既不像直方图那样直观也不像散点图那样有信息量于是匆匆翻过去。但真正做过数据探查的人会告诉你茎叶图是被严重低估的工具——它能在不丢失任何一个原始数据点的前提下把数据的分布形态、集中趋势、离散程度甚至异常值一次性摊开在你面前。尤其是样本量在二三十到一两百之间的时候茎叶图的性价比高得离谱你不需要画图软件一张纸一支笔或者一段十几行的代码就能把整组数据的骨架看得清清楚楚。这篇内容适合三类人看一是正在学统计基础、被茎叶图搞得一头雾水的同学二是需要快速做数据初探、又不想动不动就上重型可视化工具的分析从业者三是想给团队做数据素养培训、需要一个轻量级入门案例的人。我会从茎叶图到底解决什么问题讲起把构造逻辑、参数取舍、手工与代码两条实现路径、常见坑点、以及它和直方图的本质差异全部拆开讲中间穿插我自己在实操中踩过的坑和总结的小技巧。读完你至少能做到拿到一组数据五分钟内手工画出一张规范的茎叶图并且能从中读出分布形状和可疑值。1. 茎叶图到底在解决什么问题1.1 它和直方图的根本分歧要不要保留原始值先说一个容易被忽略的事实直方图在画出来的那一刻原始数据就被丢了。你把100个身高数据分成10个区间每个区间数出频数画成柱子——这时候你只知道170到175之间有23个人但具体是170.2还是174.8已经无从得知。茎叶图不一样它把每个数据点拆成茎和叶两部分茎决定它落在哪一行叶把这个数值本身保留下来。换句话说茎叶图是带原始数据的直方图或者说横过来的、按位拆解的有序列表。这个差别在实际分析中非常关键。举个场景你拿到一组用户年龄数据直方图告诉你分布是右偏的大部分集中在25到35岁尾部拖到60岁以上。但你想知道尾部那几个到底是58、59还是刚好60直方图给不了答案茎叶图可以。再比如你做数据清洗怀疑有几个录入错误的值茎叶图会把它们以孤零零的叶子形式暴露出来而直方图只会把它们混进某个区间里你根本注意不到。所以茎叶图的核心价值可以概括成一句话在样本量不大、需要同时看分布形态和具体数值的场合它是信息损失最小的可视化手段。这个定位决定了它的适用边界——样本太大比如上万条时叶子会挤成一团失去可读性样本太小比如只有五六个时又不如直接列表格。经验上20到200这个区间是茎叶图的甜区。1.2 一个具体例子先建立直觉光说概念太虚直接上一组数据。假设某次小测验15个学生的分数是62, 67, 71, 71, 73, 75, 78, 80, 82, 82, 85, 88, 91, 94, 97手工画茎叶图十位数做茎个位数做叶得到6 | 2 7 7 | 1 1 3 5 8 8 | 0 2 2 5 8 9 | 1 4 7就这么四行你立刻能读出好几件事分数集中在70到89之间中间两行叶子最多71出现了两次重复值一目了然最低62、最高97极差35分布大致对称略微中间偏右。这些信息你盯着原始那串数字看半分钟未必能反应过来但茎叶图一画形状直接跳出来了。这就是茎叶图的妙处它同时是数据表和图形。你既可以把它当有序数据读每片叶子就是一个真实分数也可以把它当分布图看叶子的疏密就是频数的高低。这种双重身份是它区别于其他所有统计图的独特之处。1.3 什么时候该用它什么时候该换工具我总结了一个简单的判断表实操时照着对号入座就行场景特征推荐工具理由样本量20到200需要看分布具体值茎叶图信息无损手工可画样本量超过500直方图/密度图叶子过密茎叶图失去可读性需要精确比较两组分布背靠背茎叶图共享茎左右对照数据跨度极大如1到10000直方图茎的位数太多行数爆炸需要展示给非技术受众直方图/箱线图茎叶图需要一定解读门槛数据含大量重复值茎叶图配合计数重复值堆叠反而强化分布感知这里要特别提醒一句茎叶图不是万能的它的最大敌人是数据跨度。如果你的数据从个位数跨到五位数按十位做茎会产生上百行画出来比原始数据还长完全失去意义。遇到这种情况要么先做数量级压缩比如全部除以1000要么老老实实换直方图。我见过有人硬要用茎叶图展示全国各城市的GDP数据结果茎有几百行纯属自找麻烦。2. 茎和叶怎么切位数选择的门道2.1 默认规则与它的失效场景最朴素的切法数据是两位数十位做茎、个位做叶三位数百位做茎、后两位做叶。这个默认规则在数据范围适中时很好用但它有个隐含假设——茎的取值种类不能太多也不能太少。理想情况下茎应该有5到15行左右太少比如只有2行分布形状看不出来太多比如30行以上又显得稀疏冗长。问题在于现实数据很少刚好满足这个条件。比如一组数据全在20到29之间按默认规则茎只有2这一行所有叶子挤在一起等于没画。反过来数据从10跨到99茎有9行还算合理但如果数据是100到999且分布很散茎可能有几十行就得考虑压缩。所以位数选择不是死规则而是一个需要根据数据实际范围动态调整的决策。核心判断标准就一条让茎的行数落在5到15这个区间同时每行的叶子数量不要超过20片。这两个约束有时候会打架需要权衡。2.2 茎重复法行数不够时的补救当数据范围太窄、茎的行数太少时标准解法是茎重复——把同一个茎拆成多行通常拆成两行或五行。拆两行的规则是第一行放叶子0到4第二行放叶子5到9。拆五行的规则是每行放两个叶子值0-1、2-3、4-5、6-7、8-9。还是用分数例子假设数据集中在70到79这一段特别密原始70,71,72,72,73,74,75,76,76,77,78,79如果只写一行7 | 0 1 2 2 3 4 5 6 6 7 8 9看不出内部结构。拆成两行7 | 0 1 2 2 3 4 7 | 5 6 6 7 8 9立刻能看出低半段和高半段各6个分布均匀。如果拆五行7 | 0 1 7 | 2 2 7 | 3 4 7 | 5 6 6 7 | 7 8 9粒度更细能看出72、76各有重复。拆几行取决于你想看多细——两行适合快速看偏态五行适合精细看聚集。我的习惯是先画单行版看整体如果发现某一行特别长超过15片叶子再拆成两行重画。这样既不浪费时间又不会漏掉结构信息。2.3 叶子位数与四舍五入的取舍当数据带小数时叶子的处理就变得微妙了。比如一组数据是3.1、3.2、3.4、3.7、4.0……你可以把整数部分做茎、十分位做叶得到3 | 1 2 4 7。但如果数据是3.14、3.27、3.41这种两位小数叶子就有两位画出来是3 | 14 27 41可读性还行但行会变宽。这里有个实操技巧叶子位数尽量控制在1到2位超过2位就考虑四舍五入或截断。比如3.14159这种截成3.1就够了因为茎叶图的目的是看分布不是做精确记录。当然截断会损失信息所以要在图旁边注明叶子已截断至十分位避免误导。还有一个坑四舍五入可能制造虚假的重复值。比如3.14和3.16都四舍五入成3.2茎叶图上就出现两个2看起来像重复测量其实是舍入造成的。如果重复值对分析结论有影响比如你在研究测量精度就不能随便舍入宁可保留两位叶子。这个细节教材上很少提但实际做数据质量分析时很容易踩。提示叶子位数一旦确定整张图必须统一。不能有的行一位、有的行两位否则茎的位权就乱了读者会误读数量级。3. 手工画一张规范茎叶图的完整流程3.1 从排序到落笔五步法虽然现在都用代码但手工画一遍能帮你真正理解茎叶图的构造逻辑而且面试或白板讨论时用得上。我把流程拆成五步照着做不会乱第一步确定数据范围和位数。扫一遍数据找出最小值和最大值判断用几位做茎。比如数据在45到98之间两位数十位做茎个位做叶茎的范围是4到9共6行合理。第二步列出所有可能的茎从大到小或从小到大排列。注意是所有可能出现的茎哪怕某个茎暂时没有数据也要先占位后面可以删。这一步是为了避免画到一半发现漏了行。第三步逐个数据落叶。拿一支笔从第一个数据开始找到它的茎把叶写到对应行。这里有个关键技巧每写一片叶子就在原始数据上划掉一个防止漏写或重复写。数据多的时候这一步最容易出错。第四步每行的叶子从小到大排序。落叶时是乱序的必须重新排。排序后重复值会挨在一起分布形态才看得清。这一步不能省否则茎叶图就退化成分组的原始数据失去有序这个核心优势。第五步加图例和说明。在图的角落写一行茎单位10叶单位1或者1|5表示15让读者知道怎么读。这个习惯很多人没有但它是规范的一部分尤其是叶子做过舍入或截断时必须注明。3.2 一个完整的手工案例复盘拿一组稍大的数据练手20个某型号零件的尺寸测量值单位毫米已减去基准值12, 15, 15, 18, 21, 23, 23, 24, 27, 29, 31, 32, 34, 36, 38, 41, 43, 45, 48, 52第一步范围12到52两位数十位做茎茎从1到5共5行。第二步列茎1、2、3、4、5。第三步落叶边落边划掉原数据。第四步排序得到1 | 2 5 5 8 2 | 1 3 3 4 7 9 3 | 1 2 4 6 8 4 | 1 3 5 8 5 | 2第五步加说明茎单位10叶单位11|2表示12。画完之后读图数据大致对称中间行2和3开头叶子最多两端稀疏15出现两次23出现两次最小值12、最大值5252是明显的孤立点只有一片叶子且离前一行跨度大。这个52就值得回头查一下——是真实测量还是录入错误这就是茎叶图暴露异常值的实战价值。3.3 背靠背茎叶图两组数据的对照画法实际工作中经常要比较两组数据比如实验组和对照组。这时候用背靠背茎叶图两组共享中间的茎叶子分别向左右两侧生长叶(组A) | 茎 | 叶(组B) | 1 | 2 5 5 8 1 3 3 | 2 | 1 4 7 9 1 2 4 | 3 | 6 8 1 | 4 | 3 5 8 | 5 | 2读法是左边是A组的叶子从茎向外读即3|1表示A组有31右边是B组的叶子。这样两组的分布形态、集中位置、离散程度一眼就能对比。注意左边的叶子要按从大到小排列靠近茎的是大值这样左右两侧的数值增长方向都是从茎向外符合直觉。背靠背画法的坑在于两组的茎范围必须统一。如果A组数据在10到50B组在20到80茎要取并集1到8没有数据的行留空。不能各画各的否则对比就失真了。另外两组样本量差很多时叶子密度不可直接比最好在图旁标注各自的样本量n。4. 用代码批量生成从十几行到一条命令4.1 Python手写实现理解每一步在干什么现成的库当然有但我建议先自己写一遍因为茎叶图的逻辑太简单了手写一遍比调库更能掌握细节。下面这段代码把分茎、落叶、排序、格式化四步拆得清清楚楚def stem_leaf(data, stem_unit10): # 处理负数整体平移避免整除取整出错 offset 0 if min(data) 0: offset -min(data) shifted [x offset for x in data] stems {} for x in shifted: stem int(x // stem_unit) leaf int(x % stem_unit) stems.setdefault(stem, []).append(leaf) lines [] for stem in sorted(stems.keys()): leaves sorted(stems[stem]) leaf_str .join(str(l) for l in leaves) lines.append(f{stem:3} | {leaf_str}) return \n.join(lines) data [12, 15, 15, 18, 21, 23, 23, 24, 27, 29, 31, 32, 34, 36, 38, 41, 43, 45, 48, 52] print(stem_leaf(data))输出就是前面手工画的那张图。几个细节值得说//是整除%是取余这两个运算把十位和个位干净地分开setdefault保证每个茎第一次出现时自动建列表sorted对叶排序是必须的。负数处理是个隐藏坑——Python的//对负数是向下取整-15 // 10等于-2而不是-1直接算会错位所以先整体平移到非负区间再算最后展示时再减回去上面代码为了简洁省略了减回步骤实际用的时候记得补上。4.2 处理小数和自定义茎单位真实数据经常带小数这时候stem_unit要相应调整。比如数据是3.1到5.8你想用整数部分做茎就把stem_unit设成1然后叶子取十分位def stem_leaf_decimal(data, stem_unit1, leaf_digits1): factor 10 ** leaf_digits stems {} for x in data: scaled round(x * factor) # 放大后取整 stem int(scaled // (stem_unit * factor)) leaf int(scaled % (stem_unit * factor)) stems.setdefault(stem, []).append(leaf) # 格式化时叶子补零保证位宽一致 lines [] for stem in sorted(stems.keys()): leaves sorted(stems[stem]) width len(str(int(stem_unit * factor) - 1)) leaf_str .join(str(l).zfill(width) for l in leaves) lines.append(f{stem:3} | {leaf_str}) return \n.join(lines)这里的关键是factor放大——把小数乘10的幂变成整数再拆避免浮点误差。zfill补零是为了让05和5看起来位宽一致否则读者会误以为05是两位数。这个补零细节很多现成实现都忽略了导致叶子列参差不齐读起来别扭。4.3 现成工具与它们的局限不想手写的话有几个现成选择。R语言的stem()函数是内置的一行stem(data)就出图还能用scale参数控制茎重复的行数scale越大行越多。Python这边stemgraphic库功能比较全支持背靠背和多种缩放。但现成工具有两个通病一是格式化不够灵活比如想加自定义图例、想控制叶子位宽往往要绕一大圈二是对异常输入的处理不透明比如数据里有缺失值或极端值时不同库的行为不一致容易出静默错误。我的建议是日常快速看数据用现成函数正式报告或需要定制格式时用自己写的函数。自己写的代码虽然多几行但每个行为都可控出了问题也好排查。尤其是做数据质量检查时你往往需要在图上标注这个值疑似异常现成工具很难做到自己写就随便加。5. 读图从茎叶图里能挖出哪些信息5.1 分布形态的三种典型模式茎叶图最直接的用途是看分布形状。常见的三种模式读法各不相同对称型——叶子从中间向两端均匀减少像一座小山。比如前面分数例子中间行叶子多、两端少大致对称。这种分布通常意味着数据来自稳定的过程没有系统性偏差。右偏型正偏——低值端叶子密集高值端拖出一条长尾。比如收入数据大部分人集中在低段少数高收入者把尾巴拉长。茎叶图上表现为下面几行叶子挤满、上面几行稀稀拉拉。右偏数据做均值分析要小心均值会被尾部拉高中位数更能代表典型值。双峰型——中间某一行叶子特别少形成凹陷两侧各有一个峰。这往往暗示数据里混了两个不同的总体比如把两个班级的分数混在一起或者把两种工艺的产品尺寸混在一起。双峰是茎叶图最容易暴露、而直方图容易掩盖的特征因为直方图的区间划分可能刚好把凹陷抹平。看到双峰第一反应应该是数据能不能拆开看。5.2 异常值与孤立叶子的识别茎叶图识别异常值的方法很朴素看有没有哪片叶子离大部队特别远。比如前面零件尺寸例子里52孤零零地待在最后一行前面一行是4开头中间隔了一整个数量级的空白这就是典型的可疑点。但要注意区分统计上的异常和业务上的异常。52这个值从统计上看是离群点但从业务上看可能只是某个特殊规格的零件完全合理。所以茎叶图给出的是值得看一眼的线索不是必须删除的判决。我的习惯是凡是孤立叶子都回头查原始记录确认是真实值还是录入错误再决定保留还是剔除。这个动作花不了几分钟但能避免很多后续分析的错误。还有一种隐蔽的异常某一行叶子数量突然暴增。比如正常每行5到8片突然有一行20片这可能是测量仪器在某个值附近有分辨率限制比如只能读到整数导致大量值堆在同一个叶子上也可能是数据录入时的默认值填充。这种堆积在直方图上看起来只是某根柱子特别高容易被当成正常波动但在茎叶图上因为能看到具体叶子反而更容易起疑。5.3 用茎叶图辅助判断该用什么统计量茎叶图还能帮你决定后续用什么统计方法。看几个信号如果分布对称、无明显异常值均值和标准差是合理的概括如果明显偏斜中位数和四分位距更稳妥如果双峰任何单一概括统计量都是误导必须先分组如果叶子在某一行大量重复比如同一个值出现十几次说明数据分辨率不足或有截断这时候算出来的方差会偏小要谨慎解读。这个看图选统计量的习惯比死记什么分布用什么检验实用得多。因为真实数据很少完美符合某种理论分布而茎叶图让你直接看到数据的脾气再决定怎么对待它。我见过太多人拿到数据直接跑t检验结果数据是双峰的检验结论毫无意义——如果先画个茎叶图这个问题一眼就能发现。6. 那些教材不会告诉你的实操坑6.1 负数和小数的双重陷阱负数做茎叶图最容易在茎的取整上翻车。前面提过Python整除对负数是向下取整-15 // 10 -2但直觉上-15的十位应该是-1。这个差异会导致茎的编号整体偏移画出来的图行序错乱。解决办法有两个一是整体平移到非负区间前面代码用的二是用int(x / stem_unit)配合math.floor手动处理。我倾向第一种因为平移后所有运算都是非负的不容易出岔子展示时再减回去即可。小数则是位宽不一致的坑。3.1的叶子是13.15的叶子是15如果混在一起3 | 1 15看起来像1和15两个数其实前者是一位叶子、后者是两位。所以同一张图里叶子位数必须统一要么全部截成一位要么全部保留两位。这个规则听起来简单但实际写代码时如果忘了zfill补零很容易画出位宽混乱的图。6.2 茎重复后行序与读法的混乱茎重复拆成两行或五行之后同一个茎出现多次读图时容易搞混哪一行在前。规范做法是同一个茎的多行按叶子值从小到大从上往下排。比如拆两行时0-4那行在上5-9那行在下。这样从上往下读数值是递增的符合直觉。但有些实现会反过来排或者不标注哪行是低半段导致读者误判。所以自己画图时最好在重复的茎旁边加个小标记比如7.和7*或者干脆在图例里说明同一茎的多行自上而下为低半段到高半段。这个细节虽小但直接影响图的可读性。6.3 样本量过大时的降级策略前面说过样本量超过几百就不适合茎叶图但现实中你往往拿到数据就想先画一个看看。这时候的降级策略是先抽样再画图。从大样本里随机抽100到200条画茎叶图看形态确认没有异常后再上直方图看全量。抽样看形态这个做法比直接对全量数据画一张糊成一团的茎叶图有用得多。另一个降级思路是提高茎单位。比如数据是1000到9999按百位做茎只有90行还是太多按千位做茎只有9行但叶子变成三位数行会很宽。折中方案是按百位做茎、叶子取十位和个位两位行数控制在合理范围。总之茎单位、叶子位数、行数这三个参数要联动调整没有一劳永逸的配置。6.4 别把茎叶图当精确数据表用最后一个坑是心态上的茎叶图虽然保留了原始值但它终究是图不是表。有人拿茎叶图去核对具体某个数据是否存在结果因为叶子排序或舍入对不上号白折腾。正确用法是茎叶图用来发现模式和异常精确核对回到原始数据表。它是侦察兵不是档案员。想清楚这个定位用起来就不会拧巴。我自己在实际操作中的体会是茎叶图最大的价值不在于画出来多好看而在于画的过程强迫你把数据逐个过一遍。这个逐个过的动作往往比图本身更能发现问题——你会注意到某个值反复出现、某个区间突然断档、某个数字看起来就不对劲。这种手感和数据接触的体验是任何自动化工具都替代不了的。所以哪怕现在代码一键出图我遇到关键数据集时还是会手动落一遍叶就当是跟数据打个照面。