利用Pandas进行数据分组与统计分析的高级方法

一、为什么要做数据分组

大部分时候,我们手头的数据都是杂乱无章的。比如一个超市的销售记录,里面有日期、商品类别、价格、销量等等。如果直接看原始表格,很难发现什么规律。但要是按商品类别把数据分成几组,再算算每组的总销量或者平均价格,那就一目了然了。这就是分组分析的价值——它能帮我们从整体中找出不同群体之间的差异和共性。

Pandas 里的分组操作非常灵活,它不光是简单的“分组+求和”,还能做很多高级变换。比如你想知道每个销售员每个月的业绩排名,或者想挑出那些销售额连续三个月都在下降的客户,这些都可以用分组方法实现。这篇文章会一步步带你走完这些技巧,让你以后处理数据时不再只会写 groupby().sum()。

二、分组操作的基础:groupby

2.1 groupby 的基本用法

先造一份模拟数据,假设你有一家小公司的员工信息,包括部门、姓名和月薪。

技术栈:Python + Pandas

import pandas as pd

# 构造示例数据

data = {

'部门': ['技术', '技术', '市场', '市场', '技术', '市场'],

'姓名': ['张三', '李四', '王五', '赵六', '陈七', '周八'],

'月薪': [15000, 18000, 12000, 11000, 20000, 13000]

}

df = pd.DataFrame(data)

print("原始数据:")

print(df)

输出:

部门 姓名 月薪

0 技术 张三 15000

1 技术 李四 18000

2 市场 王五 12000

3 市场 赵六 11000

4 技术 陈七 20000

5 市场 周八 13000

现在我们来按“部门”分组,并计算每个部门的平均月薪:

# 按部门分组,取月薪列求均值

result = df.groupby('部门')['月薪'].mean()

print("各部门平均月薪:")

print(result)

输出:

部门

市场 12000.0

技术 17666.7

Name: 月薪, dtype: float64

groupby 返回的是一个 GroupBy 对象,它不会直接给你一个表格,而是等待你后面加上聚合函数(比如 mean、sum、count)。这种设计的好处是你可以链式调用,非常灵活。

2.2 多级分组

有时候我们需要按多个维度分组。比如上面的员工数据,再增加一个“子公司”字段,想看每个子公司每个部门的平均薪酬。

# 增加子公司字段

df['子公司'] = ['北京', '北京', '上海', '上海', '上海', '北京']

print("新增子公司列后:")

print(df)

# 按子公司和部门两列分组

multi_group = df.groupby(['子公司', '部门'])['月薪'].mean()

print("每个子公司各部门平均月薪:")

print(multi_group)

输出:

部门 姓名 月薪 子公司

0 技术 张三 15000 北京

1 技术 李四 18000 北京

2 市场 王五 12000 上海

3 市场 赵六 11000 上海

4 技术 陈七 20000 上海

5 市场 周八 13000 北京

每个子公司各部门平均月薪:

子公司 部门

北京 市场 13000.0

技术 16500.0

上海 市场 11500.0

技术 20000.0

Name: 月薪, dtype: float64

多级分组返回的结果是一个带有多级索引(MultiIndex)的 Series,看起来像两层的表格。如果觉得这样不直观,可以调用 reset_index() 把它变成普通表格。

三、聚合函数与 agg 方法

3.1 常见聚合函数

Pandas 内置了很多聚合函数,常用的有:

sum():求和

mean():均值

count():非空值计数

min() / max():最小值 / 最大值

std() / var():标准差 / 方差

还是用刚才的员工数据,我们一次算出每个部门的总月薪、人数和最高月薪:

# 对月薪列同时做多个聚合

agg_result = df.groupby('部门')['月薪'].agg(['sum', 'count', 'max'])

print("各部门统计:")

print(agg_result)

输出:

sum count max

部门

市场 36000 3 13000

技术 53000 3 20000

agg 可以接收一个函数列表,Pandas 会自动把每个函数名作为列名。

3.2 使用 agg 进行多样化聚合

更强大的是,你可以为不同的列指定不同的聚合方式。比如你想同时看每个部门的“月薪均值”和“员工姓名列表”:

# 指定不同列的不同聚合

complex_agg = df.groupby('部门').agg({

'月薪': ['mean', 'max'],

'姓名': 'count' # 这里count就是人数

})

print("复杂聚合结果:")

print(complex_agg)

输出:

月薪 姓名

mean max count

部门

市场 12000 13000 3

技术 17666 20000 3

复杂的 agg 返回的列名是多层索引,有点儿不好看。你可以通过修改列名或者用 reset_index 来美化。不过在实际工作中,多数情况下我们不会同时用多个函数对多个列,而是按需选择。

四、转换与过滤:transform 与 filter

4.1 transform 的应用

聚合操作会返回一个缩减后的结果,比如分组后的平均值。但有时候你想保留原始数据的行数,同时给每一行添加上它所属组的某个统计值。比如你想知道每个员工的月薪相对于所在部门平均值的差值。这时候 transform 就派上用场了。

transform 和 agg 类似,但它返回的结果和原始 DataFrame 形状相同,不会缩减行数。

# 计算每个部门平均月薪

dept_mean = df.groupby('部门')['月薪'].transform('mean')

print("部门平均月薪(每行都有):")

print(dept_mean)

# 计算每个员工月薪与部门均值的差值

df['偏差'] = df['月薪'] - dept_mean

print("添加偏差列后的数据:")

print(df)

输出:

部门平均月薪(每行都有):

0 17666.666667

1 17666.666667

2 12000.000000

3 12000.000000

4 17666.666667

5 12000.000000

Name: 月薪, dtype: float64

