Pandas: 分组聚合

最后更新:2026-08-26

分组聚合是数据分析的"心脏"——按地区看销售额、按类别算均价、按月份统计订单量,本质都是"分组 → 计算 → 合并"。Pandas 的 groupby 实现了经典的 split-apply-combine 范式,本节用 Mermaid 图解让你彻底理解这个过程,并掌握 agg / transform / filter 三大操作。

⚠️ 注意: 以下代码需在本地 Python 环境中运行。

1. 你将学到


2. Alice 按类别统计咖啡店销售

(1) 痛点:手动循环分组太繁琐

Alice 想按饮品类别统计销售额,手动写循环:

PYTHON
import pandas as pd

df = pd.DataFrame({
    'drink': ['Latte', 'Americano', 'Mocha', 'Latte', 'Espresso',
              'Americano', 'Latte', 'Mocha', 'Espresso', 'Latte'],
    'category': ['Hot', 'Hot', 'Hot', 'Hot', 'Hot',
                 'Iced', 'Iced', 'Iced', 'Iced', 'Iced'],
    'sales': [320, 280, 350, 310, 150, 200, 180, 160, 90, 120],
    'quantity': [64, 56, 50, 62, 30, 40, 36, 22, 18, 24]
})

# Manual loop — tedious and error-prone
for cat in df['category'].unique():
    subset = df[df['category'] == cat]
    print(f"{cat}: sales={subset['sales'].sum()}, qty={subset['quantity'].sum()}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) 解法:groupby 一行搞定

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:groupby 基础(难度⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'drink': ['Latte', 'Americano', 'Mocha', 'Latte', 'Espresso',
              'Americano', 'Latte', 'Mocha', 'Espresso', 'Latte'],
    'category': ['Hot', 'Hot', 'Hot', 'Hot', 'Hot',
                 'Iced', 'Iced', 'Iced', 'Iced', 'Iced'],
    'sales': [320, 280, 350, 310, 150, 200, 180, 160, 90, 120],
    'quantity': [64, 56, 50, 62, 30, 40, 36, 22, 18, 24]
})

# One line replaces the entire loop!
result = df.groupby('category')['sales'].sum()
print(result)
# category
# Hot     1410
# Iced     750
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

3. split-apply-combine 原理

(1) Mermaid 图解分组流程

100%
graph TB
    subgraph Split["① Split — 按类别拆分"]
        S1["Hot: Latte 320<br>Americano 280<br>Mocha 350<br>Latte 310<br>Espresso 150"]
        S2["Iced: Americano 200<br>Latte 180<br>Mocha 160<br>Espresso 90<br>Latte 120"]
    end
    subgraph Apply["② Apply — 每组求和"]
        A1["Hot → 320+280+350<br>+310+150 = 1410"]
        A2["Iced → 200+180+160<br>+90+120 = 750"]
    end
    subgraph Combine["③ Combine — 合并结果"]
        C1["category | sales<br>Hot      | 1410<br>Iced     | 750"]
    end
    Split --> Apply --> Combine
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) GroupBy 对象是惰性的

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:GroupBy 惰性求值(难度⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'category': ['Hot', 'Hot', 'Iced', 'Iced'],
    'sales': [320, 280, 200, 180]
})

# groupby returns a GroupBy object — no computation yet
grouped = df.groupby('category')
print(type(grouped))  # <class 'pandas.core.groupby.DataFrameGroupBy'>

# Computation happens when you call an aggregation
print(grouped['sales'].sum())

# Inspect groups
print(grouped.groups)  # {'Hot': [0, 1], 'Iced': [2, 3]}
print(grouped.ngroups)  # 2

# Iterate over groups (rarely needed)
for name, group in grouped:
    print(f"Group: {name}")
    print(group)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

4. 常用聚合函数

(1) 内置聚合

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:内置聚合函数(难度⭐⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Home', 'Home'],
    'product': ['Laptop', 'Phone', 'Shirt', 'Pants', 'Lamp', 'Chair'],
    'price': [999, 699, 45, 65, 30, 120],
    'stock': [50, 120, 200, 180, 300, 80],
    'rating': [4.7, 4.5, 4.2, 4.0, 3.8, 4.3]
})

# Single aggregation per column
print(df.groupby('category')['price'].mean())
# Electronics    849.0
# Clothing        55.0
# Home            75.0

# Multiple columns, same aggregation
print(df.groupby('category')[['price', 'stock']].sum())

# Common aggregation methods:
# .sum() .mean() .median() .min() .max()
# .count() .size() .std() .var() .first() .last()
# .nunique() .idxmax() .idxmin()
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) size vs count 区别

方法 计算内容 NaN 处理
size 每组行数(含 NaN) 包含 NaN
count 每组非 NaN 值数 排除 NaN

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:size vs count(难度⭐)

PYTHON
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'category': ['A', 'A', 'B', 'B', 'A'],
    'value': [1, np.nan, 3, 4, 5]
})

print(df.groupby('category').size())
# category
# A    3  ← 3 rows (including NaN row)
# B    2

