第四章 分组

分组模式及其对象

1. 分组的一般模式

模式 分类 例子 备注
df.groupby(分组依据)[数据来源].使用操作 单一维度 df.groupby('Gender')['Longevity'].mean() 明确三个要素:\color{#FF0000}{分组依据}\color{#00FF00}{数据来源}\color{#0000FF}{操作及其返回结果}
多维度 df.groupby(['School', 'Gender'])['Height'].mean()
复杂逻辑 condition = df.Weight > df.Weight.mean()<br />df.groupby(condition)['Height'].mean()
分组依据的本质:只需在 groupby 中传入相应列名构成的列表即可 自定义分组依据 item = np.random.choice(list('abc'), df.shape[0])<br />df.groupby(item)['Height'].mean() df里并没有item列
df.groupby([condition, item])['Height'].mean() 传入两个分组依据,最后分组的一句就是这两个序列对应的唯一组合

【练一练】

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

df['Wight2'] =  df['Weight'].mask(df.Weight < df.Weight.quantile(0.25), 'low').mask((df.Weight >= df.Weight.quantile(0.25))&(df.Weight <= df.Weight.quantile(0.75)), 'normal').mask(df.Weight > df.Weight.quantile(0.75), 'high')
df.groupby('Wight2')['Height'].mean()
'''

Wight2
high      174.935714
low       153.753659
normal    161.883516
Name: Height, dtype: float64
'''

2. Groupby对象

gb = df.groupby(['School', 'Grade'])
属性,方法 例子 备注
ngroups gb.ngroups 分组个数
groups res = gb.groups<br />res.keys() 可以返回从 组名 映射到 组索引列表 的字典
size gb.size() groupby 对象上表示统计每个组的元素个数
get_group gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3] 可以直接获取所在组对应的行,此时必须知道组的具体名字

【练一练】

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

res = gb.groups
reslist= []
for element in res.keys():
    reslist.append(list(element))
df2 = pd.DataFrame(data = reslist,
                   columns = ['school', 'grade'])
df2
'''

                school              grade
0   Fudan University                Freshman
1   Fudan University                Junior
2   Fudan University                Senior
3   Fudan University                Sophomore
4   Peking University               Freshman
5   Peking University               Junior
6   Peking University               Senior
7   Peking University               Sophomore
8   Shanghai Jiao Tong University   Freshman
9   Shanghai Jiao Tong University   Junior
10  Shanghai Jiao Tong University   Senior
11  Shanghai Jiao Tong University   Sophomore
12  Tsinghua University             Freshman
13  Tsinghua University             Junior
14  Tsinghua University             Senior
15  Tsinghua University             Sophomore
'''

分组的三大操作:聚合agg

1. 内置聚合函数

因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。

max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod

使用:df.groupby('Wight2')['Height'].mean()

练一练

请查阅文档,明确 all/any/mad/skew/sem/prod 函数的含义。

函数 参数 含义
all axis : {index (0), columns (1)}
skipna : boolean, default True
排除NA/null值,如果一整行或一整列都是缺失值,则结果为NA
level : int or level name, default None
If the axis is a MultiIndex (hierarchical), count along a particular level, collapsing into a Series
bool_only : boolean, default None
只包括布尔型的列。如果没有,会尝试使用所有(?),然后会只使用布尔类型的数据。系列值没有这个选项
如果这个组所有值都是True,则为true,否则为false
any axis : {index (0), columns (1)}
skipna : boolean, default True
排除NA/null值,如果一整行或一整列都是缺失值,则结果为NA
level : int or level name, default None
If the axis is a MultiIndex (hierarchical), count along a particular level, collapsing into a Series
bool_only : boolean, default None
只包括布尔型的列。如果没有,会尝试使用所有(?),然后会只使用布尔类型的数据。系列值没有这个选项
如果这个组有任意值是True,则为true,否则为false
mad axis : {index (0), columns (1)}
skipna : boolean, default True
排除NA/null值,如果一整行或一整列都是缺失值,则结果为NA
level : int or level name, default None
如果轴是多索引的,计算特定的层级
numeric_only : boolean, default None
Include only float, int, boolean columns. If None, will attempt to use everything, then use only numeric data. Not implemented for Series.只包括浮点型,布尔型,整型的列。如果没有,会尝试使用所有(?),然后会只使用数值类型的数据。系列值没有这个选项
返回平均绝对离差
skew axis : {index (0), columns (1)}
skipna : boolean, default True
排除NA/null值,如果一整行或一整列都是缺失值,则结果为NA
level : int or level name, default None
如果轴是多索引的,计算特定的层级
numeric_only : boolean, default None
只包括浮点型,布尔型,整型的列。如果没有,会尝试使用所有(?),然后会只使用数值类型的数据。系列值没有这个选项
返回组的样本偏度
sem ddof : integer,默认值为1,表示自由度 返回标准误差,不包含缺失值
prod 每个组所有数的乘积