添加偏差列后的数据:

部门 姓名 月薪 子公司 偏差

0 技术 张三 15000 北京 -2666.666667

1 技术 李四 18000 北京 333.333333

2 市场 王五 12000 上海 0.000000

3 市场 赵六 11000 上海 -1000.000000

4 技术 陈七 20000 上海 2333.333333

5 市场 周八 13000 北京 1000.000000

transform 内部也可以接受自定义函数,但要注意它必须返回与原始数组相同长度的结果。

4.2 filter 过滤分组

有时候我们只关心那些满足某些条件的组,比如只想保留员工人数大于等于2的部门。这时可以用 filter。

# 筛选出员工数 >=2 的部门

filtered = df.groupby('部门').filter(lambda x: len(x) >= 2)

print("过滤后(只保留员工数>=2的部门):")

print(filtered)

输出:

部门 姓名 月薪 子公司 偏差

0 技术 张三 15000 北京 -2666.666667

1 技术 李四 18000 北京 333.333333

2 市场 王五 12000 上海 0.000000

3 市场 赵六 11000 上海 -1000.000000

4 技术 陈七 20000 上海 2333.333333

5 市场 周八 13000 北京 1000.000000

因为我们这个例子中两个部门都有3个人,所以所有行都保留了。如果有一个部门只有一个人,那它就会被过滤掉。filter 的入参是一个函数,该函数接收一个分组后的 DataFrame 子集,返回 True 或 False。

五、apply 的灵活运用

5.1 apply 与自定义函数

当内置的聚合或者 transform 无法满足你的需求时,apply 就是万能钥匙。它可以对每个分组应用任意的函数,函数的返回值可以是标量、Series 或者 DataFrame。

比如,你想给每个部门的员工按照月薪从高到低排名(同部门内排名):

# 自定义排名函数

def rank_salary(group):

# group 是一个 DataFrame,包含该部门的所有行

group['部门内排名'] = group['月薪'].rank(ascending=False, method='dense')

return group

# 使用 apply,注意这里需要指定 group_keys=False 来避免自动添加分组键

ranked = df.groupby('部门', group_keys=False).apply(rank_salary)

print("每个部门内的月薪排名:")

print(ranked)

输出:

部门 姓名 月薪 子公司 偏差 部门内排名

0 技术 张三 15000 北京 -2666.666667 3.0

1 技术 李四 18000 北京 333.333333 2.0

2 市场 王五 12000 上海 0.000000 2.0

3 市场 赵六 11000 上海 -1000.000000 3.0

4 技术 陈七 20000 上海 2333.333333 1.0

5 市场 周八 13000 北京 1000.000000 1.0

apply 非常强大,但也可能带来性能问题,因为它是逐组调用函数。对于大数据集,尽量使用内置的向量化方法(如 transform、agg)代替。

六、分组后的重置索引与合并

6.1 reset_index

之前我们用 groupby 得到的结果,索引往往是分组键本身。如果希望把分组键变成普通的列,可以调用 reset_index()。

# 按子公司和部门分组,计算月薪总和

sum_result = df.groupby(['子公司', '部门'])['月薪'].sum()

print("分组后的结果(多级索引):")

print(sum_result)

print("\n重置索引后:")

print(sum_result.reset_index())

输出:

分组后的结果(多级索引):

子公司 部门

北京 市场 13000

技术 33000

上海 市场 23000

技术 20000

Name: 月薪, dtype: int64

重置索引后:

子公司 部门 月薪

0 北京 市场 13000

1 北京 技术 33000

2 上海 市场 23000

3 上海 技术 20000

重置索引后就能像普通 DataFrame 一样使用了,方便后续合并或者导出。

七、应用场景分析

场景1:电商销售分析

你想知道每个商品类目在每个季度的销售额趋势。先用 groupby('类目','季度') 汇总销售额,再用 pivot 把季度变成列,就能画出热力图。

场景2:用户行为分析

比如你有用户每周的登录天数,想找出那些平均登录天数大于5天的用户组,可以用 filter 按用户分组后过滤。

场景3:金融数据中的分组排名

基金公司需要每天计算每个投资组合中每只股票的占比排名,用 groupby('组合')['市值'].rank() 就可以轻松搞定。

八、技术优缺点

优点:

语法直观,链式调用让代码读起来像自然语言。

内置聚合函数丰富,能覆盖大部分常见需求。

transform 和 apply 提供了极高的灵活性,几乎任何分组逻辑都能实现。

缺点:

大数据集下性能可能不如 SQL 或专门的并行框架(如 Dask)。

多层索引有时让人头疼,需要配合 reset_index 或 droplevel 使用。

apply 如果写得不小心,容易引起性能灾难,因为它是逐组迭代。

九、注意事项

缺失值处理:分组时,如果分组键列中有 NaN,Pandas 会默认把这些行放在同一组中。你可以用 dropna=True 参数丢弃它们。

性能优化:能用 agg 或 transform 的地方,尽量别用 apply。如果必须用,考虑用 numba 加速。

索引混乱:多次链式操作后,索引可能会变成多层,记得在合适的时候 reset_index。

内存占用:分组结果如果是 DataFrame 的副本,会占用额外内存。可以用 inplace=False(默认)来避免意外修改原数据。

十、文章总结

Pandas 的分组与统计功能是数据分析的基本功。通过 groupby 可以快速把数据按维度切分,再用 agg、transform、filter 和 apply 来做不同类型的计算。掌握了这些高级方法,你就能从原始数据中提炼出有价值的信息,而不仅仅是会念“平均值”。希望这篇文章能让你在以后处理分组问题时更加得心应手。