print(df.groupby('category').count())
#           value
# category
# A            2  ← 2 non-NaN values
# B            2
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

5. agg 多聚合

(1) 多聚合函数

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:agg 多聚合(难度⭐⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Home', 'Home'],
    'product': ['Laptop', 'Phone', 'Shirt', 'Pants', 'Lamp', 'Chair'],
    'price': [999, 699, 45, 65, 30, 120],
    'stock': [50, 120, 200, 180, 300, 80],
    'rating': [4.7, 4.5, 4.2, 4.0, 3.8, 4.3]
})

# Multiple aggregations on one column
print(df.groupby('category')['price'].agg(['mean', 'min', 'max']))
#              mean  min   max
# category
# Clothing     55.0   45    65
# Electronics 849.0  699   999
# Home         75.0   30   120

# Different aggregations per column
print(df.groupby('category').agg({
    'price': ['mean', 'max'],
    'stock': 'sum',
    'rating': 'mean'
}))

# Named aggregations (cleaner column names)
result = df.groupby('category').agg(
    avg_price=('price', 'mean'),
    max_price=('price', 'max'),
    total_stock=('stock', 'sum'),
    avg_rating=('rating', 'mean')
)
print(result)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) 自定义聚合函数

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:自定义聚合(难度⭐⭐⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'category': ['A', 'A', 'A', 'B', 'B', 'B'],
    'value': [10, 20, 30, 100, 200, 300]
})

# Custom function: range (max - min)
def value_range(series):
    return series.max() - series.min()

result = df.groupby('category')['value'].agg(['mean', value_range])
print(result)
#           mean  value_range
# category
# A         20.0           20
# B        200.0          200

# Lambda in agg (less readable but concise)
result2 = df.groupby('category')['value'].agg([
    ('mean', 'mean'),
    ('range', lambda x: x.max() - x.min()),
    ('cv', lambda x: x.std() / x.mean())  # coefficient of variation
])
print(result2)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

6. transform 组内变换

(1) transform 保持原形状

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:transform 组内标准化(难度⭐⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'student': ['Alice', 'Alice', 'Bob', 'Bob', 'Charlie', 'Charlie'],
    'subject': ['Math', 'English', 'Math', 'English', 'Math', 'English'],
    'score': [85, 90, 92, 88, 78, 82]
})

# Z-score normalization per student
df['z_score'] = df.groupby('student')['score'].transform(
    lambda x: (x - x.mean()) / x.std()
)
print(df)

# Fill missing with group mean (instead of global mean)
df2 = pd.DataFrame({
    'category': ['A', 'A', 'A', 'B', 'B'],
    'value': [10, 20, None, 100, None]
})
df2['value_filled'] = df2.groupby('category')['value'].transform(
    lambda x: x.fillna(x.mean())
)
print(df2)

# Rank within group
df['rank'] = df.groupby('student')['score'].transform('rank', method='min')
print(df[['student', 'subject', 'score', 'rank']])
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

(2) transform vs agg 对比

特性 agg transform
返回形状 每组 1 行 与原 DataFrame 同长度
结果用途 汇总报告 回填原表
典型操作 sum/mean/count 标准化/填充/排名
广播 ✅ 自动广播到原行

7. filter 组级筛选

(1) 按组条件保留或丢弃

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:filter 筛选组(难度⭐⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'category': ['Electronics', 'Electronics', 'Electronics',
                 'Books', 'Books',
                 'Clothing', 'Clothing', 'Clothing', 'Clothing'],
    'product': ['Laptop', 'Phone', 'Tablet', 'Novel', 'Textbook',
                'Shirt', 'Pants', 'Jacket', 'Socks'],
    'sales': [5000, 3000, 2000, 200, 300, 800, 600, 400, 100]
})

# Keep only groups where total sales > 1000
big_categories = df.groupby('category').filter(lambda x: x['sales'].sum() > 1000)
print(big_categories)
# Electronics and Clothing kept; Books dropped (200+300=500 < 1000)

# Keep groups with at least 3 items
large_groups = df.groupby('category').filter(lambda x: len(x) >= 3)
print(large_groups['category'].unique())  # ['Electronics', 'Clothing']

# Keep groups where any product has sales > 4000
has_top_seller = df.groupby('category').filter(lambda x: x['sales'].max() > 4000)
print(has_top_seller['category'].unique())  # ['Electronics']
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

8. 多列分组与 as_index

(1) 多列分组

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:多列分组聚合(难度⭐⭐)

PYTHON
import pandas as pd

df = pd.DataFrame({
    'region': ['North', 'North', 'North', 'South', 'South', 'South'],
    'category': ['Electronics', 'Clothing', 'Electronics',
                 'Electronics', 'Clothing', 'Clothing'],
    'product': ['Laptop', 'Shirt', 'Phone', 'Tablet', 'Pants', 'Jacket'],
    'sales': [5000, 800, 3000, 2000, 600, 400]
})

# Group by multiple columns
result = df.groupby(['region', 'category'])['sales'].sum()
print(result)
# region  category
# North  Clothing       800
#        Electronics   8000
# South  Clothing      1000
#        Electronics   2000

