在将数据集加载、融合、准备好之后,通常就是计算分组统计或生成透视表。pandas提供了一个灵活高效的groupby功能,对数据集进行切片、切块、摘要等操作。
- 使用一个或多个键(形式可以是函数、数组或DataFrame列名)分割pandas对象。
- 计算分组的概述统计,比如数量、平均值或标准差,或是用户定义的函数。
- 应用组内转换或其他运算,如规格化、线性回归、排名或选取子集等。
- 计算透视表或交叉表。
- 执行分位数分析以及其它统计分组分析。
下图为pandas的GroupBy机制的简单示意图:
- split: 根据提供的一个或多个键拆分pandas对象(如,Series、DataFrame)
- apply: 将一个函数应用到各个分组并产生一个新值
- combine: 将函数执行结果合并到最终对象中
# 导入包 import pandas as pd import numpy as np
df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
'key2' : ['one', 'two', 'one', 'two', 'one'],
'data1' : np.random.randn(5),
'data2' : np.random.randn(5)})
df
| key1 | key2 | data1 | data2 | |
|---|---|---|---|---|
| 0 | a | one | 0.115582 | 0.283518 |
| 1 | a | two | -0.413824 | -0.333613 |
| 2 | b | one | -0.138182 | -0.462852 |
| 3 | b | two | 0.887625 | 0.756710 |
| 4 | a | one | 1.404586 | -0.084644 |
GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。可以对数据片段执行自定义操作。
for (k1,k2), group in df.groupby(['key1','key2']):
print((k1,k2))
print(group)
('a', 'one')
key1 key2 data1 data2
0 a one 0.115582 0.283518
4 a one 1.404586 -0.084644
('a', 'two')
key1 key2 data1 data2
1 a two -0.413824 -0.333613
('b', 'one')
key1 key2 data1 data2
2 b one -0.138182 -0.462852
('b', 'two')
key1 key2 data1 data2
3 b two 0.887625 0.75671
groupby默认是在axis=0上进行分组的,通过设置也可以在其他任何轴上进行分组。拿上面例子中的df来说,我们可以根据dtype对列进行分组:
grouped = df.groupby(df.dtypes, axis=1)
for dtype, group in grouped:
print(dtype)
print(group)
float64
data1 data2
0 0.115582 0.283518
1 -0.413824 -0.333613
2 -0.138182 -0.462852
3 0.887625 0.756710
4 1.404586 -0.084644
object
key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one
分组方式
通过列名分组
通常,分组信息就位于相同的要处理DataFrame中。上一小节中的示例即是将列名(可以是字符串、数字或其他Python对象)用作分组键。
通过字典进行分组现在,假设已知列的分组关系,并希望根据分组计算列的和:
people = pd.DataFrame(np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people
| a | b | c | d | e | |
|---|---|---|---|---|---|
| Joe | 0.577199 | -0.017674 | 0.822495 | 0.236686 | -0.186748 |
| Steve | 0.403552 | -1.162806 | 0.123285 | -0.534835 | -1.333789 |
| Wes | 2.398688 | -1.268945 | 0.134579 | 1.204143 | 0.569019 |
| Jim | 0.493204 | 1.242734 | 0.399500 | 1.080515 | -3.530037 |
| Travis | 0.691376 | -0.239679 | 1.542502 | 1.445370 | 0.035521 |
可以通过字典指定列的分组关系:
mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
'd': 'blue', 'e': 'red', 'f' : 'orange'}
people.groupby(mapping, axis =1).sum()
| blue | red | |
|---|---|---|
| Joe | 1.059181 | 0.372778 |
| Steve | -0.411549 | -2.093043 |
| Wes | 1.338722 | 1.698763 |
| Jim | 1.480015 | -1.794099 |
| Travis | 2.987872 | 0.487218 |
任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称。比如,在上一小节的DataFrame示例中,其索引值为人名,可以根据其字符串长度进行分组,传入len函数即可:
people.groupby(len).sum()
| a | b | c | d | e | |
|---|---|---|---|---|---|
| 3 | -3.875764 | 2.603252 | 2.365206 | -2.131822 | 0.033895 |
| 5 | 0.197823 | 0.324744 | -0.465613 | 1.005641 | 0.491256 |
| 6 | 1.530411 | -0.863593 | -1.362848 | -0.701741 | -0.307557 |
此外,也可以将函数跟数组、列表、字典、Series混合使用。
通过索引级别分组对于层次化索引数据集,可以根据轴索引的一个级别进行聚合:
columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
hier_df
| cty | US | JP | |||
|---|---|---|---|---|---|
| tenor | 1 | 3 | 5 | 1 | 3 |
| 0 | -1.230404 | 1.182842 | 0.091481 | 3.029558 | 0.422223 |
| 1 | -0.848426 | 1.463393 | -0.220321 | -0.576434 | 1.587866 |
| 2 | 0.036692 | -1.020126 | 0.247783 | 0.460074 | -0.047801 |
| 3 | -0.275390 | -0.554516 | -1.301136 | 0.079235 | 0.502412 |
要根据级别分组,使用level关键字传递级别序号或名字:
hier_df.groupby(level='cty', axis=1).count()
| cty | JP | US |
|---|---|---|
| 0 | 2 | 3 |
| 1 | 2 | 3 |
| 2 | 2 | 3 |
| 3 | 2 | 3 |
聚合指的是任何能够从数组产生标量值的数据转换过程。
| 选项 | 说明 |
|---|---|
| count | 分组中非NA值的数量 |
| nunique | 分组中非重复值的数量(NA值也统计) |
| unique | 分组中非重复值(NA值也统计) |
| sum | 非NA值的和 |
| mean | 非NA值的平均值 |
| median | 非NA值的算术中位数 |
| std、var | 无偏(分母为n-1)标准差和方差 |
| min、max | 非NA值的最小值和最大值 |
| prod | 非NA值的积 |
| first、last | 非NA值的第一个和最后一个 |
下面具体列出一些常用示例:
统计数量统计分组后,各组内数量:GroupBy的size方法,它可以返回一个含有分组大小的Series。
比如,以下使用key1、key2列对df进行分组,并统计每组记录数量:
df
| key1 | key2 | data1 | data2 | |
|---|---|---|---|---|
| 0 | a | one | -0.213740 | 0.082804 |
| 1 | a | two | -0.871891 | -0.232010 |
| 2 | b | one | 0.454973 | 0.246116 |
| 3 | b | two | -0.702433 | 1.957607 |
| 4 | a | one | -1.136174 | 1.726216 |
df.groupby(['key1','key2']).size()
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
使用reset_index()可以将结果转化为DataFrame形式:
df.groupby(['key1','key2']).size().reset_index()
| key1 | key2 | 0 | |
|---|---|---|---|
| 0 | a | one | 2 |
| 1 | a | two | 1 |
| 2 | b | one | 1 |
| 3 | b | two | 1 |
df.groupby('key1')['data1'].describe()
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| key1 | ||||||||
| a | 3.0 | -0.740601 | 0.475025 | -1.136174 | -1.004032 | -0.871891 | -0.542815 | -0.213740 |
| b | 2.0 | -0.123730 | 0.818410 | -0.702433 | -0.413082 | -0.123730 | 0.165621 | 0.454973 |
比如,以下记录为学生英语四六级考试成绩数据,选取学号(xh)、考试时间(kssj)、英语级别(yyjb)、总分(zf)四列,并筛选少量记录用于示例:
score = pd.read_csv('xs_yysljkscjsj.csv', usecols=['xh','kssj','yyjb','zf'],low_memory=False)
# 仅保留英语四六级考试成绩 score = score[score.yyjb.isin(['CET4','CET6'])]
# 随机选择5名学生 score = score[score.xh.isin(score.xh.sample(5))]
score.sort_values('xh').head(10)
| yyjb | xh | kssj | zf | |
|---|---|---|---|---|
| 46568 | CET4 | 0992447 | 201006 | 488 |
| 49582 | CET6 | 0992447 | 201012 | 390 |
| 58822 | CET6 | 0992447 | 201106 | 396 |
| 64551 | CET6 | 0992447 | 201112 | 421 |
| 80369 | CET6 | 0992447 | 201212 | 416 |
| 127382 | CET6 | 1143140 | 201412 | 356 |
| 109293 | CET6 | 1143140 | 201312 | 319 |
| 118854 | CET6 | 1143140 | 201406 | 333 |
| 92735 | CET6 | 1143140 | 201212 | 327 |
| 75478 | CET4 | 1143140 | 201206 | 439 |
对于同一名学生、同一外语水平考试,存在多次考试的情况:
score.groupby(['xh','yyjb']).size()
xh yyjb
0992447 CET4 1
CET6 4
1143140 CET4 1
CET6 5
1932108 CET4 1
CET6 2
1957104 CET4 1
CET6 3
q0151111 CET6 1
dtype: int64
场景一: 每名学生、每门考试仅保留最高成绩记录。可以先根据总分进行排序,后执行分组及数据聚合操作。
score.sort_values('zf',ascending=False).groupby(['xh','yyjb']).first()
| kssj | zf | ||
|---|---|---|---|
| xh | yyjb | ||
| 0992447 | CET4 | 201006 | 488 |
| CET6 | 201112 | 421 | |
| 1143140 | CET4 | 201206 | 439 |
| CET6 | 201412 | 356 | |
| 1932108 | CET4 | 202012 | 466 |
| CET6 | 202106 | 311 | |
| 1957104 | CET4 | 201912 | 500 |
| CET6 | 202112 | 539 | |
| q0151111 | CET6 | 200506 | 0 |
如果希望选取最高的几条记录,可以使用head()命令:
score.sort_values('zf',ascending=False).groupby(['xh','yyjb']).head(2)
| yyjb | xh | kssj | zf | |
|---|---|---|---|---|
| 283135 | CET6 | 1957104 | 202112 | 539 |
| 197433 | CET4 | 1957104 | 201912 | 500 |
| 279294 | CET6 | 1957104 | 202106 | 498 |
| 46568 | CET4 | 0992447 | 201006 | 488 |
| 266499 | CET4 | 1932108 | 202012 | 466 |
| 75478 | CET4 | 1143140 | 201206 | 439 |
| 64551 | CET6 | 0992447 | 201112 | 421 |
| 80369 | CET6 | 0992447 | 201212 | 416 |
| 127382 | CET6 | 1143140 | 201412 | 356 |
| 118854 | CET6 | 1143140 | 201406 | 333 |
| 276130 | CET6 | 1932108 | 202106 | 311 |
| 251893 | CET6 | q0151111 | 200506 | 0 |
| 282211 | CET6 | 1932108 | 202112 | 0 |
场景二:每名学生、每门考试仅保留最新的一次成绩记录。可以先根据时间戳进行排序,后执行分组及数据聚合操作。
score.sort_values('kssj',ascending = False).groupby(['xh','yyjb']).first()
| kssj | zf | ||
|---|---|---|---|
| xh | yyjb | ||
| 0992447 | CET4 | 201006 | 488 |
| CET6 | 201212 | 416 | |
| 1143140 | CET4 | 201206 | 439 |
| CET6 | 201412 | 356 | |
| 1932108 | CET4 | 202012 | 466 |
| CET6 | 202112 | 0 | |
| 1957104 | CET4 | 201912 | 500 |
| CET6 | 202112 | 539 | |
| q0151111 | CET6 | 200506 | 0 |
场景三:计算每门考试的成绩排名。根据英语级别分组后,在组内使用rank()函数。
score['rank'] = score.groupby('yyjb')['zf'].rank(ascending = False)
score[score.yyjb == 'CET4']
| yyjb | xh | kssj | zf | rank | |
|---|---|---|---|---|---|
| 46568 | CET4 | 0992447 | 201006 | 488 | 2.0 |
| 75478 | CET4 | 1143140 | 201206 | 439 | 4.0 |
| 197433 | CET4 | 1957104 | 201912 | 500 | 1.0 |
| 266499 | CET4 | 1932108 | 202012 | 466 | 3.0 |
希望对不同的列使用不同的聚合函数,或一次应用多个函数,需要使用agg()方法。
一次应用多个函数:如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名:
grouped = score.groupby('yyjb')
functions = ['mean', 'median', 'count'] grouped['zf'].agg(functions)
| mean | median | count | |
|---|---|---|---|
| yyjb | |||
| CET4 | 473.25 | 477 | 4 |
| CET6 | 316.20 | 356 | 15 |
如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):
tfunctions = [('平均分','mean'),('中位数','median'), ('记录数','count')]
grouped['zf'].agg(tfunctions)
| 平均分 | 中位数 | 记录数 | |
|---|---|---|---|
| yyjb | |||
| CET4 | 473.25 | 477 | 4 |
| CET6 | 316.20 | 356 | 15 |
对不同的列应用不同的函数。具体的办法是向agg传入一个从列名映射到函数的字典:
grouped.agg({
'zf': ['max','min','mean'],
'kssj': 'nunique'
})
| zf | kssj | |||
|---|---|---|---|---|
| max | min | mean | nunique | |
| yyjb | ||||
| CET4 | 500 | 439 | 473.25 | 4 |
| CET6 | 539 | 0 | 316.20 | 12 |
用特定于分组的值填充缺失值自定义函数,通过apply命令应用于各分组。
假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可。
states = ['Ohio', 'New York', 'Vermont', 'Florida',
'Oregon', 'Nevada', 'California', 'Idaho']
regions = ['East'] * 4 + ['West'] * 4
data = pd.DataFrame({'data':np.random.randn(8),
'region':regions},
index=states)
data.loc[['Vermont', 'Nevada', 'Idaho'],'data'] = np.nan
data
| data | region | |
|---|---|---|
| Ohio | 1.393924 | East |
| New York | -0.522025 | East |
| Vermont | NaN | East |
| Florida | 0.484822 | East |
| Oregon | 1.547784 | West |
| Nevada | NaN | West |
| California | -0.953454 | West |
| Idaho | NaN | West |
使用同一地区的平均值填补缺失值,可以使用"region"字段分组,再用分组平均值填充NA值,定义缺失值填充函数:
fill_mean = lambda g:g.fillna(g.mean())
data.groupby('region').apply(fill_mean)
| data | region | ||
|---|---|---|---|
| region | |||
| East | Ohio | 1.393924 | East |
| New York | -0.522025 | East | |
| Vermont | 0.452240 | East | |
| Florida | 0.484822 | East | |
| West | Oregon | 1.547784 | West |
| Nevada | 0.297165 | West | |
| California | -0.953454 | West | |
| Idaho | 0.297165 | West |
从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:
data.groupby('region',group_keys=False).apply(fill_mean)
| data | region | |
|---|---|---|
| Ohio | 1.393924 | East |
| New York | -0.522025 | East |
| Vermont | 0.452240 | East |
| Florida | 0.484822 | East |
| Oregon | 1.547784 | West |
| Nevada | 0.297165 | West |
| California | -0.953454 | West |
| Idaho | 0.297165 | West |
另一个类似于上一小节的应用场景时,数据在填写或转录过程存在错误或缺失,希望使用同一组内最高频出现的值填补缺失值:
data['region_code'] = [1,1,1,np.nan,0,0,0,0]
data
| data | region | region_code | |
|---|---|---|---|
| Ohio | 1.393924 | East | 1.0 |
| New York | -0.522025 | East | 1.0 |
| Vermont | NaN | East | 1.0 |
| Florida | 0.484822 | East | NaN |
| Oregon | 1.547784 | West | 0.0 |
| Nevada | NaN | West | 0.0 |
| California | -0.953454 | West | 0.0 |
| Idaho | NaN | West | 0.0 |
下面希望,使用同一"region"出现次数最多的"region_code"对缺失值进行填充:
fill_mod = lambda g:g.fillna(g.value_counts().index[0])
data.groupby('region')['region_code'].apply(fill_mod)
Ohio 1.0 New York 1.0 Vermont 1.0 Florida 1.0 Oregon 0.0 Nevada 0.0 California 0.0 Idaho 0.0 Name: region_code, dtype: float64
有的时候,存在数据填写的错误,比如如下Oregon的区域码填写成了1。
data['region_code'] = [1,1,1,1,1,0,0,0]
data
| data | region | region_code | |
|---|---|---|---|
| Ohio | 1.393924 | East | 1 |
| New York | -0.522025 | East | 1 |
| Vermont | NaN | East | 1 |
| Florida | 0.484822 | East | 1 |
| Oregon | 1.547784 | West | 1 |
| Nevada | NaN | West | 0 |
| California | -0.953454 | West | 0 |
| Idaho | NaN | West | 0 |
下面希望,同一"region"的"region_code"应保持一致,注意此处使用的是transform功能:
data.groupby('region')['region_code'].transform(lambda g:g.value_counts().index[0])
Ohio 1 New York 1 Vermont 1 Florida 1 Oregon 0 Nevada 0 California 0 Idaho 0 Name: region_code, dtype: int64分组加权平均
根据groupby的“拆分-应用-合并”范式,可以进行DataFrame的列与列之间或两个Series之间的运算(比如分组加权平均)。
df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
'b', 'b', 'b', 'b'],
'data': np.random.randn(8),
'weights': np.random.rand(8)})
df
| category | data | weights | |
|---|---|---|---|
| 0 | a | 0.442123 | 0.173591 |
| 1 | a | -0.033089 | 0.269083 |
| 2 | a | 0.813126 | 0.440622 |
| 3 | a | -0.653577 | 0.526061 |
| 4 | b | -0.363492 | 0.471360 |
| 5 | b | 0.759573 | 0.619617 |
| 6 | b | 0.301565 | 0.453474 |
| 7 | b | -1.908321 | 0.596430 |
然后可以利用category计算分组加权平均数:
def get_wavg(g,value,weight):
return np.average(g[value], weights=g[weight])
grouped = df.groupby('category')
注意下方为在apply函数中传入参数的方法:
grouped.apply(get_wavg,'data','weights')
category a 0.058399 b -0.327958 dtype: float64
往期:
利用Python进行数据分析:准备工作
利用Python进行数据分析:缺失数据(基于DataFrame)
利用Python进行数据分析:数据转换(基于DataFrame)
利用Python进行数据分析:数据规整(基于DataFrame)



