做数据分析的,Pandas 里的 describe() 和 groupby() 真的是两个绕不开的函数。这两天正好在帮某电商公司整理一份销售明细,我顺手把这两个函数的分析思路、参数细节和踩过的坑完整过了一遍,整理成这篇实操记录。新手可以直接照着敲,有经验的人也可以翻一翻,里面有不少平时容易忽略的细节。
这套东西解决的痛点很明确:拿到一张几十万行的表,第一件事不是直接去做可视化,而是先用 describe() 快速掌握整体数据分布,再用 groupby() 把数据按业务维度拆开对比,找出值得深挖的规律。整个流程不复杂,但每一步都有讲究。如果你接下来要做探索性数据分析(EDA)、写月度报表,或者准备面试中的数据分析场景题,这篇文章应该能给你省下不少试错时间。
1. 为什么这两个函数是分析标配:整体设计思路
先聊点方法论。很多人拿到数据就开始跑模型,这是最忌讳的。正确的打开方式是先“描述”再“分组”,而这两个函数正好对应了这两步。
1.1 一纵一横的分析解法
我的理解里,describe() 做的是“纵向体检”,groupby() 做的是“横向切分”。
什么叫纵向体检?就是不看业务维度,只看每个数值列本身的统计特征。比如销售额这一列,它有多少条有效记录、平均值是多少、波动大不大、最小值有没有异常、中位数和平均数差距大不大。这些信息像是体检报告上的基础指标,能帮你快速判断数据质量和整体形态。
横向切分就更好理解了。同样是销售额,华东区域和华南区域可能差别很大,数码类产品和服饰类产品的利润结构完全不同。这时候用 groupby() 按区域、按类别把数据切开,每一块单独算均值、求和、计数,就能看到业务差异。
一纵一横两个动作做完,数据的基本画像就出来了。整套思路可以用一句大白话概括:先看整体是什么水平,再拆开看差异在哪里。
1.2 什么时候该用哪个,别搞反
这里分享一个我自己的判断标准。如果你的分析目的是“了解数据有没有问题”,比如有没有缺失值、有没有极端值、字段类型是否合理,那优先用 describe()。如果目的是“对比不同群体的表现”,比如哪个区域卖得好、哪个品类的利润率更高,那就直接用 groupby()。
两个函数也可以串联使用,这是我强烈推荐的习惯。先 describe() 发现“销售额均值远大于中位数”,说明数据右偏严重,可能存在少量超高订单;再用 groupby(['区域'])['销售额'].mean() 定位究竟是哪个区域拉高了整体水平。先宏观后微观,排查效率非常高。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的准备工作:环境与一份能跑的数据
在进入函数细节前,先把环境准备好。这部分不啰嗦,但很重要,很多新手一上来就报错,多半是环境问题。
2.1 环境安装与版本说明
Pandas 是 Python 数据分析的核心库,安装方式很简单。如果你用的是 Anaconda,它已经内置了 Pandas,不需要额外装。如果是纯净的 Python 环境,打开终端执行下面这行命令就行:
bash复制pip install pandas
版本方面,建议使用 Pandas 1.5 以上版本,因为我下面要讲的很多细节在旧版本上表现不一致。比如 describe() 的 include 参数和 groupby() 的 sort 参数,老版本的行为跟新版本有差异。如果你用的是 Pandas 2.x,那体验会更好。
2.2 手工构造一份销售明细数据
为了演示,我直接构造了一份模拟的销售数据,字段包括订单编号、销售区域、产品类别、销售数量、销售额、成本和销售日期。用 Pandas 的随机数生成,保证例子可复现。
python复制import numpy as np
import pandas as pd
np.random.seed(42)
n = 300
df = pd.DataFrame({
'订单编号': [f'DK{2024000 + i}' for i in range(n)],
'销售区域': np.random.choice(['华东', '华南', '华北', '西南', '东北'], size=n),
'产品类别': np.random.choice(['数码', '服饰', '美妆', '食品', '家居'], size=n),
'销售数量': np.random.randint(1, 20, size=n),
'销售额': np.round(np.random.uniform(50, 2000, size=n), 2),
'成本': np.round(np.random.uniform(20, 800, size=n), 2)
})
df['销售日期'] = pd.date_range('2024-01-01', periods=n, freq='D')
这里我特意让“销售额”和“成本”之间没有强关联,因为实际业务中成本不仅和销售额线性相关,还会受品类、促销等因素影响。这份数据加载后,先用 df.head() 看看前几行,再用 df.info() 检查字段类型和缺失值情况。
python复制print(df.head())
print(df.info())
这一步的意义是确认数据列名、类型都符合预期,避免后面调用 describe() 时发现“销售日期”这种对象列也参与了统计,造成干扰。
3. describe() 的完整打开方式:不只是“看一眼”
很多人对 describe() 的理解停留在“输出一行统计数字”,实际上它输出的信息量非常大。读懂每个数字背后代表的业务含义,比会调用函数更重要。
3.1 默认输出中的每一行都代表什么
直接跑一下:
python复制df.describe()
输出结果里会包含 count、mean、std、min、25%、50%、75%、max 这八个指标。逐个拆开看:
- count:该列有多少个非空值。如果 count 少于总行数,说明存在缺失值,这是最早能发现的数据质量问题。
- mean:平均值。它容易受极端值影响,所以只看 mean 不够,还要结合中位数。
- std:标准差,衡量波动程度。标准差很大,说明数据点分布很分散,业务上可能意味着销售不稳定。
- min 和 max:最小值和最大值。如果 min 是 0 或者负数,或者 max 明显超出合理范围,基本可以断定存在异常值。
- 25%、50%、75%:三个分位数。50% 就是中位数,把数据从小到大排列后处在正中间的值。
我特别想强调 mean 和 50%(中位数)的对比。如果 mean 远大于 50%,说明数据右偏,少数几个大额订单把平均值拉高了。比如我的模拟数据里,销售额均值可能是 1000,中位数只有 900,这时候你在做销售目标制定时,用均值做参考会过于乐观,应该更关注中位数。
3.2 控制描述范围的参数细节
describe() 默认只描述数值列,但业务数据里经常有文本型的关键字段,比如产品类别、销售区域。这时候可以用 include 和 exclude 参数来控制范围。
python复制# 只描述所有列(包括文本列)
df.describe(include='all')
# 只描述对象类型列(文本列)
df.describe(include=['object'])
# 排除数值列,只看文本列的分布
df.describe(exclude=[np.number])
这里有一个非常容易踩的坑:include 参数必须传列表,直接传字符串虽然在某些情况下也能跑,但语义容易搞混。include='all' 是特例,它会把所有列都纳入描述,并且对文本列输出 unique、top、freq 这三个额外指标。
unique 表示有几种不同的取值,top 是出现次数最多的值,freq 是 top 出现的次数。比如对产品类别做 include=['object'] 的描述,你能一眼看出哪个类别卖得最多。
3.3 实际业务怎么看这些结果
我用这份销售数据做个演示。只看销售额和成本这两列:
python复制df[['销售额', '成本']].describe(percentiles=[0.1, 0.9])
percentiles 参数可以自定义分位数,默认是 [0.25, 0.5, 0.75],我改成 10% 和 90% 分位数,可以看到更极端的分布情况。如果 10% 分位数对应的销售额只有 50,而 90% 分位数有 1800,说明大部分订单集中在低价区间,高价值订单很少但贡献很高。
结合业务场景,这种分布意味着你需要在“提高客单价”和“扩大订单量”之间做个取舍。如果低价订单占大多数,那促销策略可能比高端路线更有效。
4. groupby() 从分组到聚合:拆开看、合起来算
groupby() 是 Pandas 里最强大的工具之一,它的核心思想说起来简单,但用好了能解决绝大部分分组统计需求。
4.1 groupby 的分组逻辑:split-apply-combine
用一句话解释 groupby() 的工作机制:先把数据按某个字段拆成多个小组,然后对每个小组应用同样的函数,最后把结果合并起来展示。这个机制在 Pandas 官方文档里叫 split-apply-combine,我觉得用食堂打菜的类比最好理解。
把数据想象成一堆食材,groupby(['产品类别']) 就是按荤素把食材分成几个篮子,apply 是对每个篮子分别做烹饪(求和、算平均),combine 是最后把每道菜摆到同一个餐台上供你挑选。
代码如下:
python复制df.groupby('产品类别')['销售额'].sum()
这条代码的执行逻辑是:按产品类别分组,然后针对每一组的销售额列求和。结果是每个类别的销售总额,非常直观。
4.2 最常用的聚合方式:sum、mean、count、agg
实际工作中,单纯一个 sum 往往不够,经常要同时看销售额总和、平均订单金额和订单数量。这时候我用 agg() 方法:
python复制df.groupby('产品类别')['销售额'].agg(['sum', 'mean', 'count', 'median'])
输出的每一列分别是总和、平均值、订单数量、中位数。一次性拿到最常用的几个统计量,效率很高。
不过这里有个细节值得注意:直接用字符串列表作为 agg 的参数,输出的列名就是 'sum'、'mean' 这种,时间长了自己都分不清哪个是哪个。更规范的做法是给聚合结果重命名,用元组:
python复制df.groupby('产品类别')['销售额'].agg(
销售总额='sum',
平均客单价='mean',
订单笔数='count',
销售中位数='median'
)
这样输出的列名直接就是业务语义,后续做可视化或者导报表的时候,不需要再改列名。
4.3 多种数据,多种聚合方式:字典参数一次搞定
如果我想对不同列用不同的聚合方式,比如销售额算总和,成本算平均,销售数量算最大,字典参数是最优雅的解法:
python复制df.groupby('产品类别').agg({
'销售额': 'sum',
'成本': 'mean',
'销售数量': 'max'
})
这种写法非常实用。比如分析各品类经营情况时,我可以同时看“总销售额”“平均成本”“最大单笔销售数量”。注意字典的键必须和列名完全一致,否则会报错。
4.4 多列分组与复合索引的处理
分组不一定只有一个维度,按“区域”和“产品类别”同时分组非常常见:
python复制result = df.groupby(['销售区域', '产品类别'])['销售额'].sum()
这个操作返回的是一个带有复合索引(MultiIndex)的 Series。直接用 print 看没问题,但如果我想把结果转成 DataFrame 或者继续操作,通常会执行 reset_index():
python复制result_df = result.reset_index()
reset_index() 的作用是把分组键从索引里拿出来,变成普通列。很多人第一次用 groupby 后想再筛选结果,发现 result_df['销售区域'] 会报 KeyError,就是因为忘了做这一步。
还有一个小技巧:多列分组后,如果想做“区域为行、类别为列”的透视表,可以用 unstack():
python复制pivot_data = result_df.pivot_table(index='销售区域', columns='产品类别', values='销售额', aggfunc='sum')
对,直接用 pivot_table 更省事。Pandas 里的 groupby().unstack() 也能实现类似效果,但 pivot_table 的语法更直观。
5. 实战:从一份销售明细里挖出结论
前面说了那么多理论,这一节我用实际数据完整跑一遍分析流程,从全局体检到分组对比,最后得出业务结论。你会发现报告其实并不复杂,就是两个函数的组合拳。
5.1 第一步:对整体数据做体检
先用 describe() 看整体:
python复制overview = df[['销售额', '成本', '销售数量']].describe()
print(overview)
我拿模拟数据跑出来的结果,重点看销售额这一列。count 是 300,说明没有缺失。mean 约 1000,50% 分位数约 950,两者差距不算极端,但能感觉到高额订单对均值的拉动作用。max 是 2000,min 只有 50,跨度很大,说明订单金额分布较广。
接着算一个毛利率的分布:
python复制df['毛利额'] = df['销售额'] - df['成本']
df['毛利率'] = df['毛利额'] / df['销售额']
print(df['毛利率'].describe())
这里注意一个细节:如果在原 DataFrame 上新增列,后面所有分组分析都能直接用这个新列,非常方便。毛利率可能存在极端值,要看 describe 输出的 min 和 max,如果出现负数,说明有亏本订单,这本身就是业务问题。
5.2 第二步:按区域和类别做分组对比
先看各区域的销售表现:
python复制region_summary = df.groupby('销售区域')['销售额'].agg(
销售总额='sum',
平均单笔='mean',
订单数='count'
).reset_index()
print(region_summary)
再按产品类别看利润情况:
python复制category_summary = df.groupby('产品类别').agg(
销售总额=('销售额', 'sum'),
平均毛利率=('毛利率', 'mean'),
订单数=('销售额', 'count')
).reset_index()
print(category_summary)
语法说明一下:agg 里接收的可以是元组,形式是 (列名, 聚合函数),这样不用提前对每一列算好,直接一次性得到结果,代码也更紧凑。
跑完后我发现一个有意思的规律:某个产品类别订单数最多,但平均毛利率偏低;另一个类别订单数少,毛利率却很高。这就是典型的“走量”和“走利”的区别。
5.3 第三步:把数据结论落地成行动建议
到这一步,分析已经从“做统计”变成“做业务”。假设华东区域销售额最高,但毛利率低于公司平均水平;西南区域销售额不高,但毛利率一骑绝尘。那我的建议可能是:华东区域需要优化促销折扣政策,控制成本;西南区域则需要加大流量投入,把高毛利优势放大。
这个推导过程在代码上非常简单,就是对比上面两个分组结果。但它的价值在于:你通过 describe 和 groupby 的组合,把“数据异常”转化成了“行动方向”。
5.4 一份可以直接复制的完整分析脚本
把上面所有代码整合成一段,方便你在自己数据上替换使用:
python复制import pandas as pd
import numpy as np
# 读取或构造数据
# df = pd.read_excel('销售数据.xlsx')
# 新增衍生字段
df['毛利额'] = df['销售额'] - df['成本']
df['毛利率'] = df['毛利额'] / df['销售额']
# 整体体检
print(df[['销售额', '成本', '销售数量', '毛利率']].describe())
# 区域维度分析
region = df.groupby('销售区域').agg(
销售总额=('销售额', 'sum'),
平均单笔=('销售额', 'mean'),
毛利率=('毛利率', 'mean'),
订单数=('销售额', 'count')
).reset_index().sort_values('销售总额', ascending=False)
# 类别维度分析
category = df.groupby('产品类别').agg(
销售总额=('销售额', 'sum'),
平均单笔=('销售额', 'mean'),
毛利率=('毛利率', 'mean'),
订单数=('销售额', 'count')
).reset_index().sort_values('毛利率', ascending=False)
print(region)
print(category)
sort_values 我放在最后,为了让结果按金额或毛利率排序,阅读体验更好。这一步在业务汇报时很重要,因为决策者永远想先看排在前面的重点。
6. 常见问题与排查思路实录
实操中总会遇到各种报错和“感觉哪里不对”的情况。这一节我整理了一张速查表,外加我自己踩过的几个坑。
6.1 常见异常速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| describe() 输出里没有文本列 | 默认只描述数值列 | 加 include='all' 或 include=['object'] |
| groupby 后无法用列名筛选 | 分组键还在索引里 | 调用 reset_index() |
| agg 多个聚合后列名不直观 | 默认用函数名 | 使用 (旧列名, 函数) 的元组写法 |
| 分组结果里出现 NaN | 分组键中有空值 | 先 dropna() 或用 dropna=False 保留 |
| 透视结果结构复杂 | 复合索引不方便查看 | 用 pivot_table 代替 groupby().unstack() |
| 某些类别不显示 | 默认只显示分类中出现过的值 | 用 pd.Categorical 指定所有类别 |
6.2 几个我踩过的坑
第一个坑是 describe(include='all') 用错位置。我想看文本列分布,直接写 df.describe('all'),结果没有任何变化。原因是位置参数被当作 percentiles 处理了,必须写成 include='all' 才能生效。这种小细节报错不会很明显,但结果就是不对,排查半天才发现。
第二个坑是 groupby 后忘记 reset_index,导致后续用 merge 关联其他表时找不到分组键列。有一次做多表关联,结果一直报 KeyError,我还以为是字段名打错了。其实原因就是 groupby 默认会把分组键放到索引里。解决方案很简单:groupby 完立刻 reset_index,养成习惯。
第三个坑是 agg 结果列名太抽象。我最早用 .agg(['sum', 'mean']),跑出来的表只有 sum 和 mean 两个列名。过两天回来看数据,压根想不起来哪个列对应哪个字段。后来改用元组命名法,清晰多了。强烈建议从第一次用 agg 就规范命名。
第四个坑是数据里有分类值但某个类别没有订单。groupby 默认只显示有数据的类别,这在业务上可能会造成误解。解决方案是用 pd.Categorical 手动指定所有类型,这样没订单的类别也会显示为 0,而不是直接消失。
我个人的实操体会
用这两个函数这几年,我最深的体会是:工具本身不难,难的是把分析思路理清楚。如果你拿到数据直接就开始写 groupby,很容易陷入“到处试”的误区。先跑一下 describe,知道数据的底线在哪里,再决定从哪里切分,整个过程会顺畅得多。
另外分享一个小习惯:每次分析我都会把 describe 和 groupby 的结果输出到 CSV 文件保存下来,作为数据快照。理由很简单,分析结束后如果发现结论有误,回头检索数据时有一个可追溯的中间状态,不用重新跑一遍代码。
最后想说的是,Pandas 的函数很多,但最常用的就那么几个。把 describe 和 groupby 玩熟练,配合 agg、reset_index、sort_values,你已经能独立完成绝大部分探索性数据分析了。剩下的,就是在实际业务数据里多磨、多踩坑、多沉淀。
