📊 第4章 Pandas 进阶
📄 一句话概括

迭代给的是(组名, 子表),子表就是一个完整的 DataFrame,想干什么都行。 agg 传列表是「同一列跑多个函数」,传字典是「不同列配不同函数」。 自定义函数只要能接一个 Series 返回一个数,就能直接塞进 agg。

📝 本章和前面几章的关系

上一篇只会接 mean、sum 这些现成的。这一篇把 agg 的三种写法和迭代都讲透。 迭代慢但灵活,agg 快但要求规整。选哪个看「每组做的事一不一样」。

一、迭代每一组

for k, g in df.groupby('班级') 每轮拿到组名和一张子表。 一班 5 人、语文均值 86.00;二班 5 人、语文均值 79.40。 子表是完整的 DataFrame,想算什么都行,想画图、想存文件都行。

迭代慢。能用 agg 就别迭代,只有「每组要做不同的动作」时才值得循环。 行数一多,迭代的代价是几十倍的差距,不是百分之几。

二、不同列配不同函数:字典 agg

班级 语文 数学 英语
一班 86.0 95 79
二班 79.4 91 60

agg({'语文': 'mean', '数学': 'max', '英语': 'min'}),三列三种算法。 键是列名,值是函数名。看不出来的是:这三列的语义已经不一样了。 一列是均值、一列是最高、一列是最低——表头得写清楚,不然读者会横向比较。

这一句是分组统计的主力写法。它把「哪列配哪个函数」写得明明白白。

三、塞一个自定义函数

定义一个极差函数:def spread(s): return s.max() - s.min()。 直接塞进 agg:df.groupby('班级')[ ['语文','数学'] ].agg(spread)。

班级 语文 数学
一班 17 27
二班 27 26

一班语文极差 17 分,二班 27 分——二班分得更散,虽然均值只差 6.6。 自定义函数拿到的是「一列数」,也就是一个 Series。按 Series 的写法写就行。 它必须返回一个数。返回一整列会报错,因为 agg 要的是「一组压成一个值」。

四、三种写法对照

写法 含义 例子
agg('mean') 所有列一个函数 agg('mean')
agg(['min','max']) 同一列多个函数 agg(['min','max'])
agg({列: 函数}) 不同列不同函数 agg({'语文':'mean'})
agg(自定义) 自己写的聚合 agg(spread)

四种可以混:agg({'语文': ['mean', spread]}),一列上跑一个现成的加一个自定义的。 结果会变成多级列名,上面一层是列名、下面一层是函数名。想拉平用 droplevel。

五、动手 Demo:分组之后:迭代、agg 与自定义函数

先迭代一遍看清每组长什么样,再用字典 agg 和自定义函数各算一次。 最后把三种 agg 写法摆在一起比。

import pandas as pd

DF = pd.DataFrame({
    "姓名": ["张伟", "王芳", "李娜", "刘洋", "陈静",
             "杨帆", "赵敏", "黄磊", "周涛", "吴霞"],
    "班级": ["一班"] * 5 + ["二班"] * 5,
    "语文": [88, 92, 75, 85, 90, 68, 95, 72, 83, 79],
    "数学": [92, 95, 68, 88, 73, 82, 91, 65, 86, 90],
    "英语": [85, 90, 80, 79, 95, 71, 88, 60, 84, 87],
})

for k, g in DF.groupby("班级"):
    print("%s:%d 人,语文均值 %.2f" % (k, len(g), g["语文"].mean()))

def spread(s):
    return s.max() - s.min()

M = DF.groupby("班级").agg({"语文": "mean", "数学": "max", "英语": "min"})
print(M.round(2).to_string())

print(DF.groupby("班级")[ ["语文", "数学"]].agg(spread).to_string())

print(DF.groupby("班级")["语文"].agg(["mean", spread]).round(2).to_string())

实跑输出:

--------------------------------------------------------------------
分组之后:迭代、agg 与自定义函数
--------------------------------------------------------------------

  ① 迭代每一组
  --------------------------------------------------------------------
  一班:5 人,语文均值 86.00。
  二班:5 人,语文均值 79.40。
  迭代给的是(组名, 子表)两样东西,子表就是一个完整的 DataFrame。
  想对每组单独做复杂的事(画图、存文件),就在这个循环里写。
  ! 迭代慢,能用 agg 就别迭代。只有「每组要做不同的动作」时才值得循环。

  ② 不同列配不同函数
  --------------------------------------------------------------------

| 班级 | 语文 | 数学 | 英语 |
|---|---:|---:|---:|
| 一班 | 86.0 | 95 | 79 |
| 二班 | 79.4 | 91 | 60 |

  传一个字典,键是列名、值是函数名,一次算出三样不同的东西。
  一班数学最高 95,二班英语最低 60。
  ! 字典 agg 是分组统计的主力写法。它把「哪列配哪个函数」写得明明白白。

  ③ 塞一个自定义函数
  --------------------------------------------------------------------
  自定义一个极差函数,直接塞进 agg:

| 班级 | 语文 | 数学 |
|---|---:|---:|
| 一班 | 17 | 27 |
| 二班 | 27 | 26 |

  一班语文极差 17,二班 27。二班分得更散。
  ! 自定义函数拿到的是「一列数」,也就是一个 Series,按 Series 的写法写就行。

  ④ 三个易错点
  --------------------------------------------------------------------

| 现象 | 原因 |
|---|---|
| 分组后少了一组 | 那列是空值,groupby 默认丢掉(dropna=False 可留) |
| 结果行标签乱了 | 分组键成了标签,reset_index 拉平 |
| 自定义函数报错 | 函数要接 Series 返回单值,别返回整列 |

  这三条几乎每次做分组都会撞上一条,记住了能省不少时间。
  ! 分组统计的数一定要和总量对得上:各组人数加起来要等于总行数。

最后一个把现成函数和自定义函数放在一个列表里,一列上跑两个——这是 agg 最灵活的地方。 迭代里能做的事,agg 里未必能做;agg 能做的事,迭代一定也能做,只是慢。

30 秒实验

  1. 把 spread 改成返回 s.mean() + 1,看结果变化。
  2. 给 agg 传 {'语文': ['mean', 'max']},看列名变成几层。
  3. 想一想:为什么 agg 要求函数「返回一个数」而不是「返回一列」?

常见误区

  1. 自定义函数返回整列:agg 报错,因为一组要压成一个值。
  2. 字典 agg 的键写错列名:报 KeyError,或者悄悄少一列。
  3. 迭代里改原表:改的是子表的副本,原表不动。
  4. 多级列名不拉平就导出:Excel 里表头变成两行,下游对不上。
自测三问(点开对答案) 1. 迭代 groupby 拿到的是什么? —— 两样东西:组名,和这一组对应的子表(完整的 DataFrame)。 2. agg 传列表和传字典有什么区别? —— 列表是「同一批列都跑这几个函数」,字典是「每列配自己的函数」。 3. 自定义聚合函数有什么要求? —— 接一个 Series,返回一个标量。返回整列或返回 None 都会出错。

分组键一直是表里的一列。下一篇讲键不在表里怎么办:用字典和 Series 指认。

相关笔记