RSS
菜单
全部文章快讯开发科技深度热点

第 13 章 分组聚合:groupby

内容摘要

`groupby` 是 pandas **最强大**的统计分析武器:按一个或多个键把数据分组,然后对每组做聚合、变换、过滤。它对应 SQL 的 `GROUP BY`,也是"拆分-应用-合并"(split-apply-combine)思想的典范。

groupby 是 pandas 最强大的统计分析武器:按一个或多个键把数据分组,然后对每组做聚合、变换、过滤。它对应 SQL 的 GROUP BY,也是"拆分-应用-合并"(split-apply-combine)思想的典范。

目录

1. groupby 思想:拆分-应用-合并

2. 基本用法

3. 聚合:agg

4. 遍历分组

5. 变换:transform

6. 过滤:filter

7. 多键分组

8. apply 与 groupby 的组合

9. 分组排序与 TopN

10. 分组实战案例

11. 常见坑与注意事项

12. 本章小结与练习


1. groupby 思想:拆分-应用-合并

groupby 的经典三步骤:

1. 拆分(split):   按分组键把行分成若干组
2. 应用(apply):   对每组执行函数(聚合/变换/过滤)
3. 合并(combine): 把结果组合起来
import pandas as pd

df = pd.DataFrame({
    "部门": ["技术", "技术", "销售", "销售", "市场"],
    "姓名": ["张三", "李四", "王五", "赵六", "孙七"],
    "工资": [12000, 15000, 9000, 11000, 8000],
})

# 按部门分组,求每组平均工资
result = df.groupby("部门")["工资"].mean()
print(result)
# 部门
# 市场     8000.0
# 技术    13500.0
# 销售    10000.0
# Name: 工资, dtype: float64

2. 基本用法

2.1 选择分组键的方式

# 方式 1:按列名
df.groupby("部门")

# 方式 2:按多个列名
df.groupby(["部门", "城市"])

# 方式 3:按 Series / 数组(长度须等于行数)
df.groupby(df["部门"])

# 方式 4:按自定义规则
df.groupby(lambda idx: df.loc[idx, "工资"] > 10000)

# 方式 5:按索引层级
# df.groupby(level=0)

2.2 查看分组对象

g = df.groupby("部门")
print(type(g))            # <class 'pandas.core.groupby.generic.DataFrameGroupBy'>
print(g.groups)           # 每个组的行索引字典
print(g.size())           # 每组行数
print(g.ngroups)          # 组数
{'市场': [4], '技术': [0, 1], '销售': [2, 3]}
部门
市场    1
技术    2
销售    2
dtype: int64

2.3 选择要聚合的列

# 对整个分组的数值列聚合
print(df.groupby("部门").mean())

# 只对指定列聚合
print(df.groupby("部门")["工资"].mean())

# 多列
print(df.groupby("部门")[["工资"]].mean())

# 一行内完成:mean/sum/count...
print(df.groupby("部门").sum())
print(df.groupby("部门").count())   # 非空计数
print(df.groupby("部门").size())    # 行数(含 NaN)
print(df.groupby("部门").first())   # 每组第一行
print(df.groupby("部门").last())    # 每组最后一行
print(df.groupby("部门").max())
print(df.groupby("部门").min())
print(df.groupby("部门").std())
count() 统计非空值,size() 统计行数,两者在有缺失值时结果不同。

3. 聚合:agg

agg(aggregate)是聚合的"瑞士军刀",支持多种聚合方式组合、自定义函数、重命名结果列。

3.1 多个聚合函数

# 对一列做多种聚合
print(df.groupby("部门")["工资"].agg(["sum", "mean", "max", "min", "count"]))

# 对不同列做不同聚合
print(df.groupby("部门").agg({
    "工资": ["sum", "mean"],
    "姓名": "count",
}))
             工资          姓名
             sum      mean count
部门
市场   8000.0  8000.000000     1
技术  27000.0 13500.000000     2
销售  20000.0 10000.000000     2

3.2 自定义聚合函数

# lambda 或自定义函数
result = df.groupby("部门")["工资"].agg(
    total=lambda x: x.sum(),
    avg=lambda x: x.mean(),
    spread=lambda x: x.max() - x.min(),
)
print(result)
       total      avg  spread
部门
市场    8000.0   8000.0     0.0
技术   27000.0  13500.0  3000.0
销售   20000.0  10000.0  2000.0

3.3 named agg(新式命名聚合)

# pandas 0.25+ 的命名聚合语法
result = df.groupby("部门")["工资"].agg(
    平均工资="mean",
    最高工资="max",
    工资中位数="median",
)
print(result)
       平均工资   最高工资  工资中位数
