matplotlib多数据集直方图绘制:参数选型与叠加模式全解析
发布时间:2026/10/2 16:48:56来源:尧图网络
今年年初做版本性能复盘的时候产品同学拿着两份响应时间报表问我“这两个版本到底哪个更稳定”我当时的反应是别问我问直方图去。几千行的原始数据用描述性统计输出平均数和P95固然能看到一些信息但是当你想比较两组甚至更多组连续数据的分布形态——谁更集中、谁有长尾、谁冒出双峰——直方图hist几乎是最直接的工具。这篇文章把我这些年用 matplotlib 的 hist 函数绘制多个数据集直方图的方法整理了一遍包括参数选型、三种叠加模式的实现与取舍以及几个我在真实项目中踩过的坑。适合正在做数据分析、机器学习特征分布检查或者纯粹想用 Python 把图画明白的朋友。我会从最简单的用法讲起再逐步把并列式、堆叠式、分面、KDE 叠加这些进阶操作拆开说清楚每一步都给出对应代码和选择理由。1. 多组数据对比时直方图到底在帮你回答什么问题直方图本质上回答的是一个分布问题一组连续数值落在不同区间的次数或比例是怎样的。而当你把多组数据放进同一张图时它要回答的问题就变成了“这几组数据从形态上到底像不像”。这个“像不像”不是看平均数而是看整体形状。平均数一样的两批数据可能一个是尖峰集中一个是平缓分散直方图一眼就能看出差别用统计指标反而不容易讲明白。1.1 适合用直方图对比的四类典型场景我自己实际工作中最常遇到的多数据集直方图场景大致有四类大家可以对照着看看自己属于哪一种。第一类是 A/B 实验。对照组和实验组的某个指标点击时长、转化金额、响应时间各有一大堆数值需要判断实验组是不是真的把分布“推”向了更好的方向。只看均值容易遗漏尾部风险比如均值涨了但长尾也变长了这种变化直方图非常灵敏。第二类是分组特征分布。比如按会员等级看消费额度、按城市级别看订单金额、按渠道看用户活跃时长。这类场景要关心的是不同组的分布重心和离散程度是否一致。第三类是改造前后的对比。系统优化前后的耗时分布、流程调整前后的数值分布直方图能直观看出“峰有没有左移”“尾部有没有变矮”。第四类是模型诊断中的分布偏移。训练集和线上预测集的某个特征分布是否一致可以用多数据集直方图快速做初筛后续再算 PSI 之类的指标。画图不是目的回答业务问题才是。所以你在动手之前先想清楚你关注的是中心位置、离散程度、偏态还是多峰形态。这个决定了后面参数怎么配。1.2 先分清直方图和柱状图否则后面全是糊涂账这个区分如果你已经非常清楚可以先跳过但根据我带人的经验至少一半初学者会把两者混着用导致数据类别被塞进 hist 之后画出完全没法看的图。柱状图的横轴是类别条与条之间有间隔高度代表类别对应的数值直方图的横轴是连续数值切分出的区间条与条靠在一起高度代表落在区间内的样本个数。柱状图是“离散对离散”直方图是“连续对连续”。如果硬把类别数据比如“葡萄、苹果、香蕉”传给 plt.hist会发生什么matplotlib 会把这些字符串映射成某个整数刻度再切区间出来的图不仅没有业务含义还会因为字符串顺序问题产生一堆奇怪的浮点区间。所以第一原则是看到 hist先确认数据是连续数值型。另外还有一个深层差异直方图的矩形宽度是有意义的它代表区间跨度。区间越宽单个条内样本越多区间越窄条越多、形状越碎。所以 bins 的选取本质上是“分辨率设置”这个后面专门讲。1.3 多数据集直方图的三个硬骨头把多组数据放进同一张图里比各画各的难在三件事上。第一是 bin 对齐。如果每组数据各自用默认 bins区间边界会不一样两个直方图叠在一个坐标系里条的位置根本对不齐视觉上就像两张不同的图硬拼在一起。第二是视觉区分度。多组数据叠加重合区域大颜色、透明度、边框样式如果配不好重叠部分会变成一团黑。第三是样本量不均衡的归一化。一组数据 2000 条、另一组 200 条直接比频数没有意义大样本组会把小样本组的形态完全压扁。这三个问题后面都会给到对应的解决方案。先记住结论多数据集直方图不是“多写一行 plt.hist”那么简单。2. 吃透 hist 参数bins、density、alpha 怎么配才科学2.1 bins 到底该填多少三种方式与选择逻辑bins 是直方图最重要的参数没有之一。它的直观含义是“横轴切成多少段”也可以是“切成哪些段”。有三种常见填法。第一种填整数。比如bins30表示把数据范围均匀切成 30 个区间。对大多数场景30 是一个不错的起点数据量几千条时能看到比较平滑的形态也不能算过密。如果你只填plt.hist(data)matplotlib 会用自己的默认整数但不同版本的默认值不一样复现性差。第二种填序列。比如bins[0, 20, 40, 60, 80, 100]表示用自定义边界切分。这种方式的优势在于你可以完全控制区间边界多数据集对比时特别有用因为你可以给所有数据集用同一个边界列表保证 bin 完全对齐。第三种填auto或fd这类自动算法。binsauto会基于数据量和范围自动选择一个较优的条数适合快速探索。但它不是万能膏药如果你后续要和其他数据集做严格对比自动算法会给每组算出不同的 bins反而制造问题。还有一个经验公式也值得收藏Sturges 规则适用于数据量不太大的场景bins ceil(log2(n) 1)n 是样本量。比如 1000 条数据ceil(log2(1000)1)约等于 11 个条。这个规则给出的条数偏少图会比较平滑适合初步看形态。区间宽度的计算很简单区间宽度 (最大值 - 最小值) / bins 数量。区间宽了单条内样本多高度高区间窄了条多可能有锯齿。多组数据对比时我的建议是先看所有组数据的全局范围用np.linspace(min_all, max_all, bins1)生成统一的边界序列然后所有数据集都用这个边界。import numpy as np all_min min(d.min() for d in datasets) all_max max(d.max() for d in datasets) shared_bins np.linspace(all_min, all_max, 31) # 30个区间31个边界这样画出来的多组直方图每条柱的位置严格一一对应比各画各的可靠得多。2.2 density 与 weights样本量不均衡时的正确归一化再强调一次频数直方图的纵轴是“落入区间的样本个数”。当几组数据的样本量差别很大时直接比较频数是有误导性的。2000 条的那组随便一个区间都有几百条200 条的那组最高柱可能也就几十条小样本组会从视觉上“消失”。解决办法是densityTrue。加上这个参数后matplotlib 会把纵轴换算成密度并且保证所有矩形面积之和为 1。这时候直方图就不再受样本量影响所有组都在同一个尺度上做形态比较。注意它的纵轴并不是概率单根柱的高度乘以区间宽度才是该区间的概率估计。用 density 有一个副作用如果你的多组样本量相差很大但你想同时表达“绝对规模”和“形态”密度图会把规模信息丢掉了。这种情况下可以考虑画两个图一个用频数看规模一个用密度看形态不要试图在一张图里同时表达两个维度的信息。再说 weights。weights 参数给每个样本加一个权重常用于样本加权场景。它和 density 同时使用时行为会比较微妙先加权再归一化出来的是加权密度。我的经验是普通的数据分析场景尽量别把 weights 和 density 一起用容易让读者包括你自己对纵轴的物理意义产生困惑。如果只是需要按某个字段加权单独用 weights纵轴保持为加权频数语义更清晰。2.3 alpha 与 histtype多层叠加时间区分度的关键多数据集直方图和单数据集最大的不同在于重叠区域多颜色区分必须靠 alpha 和填充样式共同完成。alpha 控制透明度0 是完全透明1 是完全不透明。两层叠加时我推荐 0.5 左右既能看清新来的数据集又不会完全盖住底层可以辨认出两组数据的轮廓。但如果叠加三层甚至四层半透明叠加区会越来越暗最后变成一坨深色阴影颜色信息失灵。对付多层叠加光调 alpha 是不够的更可靠的手段是换 histtype。hist 的类型有bar默认实心填充条、step只画轮廓线不填充、stepfilled填充但不描边。多组数据超过两层时我通常会至少让其中一组用histtypestep画轮廓线这样即使填充区叠成深色轮廓线依然清晰可辨每条数据集的边界都能被单独追踪。ax.hist(data_a, binsshared_bins, alpha0.5, histtypestepfilled, label版本A) ax.hist(data_b, binsshared_bins, alpha0.8, histtypestep, lw2, label版本B)颜色选择也需要动点脑子。不要直接啃默认配色里的前两个颜色它们可能区分度不够。实用的做法是先选一组高区分度调色板比如[#4C72B0, #DD8452, #55A868, #C44E52]再配合 alpha/step 组合使用。区分度比美观重要颜色首先要让人分得清其次才是好不好看。参数作用多数据集推荐设置bins控制区间数量或边界多组时用统一的 np.linspace 边界density面积归一化消除样本量差异样本量不均时设为 Trueweights样本加权慎用不与 density 同时用alpha透明度两层约 0.5三层以上配 outlinehisttype填充/轮廓样式多层叠加时用 step 保底edgecolor柱边缘颜色设深色可提升区分度3. 三种叠加形态实现从堆叠到并列的完整代码与选型多数据集直方图的呈现形态不只是“叠在一起”一种。根据你想强调的信息有三种主流形态透明叠加、并列式分组、堆叠式。下面分别给出代码和适用场景。3.1 透明叠加overlay一行代码上手但要防“视觉欺骗”透明叠加是最简单也最常见的做法直接把多组数据统一传给 plt.hist用一个列表包起来即可。import matplotlib.pyplot as plt import numpy as np np.random.seed(42) data_a np.random.normal(loc80, scale15, size500) data_b np.random.normal(loc75, scale12, size600) fig, ax plt.subplots(figsize(10, 6)) shared_bins np.linspace( min(data_a.min(), data_b.min()), max(data_a.max(), data_b.max()), 31 ) ax.hist( [data_a, data_b], binsshared_bins, alpha0.5, color[#4C72B0, #DD8452], label[版本A, 版本B] ) ax.legend(locbest, frameonFalse) plt.show()传入列表时hist 会自动把每组数据单独统计颜色列表一一对应。注意此时的返回值n的维度变成了(组数, bins数)这是一个容易踩的数据结构问题后面讲。透明叠加最适合对比两组数据的整体形态差异谁更左、谁更胖、谁有第二个峰。但它对重叠区域有天然的视觉局限——重叠部分颜色加深人眼判断的“重叠面积”会受透明度数学影响这点放到坑位部分详细讲。3.2 并列式分组直方图手动偏移 bins 的规范写法如果你希望每一项的数据都能被精确读到不依赖“透过重叠区猜测”那就用并列式。遗憾的是 matplotlib 没有内置参数直接把多组直方图变成并列模式需要自己用np.histogram算频数再用bar绘制。这个过程不难但很多人第一次会卡在偏移量的计算上。labels [版本A, 版本B] colors [#4C72B0, #DD8452] datasets [data_a, data_b] all_min min(d.min() for d in datasets) all_max max(d.max() for d in datasets) bins np.linspace(all_min, all_max, 21) # 20个区间 n_groups len(datasets) bin_width bins[1] - bins[0] bar_width bin_width * 0.8 / n_groups fig, ax plt.subplots(figsize(10, 6)) for i, (data, label, color) in enumerate(zip(datasets, labels, colors)): counts, edges np.histogram(data, binsbins) # edges 比 bins 多一个值取每个区间的左端点再加偏移量 lefts edges[:-1] i * bar_width ax.bar(lefts, counts, widthbar_width, labellabel, colorcolor, alpha0.9) ax.legend(locbest, frameonFalse) plt.show()关键点有两个。第一所有组共享同一个bins边界保证每个区间起点一致否则并列会错位。第二偏移量计算同组内所有柱都往右平移i * bar_widthbar_width等于 bin 宽度的八成再除以组数留出组内间隙。这样两组数据在同一个区间内并行排列毫不重叠每条柱的高度都能被精确读取。并列式的缺点是当组数大于 4 时每根柱会变得很窄视觉上密密麻麻组数少2-4 组时这是最推荐的一种尤其是你要在报告里给非专业人士看图的场景任何人一眼就能看出对比关系。3.3 堆叠式直方图适合看构成不适合看差异堆叠式只需一个参数stackedTrue。ax.hist([data_a, data_b, data_c], binsshared_bins, stackedTrue, color[#4C72B0, #DD8452, #55A868], label[渠道A, 渠道B, 渠道C])堆叠的语义是“累计构成”最底层是第一组第二组垒在第一组上方第三组继续往上垒。因此每个区间内的总高度是所有组样本数之和各组在这个区间的占比通过色带厚度体现。这个形态特别适合看“总量构成随横轴的变化”比如不同时段的流量构成、不同年龄段的收入构成。但要注意堆叠式的致命缺点是后垒上去的组被前面的组遮住难以单独比较它们的形态——比如你想看渠道B和渠道C的分布差异在堆叠图里只能靠色带厚度变化去猜。所以记住一条选型经验看构成用堆叠看差异用透明叠加或并列。另外stacked 模式对负值要特别小心。如果数据里存在负数条形会往下叠加group 之间的视觉关系会瞬间混乱有负数时建议不要用堆叠。三种形态的选择其实是一个问题你想让读者关注什么。关注整体构成选堆叠关注逐组合个区间读数选并列关注分布形态的相对关系选透明叠加。没有哪一种是绝对最优关键是匹配问题。场景推荐形态原因两组分布形态对比透明叠加轮廓关系一目了然2-4组精确读数并列式每根柱可单独读取3组以上形态快速筛查透明叠加 step避免重叠区过暗构成占比分析堆叠式直观展示各组贡献组数超过5组分面网格叠加/并列都太拥挤4. 数据多到一张图放不下时分面、KDE 与箱线辅助如果数据组数到了 5 组以上再好的调色板和透明度设计也救不了那张叠加图。这时候的正确操作不是继续加颜色而是换布局思路。4.1 分面网格用 subplots 让每张图各司其职分面不是把多组数据放进同一坐标系而是给每组一个小图再把小图排成网格对齐展示。matplotlib 的subplots配合sharexTrue, shareyTrue是标准做法。共享坐标轴是重点只有共享了横纵轴刻度读者才能在扫视中完成跨图比较否则每张小图的坐标范围不一致等于变相欺骗。fig, axes plt.subplots(1, 4, figsize(16, 4), sharexTrue, shareyTrue) for ax, data, name in zip(axes, datasets, names): ax.hist(data, binsshared_bins, color#4C72B0, alpha0.8) ax.set_title(name) ax.set_xlabel(响应时间 (ms)) axes[0].set_ylabel(频数) fig.suptitle(不同渠道响应时间分布对比, fontsize14) plt.tight_layout() plt.show()当组数在 4-8 之间时分面明显优于叠加。每组数据有自己的完整空间重叠区域的问题消失且分布形态不会被其他组的颜色干扰。如果组数超过 8分面也会变得零碎这时我建议放弃“一张图展示所有”改用热力图之类的手段或者回到表格输出统计摘要。图表的功能是辅助判断不是给读者制造视觉迷宫。4.2 KDE 曲线与箱线图叠加直方图的黄金搭档直方图受 bins 影响形态有阶梯感。想要更平滑的对比可以在直方图之上叠加核密度估计KDE曲线。matplotlib 本身不直接提供 KDE但 scipy.stats.gaussian_kde 很简单。from scipy.stats import gaussian_kde fig, ax plt.subplots(figsize(10, 6)) for data, color, label in zip(datasets, colors, labels): counts, edges np.histogram(data, binsshared_bins, densityTrue) ax.bar(edges[:-1], counts, widthnp.diff(edges), colorcolor, alpha0.3, labellabel) kde gaussian_kde(data) xs np.linspace(data.min(), data.max(), 200) ax.plot(xs, kde(xs), colorcolor, lw2, labelf{label} KDE) ax.legend(locbest, frameonFalse) plt.show()注意这里直方图使用了densityTrue目的是让直方图的高度和 KDE 曲线处于同一个“概率密度”尺度否则 KDE 曲线纵轴是密度直方图纵轴是频数两者根本没法叠加。KDE 的带宽默认是自动选择的大多数时候效果还行如果数据本身是大致正态的KDE 曲线会非常接近正态曲线。直方图负责还原“每一段里的实际分布”KDE 负责给出“连续平滑的形状估计”两者配合使用是我做特征分布对比时的标准动作。另一个可以叠加的是箱线图。箱线图放在直方图上方或下方用同一个横轴能补充分位数信息比如中位数、四分位距、离群点。一个实用做法是主坐标轴画直方图用次坐标轴或手动把箱线图横置在图的顶部。这样可以在一张图里同时读到“分布形态”和“分位摘要”很适合汇报场景。fig, ax plt.subplots(figsize(10, 6)) # 直方图画在下方 ax.hist(data_a, binsshared_bins, densityTrue, alpha0.4, label版本A) # 箱线图横置在绘图区顶部 bp ax.boxplot([data_a, data_b], vertFalse, positions[1.15, 1.08], widths0.02, patch_artistTrue, manage_ticksFalse)当然位置参数需要根据你的直方图纵轴范围调整核心思路是用一组小尺寸箱线放在图的上方空档处既不干扰直方图主视觉又能提供分位数信息。4.3 中文显示与横坐标拥挤国内用户最容易卡住的两个点在中文环境下画图字体会让不少人翻车。默认字体不包含中文字符一旦图里有中文标签会显示成方框。解决方式是在绘图前设置 rcParamsimport matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False第一行是中文字体候选列表不同系统能用的字体不一样Windows 常用 SimHei 或 Microsoft YaHeimacOS 用 PingFang SC。第二行必须设置否则坐标轴上的负号会显示成方框因为 Unicode 减号在处理中文字体时经常对不上。另一个高频问题是横坐标标签太密集。当你的直方图横轴是日期字符串、长数值或者 tick 自动生成了一堆小数并排显示时图会变成一团浆糊。常见的处理手段是旋转标签、限制刻度数量、关闭科学计数偏移plt.xticks(rotation45) ax.ticklabel_format(styleplain, axisx) from matplotlib.ticker import MaxNLocator ax.xaxis.set_major_locator(MaxNLocator(nbins8))rotation45适合字符型标签MaxNLocator(nbins8)强制横轴至多显示 8 个刻度治“太密集”最有效ticklabel_format防止大数变成1e4这种看不懂的显示。5. 复盘多数据集直方图高频坑与排查思路这部分是我最想写的因为每一个坑都是真金白银踩出来的。下面的问题都曾经在项目里让我画出过“自我感觉良好但结论错误”的图现在整理成清单希望你能直接绕过去。5.1 各组 bins 范围不一致图看着挺好看但结论是错的我见过太多人把两组数据直接plt.hist(data1)、plt.hist(data2)叠加出来的图乍一看没什么问题但仔细看横轴刻度两组柱子的 bin 宽度根本不一样。为什么因为 hist 默认会自动根据每组数据的范围单独计算 bins。数据 A 范围 0-100自动分了 20 个区间数据 B 范围 30-80自动分了 10 个区间。两组叠在同一坐标系里A 的柱子细密B 的柱子粗疏视觉上 A 的峰更高更尖B 更矮更平但这个对比完全失真很可能只是 bins 数量不同造成的假象。排查思路很简单画完图之后故意把两组直方图分别导出检查它们的柱宽是否一致或者直接打印bins数组看边界。修复方法就是第 2 节说的统一shared_bins所有数据集用同一组边界。我现在已经把这步固化成模板了只要是多数据集直方图第一件事就是算全局最小值和最大值生成统一 bins。5.2 alpha 叠加的数学陷阱透明叠加图里有一个反直觉现象两层透明度 0.5 的柱子在重叠区域的视觉深度并不是“0.5 0.5 1”的不透明而是 1 - (1-0.5) × (1-0.5) 0.75 的“视觉遮罩深度”。如果是三层 0.5重叠区视觉深度达到 1 - 0.5³ 0.875几乎全黑。这就是为什么三层以上叠加时重叠区的人眼判断会严重偏向“深色区域很多”而真实的重叠范围可能并没有那么大。排查思路如果你看到重叠区呈现出“深不见底”的颜色第一反应不是“重叠太多”而是检查层数和 alpha 算一下有效遮罩深度。这时候最省事的修正不是把 alpha 调得更低调低会让非重叠区也看不清楚而是切换第二组到histtypestep用轮廓线代替填充或者像我在 2.3 里写的那样分组差异化处理。# 三层叠加推荐写法填充轮廓混合 ax.hist(data1, binsshared_bins, alpha0.5, histtypestepfilled, colorc1) ax.hist(data2, binsshared_bins, alpha0.6, histtypestepfilled, colorc2) ax.hist(data3, binsshared_bins, alpha0.9, histtypestep, lw2, colorc3)5.3 用 counts 还是用比例先想清楚再执行样本量不均衡时直接画频数是性价比最高的“错误示范”。假设一组数据 2000 条、另一组 200 条counts 图上大样本组随便一个区间都是小样本组的好几倍小样本组只能贴地前进形态完全不可见。如果你忘记densityTrue你甚至会觉得“版本B明显更差”错误结论就是这样产生的。排查方法先看一眼每组样本量如果最低和最高相差超过 2 倍直接默认使用densityTrue对比形态。如果还想兼顾规模差异我建议画两幅图一幅 counts 用分面展示绝对规模一幅 density 用叠加展示相对形态。另外要提醒一句densityTrue 后的纵轴不是“百分比”而是“概率密度”单根柱面积才是概率读者如果不懂这点解读时容易懵。5.4 善用 plt.hist 返回值一个被忽视的高级技巧很多人只把plt.hist()当“画图语句”忽略了它有返回值n, bins, patches。这三个值的组合可以做很多高级操作。n 是每组频数数组维度取决于传入了几组数据bins 是边界数组patches 是每个柱子的图形对象列表可以逐个修改它们的视觉属性。用这个返回值可以对直方图做二次标注。比如我想标出频数最高的区间并把柱子变色fig, ax plt.subplots(figsize(10, 6)) n, bins, patches ax.hist(data_a, binsshared_bins, color#4C72B0, alpha0.7) threshold np.percentile(n, 90) for patch, count in zip(patches, n): if count threshold: patch.set_facecolor(#C44E52) ax.set_title(版本A 响应时间分布红色为Top10%区间) plt.show()也可以遍历 patches 在柱顶加数值标签。这个技巧尤其适合做汇报材料因为它让图表不仅可看还能直接抽取“哪个区间集中了最多样本”这类信息省掉再去想 numpy 里自己统计的过程。5.5 一个值得固化的绘图习惯以上这些方法组合起来我现在处理多数据集直方图已经形成了一套固定套路先统计所有组的全局范围生成统一 bins如果样本量差异明显就直接 densityTrue两层用填充alpha超过两层把其中一组换成 step 轮廓做正式汇报前多问自己一句——用这张图的人能不能在三秒内说出各组差异是什么。如果能图就是合格的。如果你的数据源是 DataFrame替换成实际列名即可比如df[response_time].to_numpy()。matplotlib 的 hist 和 numpy 的接口都很稳定这套代码只要你把数据组织成一个列表跑起来几乎没有版本上的意外。希望这篇能帮你在下次需要对比多组连续分布时少走几次弯路。
网站建设高端定制企业官网