一、分组模式及其对象

1. 分组的一般模式

分组操作在日常生活中使用极其广泛,例如:

  • 依据 性别 分组,统计全国人口 寿命平均值
  • 依据 季节 分组,对每一个季节的 温度 进行 组内标准化
  • 依据 班级 分组,筛选出组内 数学分数平均值超过80分的班级

从上述的几个例子中不难看出,想要实现分组操作,必须明确三个要素:分组依据数据来源操作及其返回结果 。同时从充分性的角度来说,如果明确了这三方面,就能确定一个分组操作,从而分组代码的一般模式即:

df.groupby(分组依据)[数据来源].使用操作

例如第一个例子中的代码就应该如下:

df.groupby('Gender')['Longevity'].mean()

现在返回到学生体测的数据集上,如果想要按照性别统计身高中位数,就可以如下写出:

In [3]: df = pd.read_csv('data/learn_pandas.csv')
In [4]: df.groupby('Gender')['Height'].median()
Out[4]: 
Gender
Female    159.6
Male      173.4
Name: Height, dtype: float64

2. 分组依据的本质

前面提到的若干例子都是以单一维度进行分组的,比如根据性别,如果现在需要根据多个维度进行分组,该如何做?事实上,只需在 groupby 中传入相应列名构成的列表即可。例如,现希望根据学校和性别进行分组,统计身高的均值就可以如下写出:

In [5]: df.groupby(['School', 'Gender'])['Height'].mean()
Out[5]: 
School                         Gender
Fudan University               Female    158.776923
                               Male      174.212500
Peking University              Female    158.666667
                               Male      172.030000
Shanghai Jiao Tong University  Female    159.122500
                               Male      176.760000
Tsinghua University            Female    159.753333
                               Male      171.638889
Name: Height, dtype: float64

目前为止, groupby 的分组依据都是直接可以从列中按照名字获取的,那如果希望通过一定的复杂逻辑来分组,例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值。

首先应该先写出分组条件:

In [6]: condition = df.Weight > df.Weight.mean()

然后将其传入 groupby 中:

In [7]: df.groupby(condition)['Height'].mean()
Out[7]: 
Weight
False    159.034646
True     172.705357
Name: Height, dtype: float64

练一练

请根据上下四分位数分割,将体重分为high、normal、low三组,统计身高的均值。

从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是 TrueFalse )来分组,下面用随机传入字母序列来验证这一想法:

In [8]: item = np.random.choice(list('abc'), df.shape[0])
In [9]: df.groupby(item)['Height'].mean()
Out[9]: 
a    163.924242
b    162.928814
c    162.708621
Name: Height, dtype: float64

此处的索引就是原先item中的元素,如果传入多个序列进入 groupby ,那么最后分组的依据就是这两个序列对应行的唯一组合:

In [10]: df.groupby([condition, item])['Height'].mean()
Out[10]: 
Weight   
False   a    160.193617
        b    158.921951
        c    157.756410
True    a    173.152632
        b    172.055556
        c    172.873684
Name: Height, dtype: float64

由此可以看出,之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据来自于数据来源组合的unique值,通过 drop_duplicates 就能知道具体的组类别:

In [11]: df[['School', 'Gender']].drop_duplicates()
Out[11]: 
                           School  Gender
0   Shanghai Jiao Tong University  Female
1               Peking University    Male
2   Shanghai Jiao Tong University    Male
3                Fudan University  Female
4                Fudan University    Male
5             Tsinghua University  Female
9               Peking University  Female
16            Tsinghua University    Male
In [12]: df.groupby([df['School'], df['Gender']])['Height'].mean()
Out[12]: 
School                         Gender
Fudan University               Female    158.776923
                               Male      174.212500
Peking University              Female    158.666667
                               Male      172.030000
Shanghai Jiao Tong University  Female    159.122500
                               Male      176.760000
Tsinghua University            Female    159.753333
                               Male      171.638889
Name: Height, dtype: float64

3. Groupby对象

能够注意到,最终具体做分组操作时,所调用的方法都来自于 pandas 中的 groupby 对象,这个对象上定义了许多方法,也具有一些方便的属性。

In [13]: gb = df.groupby(['School', 'Grade'])
In [14]: gb
Out[14]: <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002647F096DC8>

通过 ngroups 属性,可以得到分组个数:

In [15]: gb.ngroups
Out[15]: 16

通过 groups 属性,可以返回从 组名 映射到 组索引列表 的字典:

In [16]: res = gb.groups
In [17]: res.keys() # 字典的值由于是索引,元素个数过多,此处只展示字典的键
Out[17]: dict_keys([('Fudan University', 'Freshman'), ('Fudan University', 'Junior'), ('Fudan University', 'Senior'), ('Fudan University', 'Sophomore'), ('Peking University', 'Freshman'), ('Peking University', 'Junior'), ('Peking University', 'Senior'), ('Peking University', 'Sophomore'), ('Shanghai Jiao Tong University', 'Freshman'), ('Shanghai Jiao Tong University', 'Junior'), ('Shanghai Jiao Tong University', 'Senior'), ('Shanghai Jiao Tong University', 'Sophomore'), ('Tsinghua University', 'Freshman'), ('Tsinghua University', 'Junior'), ('Tsinghua University', 'Senior'), ('Tsinghua University', 'Sophomore')])

练一练

上一小节介绍了可以通过 drop_duplicates 得到具体的组类别,现请用 groups 属性完成类似的功能。

size 作为 DataFrame 的属性时,返回的是表长乘以表宽的大小,但在 groupby 对象上表示统计每个组的元素个数:

In [18]: gb.size()
Out[18]: 
School                         Grade  
Fudan University               Freshman      9
                               Junior       12
                               Senior       11
                               Sophomore     8
Peking University              Freshman     13
                               Junior        8
                               Senior        8
                               Sophomore     5
Shanghai Jiao Tong University  Freshman     13
                               Junior       17
                               Senior       22
                               Sophomore     5
Tsinghua University            Freshman     17
                               Junior       22
                               Senior       14
                               Sophomore    16
dtype: int64

通过 get_group 方法可以直接获取所在组对应的行,此时必须知道组的具体名字:

In [19]: gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3] # 展示一部分
Out[19]: 
              School     Grade             Name
15  Fudan University  Freshman  Changqiang Yang
28  Fudan University  Freshman     Gaoqiang Qin
63  Fudan University  Freshman     Gaofeng Zhao

这里列出了2个属性和2个方法,而先前的 meanmedian 都是 groupby 对象上的方法,这些函数和许多其他函数的操作具有高度相似性,将在之后的小节进行专门介绍。

4. 分组的三大操作

熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型分组返回的数据型态并不一样:

  • 第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量 size
  • 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个 Series 类型
  • 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了 DataFrame 类型

由此,引申出分组的三大操作:聚合、变换和过滤,分别对应了三个例子的操作,下面就要分别介绍相应的 aggtransformfilter 函数及其操作。