部门
市场   8000.0  8000.0    8000.0
技术  13500.0  15000.0  13500.0
销售  10000.0  11000.0  10000.0

3.4 agg 后重置索引

# groupby 结果默认以分组键为索引
result = df.groupby("部门")["工资"].mean()
print(result.index)    # Index(['市场', '技术', '销售'], ...)

# 转回普通列
result2 = result.reset_index()
print(result2.columns)  # Index(['部门', '工资'], ...)

4. 遍历分组

g = df.groupby("部门")

for name, group in g:
    print(f"===== 部门: {name} =====")
    print(group)
    print()
===== 部门: 市场 =====
   部门  姓名   工资
4  市场  孙七  8000

===== 部门: 技术 =====
   部门  姓名   工资
0  技术  张三  12000
1  技术  李四  15000

===== 部门: 销售 =====
   部门  姓名   工资
2  销售  王五   9000
3  销售  赵六  11000
遍历常用于:对每组画图、导出每个组到单独文件、复杂的逐组处理。
# 按组分块导出
for name, group in df.groupby("部门"):
    group.to_csv(f"{name}.csv", index=False)

5. 变换:transform

transform 与 agg 的关键区别:agg 压缩行数(每组一行),transform 保持行数不变(每组内做广播)。

# 给每个人算"工资 - 部门平均工资"
df["部门平均"] = df.groupby("部门")["工资"].transform("mean")
df["工资差"] = df["工资"] - df["部门平均"]
print(df)
   部门  姓名   工资     部门平均    工资差
0  技术  张三  12000  13500.0  -1500.0
1  技术  李四  15000  13500.0   1500.0
2  销售  王五   9000  10000.0  -1000.0
3  销售  赵六  11000  10000.0   1000.0
4  市场  孙七   8000   8000.0      0.0

transform 常见应用:

# 1. 组内归一化
df["工资占比"] = df["工资"] / df.groupby("部门")["工资"].transform("sum")

# 2. 组内排名
df["组内排名"] = df.groupby("部门")["工资"].rank(ascending=False)

# 3. 组内填充缺失值
df["缺失列"] = df.groupby("部门")["工资"].transform(lambda x: x.fillna(x.mean()))

# 4. 组内累计和
df["累计工资"] = df.groupby("部门")["工资"].transform("cumsum")
transform 的灵魂:结果行数与原始数据一一对应,可以直接作为新列加入原表。

6. 过滤:filter

filter 根据整组的条件筛选组(不是筛选行)。

# 只保留"总工资 > 15000"的部门
result = df.groupby("部门").filter(lambda g: g["工资"].sum() > 15000)
print(result)
   部门  姓名   工资
0  技术  张三  12000
1  技术  李四  15000
2  销售  王五   9000
3  销售  赵六  11000

(市场组总和 8000 < 15000,整组被过滤掉了)

# 只保留行数 >= 2 的组
df.groupby("部门").filter(lambda g: len(g) >= 2)

# 保留均值 > 10000 的组
df.groupby("部门").filter(lambda g: g["工资"].mean() > 10000)
filter 与布尔索引的区别:布尔索引是"按行条件筛选",filter 是"按组条件筛选整组"。

7. 多键分组

df2 = pd.DataFrame({
    "部门": ["技术", "技术", "技术", "销售", "销售", "销售"],
    "城市": ["北京", "北京", "上海", "北京", "上海", "上海"],
    "工资": [12000, 13000, 15000, 9000, 11000, 10000],
})

# 两个键分组
g2 = df2.groupby(["部门", "城市"])
print(g2["工资"].mean())
部门  城市
技术  北京    12500.0
     上海    15000.0
销售  北京     9000.0
     上海    10500.0
Name: 工资, dtype: Float64
# as_index=False:分组键作为普通列
print(df2.groupby(["部门", "城市"], as_index=False)["工资"].mean())

# reset_index 等价效果
print(df2.groupby(["部门", "城市"])["工资"].mean().reset_index())

# 多种聚合
print(df2.groupby(["部门", "城市"])["工资"].agg(["sum", "mean", "count"]))

多键分组的索引访问

g2 = df2.groupby(["部门", "城市"])["工资"].mean()

# 按第一层
print(g2.loc["技术"])

# 按多层
print(g2.loc[("技术", "北京")])

8. apply 与 groupby 的组合

groupby.apply 可以做任意复杂的"按组处理",但要注意性能与行为差异。

