一、分组模式及其对象
1. 分组的一般模式
想要实现分组操作,必须明确三个要素:分组依据
、 数据来源
、 操作及其返回结果
。一般模式:df.groupby(分组依据)[数据来源].使用操作
df = pd.read_csv('data/learn_pandas.csv')
# 按照性别统计身高中位数
df.groupby('Gender')['Height'].median()
Gender
Female 159.6
Male 173.4
Name: Height, dtype: float64
2. 分组依据的本质
多个维度进行分组,只需在 groupby
中传入相应列名构成的列表即可。
# 根据学校和性别进行分组,统计身高的均值
df.groupby(['School','Gender'])['Height'].mean()
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
中。
# 根据学生体重是否超过总体均值来分组,同样还是计算身高的均值
condition = df.Weight > df.Weight.mean()
df.groupby(condition)['Height'].mean()
Weight
False 159.034646
True 172.705357
Name: Height, dtype: float64
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是 True 和 False )来分组
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
a 162.415000
b 163.400000
c 163.773913
Name: Height, dtype: float64
此处的索引就是原先item中的元素,如果传入多个序列进入 groupby
,那么最后分组的依据就是这两个序列对应行的唯一组合
df.groupby([condition, item])['Height'].mean()
Weight
False a 157.170000
b 160.186842
c 159.663265
True a 172.905000
b 171.031250
c 173.845000
Name: Height, dtype: float64
事实上等价于传入的是一个或多个列,最后分组的依据来自于数据来源组合的unique值
df[['Grade', 'Gender']].drop_duplicates()
Grade Gender
0 Freshman Female
1 Freshman Male
2 Senior Male
3 Sophomore Female
4 Sophomore Male
7 Junior Female
12 Senior Female
16 Junior Male
df.groupby([df['Grade'], df['Gender']])['Height'].mean()
Grade Gender
Freshman Female 159.689189
Male 175.260000
Junior Female 159.782500
Male 171.207143
Senior Female 158.480556
Male 175.594118
Sophomore Female 158.363158
Male 172.030000
Name: Height, dtype: float64
3. Groupby对象
定义 | 用法 | |
---|---|---|
属性 | ngroups | 分组个数 |
属性 | groups | 返回从 组名 映射到 组索引列表 的字典 |
方法 | size | 统计每个组的元素个数 |
方法 | get_group | 获取所在组对应的行 |
分组操作时,所调用的方法都来自于 pandas
中的 groupby
对象,这个对象上定义了许多方法
gb = df.groupby(['Grade', 'Gender'])
gb
>>> <pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000000008103A60>
通过 ngroups 属性,可以得到分组个数
gb.ngroups
>>> 8
通过 groups 属性,可以返回从 组名
映射到 组索引列表
的字典:
res = gb.groups
res
>>> {('Freshman', 'Female'): [0, 5, 6, 8, 15, 28, 32, 33, 34, 43, 44, 45, 47, 51, 57, 62, 63, 67, 70, 81, 88, 96, 105, 108, 111, 114, 119, 121, 125, 133, 136, 140, 141, 142, 146, 148, 149, 157, 185, 186], ('Freshman', 'Male'): [1, 10, 35, 36, 38, 54, 60, 73, 99, 117, 153, 184], ('Junior', 'Female'): [7, 9, 11, 20, 26, 27, 31, 42, 56, 58, 59, 64, 69, 75, 84, 85, 90, 93, 107, 113, 115, 118, 122, 128, 137, 143, 145, 154, 155, 159, 163, 164, 169, 172, 173, 174, 176, 183, 187, 188, 189, 191, 195], ('Junior', 'Male'): [16, 17, 41, 50, 72, 82, 94, 95, 102, 150, 152, 158, 160, 162, 177, 190], ('Senior', 'Female'): [12, 14, 19, 22, 25, 30, 39, 46, 49, 52, 77, 78, 79, 86, 87, 89, 92, 100, 103, 104, 109, 112, 123, 126, 129, 130, 132, 138, 144, 156, 161, 166, 168, 175, 180, 194, 196, 197], ('Senior', 'Male'): [2, 18, 21, 23, 24, 66, 116, 127, 131, 134, 147, 165, 171, 179, 192, 193, 198], ('Sophomore', 'Female'): [3, 13, 29, 37, 53, 55, 65, 68, 80, 83, 97, 101, 106, 110, 120, 124, 139, 151, 167, 170], ('Sophomore', 'Male'): [4, 40, 48, 61, 71, 74, 76, 91, 98, 135, 178, 181, 182, 199]}
res.keys()
>>> dict_keys([('Freshman', 'Female'), ('Freshman', 'Male'), ('Junior', 'Female'), ('Junior', 'Male'), ('Senior', 'Female'), ('Senior', 'Male'), ('Sophomore', 'Female'), ('Sophomore', 'Male')])
当 size 作为 DataFrame 的属性时,返回的是表长乘以表宽的大小,但在 groupby 对象上表示统计每个组的元素个数
gb.size()
Grade Gender
Freshman Female 40
Male 12
Junior Female 43
Male 16
Senior Female 38
Male 17
Sophomore Female 20
Male 14
dtype: int64
通过 get_group 方法可以直接获取所在组对应的行(此时必须知道组的具体名字)
gb.get_group(('Freshman','Male')).iloc[:3,:4]
School Grade Name Gender
1 Peking University Freshman Changqiang You Male
10 Shanghai Jiao Tong University Freshman Xiaopeng Zhou Male
35 Peking University Freshman Gaoli Zhao Male
4. 分组的三大操作
· 每一个组返回一个标量值,可以是平均值、中位数、组容量 size 等
· 做原序列的标准化处理,每组返回的是一个 Series 类型
· 既不是标量也不是序列,返回的整个组所在行的本身,即返回了 DataFrame 类型
二、聚合函数
1. 内置聚合函数
直接定义在groupby对象的聚合函数:max
/min
/mean
/median
/count
/all
/any
/idxmax
/idxmin
/mad
/nunique
/skew
/quantile
/sum
/std
/var
/sem
/size
/prod
gb = df.groupby('Gender')['Height']
gb.idxmin()
Gender
Female 143
Male 199
Name: Height, dtype: int64
gb.skew() # 求偏度
Gender
Female -0.219253
Male 0.437535
Name: Height, dtype: float64
这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
gb = df.groupby('Gender')[['Weight','Height']]
gb.mean()
Weight Height
Gender
Female 47.918519 159.19697
Male 72.759259 173.62549
2. agg方法
groupby
对象上内置的聚合函数有以下几个缺点:
- 无法同时使用多个函数
- 无法对特定的列使用特定的聚合函数
- 无法使用自定义的聚合函数
- 无法直接对结果的列名在聚合前进行自定义命名
2.1 使用多个函数
gb.agg(['max', 'idxmax'])
Weight Height
max idxmax max idxmax
Gender
Female 63.0 28 170.2 28
Male 89.0 2 193.9 193
2.2 对特定的列使用特定的聚合函数
gb.agg({'Weight':['min','idxmin'], 'Height':['max','idxmax']})
Weight Height
min idxmin max idxmax
Gender
Female 34.0 49 170.2 28
Male 51.0 199 193.9 193
2.3 使用自定义函数
在 agg 中可以使用具体的自定义函数, 需要注意传入函数的参数是之前数据源中的列,逐列进行计算 。
# 分组计算身高和体重的极差
gb.agg(lambda x: x.mean() - x.min())
Weight Height
Gender
Female 13.918519 13.79697
Male 21.759259 17.92549
由于传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,只需保证返回值是标量即可。
def my_func(x):
res = 'High'
if x.mean() <= df[x.name].mean():
res = 'Low'
return res
gb.agg(my_func)
Weight Height
Gender
Female Low Low
Male High High
2.4 聚合结果重命名
如果想要对聚合结果的列名进行重命名,只需要将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数
gb.agg([('range', lambda x: x.max()-x.mean()), ('my_sum','sum')])
Weight Height
range my_sum range my_sum
Gender
Female 15.081481 6469.0 11.00303 21014.0
Male 16.240741 3929.0 20.27451 8854.9
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x: x.max()})
Height Weight
my_func sum <lambda>
Gender
Female Low 21014.0 63.0
Male High 8854.9 89.0
使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串
# 修改上一个例子
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('max',lambda x: x.max())]})
Height Weight
my_func sum max
Gender
Female Low 21014.0 63.0
Male High 8854.9 89.0
三、变换和过滤
1. 变换函数与transform方法
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数: cumcount
/cumsum
/cumprod
/cummax
/cummin
,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。
gb.cumsum().head() # 样本的累计最大值
Weight Height
0 46.0 158.9
1 70.0 166.5
2 159.0 355.4
3 87.0 NaN
4 233.0 529.4
当用自定义变换时需要使用 transform
方法,被调用的自定义函数, 其传入值为数据源的序列 ,与 agg
的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的 DataFrame
# 现对身高和体重进行分组标准化,即减去组均值后除以组的标准差
gb.transform(lambda x: (x-x.mean())/x.std()).head()
Weight Height
0 -0.354888 -0.058760
1 -0.355000 -1.010925
2 2.089498 2.167063
3 -1.279789 NaN
4 0.159631 0.053133
前面提到了 transform
只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种 标量广播
的技巧在特征工程中是非常常见的。
gb.transform('mean').head()
Weight Height
0 47.918519 159.19697
1 72.759259 173.62549
2 72.759259 173.62549
3 47.918519 159.19697
4 72.759259 173.62549
2. 组索引与过滤
组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回 True
则会被保留, False
则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame
返回。
# 在原表中通过过滤得到所有容量大于100的组
gb.filter(lambda x: x.shape[0] > 100).head()
Weight Height
0 46.0 158.9
3 41.0 NaN
5 51.0 158.0
6 52.0 162.5
7 50.0 161.9
四、跨列分组
1. apply的引入
B M I = W e i g h t H e i g h t 2 {\rm BMI} = {\rm\frac{Weight}{Height^2}} BMI=Height2Weight,其中体重和身高的单位分别为千克和米,需要分组计算组BMI的均值。
这显然不是过滤操作,因此 filter
不符合要求;其次,返回的均值是标量而不是序列,因此 transform
不符合要求;最后,似乎使用 agg
函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够 多列数据同时处理 。由此,引出了 apply
函数来解决这一问题。
2. apply的使用
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
方法还可以返回一维 Series
和二维 DataFrame
,但它们产生的数据框维数和多级索引的层数应当如何变化?
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x: 0)
Gender Test_Number
Female 1 0
2 0
3 0
Male 1 0
2 0
3 0
dtype: int64
gb.apply(lambda x: [0, 0])
Gender Test_Number
Female 1 [0, 0]
2 [0, 0]
3 [0, 0]
Male 1 [0, 0]
2 [0, 0]
3 [0, 0]
dtype: object
五、练一练
练习1
请根据上下四分位数分割,将体重分为high、normal、low三组,统计身高的均值。
# 先根据上四分位数和下四分位数分组(只会用最笨的办法):
def cond(x):
if x < df.Weight.quantile(0.25):
return 'low'
elif x < df.Weight.quantile(0.75):
return 'normal'
else:
return 'high'
df.groupby(df.Weight.apply(cond))['Height'].mean()
Weight
high 172.920755
low 153.753659
normal 161.800000
Name: Height, dtype: float64
练习2
上一小节介绍了可以通过 drop_duplicates 得到具体的组类别,现请用 groups 属性完成类似的功能。
df[['Grade', 'Gender']].drop_duplicates()
Grade Gender
0 Freshman Female
1 Freshman Male
2 Senior Male
3 Sophomore Female
4 Sophomore Male
7 Junior Female
12 Senior Female
16 Junior Male
gb = df.groupby(['Grade', 'Gender'])
pd.DataFrame(data = gb.groups.keys(), columns=['Grade','Gender'])
Grade Gender
0 Freshman Female
1 Freshman Male
2 Junior Female
3 Junior Male
4 Senior Female
5 Senior Male
6 Sophomore Female
7 Sophomore Male
练习3
请查阅文档,明确 all/any/mad/skew/sem/prod 函数的含义。
用法 | |
---|---|
all | all() 函数用于判断给定的可迭代参数 iterable 中的所有元素是否都为 TRUE,如果是返回 True,否则返回 False。元素除了是 0、空、None、False 外都算 True。 |
any | any() 函数用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True。元素除了是 0、空、FALSE 外都算 TRUE。 |
mad | 根据平均值计算平均绝对距离差。DataFrame.mad(axis=None, skipna=None, level=None)[source] |
skew | 偏度(skewness),是统计数据分布偏斜方向和程度的度量,是统计数据分布非对称程度的数字特征。偏度(Skewness)亦称偏态、偏态系数。DataFrame.skew(axis=None, skipna=None, level=None, numeric_only=None, **kwargs) |
sem | 返回平均值的无偏标准误差。DataFrame.sem(axis=None, skipna=None, level=None, ddof=1, numeric_only=None, **kwargs) |
prod | 返回不同维度上的乘积。 |
练习4
请使用【b】中的传入字典的方法完成【a】中等价的聚合任务。
gb = df.groupby('Gender')[['Weight','Height']]
gb.agg(['sum', 'idxmax', 'skew'])
Weight Height
sum idxmax skew sum idxmax skew
Gender
Female 6469.0 28 -0.268482 21014.0 28 -0.219253
Male 3929.0 2 -0.332393 8854.9 193 0.437535
gb.agg({'Weight':['sum','idxmax','skew'], 'Height':['sum','idxmax','skew']})
Weight Height
sum idxmax skew sum idxmax skew
Gender
Female 6469.0 28 -0.268482 21014.0 28 -0.219253
Male 3929.0 2 -0.332393 8854.9 193 0.437535
练习5
在 groupby 对象中可以使用 describe 方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。
gb.describe()
Weight Height
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
Gender
Female 135.0 47.918519 5.405983 34.0 44.0 48.0 52.00 63.0 132.0 159.19697 5.053982 145.4 155.675 159.6 162.825 170.2
Male 54.0 72.759259 7.772557 51.0 69.0 73.0 78.75 89.0 51.0 173.62549 7.048485 155.7 168.900 173.4 177.150 193.9
gb.agg(['count','mean','std','min',('25%',lambda x: x.quantile(0.25)),('50%','quantile'),('75%',lambda x: x.quantile(0.75)),'max'])
Weight Height
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
Gender
Female 135.0 47.918519 5.405983 34.0 44.0 48.0 52.00 63.0 132.0 159.19697 5.053982 145.4 155.675 159.6 162.825 170.2
Male 54.0 72.759259 7.772557 51.0 69.0 73.0 78.75 89.0 51.0 173.62549 7.048485 155.7 168.900 173.4 177.150 193.9
练习6
在 groupby
对象中, rank
方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。
# 根据不同的性别对身高和体重进行排序
gb.rank().head()
Weight Height
0 47.5 58.0
1 19.0 5.0
2 54.0 50.0
3 14.5 NaN
4 31.5 27.0
练习7
对于 transform
方法无法像 agg
一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform
的调用中实现这种功能,请给出解决方案。
gb.agg({'Weight':'max','Height':'mean'})
Weight Height
Gender
Female 63.0 159.19697
Male 89.0 173.62549
六、练习
Ex1:汽车数据集
现有一份汽车数据集,其中 Brand
, Disp.
, HP
分别代表汽车品牌、发动机蓄量、发动机输出。
- 先过滤出所属
Country
数超过2个的汽车,即若该汽车的Country
在总体数据集中出现次数不超过2则剔除,再按Country
分组计算价格均值、价格变异系数、该Country
的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
。
参考文献
- https://datawhalechina.github.io/joyful-pandas/build/html/%E7%9B%AE%E5%BD%95/ch4.html#id10
- https://www.runoob.com/python/python-built-in-functions.html
- https://blog.csdn.net/u010665216/article/details/78591288?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522160890518016780276319374%252522%25252C%252522scm%252522%25253A%25252220140713.130102334…%252522%25257D&request_id=160890518016780276319374&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-78591288.nonecase&utm_term=pandas%20skew
- https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.mad.html
- https://blog.csdn.net/ge_nious/article/details/78959004?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522160890534716780273310094%252522%25252C%252522scm%252522%25253A%25252220140713.130102334…%252522%25257D&request_id=160890534716780273310094&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-4-78959004.nonecase&utm_term=pandas%20prod