news 2026/10/18 18:09:32

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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(normalize=True)配合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(nrows=1, ncols=len(categories), figsize=(14, 4)) for ax, cat in zip(axes, categories): subset = df[df['category'] == cat]['amount'] subset.hist(bins=15, ax=ax, edgecolor='white') 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(data=df, x='amount', hue='category', stat='density', alpha=0.5, element='step')

element='step'只画轮廓不填充内部,重叠时更清爽。实测下来,组数超过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'], bins=bins, labels=labels, right=False) freq_table = (df.groupby('amount_bin', observed=False) .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产生的区间里没有数据,该区间也会自动消失。如果想保留所有区间,需要加上dropna=False参数。

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的“意外”后,才第一次知道银行家舍入(banker's rounding)。round(2.5)返回2而不是3,round(3.5)返回4而不是4。它遵循的是:当小数位后是5时,向偶数方向舍入,而不是总是向上进位。这种舍入在统计上的好处是偏差更小,但对于习惯了“四舍五入”的业务人员来说,这是一个经常引发混乱的点。

如果在数据分析中明确需要“四舍五入”的规则,建议直接用Decimal模块,或者自己定义一个函数:

import math from decimal import Decimal, ROUND_HALF_UP def round_half_up(x, n=0): return float(Decimal(str(x)).quantize(Decimal('0.' + '0'*n), rounding=ROUND_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 分箱逻辑与groupby+agg的组合流程

针对订单金额,结合业务和Sturges规则,我选择组距为50元,用pd.cut分箱:

bins = pd.cut(df['amount'], bins=range(0, 1001, 50), right=False) df['amount_bin'] = bins

这里range(0, 1001, 50)生成了0到1000之间的区间,最后一组是950-1000。right=False表示区间左闭右开([0,50)),这样0元记录不被忽略,50元的记录归入下一组。区间分组后,再用groupby+agg汇总组内订单量和总营收:

result = (df.groupby('amount_bin', observed=False) .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设置为组距(50),x轴用区间标签,y轴用百分比。加一条累计频率折线,用右侧的y轴显示累计百分比,这是业务分析中非常常见的组合图,能同时看到每个区间的占比和累计到某个金额段的覆盖度。

fig, ax1 = plt.subplots(figsize=(12, 5)) ax1.bar(result['amount_bin'], result['percentage'], color='#6A9FB5', edgecolor='white', width=0.8) ax1.set_xlabel('订单金额区间(元)') ax1.set_ylabel('订单占比(%)') cumsum = result['percentage'].cumsum() ax2 = ax1.twinx() ax2.plot(range(len(result)), cumsum, color='#D97742', marker='o', markersize=4, linewidth=2) ax2.set_ylabel('累计占比(%)') plt.xticks(rotation=45) 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的right=False正好相反。这意味着同一个数据点,在pd.cut和plt.hist里可能进入不同的区间。解决方法是让两者完全一致:要么cut时用right=True,要么直方图传入range参数并保证bins的端点重合。更稳妥的做法是,直方图直接用之前groupby生成的分箱结果,而不是让matplotlib重新分箱。

另一个边界坑是区间标签的显示。pd.cut默认生成的标签是(49.999, 50.0]这样的浮点表达式,直接画到x轴上又长又乱。我一般会自定义labels,或者在最后把区间转成字符串再处理。

6.2 groupby结果中的NaN组与空区间

三个坑:第一,groupby默认忽略NaN,用groupby(..., dropna=False)可保留NaN分组;第二,用pd.cut分箱时,如果某个区间没有数据,groupby结果里不会出现该区间,解决方法是reindex补齐所有区间;第三,在groupby多个列时,结果的索引是MultiIndex,需要reset_index()脱索引再合并,否则后续绘图取列容易取错。

6.3 频率分布直方图的纵轴选择:频数、频率还是密度

很多人画直方图不区分这三种,导致组距不同的图之间无法对比。如果只是展示单一数据集的分布形态,频数没问题;但如果要横向比较多个分组或多次实验,必须统一用频率或密度。seaborn的histplot中stat='density'会把柱子的面积总和归一化到1,stat='probability'把柱子高度总和归一化到1。两者的区别在于组距是否相等:组距相等时两者等价,组距不相等时density更科学,因为柱子的面积才代表概率质量。

实操建议:只要bins是等差数列,用probability;如果用了非等距分箱(比如0-50、50-100、100-300),必须用density或直接把每组的频数除以组距再绘图。

6.4 性能问题:大数据量下groupby和cut的加速技巧

当数据量达到千万级时,pd.cut+groupby的组合会变得很慢。比较实用的优化手段有三个:一是用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, side='right') - 1 df['amount_bin'] = bin_idx

这个方法返回的是整数索引而不是区间标签,速度非常快,后续要转回标签时再映射即可。

6.5 透视表交叉验证:分布表与groupby结果相互校验

最后再补充一个经验。拿到一份频率分布表,别急着画图发表结论,先做一次交叉验证。用pivot_table算出每个区间的原始订单量,跟groupby结果对比一遍:

check = pd.pivot_table(df, index='amount_bin', values='amount', aggfunc='count')

两边得到的结果必须完全一致。我曾经在项目中发现cut的边界写错导致同一个数据落进两个区间,就是靠pivot_table对比抓出来的。定量分析的核心是数字可复现,一个逻辑链上任何环节的精度损失都会传导到最终结论,谨慎一点不会错。

写在最后:我的几点实操体会

代码和数据都是工具,但工具用得顺不顺,完全取决于对细节的掌控。groupby、agg、round这三个函数单独看都不难,但组合在一起做频率分布分析时,细节就全暴露出来了:cut的边界开闭、agg传参的三种形式、round在不同层面的语义差异,每一项都足以改变你的分析结论。

我从这个项目里学到最重要的一件事是:直方图不是画完就结束,它只是定量分析的起点。真正的工作在于如何解释图上的形状——为什么右偏、为什么双峰、为什么某一区间出现断崖,这些都需要回到原始数据和业务逻辑中去寻找答案。多掌握几个可以交叉验证的分析工具,比如频率分布表、累计曲线、透视表,你的结论就多一分可信度。

最后分享一个小技巧:每次做分布分析,我都坚持从原始数据出发,逐步派生新列(分箱、分组、聚合),而不是直接对中途结果做二次加工。这意味着任何人都能从原始CSV复现整条分析流水线,结果可核查,逻辑可追溯。这比任何花哨的建模都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 18:07:37

生鲜电商源码解析:Spring Boot单体架构下单全链路

简介:这是一套面向生鲜电商场景的完整项目源码,采用主流框架构建,可在本地环境直接运行,主要服务于有一定编程基础的开发者,用于学习、二次开发或毕业设计。压缩包内共包含四百三十四个文件,整体大小约为十…

作者头像 李华
网站建设 2026/10/18 18:05:09

《初始化教条》终端渲染全解析:从TrueColor到ANSI动画的诗意代码

我是在某个深夜的终端模拟器里读完《初始化教条》完整渲染的。清屏指令落下那一刻,缓冲区的字符像退潮一样撤走,光标回到左上角原点,紧接着一行行诗随着渐变色的光晕浮现出来——不是静态文本,是带着呼吸节奏的逐字渲染。整个过程…

作者头像 李华
网站建设 2026/10/18 18:04:14

2022美赛代码及论文压缩包复现指南:从环境配置到灵敏度分析

简介:这份资源是2022年美国大学生数学建模竞赛(MCM/ICM)的完整参赛资料,面向准备参加美赛的高校学生及建模爱好者,尤其适合需要参考真实获奖论文与代码实现的中高级选手。包内共81个文件,以csv数据表、png图…

作者头像 李华
网站建设 2026/10/18 18:03:43

R语言提取GEO数据全流程:从getGEO下载到探针注释与差异分析

简介:GEO数据库是全球规模最大的公共基因表达数据仓库,R语言GEOquery包是获取这些数据的标准工具。然而,直接调用getGEO常因网络超时、平台注释差异或探针ID映射问题而失败。理解GEO数据的GSE、GDS与GPL结构,掌握超时设置与缓存机…

作者头像 李华