# 每组求最赚钱的员工
def top_earner(group):
    return group.nlargest(1, "工资")

print(df.groupby("部门").apply(top_earner))
        部门  姓名   工资
部门
市场 4  市场  孙七  8000
技术 1  技术  李四  15000
销售 3  销售  赵六  11000
# apply 返回多行
def summary(group):
    return pd.DataFrame({
        "组人数": [len(group)],
        "总工资": [group["工资"].sum()],
    })

print(df.groupby("部门").apply(summary))
注意:apply 是"万能但慢"的方案,能分别用 agg/transform/filter 解决的场景,优先用专用方法。

9. 分组排序与 TopN

9.1 按组内某列排序

# 按组排序(先按部门,组内按工资降序)
df_sorted = df.sort_values(["部门", "工资"], ascending=[True, False])
print(df_sorted)

9.2 每组 TopN

# 每组工资最高的 1 人
print(df.groupby("部门").apply(lambda g: g.nlargest(1, "工资")).reset_index(drop=True))

# 每组工资最低的 2 人
print(df.groupby("部门").apply(lambda g: g.nsmallest(2, "工资")).reset_index(drop=True))

# 每组排名列
df["组内排名"] = df.groupby("部门")["工资"].rank(ascending=False)

9.3 分组后整体排序(重新排序组)

# 按组均值的降序排列组
g_mean = df.groupby("部门")["工资"].mean().sort_values(ascending=False)
print(g_mean)
# 技术    13500.0
# 销售    10000.0
# 市场     8000.0

10. 分组实战案例

import pandas as pd
import numpy as np

np.random.seed(1)
sales = pd.DataFrame({
    "区域": np.random.choice(["华东", "华南", "华北", "西南"], 200),
    "品类": np.random.choice(["数码", "服装", "食品", "家居"], 200),
    "金额": np.random.uniform(10, 1000, 200).round(2),
    "月份": np.random.choice(["1月", "2月", "3月"], 200),
})

# 1. 每个区域的总销售额(降序)
print(sales.groupby("区域")["金额"].sum().sort_values(ascending=False))

# 2. 区域 × 品类的销售额透视
pt = sales.groupby(["区域", "品类"])["金额"].sum().unstack()
print(pt)

# 3. 每月各区域平均客单价
print(sales.groupby(["月份", "区域"])["金额"].mean().unstack())

# 4. 每个区域金额最高的品类
top_cat = (
    sales.groupby(["区域", "品类"])["金额"]
    .sum()
    .groupby(level=0)
    .nlargest(1)
)
print(top_cat)

# 5. 每个区域当月占整体比例
sales["区域月占比"] = sales["金额"] / sales.groupby("月份")["金额"].transform("sum")
print(sales.head())

# 6. 保留订单数 >= 50 的区域
big_regions = sales.groupby("区域").filter(lambda g: len(g) >= 50)
print(big_regions["区域"].value_counts())

11. 常见坑与注意事项

| 坑 | 现象 | 解决办法 |

|----|------|----------|

| 分组后索引多层 | 后续操作困惑 | as_index=False 或 reset_index() |

| 忘记选列 | 所有数值列都被聚合 | 明确 ["工资"] |

| count vs size | 结果不一致 | 确认需求:非空计数用 count,行数用 size |

| agg 后列名混乱 | 多层列名 | 用 named agg 或 reset_index |

| transform 忘赋值 | 结果没保存 | df["新列"] = df.groupby(...).transform(...) |

| apply 太慢 | 大数据卡顿 | 优先 agg/transform,仅复杂场景用 apply |

| 分组键有 NaN | NaN 组被单独分出或丢弃 | 先 dropna() 或 fillna |


12. 本章小结与练习

小结

  • 核心流程:拆分 → 应用 → 合并;
  • agg:多种聚合组合、命名聚合、自定义函数;
  • transform:保持行数的组内变换(归一化、排名、填充、累计);
  • filter:按组条件过滤整组;
  • apply:万能但慢,复杂逻辑兜底;
  • 多键分组 + unstack 可以生成透视表。

练习题

1. 对销售数据按"区域"分组,求金额的 sum、mean、max。

2. 用 transform 给每个员工计算"工资占部门总工资比例"。

3. 用 filter 保留订单数大于 30 的区域。

4. 按"区域×品类"分组,找出每个区域销售额最高的品类。

5. 用命名聚合(named agg)生成包含 count、sum、mean 的分组结果表。


下一篇预告:第 14 章 时间序列分析 —— 日期处理、重采样、滚动窗口,玩转时间维度数据。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 13 章 分组聚合:groupby》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。