资讯详情

用pandas绘制频率分布直方图:groupby、agg、round实战详解

📅 2026/10/18 18:09:44 | 华诺云谱 👁 阅读
用pandas绘制频率分布直方图:groupby、agg、round实战详解
从“定量分析”这四个字切入很多人第一反应是建模、回归、假设检验但真正落到日常数据处理时最常用、最朴素、也最能说明问题的手段反而是一个频率分布直方图。它一眼就能告诉你数据长什么样集中在哪个区间、有没有长尾、是不是双峰、有没有离谱的离群值。配合pandas的groupby、agg、round这三个函数这个看似基础的图表能玩出很多花样甚至能替代一部分描述性统计报表。我最近在处理某连锁零售门店的订单数据时正好用这套组合拳完成了一次完整的定量分析从数据清洗到分组区间划分再到直方图绘制和结果解读整个过程踩了几个坑也积累了一些可以“抄作业”的经验。这篇文章就围绕“频率分布直方图”这个主题把groupby、agg、round的具体用法掰开揉碎讲清楚适合正在学pandas、或者工作中经常要做数据分布分析的同学参考。1. 频率分布直方图的核心思路把连续数据离散化直方图跟条形图不一样它面对的是连续型变量。比如订单金额、用户年龄、响应时间、产品价格这些都是连续数据直接画散点图看不出规律直接算均值又掩盖了分布形态。直方图的做法是把数据的取值范围切分成若干连续的区间也叫“箱”或“组”然后统计每个区间内数据的个数或占比用矩形的高度或面积来展示分布密度。1.1 为什么“分箱”是直方图的关键步骤直方图的形状完全取决于怎么分箱。箱太宽细节被抹平可能只看到一堆柱子挤在一起箱太窄噪声被放大每个箱里只有零星几个数据看不出整体趋势。一个成熟的分析师在画直方图之前先想清楚组距和组数怎么定而不是直接丢给绘图库去自动处理。组数没有绝对标准但常用经验公式是Sturges规则组数 ceil(log2(n) 1)其中n是样本量。比如有1000条订单记录log2(1000)约等于9.97加1取整就是11组。这个公式不是银弹对于明显偏态分布的数据适当增加组数有助于暴露长尾细节。另一个常用做法是看数据的业务含义比如订单金额以50元为一个区间或者年龄以10岁为一个区间业务上更好解释。1.2 频率与频数直方图里真正要展示的指标频数就是落在每个区间里的数据条数它的好处是直观但坏处是样本总量不同时无法横向比较。比如A门店有5000笔订单B门店只有800笔两边直方图用频数画A门店的柱子永远更高看不出分布形态的差异。这个时候应该用频率或者密度。频率 频数 / 总样本数。更严谨一点直方图的纵轴可以用“频率密度”即频率除以组距这样不同组距的直方图之间也具备可比性。在pandas里value_counts(normalizeTrue)配合cut函数可以快速得到频率分布在seaborn里stat参数可以切换count、frequency、density和probability。实操中我一般会同时输出一个频率分布表和一个直方图表用来精确查数图用来看形状。2. 用groupby实现分组统计从整体分布到细分洞察单个直方图只能描述整体的分布情况但业务分析几乎不会只满足于一个整体结论。你更想知道的是不同商品类别的订单金额分布有没有差异工作日和周末的客单价分布长什么样新老用户的消费频次分布是否双峰这些问题都需要用groupby先把数据拆开再对拆开的每个子集做分布分析。2.1 groupby的基本语法与可控字段的选取groupby的核心是“拆分-应用-合并”三步。拆分就是按指定的列把DataFrame分成若干组应用就是对这些组逐一执行某种操作合并则是把各组的结果拼成一个新DataFrame。语法上只需要一行df.groupby(category)后面再接聚合操作如.mean()、.sum()、.count()等。选可控字段是有讲究的。在做频率分布分析时groupby的字段最好满足两个条件一是取值数量不能太多比如省份、品类、部门这类几十个以内还能看懂二是必须是分类意义上的“组标签”不能是连续型数值。假设你要按“是否会员”分组直接对是否会员这一列做groupby得到两组订单金额的分布如果要按“订单金额区间”分组则需要先用cut分箱再对分箱结果做groupby。2.2 分组后的聚合结果如何配合直方图groupby本身不会改变数据结构它只是把数据按类别切开了。切完之后怎么用最常见的方式是分别提取每个组的某一列再绘制各自的直方图。拿我那个零售门店的项目来说数据里有category商品类别、amount订单金额、customer_id客户ID等字段。我先按category分组然后循环取出每一类的amount列分别调用hist绘制子图。这一步如果用matplotlib自己写代码大概是import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(orders.csv) categories df[category].unique() fig, axes plt.subplots(nrows1, ncolslen(categories), figsize(14, 4)) for ax, cat in zip(axes, categories): subset df[df[category] cat][amount] subset.hist(bins15, axax, edgecolorwhite) ax.set_title(f{cat} 的订单金额分布) ax.set_xlabel(订单金额) ax.set_ylabel(频数) plt.tight_layout() plt.show()这个做法虽然简单但不是groupby的正式写法。更pandas风格的写法是先用groupby切分再对每个分组应用describe或者agg拿到各组的描述性统计量然后再决定要不要可视化。两者结合用groupby拿到各组统计量用直方图把统计量背后的分布画出来这样定量与可视化就打通了。2.3 分组后比较分布重叠直方图与分面图多组分布对比最常见的是两种图重叠直方图和分面直方图。重叠直方图把所有组的直方图画在同一坐标系里靠透明度区分重叠区域适合组数少不超过4~5组的时候。分面直方图让每个组拥有独立的子图各有各的坐标轴适合组数多的时候。重叠直方图的注意点是要设置alpha透明度否则后画的组会完全遮住先画的组。另外如果各组数据量差异很大一定要用频率或密度而不是频数否则样本量小的组因为柱子矮在视觉上会被压扁。用seaborn的histplot加上hue参数可以一句话完成分组直方图import seaborn as sns sns.histplot(datadf, xamount, huecategory, statdensity, alpha0.5, elementstep)elementstep只画轮廓不填充内部重叠时更清爽。实测下来组数超过4个时重叠图会变成一团乱麻这时分面图远优于重叠图。3. agg聚合函数的实际应用一次算完全部关键指标groupby单独用只能做一种聚合操作。但定量分析很少只关心一个指标你可能同时想知道每个分组的均值、中位数、标准差、最大值、最小值、样本量。写多行代码分别计算当然可以但pandas的agg函数就是为此设计的把多个聚合操作一次性传给分组对象效率高代码也整洁。3.1 agg支持三种传参形式字符串、函数、字典agg最基础的用法是传字符串比如df.groupby(category)[amount].agg(mean)。但它的真正威力在于同时传多个聚合函数df.groupby(category)[amount].agg([count, mean, std, min, max])这一行就输出了一个表格每一行是一个商品类别每一列是一个统计指标。如果多个列的统计口径不同可以用字典指定比如订单金额算均值和标准差客户年龄算中位数df.groupby(category).agg({ amount: [mean, std, count], customer_age: median })不管哪种写法最终返回的是一个DataFrame列名会自动带上函数名比如amount_mean、amount_std方便后续透视或者绘图。3.2 自定义聚合函数与agg的底层逻辑内置函数不够用时agg也支持传入自定义函数比如同时计算四分位距def iqr(s): return s.quantile(0.75) - s.quantile(0.25) df.groupby(category)[amount].agg([median, iqr])自定义函数的入参是一个Series返回一个标量。这里的坑在于agg会把每个分组内的Series传给自定义函数所以函数里如果依赖索引或者全局状态很容易出错。建议自定义函数保持纯粹只基于传入的Series做计算。另一个容易忽略的点是agg并不仅限于groupby之后使用。你也可以对一整个DataFrame调用df.agg([mean, std])得到每一列的描述性统计摘要。这对于数据探索阶段快速掌握所有字段的分布范围非常有用。我一般会在分析开头跑一遍df.describe()再把describe结果喂给agg做进一步定制比如加上偏度skew和峰度kurtosis这些指标直接关系到直方图形态的判断。3.3 分箱配合agg生成分组的频率分布表agg和cut结合是构造频率分布表最直接的方法。先用pd.cut把连续列分成区间再groupby区间并聚合频数bins [0, 50, 100, 200, 500, 1000] labels [0-50, 50-100, 100-200, 200-500, 500-1000] df[amount_bin] pd.cut(df[amount], binsbins, labelslabels, rightFalse) freq_table (df.groupby(amount_bin, observedFalse) .agg(total_orders(amount, count), total_revenue(amount, sum), avg_amount(amount, mean)) .reset_index())这段代码里groupby之后用agg加命名聚合让三列分别命名为total_orders、total_revenue、avg_amount结果是一个清晰的区间汇总表。注意groupby默认会忽略NaN分组如果cut产生的区间里没有数据该区间也会自动消失。如果想保留所有区间需要加上dropnaFalse参数。4. round函数在定量分析中的精度管理定量分析中round看似人畜无害实则暗藏玄机。数据一旦涉及浮点数显示、存储、计算三个不同层面的“小数位数”是完全不同的概念round函数用不好直方图的组距、频率表的百分比甚至最后的分析结论都可能出错。4.1 显示位数、存储精度与计算精度的区别pandas默认显示浮点数可能带很多位小数但显示位数不等于存储精度。举个例子一个订单金额是49.999999用round(2)得到50.0在分组区间“50-100”里会被分到50以上但不做round时它其实是49.999属于0-50区间。这就是“边界迁移”问题的来源。我建议遵循一个原则业务最终要展示的指标比如客单价、占比用round(2)规整但参与分箱、分组、聚合运算的原始字段尽量保留完整精度。换句话说round管“出口”不管“内部流通”。先让数据以高精度参与所有计算最后呈现结果时再统一round。4.2 银行家舍入Python的round和想象中的四舍五入不一样很多人在Python里遇到round的“意外”后才第一次知道银行家舍入bankers rounding。round(2.5)返回2而不是3round(3.5)返回4而不是4。它遵循的是当小数位后是5时向偶数方向舍入而不是总是向上进位。这种舍入在统计上的好处是偏差更小但对于习惯了“四舍五入”的业务人员来说这是一个经常引发混乱的点。如果在数据分析中明确需要“四舍五入”的规则建议直接用Decimal模块或者自己定义一个函数import math from decimal import Decimal, ROUND_HALF_UP def round_half_up(x, n0): return float(Decimal(str(x)).quantize(Decimal(0. 0*n), roundingROUND_HALF_UP))这个函数先转字符串再转Decimal绕开了浮点数的二进制表示误差。实测对负数和边界情况的处理都符合直觉。4.3 round在生成区间标签和纵轴坐标中的应用直方图里有两处地方常用round一处是cut的区间边界标签另一处是绘图时纵轴坐标的刻度标签。边界标签如果是一堆长小数图表看的人会晕最好在构造bins时就直接用业务上清晰的整数比如0、50、100、200、500而不是0.000001、49.999999这类浮点边界。纵向刻度用round也比较常见尤其是用matplotlib绘制频数标签时默认刻度可能产生细碎数字。可以用matplotlib.ticker中的FuncFormatter统一格式化或者直接设置轴的major_formatter为{x:.0f}这样的格式。可视化中的精度一致性能明显提升图表的可信度。5. 完整实操从原始数据到频率分布直方图的流水线前面把三个函数的原理讲清楚了这一节演示一条完整的数据分析流水线。模拟场景某公司有10000条线上订单记录字段包括用户ID、订单金额、商品类别、下单时间。目标分析整体订单金额的分布形态并比较不同商品类别的分布差异。5.1 数据清洗与异常值处理拿到数据第一件事不是画图而是摸底。用df.info()查看列类型和缺失值用df.describe()看订单金额的均值和分位数。如果最小值是负数说明有退款记录如果最大值是999999可能是错误值或测试数据。这些异常值如果不处理直方图会出现一个巨大的空档大部分数据被压在最左侧看不见。我的处理逻辑是对于订单金额小于等于0的记录要么剔除要么单独标记为退款组不混入正常订单分布对于超过某个业务合理上限比如10000元的极端值先看它们是不是真实大额订单如果确认是真实业务就保留但在绘制直方图时增加一个标注。盲目剔除极端值会丢失长尾信息这是很多新手容易犯的错。5.2 分箱逻辑与groupbyagg的组合流程针对订单金额结合业务和Sturges规则我选择组距为50元用pd.cut分箱bins pd.cut(df[amount], binsrange(0, 1001, 50), rightFalse) df[amount_bin] bins这里range(0, 1001, 50)生成了0到1000之间的区间最后一组是950-1000。rightFalse表示区间左闭右开[0,50)这样0元记录不被忽略50元的记录归入下一组。区间分组后再用groupbyagg汇总组内订单量和总营收result (df.groupby(amount_bin, observedFalse) .agg(order_count(amount, count), total_amount(amount, sum), avg_amount(amount, mean)) .reset_index()) result[percentage] (result[order_count] / len(df) * 100).round(2) result[amount_bin] result[amount_bin].astype(str)percentage列就是每个区间的频率百分比保留两位小数。这个表格既能直接输出到Excel也能作为直方图的数据源。5.3 绘制频率分布直方图与累计频率曲线有了分布表绘图就很简单。用matplotlib画柱状图width设置为组距50x轴用区间标签y轴用百分比。加一条累计频率折线用右侧的y轴显示累计百分比这是业务分析中非常常见的组合图能同时看到每个区间的占比和累计到某个金额段的覆盖度。fig, ax1 plt.subplots(figsize(12, 5)) ax1.bar(result[amount_bin], result[percentage], color#6A9FB5, edgecolorwhite, width0.8) ax1.set_xlabel(订单金额区间元) ax1.set_ylabel(订单占比%) cumsum result[percentage].cumsum() ax2 ax1.twinx() ax2.plot(range(len(result)), cumsum, color#D97742, markero, markersize4, linewidth2) ax2.set_ylabel(累计占比%) plt.xticks(rotation45) plt.tight_layout() plt.show()这张图画出来整体形态一目了然订单金额呈现右偏分布峰值落在50-100元区间说明门店的主营客单价集中在中低位累计曲线在300元左右达到80%说明绝大部分订单不超过300元。这个结论直接支撑后续的商品定价和满减活动设计。5.4 分组对比图不同品类的分布叠加整体分布分析完之后我又按品类分组做了对比。用seaborn的histplot并设置grouping参数将三个品类的订单金额分布以密度曲线的形式画在同一张图里。水果类呈现明显的单峰右偏数码配件类出现双峰一个峰在0-100元另一个峰在300-500元这说明该品类的产品线分档明显两批不同价位的商品各自聚类。对比的结果给了我一个意外的洞察整体订单金额的右偏主要是水果类贡献的数码配件类实际上呈现双峰结构如果只做全量直方图这个双峰特征会被隐藏。这就是分组分析不可替代的价值所在。6. 常见问题与实战避坑记录数据分析做多了就会发现坑位基本固定。下面按出现频率排序整理一份问题排查清单。6.1 边界划分不一致同样的数据两张图的柱子不一样用plt.hist时默认的边界处理是右闭左开跟pd.cut的rightFalse正好相反。这意味着同一个数据点在pd.cut和plt.hist里可能进入不同的区间。解决方法是让两者完全一致要么cut时用rightTrue要么直方图传入range参数并保证bins的端点重合。更稳妥的做法是直方图直接用之前groupby生成的分箱结果而不是让matplotlib重新分箱。另一个边界坑是区间标签的显示。pd.cut默认生成的标签是(49.999, 50.0]这样的浮点表达式直接画到x轴上又长又乱。我一般会自定义labels或者在最后把区间转成字符串再处理。6.2 groupby结果中的NaN组与空区间三个坑第一groupby默认忽略NaN用groupby(..., dropnaFalse)可保留NaN分组第二用pd.cut分箱时如果某个区间没有数据groupby结果里不会出现该区间解决方法是reindex补齐所有区间第三在groupby多个列时结果的索引是MultiIndex需要reset_index()脱索引再合并否则后续绘图取列容易取错。6.3 频率分布直方图的纵轴选择频数、频率还是密度很多人画直方图不区分这三种导致组距不同的图之间无法对比。如果只是展示单一数据集的分布形态频数没问题但如果要横向比较多个分组或多次实验必须统一用频率或密度。seaborn的histplot中statdensity会把柱子的面积总和归一化到1statprobability把柱子高度总和归一化到1。两者的区别在于组距是否相等组距相等时两者等价组距不相等时density更科学因为柱子的面积才代表概率质量。实操建议只要bins是等差数列用probability如果用了非等距分箱比如0-50、50-100、100-300必须用density或直接把每组的频数除以组距再绘图。6.4 性能问题大数据量下groupby和cut的加速技巧当数据量达到千万级时pd.cutgroupby的组合会变得很慢。比较实用的优化手段有三个一是用category类型代替object类型存储分箱结果减少内存占用二是用pd.cut的retbins参数获取实际的区间边界避免重复分箱三是优先用numpy的searchsorted手动分箱速度快一个数量级。import numpy as np bins_arr np.arange(0, 1001, 50) data df[amount].values bin_idx np.searchsorted(bins_arr, data, sideright) - 1 df[amount_bin] bin_idx这个方法返回的是整数索引而不是区间标签速度非常快后续要转回标签时再映射即可。6.5 透视表交叉验证分布表与groupby结果相互校验最后再补充一个经验。拿到一份频率分布表别急着画图发表结论先做一次交叉验证。用pivot_table算出每个区间的原始订单量跟groupby结果对比一遍check pd.pivot_table(df, indexamount_bin, valuesamount, aggfunccount)两边得到的结果必须完全一致。我曾经在项目中发现cut的边界写错导致同一个数据落进两个区间就是靠pivot_table对比抓出来的。定量分析的核心是数字可复现一个逻辑链上任何环节的精度损失都会传导到最终结论谨慎一点不会错。写在最后我的几点实操体会代码和数据都是工具但工具用得顺不顺完全取决于对细节的掌控。groupby、agg、round这三个函数单独看都不难但组合在一起做频率分布分析时细节就全暴露出来了cut的边界开闭、agg传参的三种形式、round在不同层面的语义差异每一项都足以改变你的分析结论。我从这个项目里学到最重要的一件事是直方图不是画完就结束它只是定量分析的起点。真正的工作在于如何解释图上的形状——为什么右偏、为什么双峰、为什么某一区间出现断崖这些都需要回到原始数据和业务逻辑中去寻找答案。多掌握几个可以交叉验证的分析工具比如频率分布表、累计曲线、透视表你的结论就多一分可信度。最后分享一个小技巧每次做分布分析我都坚持从原始数据出发逐步派生新列分箱、分组、聚合而不是直接对中途结果做二次加工。这意味着任何人都能从原始CSV复现整条分析流水线结果可核查逻辑可追溯。这比任何花哨的建模都重要。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