Pandas基础(三):分组、聚合、变换、过滤
DataWhale第十二期组队学习:python Pandas
本文所使用的数据集如下所示,获取链接:joyful-pandas
import numpy as np
import pandas as pd
df = pd.read_csv('data/table.csv',index_col='ID')
df.head(10)
School | Class | Gender | Address | Height | Weight | Math | Physics | |
---|---|---|---|---|---|---|---|---|
ID | ||||||||
1101 | S_1 | C_1 | M | street_1 | 173 | 63 | 34.0 | A+ |
1102 | S_1 | C_1 | F | street_2 | 192 | 73 | 32.5 | B+ |
1103 | S_1 | C_1 | M | street_2 | 186 | 82 | 87.2 | B+ |
1104 | S_1 | C_1 | F | street_2 | 167 | 81 | 80.4 | B- |
1105 | S_1 | C_1 | F | street_4 | 159 | 64 | 84.8 | B+ |
1201 | S_1 | C_2 | M | street_5 | 188 | 68 | 97.0 | A- |
1202 | S_1 | C_2 | F | street_4 | 176 | 94 | 63.5 | B- |
1203 | S_1 | C_2 | M | street_6 | 160 | 53 | 58.8 | A+ |
1204 | S_1 | C_2 | F | street_5 | 162 | 63 | 33.8 | B |
1205 | S_1 | C_2 | F | street_6 | 167 | 63 | 68.4 | B- |
该数据集包括了一个学生的以下信息
学校、班级、性别、地址、身高、体重、数学、物理
分组操作涉及拆分对象,应用函数以及重组结果三种,或是它们的组合。
这可用于对大量数据进行分组并在这些组上进行计算操作, 经过groupby
后会生成一个groupby
对象,该对象本身不会返回任何东西,只有当相应的方法被调用才会起作用
通过学生的学校进行分组,得到一个groupby
对象grouped_single
(后续会用到),并通过groups
属性显示分组的信息(学校1和学校2)
grouped_single = df.groupby('School')
grouped_single.groups
{'S_1': Int64Index([1101, 1102, 1103, 1104, 1105, 1201, 1202, 1203, 1204, 1205, 1301,
1302, 1303, 1304, 1305],
dtype='int64', name='ID'),
'S_2': Int64Index([2101, 2102, 2103, 2104, 2105, 2201, 2202, 2203, 2204, 2205, 2301,
2302, 2303, 2304, 2305, 2401, 2402, 2403, 2404, 2405],
dtype='int64', name='ID')}
知道了分组的信息之后,如果要取出一个组,比如显示学校1分组信息,可以通过get_group
属性来查看。结果是一个DataFrame
grouped_single.get_group('S_1')
如果我想要查看学校2,班级4的学生信息,这里的grouped_mul
后续也会用到,其中的每一个分组都对应一个学校的一个班级
grouped_mul = df.groupby(['School','Class'])
grouped_mul.get_group(('S_2','C_4'))
当分组的要求和信息多起来之后,如果我们要查看整体的信息,比如有几个组,每个组多少样本
# 查看每个组各有多少个样本
grouped_single.size()
grouped_mul.size()
# 查看总共有几个组
grouped_single.ngroups # 2
grouped_mul.ngroups # 7
如果我们想要遍历一个组,该如何操作?
其中name
对应的是我们的组名,是一个字符串,group
就是每个组对应的DataFrame
for name,group in grouped_single:
print(name)
display(group.head())
那那那如果是多级索引呢,可以看看下面这个例子。如果和之前一样还是通过学校进行分类,那么每一个类别下,比如学校1类别下,每一个样本都具有两个标签。
简单来说,就是多级索引的时候要多设置一个level
标签来决定以哪级索引为准进行分组
# 先把原来的df设定为多级索引
df.set_index(['Gender','School'])
# 以学校进行分类
df.set_index(['Gender','School']).groupby(level=1)
# 以性别进行分类
df.set_index(['Gender','School']).groupby(level=0)
之前提到,groupby
函数会返回一个groupby
对象,那这个对象有哪些可以用的方法呢,前面几个是对应的属性,后面的就是对应的方法了
print([attr for attr in dir(grouped_single) if not attr.startswith('_')])
['Address', 'Class', 'Gender', 'Height', 'Math', 'Physics', 'School', 'Weight', 'agg', 'aggregate', 'all', 'any', 'apply', 'backfill', 'bfill', 'boxplot', 'corr', 'corrwith', 'count', 'cov', 'cumcount', 'cummax', 'cummin', 'cumprod', 'cumsum', 'describe', 'diff', 'dtypes', 'expanding', 'ffill', 'fillna', 'filter', 'first', 'get_group', 'groups', 'head', 'hist', 'idxmax', 'idxmin', 'indices', 'last', 'mad', 'max', 'mean', 'median', 'min', 'ndim', 'ngroup', 'ngroups', 'nth', 'nunique', 'ohlc', 'pad', 'pct_change', 'pipe', 'plot', 'prod', 'quantile', 'rank', 'resample', 'rolling', 'sem', 'shift', 'size', 'skew', 'std', 'sum', 'tail', 'take', 'transform', 'tshift', 'var']
这里与普通的DataFrame
对象不一样的,如果使用head
函数,返回的是每个组的前几行,而不是数据集的前几行,另外first
显示的是以分组为索引的每组的第一个分组信息
grouped_single.head(2)
grouped_single.first()
之前我们的分组是通过自身的例如学校、班级这样的属性来进行分组的,但对于groupby
函数而言,分组的依据是非常自由的,只要是与数据框长度相同的列表即可,同时支持函数型分组
比如我们用a
、b
、c
来给这些数据进行分组,每一条都可以归为其中一种
例如这里就是随机生成了一个长度与数据长度相同的,包含了abc
其中一个的列表。并以此分组
df.groupby(np.random.choice(['a','b','c'],df.shape[0]))
也可以通过一样的get_group
方式来进行查看
df.groupby(np.random.choice(['a','b','c'],df.shape[0])).get_group('a')
之前提到了分组也可以通过函数进行分组,通过函数进行分组时,传入的对象就是索引
# x就是每一条数据的索引,这里将其打印出来
df[:5].groupby(lambda x:print(x)).head()
# 1101 1102 1103 1104 1105
如果是多层索引,那么函数型分组时,传入的就是元组
也可以根据奇偶行来进行分组
if
条件为真返回前面的内容,为假返回后面的内容
if not
条件为真返回后面的内容,为假返回前面的内容
df.groupby(lambda x:'奇数行' if not df.index.get_loc(x)%2==1 else '偶数行').groups
我们的groupby
对象中有数据自身例如学校、班级这样的属性,但如果是通过方法的形式调用,即通过.
的方式调用,返回的依然是一个SeriesGroupBy
对象
我们可以通过groupby
的[]
操作来选出它的某个或某几个列
下面的代码返回的就是每个分类以及一个对应的布尔值
df.groupby(['Gender','School'])['Math'].mean()>=60
Gender School
F S_1 True
S_2 True
M S_1 True
S_2 False
Name: Math, dtype: bool
也可以选出多列
df.groupby(['Gender','School'])[['Math','Height']].mean()
如果是连续性的变量,甚至可以通过cut
函数来进行分组
bins = [0,40,60,80,90,100]
cuts = pd.cut(df['Math'],bins=bins) #可选label添加自定义标签
df.groupby(cuts)['Math'].count()
Math
(0, 40] 7
(40, 60] 10
(60, 80] 9
(80, 90] 7
(90, 100] 2
Name: Math, dtype: int64
所谓聚合,就是把一堆数变成一个标量,因此
mean/sum/size/count/std/var/sem/describe/first/last/nth/min/max
都是聚合函数
假设我们现在有这样一个数据集,来自原来的grouped_single
,现在仅用数学这一列,那这就是一个根据学校进行分类的学生的数学成绩的Series
group_m = grouped_single['Math']
group_m.head() # 每一组显示前5行
ID
1101 34.0
1102 32.5
1103 87.2
1104 80.4
1105 84.8
2101 83.3
2102 50.6
2103 52.5
2104 72.2
2105 34.2
Name: Math, dtype: float64
如果我们要调用单个聚合函数,可以像这样
group_m.sem()
School
S_1 5.958578
S_2 3.933088
Name: Math, dtype: float64
如果我们要调用多个聚合函数,可以像这样
group_m.agg(['sum','mean','std'])
sum | mean | std | |
---|---|---|---|
School | |||
S_1 | 956.2 | 63.746667 | 23.077474 |
S_2 | 1191.1 | 59.555000 | 17.589305 |
如果不想用sum
、mean
等等作为列名,可以自己修改
group_m.agg([('rename_sum','sum'),('rename_mean','mean')])
如果不只是数学一列,而是有多列怎么办,那可以指定列和对应函数
grouped_mul.agg({
'Math':['mean','max'],'Height':'var'})
当然也可以使用自定义的函数进行处理,agg函数的传入是分组逐列进行的,例如计算极差
grouped_single['Math'].agg(lambda x:x.max()-x.min())
这里要介绍另一个函数NamedAgg
,可以指定聚合的列,可以指定输出的列,就不按照之前分组逐列进行了。
其有两个参数,一个是指定输出列名,一个指定要使用的方法,例如这个官网中的例子
In [79]: animals = pd.DataFrame({
'kind': ['cat', 'dog', 'cat', 'dog'],
....: 'height': [9.1, 6.0, 9.5, 34.0],
....: 'weight': [7.9, 7.5, 9.9, 198.0]})
....:
In [80]: animals
Out[80]:
kind height weight
0 cat 9.1 7.9
1 dog 6.0 7.5
2 cat 9.5 9.9
3 dog 34.0 198.0
In [81]: animals.groupby("kind").agg(
....: min_height=pd.NamedAgg(column='height', aggfunc='min'),
....: max_height=pd.NamedAgg(column='height', aggfunc='max'),
....: average_weight=pd.NamedAgg(column='weight', aggfunc=np.mean),
....: )
....:
Out[81]:
min_height max_height average_weight
kind
cat 9.1 9.5 8.90
dog 6.0 34.0 102.75
filter
函数的作用就是返回一个DataFrame
的副本,并且通过布尔值判断删除掉那些不满足要求的数据。
比如下面这个,先按照学校分组,然后取出数学、物理两列,针对每组每列的数学成绩进行一个布尔表达式的判断,然后把每一组的所有布尔值进行“与”操作,最终每组(即每个学校)都有一个布尔值。(这个值表达什么我也不知道)
grouped_single[['Math','Physics']].filter(lambda x:(x['Math']>32).all())
transform
就是通过传入的函数对DataFrame
的值进行一个值的变换,按照分组分列进行
# 两列都减去均值
grouped_single[['Math','Height']].transform(lambda x:x-x.min())
如果是一个标量值,那么组内所有的元素都会被广播为这个值
# 这两列都变成均值
grouped_single[['Math','Height']].transform(lambda x:x.mean())
我们可以用这个方法来进行组内的标准化
grouped_single[['Math','Height']].transform(lambda x:(x-x.mean())/x.std())
也可以用这个方法来填充组内的缺失值
# 先生成一个有缺失值的数据表
df_nan = df[['Math','School']].copy().reset_index()
df_nan.loc[np.random.randint(0,df.shape[0],25),['Math']]=np.nan
# 填充缺失值
df_nan.groupby('School').transform(lambda x: x.fillna(x.mean()))
apply
也是一种通过函数来对DataFrame
进行操作的函数
传递给apply
的函数必须以dataframe
作为其第一个参数,并返回dataframe
、Series
或scalar
。然后,apply
将负责将结果重新组合到单个dataframe
或Series
中。因此,apply
是一种高度灵活的分组方法
虽然apply
是一种非常灵活的方法,但它的缺点是使用它可能比使用agg
或transform
等更具体的方法要慢一些
apply函数的灵活性很大程度来源于其返回值的多样性
# 标量返回值
# 找到每个分组中数学和身高的最大值
df[['School','Math','Height']].groupby('School').apply(lambda x:x.max())
# 列表返回值
df[['School','Math','Height']].groupby('School').apply(lambda x:x-x.min()).head()
# 数据框返回值
df[['School','Math','Height']].groupby('School')\
.apply(lambda x:pd.DataFrame({
'col1':x['Math']-x['Math'].max(),
'col2':x['Math']-x['Math'].min(),
'col3':x['Height']-x['Height'].max(),
'col4':x['Height']-x['Height'].min()}))
用apply同时统计多个指标
# 有序字典类
from collections import OrderedDict
def f(df):
data = OrderedDict()
data['M_sum'] = df['Math'].sum()
data['W_var'] = df['Weight'].var()
data['H_mean'] = df['Height'].mean()
print(data)
return pd.Series(data)
grouped_single.apply(f)