本文是对《利用Python进行数据分析》中关于groupby进行分组聚合和运算的一个回顾性总结2。
目录
利用groupby进行数据聚合
含义:对于groupby后对象的数据操作,聚合指的是任何能够从数组产生标量值的数据转换过程。
分位数操作quantile
实例:# 此处对于聚合,指的是任何能够从数组 产生标量值的数据转换过程。 除了可以使用mean,count,sum等常见聚合运算外,还可以使用自己创造的聚合运算,还可以调用分组对象上已经定义好的任何方法。 如quantile 可以计算Series 或 DataFrame列的样本分位数 。
df = DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
'key2' : ['one', 'two', 'one', 'two', 'one'],
'data1' : np.random.randn(5),
'data2' : np.random.randn(5)})
grouped=df.groupby('key1')
grouped['data1'].quantile(0.9)
注意: 虽然quantile 并没有明确的实现于 groupby ,但它是一个Series方法,所以这里是能用的,实际上,groupby 会高效的对Series进行切片,然后对各片调用piece.quantile(0.9),最后将这些结果组装成最终结果
输出
key1
a 1.668413
b -0.523068
Name: data1, dtype: float64
describe函数
实例:统计函数describe会对grouped对象中的各列都进行describe包含的函数操作。
grouped.describe() # 注意,有些方法(如describe)也是可以用在这里的,即使严格来讲,它们并非聚合运算:
常见自带聚合函数
常见的自带的部分聚合函数可以直接用,性能更快。
自定义聚合函数
实例: 使用 自己的聚合函数,只需将其传入aggregate 或 agg 方法即可
# 使用 自己的聚合函数,只需将其传入aggregate 或 agg 方法即可
def peak_to_peak(arr):
return arr.max()-arr.min()
grouped.agg(peak_to_peak)
输出
data1 data2
key1
a 2.170488 1.300498
b 0.036292 0.487276
性能:
自定义聚合函数要比表中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在非常大的开销(函数调用、数据重排等)。
面向列的多函数应用
对于自定义或者自带的函数都可以用agg传入一次应用多个函数 。传入函数组成的list。所有的列都会应用这组函数。
实例
# 如果使用 你自己的聚合函数,只需将其传入aggregate 或 agg 方法即可
grouped=tips.groupby(['sex','smoker'])
def peak_to_peak(arr):
return arr.max()-arr.min()
grouped.agg(peak_to_peak)
# # 可以对不同的列使用不同的聚合函数,或一次应用多个函数
# 如果传入一组函数 或函数名 ,得到的结果DataFrame 的列就会以 相应的函数命名
grouped_pct.agg(['mean','std',peak_to_peak]) #将自带mean函数等也可以用agg传入
输出
#tips数据的前3行
total_bill tip sex smoker day time size tip_pct
0 16.99 1.01 Female No Sun Dinner 2 0.059447
1 10.34 1.66 Male No Sun Dinner 3 0.160542
2 21.01 3.50 Male No Sun Dinner 3 0.166587
mean std peak_to_peak
sex smoker
Female No 0.156921 0.036421 0.195876
Yes 0.182150 0.071595 0.360233
Male No 0.160669 0.041849 0.220186
Yes 0.152771 0.090588 0.674707
对不同的列进行不同的函数操作
传入元素为带着函数的元组的list
实例: 注意如果传入agg 的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射)
# 并非一定要接受GroupBy自动给出的那些列名,特别是lambda函数,它们的名称是'<lambda>',这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传入agg 的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):
grouped_pct=grouped['tip_pct']
grouped_pct.agg([('foo','mean'),('bar',np.std)]) # 如下面结果列中的列名即元组的第一个元素 .
输出
foo bar
sex smoker
Female No 0.156921 0.036421
Yes 0.182150 0.071595
Male No 0.160669 0.041849
Yes 0.152771 0.090588
以'无层次索引'的形式返回聚合数据
需要在groupby的时候就指定as_index=False。这样就不会根据依据的对象形成层次化索引。默认是为true 。参数 group_keys等价与as_index。
实例1
asind=tips.groupby(['sex','smoker'],as_index=False).mean() # 唯一的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向groupby传入as_index=False以禁用该功能,这样在应用函数操作grouped对象时,就不会带这分组键作为索引了,而将补充整型值索引,注意 分组的依据将形成列体现在结果中。
print(asind)
输出
sex smoker total_bill tip size tip_pct
0 Female No 18.105185 2.773519 2.592593 0.156921
1 Female Yes 17.977879 2.931515 2.242424 0.182150
2 Male No 19.791237 3.113402 2.711340 0.160669
3 Male Yes 22.284500 3.051167 2.500000 0.152771
实例2
print(tips.groupby(['sex','smoker'],as_index=True).mean()) # 会形成层次化索引
输出
total_bill tip size tip_pct
sex smoker
Female No 18.105185 2.773519 2.592593 0.156921
Yes 17.977879 2.931515 2.242424 0.182150
Male No 19.791237 3.113402 2.711340 0.160669
Yes 22.284500 3.051167 2.500000 0.152771
分组级运算和转换
增加列名后缀,将同后缀的merge到一起,这样将运算的值也带到了后面,实现同放一起看了。
函数:.add_prefix('mean_')
实例:
k1_means = df.groupby('key1').mean().add_prefix('mean_') # 对于聚合后运算对象结果中的列增加后缀名以区分不是原来的属性了
k1_means
原数据df[:3]为:
data1 data2 key1 key2
0 -0.204708 1.393406 a one
1 0.478943 0.092908 a two
2 -0.519439 0.281746 b one
输出:
mean_data1 mean_data2
key1
a 0.746672 0.910916
b -0.537585 0.525384
print(df.head(2))
print(pd.merge(df,k1_means,left_on='key1',right_index=True)) # 将同后缀的merge到一起,这样成功将运算的值也带到了后面,实现同一个看了。
输出
data1 data2 key1 key2
0 -0.20 1.39 a one
1 0.48 0.09 a two
data1 data2 key1 key2 mean_data1 mean_data2
0 -0.20 1.39 a one 0.75 0.910
1 0.48 0.09 a two 0.75 0.910
4 1.97 1.25 a one 0.75 0.910
2 -0.52 0.28 b one -0.54 0.525
3 -0.56 0.77 b two -0.54 0.525
拆分与组合apply
含义: 最一般化的groupby 方法是apply ,apply会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段组合到一起。
实例
tips=tips.round(2)
print(tips[:3])
# 小费 数据集,根据分组选出最高的 5个tip_pct值
#首先 编写一个选取指定列具有最大值的行的函数
def top(df,n=5,column='tip_pct'):
return df.sort_index(by=column)[-n:]
# 如果对smoker 分组,并用该函数调用apply
print(tips.groupby('smoker').apply(top))
上面的运算实质是 top函数在DataFrame的各个片段上调用,然后结果由 pandas.concat 组装到一起,并以分组名称进行了标记。于是,最终结果有了一个层次化的索引,其内层索引值来源于 原 DataFrame
输出
total_bill tip sex smoker day time size tip_pct
0 16.99 1.01 Female No Sun Dinner 2 0.06
1 10.34 1.66 Male No Sun Dinner 3 0.16
2 21.01 3.50 Male No Sun Dinner 3 0.17
total_bill tip sex smoker day time size tip_pct
smoker
No 185 20.69 5.00 Male No Sun Dinner 5 0.24
88 24.71 5.85 Male No Thur Lunch 2 0.24
51 10.29 2.60 Female No Sun Dinner 2 0.25
149 7.51 2.00 Male No Thur Lunch 2 0.27
232 11.61 3.39 Male No Sat Dinner 2 0.29
Yes 109 14.31 4.00 Female Yes Sat Dinner 2 0.28
183 23.17 6.50 Male Yes Sun Dinner 4 0.28
67 3.07 1.00 Female Yes Sat Dinner 1 0.33
178 9.60 4.00 Female Yes Sun Dinner 2 0.42
172 7.25 5.15 Male Yes Sun Dinner 2 0.71
# 如果传给apply的函数能够接受其他参数或关键词,则可将这些内容放在函数名后面一并传入
print(tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill'))
输出
total_bill tip sex smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 Female No Fri Dinner 2 0.14
Sat 212 48.33 9.00 Male No Sat Dinner 4 0.19
Sun 156 48.17 5.00 Male No Sun Dinner 6 0.10
Thur 142 41.19 5.00 Male No Thur Lunch 5 0.12
Yes Fri 95 40.17 4.73 Male Yes Fri Dinner 4 0.12
Sat 170 50.81 10.00 Male Yes Sat Dinner 3 0.20
Sun 182 45.35 3.50 Male Yes Sun Dinner 3 0.08
Thur 197 43.11 5.00 Female Yes Thur Lunch 4 0.12
除这些基本用法之外,能否充分发挥apply的威力很大程度上取决于你的创造力。传入的那个函数能做什么全由你说了算,它只需返回一个pandas对象或标量值即可。
禁止分组键
通常情况下,groupby 的分组键 会跟原始对象的索引共同构成 结果对象中的层次化索引。将group_keys=False传入groupby即可禁止这种效果。等价与as_index=False
例子
print(tips.groupby('smoker').apply(top))
print("**"*6)
print(tips.groupby('smoker',group_keys=False).apply(top)) # 这个smoke的值就未出现在索引中了。
print("**"*6)
print(tips.groupby('smoker',as_index=dex=False).apply(top)) # group_keys等价与as_index
分位数和桶
pandas有一些能根据指定面元或样本分位数将数据拆分成多块的工具(比如cut和qcut)。将这些函数跟groupby与apply结合起来,就能非常轻松地实现对数据集的桶(bucket)或分位数(quantile)分析了
注意: 由 cut 返回的 对象可直接用于groupby 。
例子cut
def get_stats(group):
return {'min':group.min(),'max':group.max(),'count':group.count(),'mean':group.mean()} # 注意此用字典返回,方便后续用unstack
frame=DataFrame({'data1':np.random.randn(1000),'data2':np.random.randn(1000)})
factor=pd.cut(frame.data1,4) # 等长的桶
grouped=frame.data2.groupby(factor)
print(grouped.apply(get_stats).unstack())
输出
count max mean min
data1
(-2.956, -1.23] 96.0 1.670835 -0.041782 -3.399312
(-1.23, 0.489] 602.0 3.260383 -0.014862 -2.989741
(0.489, 2.208] 291.0 2.954439 0.091055 -3.745356
(2.208, 3.928] 11.0 1.765640 0.055158 -1.929776
例子qcut
# Return quantile numbers
grouping = pd.qcut(frame.data1, 10, labels=False) #qcut ,根据样本分位数 得到大小相等的桶,传入lbels=False 即可值获取分位数的编号。
grouped = frame.data2.groupby(grouping) # groupby的依据直接应用qcut划分得到的桶
# print(grouped.apply(get_stats))
grouped.apply(get_stats).unstack() # unstack 转化为桶。
输出
count max mean min
data1
0 100.0 2.146716 -0.061124 -3.018842
1 100.0 2.038456 -0.057383 -2.641014
2 100.0 2.285401 0.082387 -2.453248
3 100.0 2.903300 -0.057825 -2.901831
4 100.0 2.209647 0.007647 -2.251291
5 100.0 2.497837 -0.056052 -3.108915
6 100.0 2.228700 -0.044572 -1.866563
7 100.0 2.430878 0.033692 -3.333767
8 100.0 2.215585 -0.001622 -2.428129
9 100.0 1.716892 -0.079520 -2.494075
填充缺失值
对于groupby后得到的对象中缺失值的填充,apply去应用填充的函数或者关于相应列或行索引与填充值的字典映射
fill_mean=lambda g : g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean) # 可以用分组平均值去填充NA值
fill_values={'East':0.5,'West':-1} # 也可以在代码中预定义各组的填充值。其中East和west均为是分组依据中值(即groupby后形成的索引的一个值
fill_func=lambda g : g.fillna(fill_values[g.name]) # 由于分组具有一个name属性,所以此可以拿来用下。注意此用的g.name. 注意 fill_values里面的key要在groupby 中的值里。
data.groupby(group_key).apply(fill_func)
计算相关性
列与列的相关性
SeriesA.corr(SeriesB)
实例
# Annual correlation of Apple with Microsoft
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
dataframe与列的相关性
dfA.corr(seriesB)
透视表
含义
透视表pivot tables是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具。 它根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域中。
在Python和pandas中,groupby功能以及(能够利用层次化索引的)重塑运算制作透视表。DataFrame有一个pivot_table方法,此外还有一个*的pandas.pivot_table函数,但pivot_table能更方便。除能为groupby提供便利之外,pivot_table还可以添加分项小计,也叫做margins。
参数
案例
数据准备
print(tips.head(2))
total_bill tip sex smoker day time size tip_pct
0 16.99 1.01 Female No Sun Dinner 2 0.06
1 10.34 1.66 Male No Sun Dinner 3 0.16
# 假设我想要根据day和smoker计算分组平均数(注意pivot_table的默认聚合类型为求平均),并将day和smoker放到行上:
tips=tips.round(2)
print(tips.pivot_table(index=['sex','smoker']))
输出
size tip tip_pct total_bill
sex smoker
Female No 2.592593 2.773519 0.156852 18.105185
Yes 2.242424 2.931515 0.182424 17.977879
Male No 2.711340 3.113402 0.161031 19.791237
Yes 2.500000 3.051167 0.152667 22.284500
# 等价于
tips.groupby(['sex','smoker']).mean().round(2) #用groupby 做. 同上面的结果是一样的(只是排列顺序有差别)
输出
total_bill tip size tip_pct
sex smoker
Female No 18.11 2.77 2.59 0.16
Yes 17.98 2.93 2.24 0.18
Male No 19.79 3.11 2.71 0.16
Yes 22.28 3.05 2.50 0.15
# pivot_table相比groupby能更方便
# 假设我们只想聚合tip_pct和size,而且想根据time进行分组。我将smoker放到列上,把day放到行上: 注意此时用groupby 就不好做了,而此时用pivot_table 很好做
tips.pivot_table(['tip_pct','size'],index=['sex','day'],columns='smoker').round(2)# 注意参数index里表示行的依据
输出
size tip_pct
smoker No Yes No Yes
sex day
Female Fri 2.50 2.00 0.16 0.21
Sat 2.31 2.20 0.15 0.16
Sun 3.07 2.50 0.16 0.24
Thur 2.48 2.43 0.16 0.16
Male Fri 2.00 2.12 0.14 0.14
Sat 2.66 2.63 0.16 0.14
Sun 2.88 2.60 0.16 0.17
Thur 2.50 2.30 0.17 0.16
还可以对这个表作进一步的处理,传入margins=True添加分项小计(默认也是求的平均)。这将会添加标签为All的行和列,其值对应于单个等级中所有数据的分组统计:
tips.pivot_table(['tip_pct','size'],index=['sex','day'],columns='smoker',margins=True) # 这里,All值为平均数:不单独考虑烟民与非烟民(All列),不单独考虑行分组两个级别中的任何单项(All行)。
输出
size tip_pct
smoker No Yes All No Yes All
sex day
Female Fri 2.50 2.00 2.11 0.16 0.21 0.20
Sat 2.31 2.20 2.25 0.15 0.16 0.16
Sun 3.07 2.50 2.94 0.16 0.24 0.18
Thur 2.48 2.43 2.47 0.16 0.16 0.16
Male Fri 2.00 2.12 2.10 0.14 0.14 0.14
Sat 2.66 2.63 2.64 0.16 0.14 0.15
Sun 2.88 2.60 2.81 0.16 0.17 0.16
Thur 2.50 2.30 2.43 0.17 0.16 0.17
All 2.67 2.41 2.57 0.16 0.16 0.16
# 要使用其他的聚合函数而不是求平均,则将函数将其传给aggfunc即可。例如,使用count或len可以得到有关分组大小的交叉表(计数或频率):
tips.pivot_table('tip_pct',index=['sex','smoker'],columns='day',aggfunc=len,margins=True)
输出
day Fri Sat Sun Thur All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106.0
Yes 9.0 42.0 19.0 NaN 70.0
Lunch No 1.0 NaN NaN 44.0 45.0
Yes 6.0 NaN NaN 17.0 23.0
All 19.0 87.0 76.0 62.0 244.0
# 如果存在空的组合(也就是NA),你可能会希望设置一个fill_value:
tips.pivot_table('size', index=['time', 'sex', 'smoker'],columns='day', aggfunc='sum', fill_value=0)
输出
day Fri Sat Sun Thur
time sex smoker
Dinner Female No 2 30 43 2
Yes 8 33 10 0
Male No 4 85 124 0
Yes 12 71 39 0
Lunch Female No 3 0 0 60
Yes 6 0 0 17
Male No 0 0 0 50
Yes 5 0 0 23
交叉表(了解)
交叉表(cross-tabulation,简称crosstab)是一种用于计算分组频率的特殊透视表。 这个用的少,用到再查吧。
案例
联邦选举委员会数据库的例子(奥巴马与对手的选举数据分析)
根据职业和老板统计赞助信息
对出资额分组
根据洲统计赞助信息