# as_index=False → keeps group columns as regular columns
result2 = df.groupby(['region', 'category'], as_index=False)['sales'].sum()
print(result2)
#   region    category  sales
# 0  North    Clothing    800
# 1  North  Electronics  8000
# 2  South    Clothing   1000
# 3  South  Electronics  2000

# sort=False → preserve original order of first occurrence
result3 = df.groupby(['region', 'category'], sort=False)['sales'].sum()
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

9. 完整示例:电商多维度分组分析

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:多维度分组分析(难度⭐⭐⭐)

PYTHON
import pandas as pd
import numpy as np

# ============================================
# Comprehensive example: E-commerce multi-dim
# groupby analysis with agg/transform/filter
# ============================================

# 1. Create sales data
np.random.seed(42)
df = pd.DataFrame({
    'region': np.random.choice(['North', 'South', 'East', 'West'], 100),
    'category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Sports'], 100),
    'product': [f'Item_{i:03d}' for i in range(100)],
    'sales': np.round(np.random.uniform(50, 2000, 100), 2),
    'quantity': np.random.randint(1, 50, 100),
    'discount': np.random.choice([0, 0.1, 0.2, 0.3], 100)
})

# 2. agg: multi-metric summary per category
summary = df.groupby('category').agg(
    total_sales=('sales', 'sum'),
    avg_sales=('sales', 'mean'),
    max_sales=('sales', 'max'),
    order_count=('sales', 'count'),
    avg_quantity=('quantity', 'mean')
).round(2)
print("=== Category Summary ===")
print(summary)

# 3. Multi-column groupby
region_cat = df.groupby(['region', 'category'], as_index=False).agg(
    total_sales=('sales', 'sum'),
    avg_discount=('discount', 'mean')
).round(3)
print("\n=== Region × Category ===")
print(region_cat.head(8))

# 4. transform: within-group ranking
df['sales_rank_in_region'] = df.groupby('region')['sales'].transform(
    'rank', method='min', ascending=False
)
df['sales_pct_in_category'] = df.groupby('category')['sales'].transform(
    lambda x: (x / x.sum() * 100).round(1)
)
print("\n=== Top 3 in North ===")
print(df[df['region'] == 'North'].nsmallest(3, 'sales_rank_in_region')
      [['product', 'sales', 'sales_rank_in_region']])

# 5. filter: keep categories with > 20 orders
big_cats = df.groupby('category').filter(lambda x: len(x) > 20)
print(f"\n=== Big Categories ===")
print(big_cats['category'].value_counts())
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

❓ 常见问题

Q groupby 返回什么?
A groupby 返回 GroupBy 对象(惰性的),不立即计算。只有调用聚合函数(sum/mean/agg 等)时才执行 split-apply-combine。这种惰性设计让你可以链式组合多个操作,不用中间变量。
Q agg 和 apply 区别?
A agg 对每列应用聚合函数,返回每组一行。apply 对每组应用任意函数,返回结果更灵活(可返回 DataFrame)。简单聚合用 agg(更快更明确),复杂组内计算用 apply。agg 还支持命名聚合(name=(col, func)),列名更清晰。
Q transform 保持形状吗?
A 是的——transform 返回与原 DataFrame 相同长度的结果,每组内广播到原行。典型用途:组内标准化(z-score)、用组均值填充缺失值、组内排名。如果结果长度与组大小不一致会报错。
Q filter 是过滤行还是组?
A filter 过滤整个组——如果组的聚合结果不满足条件,该组所有行都被移除。不是过滤组内的行!与布尔索引不同:布尔索引按行条件过滤,filter 按组条件过滤。
Q as_index=False 有什么用?
A 默认 groupby 把分组列变成 Index(不是普通列)。as_index=False 让分组列保持为普通列,结果是一个干净的 DataFrame 而非带 MultiIndex 的 Series。需要后续操作分组列时(如 merge),as_index=False 更方便。
Q 多列分组结果怎么看?
A 多列分组产生 MultiIndex(层次索引)。用 result.loc[('North', 'Electronics')] 选特定组,或用 as_index=False 避免层次索引。reset_index() 也可以把层次索引展平为普通列。
Q groupby + sort 会变慢吗?
A sort=True(默认)会对分组键排序,增加少量开销。大 DataFrame + 高基数分组键时,sort=False 可以加速。但排序后的结果更可读。建议:数据量小时用 sort=True(可读性优先),超大数据时 sort=False(性能优先)。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建销售 DataFrame(region/category/sales),用 groupby + sum/mean/count 三种聚合,对比结果。
  2. 进阶题(难度⭐⭐):创建学生成绩表,用 agg 对每科计算 mean/max/min,用 transform 计算每个学生的 z-score 标准化成绩。
  3. 挑战题(难度⭐⭐⭐):模拟 50 行电商订单(region/category/sales/quantity/discount),完成:agg 多指标汇总 → 多列分组(region+category) → transform 组内排名 → filter 保留大组 → 综合报告。

← 上一课:数据变换 · 下一课:合并连接 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