2. agg方法

可以解决内置聚合函数的4个问题:

问题 例子 备注
同时使用多个函数 gb.\color{#961E87}agg(['sum', 'idxmax', 'skew']) 用列表的形式把内置聚合函数对应的字符串传入
对特定的列使用特定的聚合函数 gb.\color{#961E87}agg({'Height':['mean','max'], 'Weight':'count'}) 通过构造字典传入 agg 中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
使用自定义的聚合函数 gb.\color{#961E87}agg(lambda x: x.mean()-x.min()) 方法1;需要注意传入函数的参数是之前数据源中的列,逐列进行计算
def my_func(s): <br /> res = 'High' <br /> if s.mean() <= df[s.name].mean(): <br /> res = 'Low' <br /> return res<br />gb.\color{#961E87}agg(my_func) 方法2
直接对结果的列名在聚合前进行自定义命名 gb.\color{#961E87}agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])<br />gb.(\color{#961E87}agg{'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()}) 将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数;<br />另外需要注意,使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串

练一练

请使用传入字典的方法完成gb.agg(['sum', 'idxmax', 'skew'])等价的聚合任务。

gb.agg({'Height':['sum', 'idxmax', 'skew'], 'Weight':['sum', 'idxmax', 'skew']})

groupby 对象中可以使用 describe 方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。

gb.agg(['count','mean','std','min',('25%', lambda x: x.quantile(0.25)),('50%', lambda x: x.quantile(0.25)),('75%', lambda x: x.quantile(0.25)),'max'])

分组的三大操作:变换

1.内置变换函数:

累计函数cumcount/cumsum/cumprod/cummax/cummin

练一练

groupby 对象中, rank 方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。

功能:计算沿轴的数值数据等级(1到n)。相等的值被分配一个等级,这个等级是这些值等级的平均值(默认)

参数:

  • axis : {0 or ‘index’, 1 or ‘columns’}, default 0

  • method : {‘average’, ‘min’, ‘max’, ‘first’, ‘dense’}相同值以什么方法取它的等级

  • dense: 类似最小值,但排名在组之间以1为步长增加

  • numeric_only : boolean, default None

    只包括布尔值,浮点型和整型数据

  • na_option : {‘keep’, ‘top’, ‘bottom’}

    • keep: 保留缺失值
    • top: 如果升序则缺失值是最小值
    • bottom: 如果降序则缺失值是最小值
  • ascending : boolean, default True是否升序

  • pct : boolean, default False计算数据的百分比排名

gb.rank(method="max") #将Height和Weight分别取它的排名
'''
    Height  Weight
0   59.0    50.0
1   5.0 21.0
2   50.0    54.0
3   NaN 16.0
4   27.0    33.0
... ... ...
195 22.0    50.0
196 83.0    86.0
197 22.0    44.0
198 31.0    24.0
199 1.0 1.0
'''

2.自定义变换:transform 方法

被调用的自定义函数, 其传入值为数据源的序列 ,与 agg 的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的 DataFrame

#1. 传入lambda或函数
gb.transform(lambda x: (x-x.mean())/x.std()).head()
#2. 传入标量
gb.transform('mean').head() 

练一练

对于 transform 方法无法像 agg 一样,通过传入字典来对指定列使用特定的变换,如果需要在一次 transform 的调用中实现这种功能,请给出解决方案。

def test(x):
    if x.name == 'Height':
        return np.sum(x)
    if x.name =='Weight':
        return np.std(x)
df.groupby('Gender')['Height','Weight'].transform(test)

分组的三大操作:过滤

gb.filter(lambda x: x.shape[0] > 100).head()

练一练

从概念上说,索引功能是组过滤功能的子集,请使用 filter 函数完成 loc[.] 的功能,这里假设 ” . “是元素列表。

 def test2(x, listA):
     result = True
     for i in listA:
         if i not in x['Height'].values:
             result = False
     return result
 gb.filter(lambda x:test2(x,[158.9,162.5]))

四、跨列分组apply

传入:和filter相同

输出:3种情况:

输出 例子 索引情况
标量 def BMI(x):
Height = x['Height']/100
Weight = x['Weight']
BMI_value = Weight/Height**2
return BMI_value.mean()
gb.apply(BMI)<br />df.groupby(['Gender','Test_Number'])[['Height','Weight']]<br />gb.apply(lambda x: 0)
结果得到的是 Series ,索引与 agg 的结果一致
一维Series gb.apply(lambda x: pd.Series([0,0],index=['a','b'])) 行索引与标量情况一致,列索引为 Series 的索引
二维 DataFrame

练一练

  1. 请尝试在 apply 传入的自定义函数中,根据组的某些特征返回相同长度但索引不同的 Series ,会报错吗?

会。。不知道是不是这样:

gb.apply(lambda x: pd.Series([0,0],index=['a',np.random.choice(list('qwertyu'))]))

报的错是索引名称需要是可以hash的类型(Series.name must be a hashable type)

  1. 请尝试在 apply 传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的 DataFrame ,会报错吗?如果只是行索引不同,会报错吗?

不报错

列索引不同:

gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = ['a','b'], columns=pd.Index([('w',np.random.choice(list('xabcd'))),('y','z')])))

<img src="typora-user-images/image-20201225223748508.png" alt="image-20201225223748508" style="zoom:50%;" />

行索引不同:

gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = [np.random.choice(list('abcd')),'b'], columns=pd.Index([('w','x'),('y','z')])))

<img src="typora-user-images/image-20201225223939672.png" alt="image-20201225223939672" style="zoom:50%;" />

  1. 在 groupby 对象中还定义了 cov 和 corr 函数,从概念上说也属于跨列的分组处理。请利用之前定义的 gb 对象,使用apply函数实现与 gb.cov() 同样的功能并比较它们的性能。

每次运行的时间不太相同,数据量小,时间差距不太大。

import datetime
beg_time = datetime.datetime.now()
gb.cov()
end_time = datetime.datetime.now()
print ("gb.cov():", end_time - beg_time)
beg_time = datetime.datetime.now()
gb.apply(lambda x:x.cov())
end_time = datetime.datetime.now()
print ("gb.apply(lambda x:x.cov()):", end_time - beg_time)
'''
gb.cov(): 0:00:00.009975
gb.apply(lambda x:x.cov()): 0:00:00.014961
'''

五、练习

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
  1. 先过滤出所属 Country 数超过2个的汽车,即若该汽车的 Country 在总体数据集中出现次数不超过2则剔除,再按 Country 分组计算价格均值、价格变异系数、该 Country 的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为 CoV
country = df['Country'].value_counts()
df2 = df.loc[df.Country.isin(country[country>2].index.tolist())] # index并不是列表,需要用tolist来转成list
def Cov_x(x):
    return np.std(x)/np.mean(x)
df2.groupby('Country')['Price','Brand'].agg({'Price':['mean',('CoV',lambda x:Cov_x(x))], 'Brand':'count' })

#答案
df.groupby('Country').filter(lambda x:x.shape[0]>2).groupby('Country')['Price'].agg([('CoV', lambda x: x.std()/x.mean()), 'mean', 'count'])
  1. 按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计 Price 的均值。
df['position'] = df.index+1
df['position'] = df['position'].mask(df.index<= df['position'].count() /3, 'low').mask((df.index<= df['position'].count()/3*2)&(df.index> df['position'].count()/3), 'middle').mask(df.index>= df['position'].count() /3*2, 'high')
df.groupby('position')['Price'].mean()
答案
condition = ['Head']*20+['Mid']*20+['Tail']*20
df.groupby(condition)['Price'].mean()
  1. 对类型 Type 分组,对 PriceHP 分别计算最大值和最小值,结果会产生多级索引,请用下划线把多级列索引合并为单层索引。
res = df.groupby('Type').agg({'Price':['max'], 'HP':'min'})
res.columns = res.columns.map(lambda x:'_'.join(x))
  1. 对类型 Type 分组,对 HP 进行组内的 min-max 归一化。
def normalize(s):
     s_min, s_max = s.min(), s.max()
     res = (s - s_min)/(s_max - s_min)
     return res
 

df.groupby('Type')['HP'].transform(normalize).head()
  1. 对类型 Type 分组,计算 Disp.HP 的相关系数。
df.groupby('Type')[['HP', 'Disp.']].apply(lambda x:np.corrcoef(x['HP'].values, x['Disp.'].values)[0,1])
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容