Pandas 数据分组与聚合

分组聚合是数据分析中的核心操作,类似于 SQL 的 GROUP BY。Pandas 的 groupby 机制可以让你轻松地对数据进行分组,然后对每个组应用聚合函数、转换函数或过滤操作,从而提取出有意义的统计信息。

📌 准备示例数据

import pandas as pd
import numpy as np

# 创建销售数据
df = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=12, freq='M'),
    '产品': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A'],
    '区域': ['华北', '华北', '华东', '华南', '华北', '华东', '华南', '华北', '华东', '华南', '华北', '华东'],
    '销售额': [100, 150, 120, 180, 130, 110, 200, 140, 125, 210, 135, 115],
    '利润': [20, 30, 25, 35, 28, 22, 40, 30, 26, 42, 29, 24]
})
print(df)

🔨 GroupBy 基础

groupby() 创建了一个分组对象,你可以在这个对象上调用聚合方法。

按单列分组
# 按产品分组,计算平均销售额
df.groupby('产品')['销售额'].mean()
按多列分组
# 按产品和区域分组,求和
df.groupby(['产品', '区域'])['销售额'].sum()

📊 常用聚合函数

Pandas 为 GroupBy 对象提供了多种内置聚合方法:

mean() sum() count() size() min() max() std() var() first() last()
# 单列单聚合
df.groupby('产品')['利润'].sum()

# 多列多聚合
df.groupby('产品')[['销售额', '利润']].mean()

# 对整个 DataFrame 分组后聚合所有数值列
df.groupby('产品').mean(numeric_only=True)

🎯 多重聚合:agg() 方法

agg() 允许你对不同列应用不同的聚合函数,甚至自定义函数。

# 对不同列应用不同聚合
result = df.groupby('产品').agg({
    '销售额': ['sum', 'mean'],
    '利润': ['sum', 'max']
})
print(result)

# 使用字典重命名列(通过命名聚合)
result = df.groupby('产品').agg(
    总销售额=('销售额', 'sum'),
    平均销售额=('销售额', 'mean'),
    总利润=('利润', 'sum')
)
print(result)

# 对同一列应用多个函数
result = df.groupby('产品')['销售额'].agg(['sum', 'mean', 'max'])
print(result)

🔄 自定义聚合函数

你可以将任何函数传递给 agg()apply()

# 自定义函数
def range_func(x):
    return x.max() - x.min()

df.groupby('产品')['销售额'].agg(range_func)

# 使用 lambda
df.groupby('产品')['销售额'].agg(lambda x: x.max() - x.min())

# 多个自定义函数
df.groupby('产品')['销售额'].agg(['mean', range_func])

📈 变换与过滤

除了聚合,groupby 还可以进行变换(transform)和过滤(filter)。

🔧 变换:transform

transform 返回与原始数据相同形状的结果,常用于计算组内比例或标准化。

# 计算每个产品的销售额占该产品总销售额的比例
df['销售额占比'] = df.groupby('产品')['销售额'].transform(lambda x: x / x.sum())
print(df)

# 计算组内均值,并添加到原 DataFrame
df['组内平均销售额'] = df.groupby('产品')['销售额'].transform('mean')
print(df)

🔍 过滤:filter

filter 根据组级别的条件筛选组,返回原 DataFrame 的子集。

# 只保留平均销售额 > 150 的产品组
df_filtered = df.groupby('产品').filter(lambda x: x['销售额'].mean() > 150)
print(df_filtered)

📊 分组后应用多个函数:apply()

apply() 是 GroupBy 最灵活的方法,可以对每个组执行任意操作,返回各种形状的结果。

# 对每个组应用函数,返回标量、Series 或 DataFrame
def top_product(group):
    return group.nlargest(2, '销售额')[['产品', '销售额']]

df.groupby('区域').apply(top_product)

📋 透视表:pivot_table()

pivot_table() 是 Excel 风格的数据透视表,本质上是多索引分组聚合的便捷方式。

# 基本透视表:行=产品,列=区域,值=销售额,聚合=sum
pivot = pd.pivot_table(df, values='销售额', index='产品', columns='区域', aggfunc='sum', fill_value=0)
print(pivot)

# 多值、多聚合
pivot = pd.pivot_table(df,
                       values=['销售额', '利润'],
                       index='产品',
                       columns='区域',
                       aggfunc={'销售额': 'sum', '利润': 'mean'})
print(pivot)

🧪 综合示例

import pandas as pd

# 创建销售数据
sales = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=20, freq='D'),
    '产品': np.random.choice(['A', 'B', 'C'], 20),
    '区域': np.random.choice(['华北', '华东', '华南'], 20),
    '销售额': np.random.randint(100, 500, 20),
    '利润': np.random.randint(20, 100, 20)
})

print("原始数据:")
print(sales.head(10))

# 1. 按产品和区域分组,计算销售额总和
group1 = sales.groupby(['产品', '区域'])['销售额'].sum().reset_index()
print("\n各产品在各区域的总销售额:")
print(group1)

# 2. 按产品分组,计算销售额和利润的统计量
group2 = sales.groupby('产品').agg({
    '销售额': ['sum', 'mean', 'max'],
    '利润': ['sum', 'mean']
})
print("\n各产品的销售统计:")
print(group2)

# 3. 计算每个产品每天的销售额占比(按天分组?这里演示组内比例)
sales['日销售额占比'] = sales.groupby('日期')['销售额'].transform(lambda x: x / x.sum())
print("\n添加每日销售额占比:")
print(sales.head())

# 4. 只保留平均利润 > 50 的产品组
high_profit_products = sales.groupby('产品').filter(lambda g: g['利润'].mean() > 50)
print("\n平均利润>50的产品记录:")
print(high_profit_products)

# 5. 创建透视表:产品为行,区域为列,值为销售额总和
pivot_table = pd.pivot_table(sales, values='销售额', index='产品', columns='区域', aggfunc='sum', fill_value=0)
print("\n销售额透视表:")
print(pivot_table)

⚠️ 注意事项

分组键的类型:分组键可以是列名、Series、或者自定义函数。确保分组键没有缺失值,否则该组会被排除。
as_index 参数:默认 groupby(..., as_index=True) 会将分组键作为索引。若希望分组键保留为列,设置 as_index=False 或之后使用 reset_index()
性能考虑:对大数据集使用 groupby 时,尽量在聚合前选择必要的列,减少内存占用。
多重索引:多列分组后返回的 Series/DataFrame 通常带有多重索引,可以使用 reset_index() 将其转为普通列。
最佳实践:
  • 先用 groupby 分组,再用 agg() 聚合,代码清晰且高效。
  • 对于复杂的聚合需求,使用 agg() 传入字典或命名聚合,避免用 apply() 循环,后者可能较慢。
  • 分组前检查缺失值,必要时用 dropna() 处理。
  • 理解分组后返回的对象类型:GroupBy 对象,需要调用聚合方法才能得到结果。