一、分组模式及其对象
分组需要明确三个要素:分组依据
、数据来源
、操作及其返回结果
,通俗上来说就是在哪些数据上对什么进行分组,做了哪些操作,一般模式为
df.groupby(分组依据)[数据来源].使用操作
## 具体点
df.groupby('School')['Height'].mean() # 根据学校分组,对分组数据中Height这一列求平均值,得到的每个学校身高的平均值
根据多列进行分组只需要将标量改成列表即可:df.groupby(['School','Gender'])['Height'].mean()
另外,pandas
还支持根据传入条件分组:
condition = df.Weight > df.Weight.mean()
df.groupby(condition)['Height'].mean()
练一练
请根据上下四分位数分割,将体重分为high、normal、low三组,统计身高的均值。
weight = df['Weight']
low = weight.quantile(0.25)
high = weight.quantile(0.75)
weight = weight.mask(weight<low,'low').mask(weight > high,'high').mask(((weight >= weight.quantile(0.25))&(weight <= weight.quantile(0.75))),'mid')
df_test = df.copy()
df_test['Weight'] = weight
df_test.groupby('Weight')['Height'].mean()
分别上面流式编程的代码可以看出,操作的都是groupby
对象,groupby
对象不可直接输出,其中定义了一些属性及其方法,搭配使用即可得到一些想要的输出,下面这些例子可以解释这句话:
gb = df.groupby(['School', 'Grade'])
gb
## 输出<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000024372714408>
## 得到分组个数
gb.ngroups
## 输出16
类似的属性还有很多:
gb.ngroups
:返回分组个数gb.size()
:统计每组元素的个数gb.get_group((想得到的分组))
:比如gb.get_group(('Fudan University', 'Freshman'))
,这里的返回值是一个DataFrame
对象,因此可以使用iloc
等方法部分展示gb.mean()
:返回每列的平均数,可以返回指定列的平均数,比如gb.mean['Height']
,当不指定时返回的是所有数字列的平均数gb.median()
:返回中位数,用法同上
二、聚合函数
1.内置聚合函数
max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod
是一些直接定义在groupby
上的聚合函数
练一练
请查阅文档,明确 all/any/mad/skew/sem/prod
函数的含义。
当聚合函数传入的数据来源包含多个列时,逐列进行迭代,如下所示:
gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
## 输出
Height Weight
Gender
Female 170.2 63.0
Male 193.9 89.0
2.agg函数
agg函数解决了以下几个问题:
- 无法同时使用多个函数
- 无法对特定的列使用特定的聚合函数
- 无法使用自定义的聚合函数
- 无法直接对结果的列名在聚合前进行自定义命名
使用多个函数聚合
gb.agg(['sum', 'idxmax', 'skew'])
对特定的列聚合
可以通过传入字典的方式
gb.agg({'Height':['mean','max'], 'Weight':'count'}).head()
练一练
请使用传入字典的方法完成等价的聚合任务。
gb.agg({'Height':['sum','idxmax','skew'], 'Weight':['sum','idxmax','skew']})
使用自定义函数
在 agg
中可以使用具体的自定义函数, 需要注意传入函数的参数是之前数据源中的列,逐列进行计算 。下面分组计算身高和体重的极差:
gb.agg(lambda x: x.mean()-x.min())
练一练
在 groupby
对象中可以使用 describe
方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。
值得注意的是agg
函数传入的是序列,因此在自定义函数中也能使用Series
所包含的属性和方法
聚合结果重命名
gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
传入的是一个由元组构成的列表,每个列表第一个元素是新起的名字,第二个是聚合函数
重命名还有以下几种语法:
## 对my_func聚合结果重命名为my_func,sum函数不重命名
gb.agg({'Height': [('my_func', my_func), 'sum'],'Weight': lambda x:x.max()})
## 别忘了括号,不然不知道是否是使用多个函数聚合
gb.agg([('my_sum', 'sum')])
三、变换和过滤
1.变换
练一练
在 groupby
对象中, rank
方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。
## 得到倒数第二高的行
df[df.Height.rank(method='dense') == 2]
transform
方法和agg
方法的传入值是一样的,都是数据源的序列Series
,结合下面的例子应该能很好的明白:
gb.transform(lambda x: (x-x.mean())/x.std()).head()
Height Weight
0 -0.058760 -0.354888
1 -1.010925 -0.355000
2 2.167063 2.089498
3 NaN -1.279789
4 0.053133 0.159631
需要注意的是返回的结果是行列索引和数据源一致的DataFrame
,从上面的例子也可以看出。
上面的transform
中参数的返回值都是一个和原数据相同长度的序列,其实还可以返回一个标量,这个标量会广播到其所在的组别,例子如下
gb.transform('mean').head() # 传入返回标量的函数也是可以的
## 结果
Height Weight
0 159.19697 47.918519
1 173.62549 72.759259
2 173.62549 72.759259
3 159.19697 47.918519
4 173.62549 72.759259
2.组过滤
和前面一章学习的索引不同,索引是对每行元素的筛选,组过滤是对于每个组别的筛选,符合条件的组别将保留,不符合条件的组别将被过滤。
pandas
中使用filter
函数完成组过滤功能,接收一个参数,传入的是DataFrame
对象本身,返回值需要为bool
值,最终会根据布尔值来选取未被过滤的组其对应的所在行拼接成一个DataFrame
返回,例子如下:
## 挑选出数量大于100的组别
gb.filter(lambda x: x.shape[0] > 100).head()
练一练
从概念上说,索引功能是组过滤功能的子集,请使用 filter
函数完成 loc[.]
的功能,这里假设 ” .
“是元素列表。
## 挑选出性别为女的组别
def my_func(x):
return x.name=='Female'
gb.filter(my_func)
四、跨列分组
当我们需要对多列进行操作,并且返回标量时,上述的过滤、转换、agg
函数都不可用了,这时候,就需要引入apply
函数帮助我们解决问题,下面举个分组计算组BMI均值的例子:
def BMI(x):
Height = x['Height']/100
Weight = x['Weight']
BMI_value = Weight/Height**2
return BMI_value.mean()
gb.apply(BMI)
## 结果
Gender
Female 18.860930
Male 24.318654
dtype: float64
apply
函数传入的参数和filter
完全一致,返回值两者有所不同,后者只能返回布尔值
apply
传入的函数较为灵活,可返回标量
、Series
、DataFrame
- 当函数返回为标量时,如上面的例子所示
- 当函数返回值为
Series
,会得到一个DataFrame
对象,行索引与返回标量的情况一致,列索引为Series
的列索引 - 当函数返回值为
DataFrame
时,行索引为分组情况加上一层该DataFrame
的索引,列索引为DataFrame
的列索引,例子如下
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)),index = ['a','b'],columns=pd.Index([('w','x'),('y','z')])))
## 结果
w y
x z
Gender
Female a 1.0 1.0
b 1.0 1.0
Male a 1.0 1.0
b 1.0 1.0
需要注意的是,虽然apply
很灵活,但是和内置函数相比性能上可能存在差距,如果可以的话建议优先使用内置函数
五、练习
Ex1:汽车数据集
现有一份汽车数据集,其中 Brand, Disp., HP
分别代表汽车品牌、发动机蓄量、发动机输出。
In [45]: df = pd.read_csv('data/car.csv')
In [46]: df.head(3)
Out[46]:
Brand Price Country Reliability Mileage Type Weight Disp. HP
0 Eagle Summit 4 8895 USA 4.0 33 Small 2560 97 113
1 Ford Escort 4 7402 USA 2.0 33 Small 2345 114 90
2 Ford Festiva 4 6319 Korea 4.0 37 Small 1845 81 63
- 先过滤出所属
Country
数超过2个的汽车,即若该汽车的Country
在总体数据集中出现次数不超过2则剔除,再按Country
分组计算价格均值、价格变异系数、该Country
的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
。 - 按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计
Price
的均值。 - 对类型
Type
分组,对Price
和HP
分别计算最大值和最小值,结果会产生多级索引,请用下划线把多级列索引合并为单层索引。 - 对类型
Type
分组,对HP
进行组内的min-max
归一化。 - 对类型
Type
分组,计算Disp.
与HP
的相关系数。
第一问:
gb_demo = df.groupby('Country')
gb_demo = gb_demo.filter(lambda x : x.shape[0] > 2)
gb_demo.groupby('Country')['Price'].agg(['mean', ('CoV',lambda x : x.std()/x.mean()), 'count'])
第二问:
df.shape[0]
## 结果
60
condition = ['Head']*20+['Mid']*20+['Tail']*20
df.groupby(condition)['Price'].mean()
上面这个是参考答案,利用传入一个列表做标识进行分组
第三问:
def func(x):
return x[0]+'_'+x[1]
col_idx = df_demo.columns.map(func)
df_copy = df_demo.copy()
df_copy.columns = col_idx
df_copy
第四问:
df_demo = df.groupby('Type')['HP'].transform(lambda x: (x-x.min())/(x.max()-x.min()))
df_demo.head()
第五问:
df.groupby('Type')['Disp.','HP'].corr()
Ex2:实现transform函数
groupby
对象的构造方法是my_groupby(df, group_cols)
- 支持单列分组与多列分组
- 支持带有标量广播的
my_groupby(df)[col].transform(my_func)
功能 pandas
的transform
不能跨列计算,请支持此功能,即仍返回Series
但col
参数为多列- 无需考虑性能与异常处理,只需实现上述功能,在给出测试样例的同时与
pandas
中的transform
对比结果是否一致
这几天比较忙,这题参考答案代码暂时还没看完,周六周日会补上