DataWhale & Pandas(四、分组)
学习大纲:
目录
补充:
分组统计方法
分组对象支持几乎所有的 df 的统计方法,见数学统计方法
grouped = s.groupby(level=0) # 唯一索引用.groupby(level=0),将同一个index的分为一组
print(grouped)
print(grouped.first(),'→ first:非NaN的第一个值\n')
print(grouped.last(),'→ last:非NaN的最后一个值\n')
print(grouped.sum(),'→ sum:非NaN的和\n')
print(grouped.mean(),'→ mean:非NaN的平均值\n')
print(grouped.median(),'→ median:非NaN的算术中位数\n')
print(grouped.count(),'→ count:非NaN的值\n')
print(grouped.min(),'→ min、max:非NaN的最小值、最大值\n')
print(grouped.std(),'→ std,var:非NaN的标准差和方差\n')
print(grouped.prod(),'→ prod:非NaN的积\n')
grouped.corr()
grouped.sem()
grouped.prod()
grouped.cummax() # 每组的累计最大值
grouped.cumsum() # 累加
grouped.mad() # 平均绝对偏差
# 特别的有
df.groupby('team').ngroups # 5 分组数
df.groupby('team').ngroup() # 分组序号
df.groupby('team').first() # 组内第一个
df.groupby('team').last() # 组内最后一个
# 库姆计数,按组对成员标记, 支持正排倒排
# 返回每个元素在所在组的序号的序列
grouped.cumcount(ascending=False)
通过字典或者Series分组
df = pd.DataFrame(np.arange(16).reshape(4,4),columns = ['a','b','c','d'])
print(df)
mapping = {'a':'one','b':'one','c':'two','d':'two','e':'three'}
by_column = df.groupby(mapping, axis = 1)
print(by_column.sum())
# mapping中,a、b列对应的为one,c、d列对应的为two,以字典来分组
s = pd.Series(mapping)
print(s,'\n')
print(s.groupby(s).count())
# s中,index中a、b对应的为one,c、d对应的为two,以Series来分组
分组 - 可迭代对象
df = pd.DataFrame({'X' : ['A', 'B', 'A', 'B'], 'Y' : [1, 4, 3, 2]})
print(df)
print(df.groupby('X'), type(df.groupby('X')))
print('-----')
print(list(df.groupby('X')), '→ 可迭代对象,直接生成list\n')
print(list(df.groupby('X'))[0], '→ 以元祖形式显示\n')
for n,g in df.groupby('X'):
print(n)
print(g)
print('###')
print('-----')
# n是组名,g是分组后的Dataframe
print(df.groupby(['X']).get_group('A'),'\n')
print(df.groupby(['X']).get_group('B'),'\n')
print('-----')
# .get_group()提取分组后的组
grouped = df.groupby(['X'])
print(grouped.groups)
print(grouped.groups['A']) # 也可写:df.groupby('X').groups['A']
print('-----')
# .groups:将分组后的groups转为dict
# 可以字典索引方法来查看groups里的元素
sz = grouped.size()
print(sz,type(sz))
print('-----')
# .size():查看分组后的长度
df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar','foo', 'bar', 'foo', 'foo'],
'B' : ['one', 'one', 'two', 'three', 'two', 'two', 'one', 'three'],
'C' : np.random.randn(8),
'D' : np.random.randn(8)})
print(df)
print()
print(df.groupby(['A','B']))
print()
grouped = df.groupby(['A','B']).groups
print(grouped)
print()
print(grouped[('foo', 'three')])
# 按照两个列进行分组
聚合 Aggregations
.aggregate()
简写为.agg()
。它的作用是将分组后的对象给定统计方法,也支持按字段分别给定不同的统计方法。
# 所有列使用一个计算计算方法
df.groupby('team').aggregate(sum)
df.groupby('team').agg(sum)
grouped.agg(np.size)
grouped['Q1'].agg(np.mean)
## 多个计算方法
# 所有列指定多个计算方法
grouped.agg([np.sum, np.mean, np.std])
# 指定列使用多个计算方法
grouped['Q1'].agg([sum, np.mean, np.std])
# 一列使用多个计算方法
df.groupby('team').agg({'Q1': ['min', 'max'], 'Q2': 'sum'})
# 指定列名,列表是为原列和方法
df.groupby('team').Q1.agg(Mean='mean', Sum='sum')
df.groupby('team').agg(Mean=('Q1', 'mean'), Sum=('Q2', 'sum'))
df.groupby('team').agg(
Q1_max=pd.NamedAgg(column='Q1', aggfunc='max'),
Q2_min=pd.NamedAgg(column='Q2', aggfunc='min')
)
# 如果列名不是有效的 python 变量,则可以用以下方法
df.groupby('team').agg(**{
'1_max':pd.NamedAgg(column='Q1', aggfunc='max')})
# 聚合结果使用函数
# lambda/函数 所有方法都可以用
def max_min(x):
return x.max() - x.min()
# 定义函数
df.groupby('team').Q1.agg(Mean='mean',
Sum='sum',
Diff=lambda x: x.max() - x.min(),
Max_min=max_min
)
# 不同列不同的计算方法
df.groupby('team').agg({'Q1': sum, # 总和
'Q2': 'count', # 总数
'Q3':'mean', # 平均
'Q4': max}) # 最大值
# 分组对象使用函数
# 定义函数
def max_min(var):
return var.max() - var.min()
# 调用函数
df.groupby('team').agg(max_min)
筛选 Filtration
filter()
筛选后后显示筛选后的原 dataframe,内容是满足组条件的所有明细:
# 值的长度都大于等于 3 的
df.groupby('team').filter(lambda x: len(x) >= 3)
# Q1成绩只要有一个大于97的组
df.groupby(['team']).filter(lambda x: (x['Q1'] > 97).any())
# 所有成员平均成绩大于 60 的组
df.groupby(['team']).filter(lambda x: (x.mean() >= 60).all())
# Q1 所有成员成绩之和超过 1060 的组
df.groupby('team').filter(lambda g: g.Q1.sum() > 1060)
时序重采样 resample
使用
TimeGrouper
对时间进行分组:
idx = pd.date_range('1/1/2000', periods=100, freq='T')
df = pd.DataFrame(data=1 * [range(2)],
index=idx,
columns=['a', 'b'])
# 三个周期一聚合(一分钟一个周期)
df.groupby('a').resample('3T').sum()
# 30 秒一分组
df.groupby('a').resample('30S').sum()
# 每月
df.groupby('a').resample('M').sum()
# 以右边时间点为标识
df.groupby('a').resample('3T', closed='right').sum()
例子:按组填充缺失值
需要在源数据中填充缺失值,在类别列标示的AB两组数据中,每列每组只有一个有值的数据,需要按组将缺失值填充为这个值。
源数据:
类别 col1 col2 col3
A NaN NaN NaN
A d NaN NaN
A NaN NaN NaN
A NaN c e
B NaN Y Z
B NaN NaN NaN
B X NaN NaN
将以上源数据中的缺失值填充完成如下样子:
类别 col1 col2 col3
0 A d c e
1 A d c e
2 A d c e
3 A d c e
4 B X Y Z
5 B X Y Z
6 B X Y Z
思路
由于要按组来处理数据,就需要用 Pandas 的 groupby 进行分组,分组后填充数据用 fillna,用 apply 调用函数来应用 fillna。
由于 fillna 的填充方法没有取唯一非空值填充的方法,我们需要用
method='bfill'
先向后填充,再用method='ffill'
向前填充,来完成所有缺失值位置的填充。
方法一:
# 结果为需求中的目标数据
df.groupby('类别')
.apply(lambda x: x.fillna(method='bfill'))
.groupby('类别')
.apply(lambda x: x.fillna(method='ffill'))
方法二:
# 一
df.groupby('类别')
.apply(lambda x: x.fillna(method='bfill').fillna(method='ffill'))
# 二
df.groupby('类别')
.apply(lambda x: x.fillna(method='bfill'))
.fillna(method='ffill')
import numpy as np
import pandas as pd
一、分组模式及其对象
1.1. 分组的一般模式
分组操作在日常生活中使用极其广泛,例如:
依据 性别 分组,统计全国人口 寿命 的 平均值
依据 季节 分组,对每一个季节的 温度 进行 组内标准化
依据 班级 分组,筛选出组内 数学分数 的 平均值超过80分的班级
df.groupby(分组依据)[数据来源].使用操作
例如:
df.groupby('Gender')['Longevity'].mean()
如果想要按照性别统计身高中位数:
df.groupby('Gender')['Height'].median()
1.2. 分组依据的本质
如果现在需要根据多个维度进行分组,该如何做?事实上,只需在
groupby
中传入相应列名构成的列表即可。
groupby
的分组依据都是直接可以从列中按照名字获取的
# 例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值。
# 首先应该先写出分组条件:
condition = df.Weight > df.Weight.mean()
#传入groupby
df.groupby(condition)['Height'].mean()
通过
drop_duplicates
就能知道具体的组类别:df[['School', 'Gender']].drop_duplicates()
1.3. Groupby对象
最终具体做分组操作时,所调用的方法都来自于
pandas
中的groupby
对象
gb = df.groupby(['School', 'Grade'])
# 通过 ngroups 属性,可以得到分组个数
gb.ngroups
# 通过 groups 属性,可以返回从 组名 映射到 组索引列表 的字典
res = gb.groups
res.keys() # 字典的值由于是索引,元素个数过多,此处只展示字典的键
gb.size()
通过
get_group
方法可以直接获取所在组对应的行,此时必须知道组的具体名字:gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3]
1.4. 分组的三大操作
三大操作:聚合、变换和过滤,分别对应了:agg 、 transform 和 filter函数及其操作
二、聚合函数
2.1 内置聚合函数
根据返回标量值的原则,包括如下函数:
max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod
2.2 agg方法
虽然在
groupby
对象上定义了许多方便的函数,但仍然有以下不便之处:
无法同时使用多个函数
无法对特定的列使用特定的聚合函数
无法使用自定义的聚合函数
无法直接对结果的列名在聚合前进行自定义命名
【a】使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数对应的字符串传入,先前提到的所有字符串都是合法的。
gb.agg(['sum', 'idxmax', 'skew'])
【b】对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入
agg
中实现,其中字典以列名为键,以聚合字符串或字符串列表为值
gb.agg({'Height':['mean','max'], 'Weight':'count'})
【c】使用自定义函数
需要注意传入函数的参数是之前数据源中的列,逐列进行计算
gb.agg(lambda x: x.mean()-x.min()) #计算身高和体重的极差
【d】聚合结果重命名
将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数
gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
注意
使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串
三、变换和过滤
3.1. 变换函数与transform方法
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数:
cumcount/cumsum/cumprod/cummax/cummin
,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。
gb.cummax().head()
gb.transform(lambda x: (x-x.mean())/x.std()).head()
- 当用自定义变换时需要使用
transform
方法,被调用的自定义函数, 其传入值为数据源的序列 ,与agg
的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的DataFrame
。transform
只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组
3.2. 组索引与过滤
- 过滤在分组中是对于组的过滤,而索引是对于行的过滤,在上述中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果符合筛选条件的则选入结果表,否则不选入。
- 组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回
True
则会被保留,False
则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame
返回。- 在
groupby
对象中,定义了filter
方法进行组的筛选,其中自定义函数的输入参数为数据源构成的DataFrame
本身,在之前例子中定义的groupby
对象中,传入的就是df[['Height', 'Weight']]
,因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
例子:在原表中通过过滤得到所有容量大于100的组:
gb.filter(lambda x: x.shape[0] > 100).head()
四、跨列分组
4.1 apply的引入
- 使用
apply
函数来进行多列数据同时处理
4.2. apply的使用
apply
的自定义函数传入参数与filter
完全一致,只不过后者只允许返回布尔值。- 除了返回标量之外,
apply
方法还可以返回一维Series
和二维DataFrame
【a】标量情况:结果得到的是 Series
,索引与 agg
的结果一致
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x: 0)
# 虽然是列表,但是作为返回值仍然看作标量
gb.apply(lambda x: [0, 0])
【b】 Series
情况:得到的是 DataFrame
,行索引与标量情况一致,列索引为 Series
的索引
gb.apply(lambda x: pd.Series([0,0],index=['a','b']))
【c】 DataFrame
情况:得到的是 DataFrame
,行索引最内层在每个组原先 agg
的结果索引上,再加一层返回的 DataFrame
行索引,同时分组结果 DataFrame
的列索引和返回的 DataFrame
列索引一致
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)),
index = ['a','b'],
columns=pd.Index([('w','x'),('y','z')])))
注意:
apply
函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的groupby
对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用自定义函数来实现。
五、练习
Ex1:汽车数据集
现有一份汽车数据集,其中
Brand, Disp., HP
分别代表汽车品牌、发动机蓄量、发动机输出。
df = pd.read_csv('data/car.csv')
df.head(3)
先过滤出所属
Country
数超过2个的汽车,即若该汽车的Country
在总体数据集中出现次数不超过2则剔除,再按Country
分组计算价格均值、价格变异系数、该Country
的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
。
按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计
Price
的均值。对类型
Type
分组,对Price
和HP
分别计算最大值和最小值,结果会产生多级索引,请用下划线把多级列索引合并为单层索引。对类型
Type
分组,对HP
进行组内的min-max
归一化。对类型
Type
分组,计算Disp.
与HP
的相关系数。
Ex2:实现transform函数
groupby
对象的构造方法是my_groupby(df, group_cols)
支持单列分组与多列分组
支持带有标量广播的
my_groupby(df)[col].transform(my_func)
功能
pandas
的transform
不能跨列计算,请支持此功能,即仍返回Series
但col
参数为多列无需考虑性能与异常处理,只需实现上述功能,在给出测试样例的同时与
pandas
中的transform
对比结果是否一致
心得:
国奖的纪念品里面有个小纸条,上面是这么写的:
- 伴过自习室不眠的分与秒
- 流过赛场上无尽的汗与泪
- 一天 一月 一年
- 你从书窗望去——
- 凌云之志 奋发之姿 是如今的你
- 国之脊梁 世界之光 是你的未来