利用Python进行数据分析:数据聚合与分组运算
在将数据集加载、融合、准备好之后,通常就是计算分组统计或生成透视表。pandas提供了一个灵活高效的groupby功能,对数据集进行切片、切块、摘要等操作。
- 使用一个或多个键(形式可以是函数、数组或DataFrame列名)分割pandas对象。
- 计算分组的概述统计,比如数量、平均值或标准差,或是用户定义的函数。
- 应用组内转换或其他运算,如规格化、线性回归、排名或选取子集等。
- 计算透视表或交叉表。
- 执行分位数分析以及其它统计分组分析。
文章目录
GroupBy机制
下图为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
命令应用于各分组。
用特定于分组的值填充缺失值
假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用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)