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

groupby 是「拆分、计算、合并」三步,只写第一步什么都不会发生。 它返回一个 GroupBy 对象,后面必须接 mean、sum、agg 这类动作才有结果。 分组结果的行标签是分组键,不是 0、1、2,想拉平就 reset_index。

📝 本章和前面几章的关系

前五篇都在「整张表上算一个数」。真实分析里更常见的是「按类别分别算」。 本表按班级分两组,一班五个人、二班五个人。

一、先分组,它什么都不给你看

df.groupby('班级') 只做「拆」这一步,返回一个 GroupBy 对象。 直接打印它,看到的是一句 <pandas.core.groupby...>,没有数据。 分组结果里有两组:一班、二班。这个信息在 .groups 里能看到。

只写 groupby 等于什么都没干。必须再接一个动作。

二、接一个均值

班级 语文 数学 英语
一班 86.0 83.2 85.8
二班 79.4 82.8 78.0

一行的差别就出来了:一班语文比二班高 6.6 分,英语高 7.8 分。 数学两个班只差 0.4 分——整张表的均值把这三个差别全抹平了。 这就是分组的价值:它把「总体差不多」拆成「哪一块差在哪」。

行标签是班级,列是科目,这是一张「班级 × 科目」的交叉表。

三、一次要多个统计量

班级 count sum mean max
一班 5 1275 255.0 277
二班 5 1201 240.2 274

df.groupby('班级')['总分'].agg(['count', 'sum', 'mean', 'max'])。 行是班级,列是四个函数名。一次跑完四个统计,比循环快也好读。 count 给的是「有几条记录」,没有缺失时和 size 一样;有缺失时 count 只数非空的。

这个区别在数据脏的时候会要命:以为有 5 条,其实只有 3 条有效。

四、分组键换成两列

df.groupby(['班级', '姓名']) 是两级分组,行标签变成两层。 姓名每个都不一样,所以每组只有一行——这种分组没有意义。 多级分组要有意义,键得有重复。班级有重复,姓名没有。

合理的多级键是「班级 + 性别」「省份 + 年份」这种,每级都有重复值。

五、动手 Demo:groupby:按类聚起来算

先只看分组、不接动作,体会「什么都没发生」。 再接 mean、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],
})
DF["总分"] = DF["语文"] + DF["数学"] + DF["英语"]

G = DF.groupby("班级")
print("分组对象 %s" % type(G).__name__)
print("分了 %d 组:%s" % (len(G), list(G.groups.keys())))

print(G[ ["语文", "数学", "英语"]].mean().round(2).to_string())

A = G["总分"].agg(["count", "sum", "mean", "max"])
print(A.round(2).to_string())

print("两列分组后 %d 组" % len(DF.groupby(["班级", "姓名"])))

实跑输出:

--------------------------------------------------------------------
groupby:按类聚起来算
--------------------------------------------------------------------

  ① 先分组
  --------------------------------------------------------------------
  groupby 只是「分组」,不写后面的计算,它什么都不返回给你看。
  它返回的是一个 GroupBy 对象,必须再接一个动作才有结果。
  分了 2 组:一班、二班。
  ! groupby 是「拆分、计算、合并」三步,只写第一步等于什么都没干。

  ② 接一个均值
  --------------------------------------------------------------------

| 班级 | 语文 | 数学 | 英语 |
|---|---:|---:|---:|
| 一班 | 86.0 | 83.2 | 85.8 |
| 二班 | 79.4 | 82.8 | 78.0 |

  两个班三科均值的对照,一行就出来了。一班语文 86.00,二班 79.40。
  行标签是班级,列是科目——这是一张「班级 × 科目」的交叉表。
  ! 分组之后结果的行标签是分组键,不是 0、1、2。想拉平就 reset_index。

  ③ 换个函数
  --------------------------------------------------------------------

| 班级 | count | sum | mean | max |
|---|---:|---:|---:|---:|
| 一班 | 5 | 1275 | 255.0 | 277 |
| 二班 | 5 | 1201 | 240.2 | 274 |

  一次给 count、sum、mean、max 四个数,行是班级,列是四个函数名。
  这里 count 给的是「有几条记录」,和 size 一样(没有缺失时)。
  ! agg 传一个函数名列表,等于一次跑好几个统计——比循环快,也比循环好读。

  ④ 分组键换成两列
  --------------------------------------------------------------------

| 班级 | 姓名 | 总分 |
|---|---:|---:|
| 一班 | 刘洋 | 252 |
| 一班 | 张伟 | 265 |
| 一班 | 李娜 | 223 |
| 一班 | 王芳 | 277 |

  传列表就是多级分组,行标签变成两层。
  姓名每个都不一样,所以这一组每组只有一行——多级分组要有意义,键得有重复。
  ! 多级分组之后想按其中一层算,用 xs 或者先把行标签 reset 成一列再筛。

type(G).__name__ 打出来是 DataFrameGroupBy——它是个中间对象,不是表。 分组后行数会变。按班级分两组就剩 2 行,这是「合并」这一步做的。

30 秒实验

  1. 把 groupby 后面的 mean() 去掉,直接 print(G),看打出来是什么。
  2. 把分组键换成「姓名」,看结果有几行、有没有意义。
  3. 想一想:按班级分组求总分之和,两个班加起来和整表的总和一样吗?

常见误区

  1. 只写 groupby 就以为算完了:什么都不会输出。
  2. 忘了 groupby 后面要挑列:数值列都算一遍,还混进没意义的列。
  3. 以为 count 就是行数:它只数非空,有缺失时偏小。
  4. 用姓名做分组键:每组一行,等于给每行加了个标签。
自测三问(点开对答案) 1. groupby 的三步是什么? —— 拆分(按键分组)、计算(每组跑一个函数)、合并(把结果拼回一张表)。 2. 为什么 groupby 之后什么都不显示? —— 它只完成第一步,返回的是 GroupBy 对象,必须再接 mean、sum、agg 才有结果。 3. count 和 size 有什么区别? —— size 数行数,count 数非空值。没有缺失时两者一样,有缺失时 count 偏小。

分组会用了,但只能接现成的函数。下一篇讲怎么把自定义函数塞进去。

相关笔记