数据聚合与分组运算
在将数据集加载、融合、准备好之后,通常就是计算分组统计或生成透视表。pandas提供一个灵活高效地groupby功能,使得能以一种自然的方式对数据集进行切片、切块、摘要等操作。
关系型数据库和SQL能够如此流行的原因之一就是其能够方便地对数据进行连接、过滤、转换和聚合。但pandas同样具有强大的表达能力,可以执行复杂的分组运算:
1)使用一个或多个键(函数、数组或者DataFrame列名)分割pandaas对象。
2)计算分组的概述统计,比如数量、平均值或标准值,或是用户定义的函数。
3)应用组内转换或其他运算,如规格化、线性化、排名或选取子集等。
4)计算透视表或者交叉表
5)执行分位数分析以及其他统计分组分析。
1.GroupBy机制
pandas对象(无论是Series、DataFrame还是其他的)中的数据会根据你所提供的一个或多个键被拆分(split)为多组。拆分操作是在对象的特定轴上执行的。然后,将一个函数应用(apply)到各个分组并产生一个新值。最后,所有这些函数的执行结果会被合(combine)
到最终的结果对象中。下图是一个简单的分组聚合过程:分组键可以有多种形式,且类型不必相同:
1)列表或数组,其长度与待分组的轴一样。
2)表示DataFrame某个列名的值。
3)字典或Series,给出待分组轴上的值与分组名之间的对应关系。
4)函数,用于处理轴索引或索引中的各个标签。
In [10]: 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)})
In [11]: df
Out[11]:
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
3 -0.555730 0.769023 b two
4 1.965781 1.246435 a one
In [12]: grouped = df['data1'].groupby(df['key1'])
In [13]: grouped
Out[13]: <pandas.core.groupby.SeriesGroupBy object at 0x7faa3153
7390>
In [14]: grouped.mean()
Out[14]:
key1
a 0.746672
b -0.537585
Name: data1, dtype: float64
In [21]: df.groupby('key1').mean()
Out[21]:
data1 data2
key1
a 0.746672 0.910916
b -0.537585 0.525384
In [22]: df.groupby(['key1', 'key2']).mean()
Out[22]:
data1 data2
key1 key2
a one 0.880536 1.319920
two 0.478943 0.092908
b one -0.519439 0.281746
two -0.555730 0.769023
In [23]: df.groupby(['key1', 'key2']).size()
Out[23]:
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
1.1 对分组进行迭代
GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。
In [24]: for name, group in df.groupby('key1'):
....: print(name)
....: print(group)
....:
a
data1 data2 key1 key2
0 -0.204708 1.393406 a one
1 0.478943 0.092908 a two
4 1.965781 1.246435 a one
b
data1 data2 key1 key2
2 -0.519439 0.281746 b one
3 -0.555730 0.769023 b two
In [25]: for (k1, k2), group in df.groupby(['key1', 'key2']):
....: print((k1, k2))
....: print(group)
....:
('a', 'one')
data1 data2 key1 key2
0 -0.204708 1.393406 a one
4 1.965781 1.246435 a one
('a', 'two')
data1 data2 key1 key2
1 0.478943 0.092908 a two
('b', 'one')
data1 data2 key1 key2
2 -0.519439 0.281746 b one
('b', 'two')
data1 data2 key1 key2
3 -0.55573 0.769023 b two
In [30]: for dtype, group in grouped:
....: print(dtype)
....: print(group)
....:
float64
data1 data2
0 -0.204708 1.393406
1 0.478943 0.092908
2 -0.519439 0.281746
3 -0.555730 0.769023
4 1.965781 1.246435
object
key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one
1.2 选取一列或列的子集
对于由DataFrame产生的GroupBy对象,如果用一个或一组列名对其进行索引,就能实现对选取部分列进行聚合的目的。
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]
In [31]: df.groupby(['key1', 'key2'])[['data2']].mean()
Out[31]:
data2
key1 key2
a one 1.319920
two 0.092908
b one 0.281746
two 0.769023
这种索引操作所返回的对象是一个已分组的DataFrame或已分组的Series:
In [32]: s
_grouped = df.groupby(['key1', 'key2'])['data2']
In [33]: s
_grouped
Out[33]: <pandas.core.groupby.SeriesGroupBy object at 0x7faa30c7
8da0>
In [34]: s
_grouped.mean()
Out[34]:
key1 key2
a one 1.319920
two 0.092908
b one 0.281746
two 0.769023
Name: data2, dtype: float64
1.3 通过字典或Series进行分组
除数组以外,分组信息还可以其他形式存在:
In [35]: people = pd.DataFrame(np.random.randn(5, 5),
....: columns=['a', 'b', 'c', 'd', 'e']
,
....: index=['Joe', 'Steve', 'Wes', 'Ji
m', 'Travis'])
In [36]: people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values
In [37]: people
Out[37]:
a b c d e
Joe 1.007189 -1.296221 0.274992 0.228913 1.352917
Steve 0.886429 -2.001637 -0.371843 1.669025 -0.438570
Wes -0.539741 NaN NaN -1.021228 -0.577087
Jim 0.124121 0.302614 0.523772 0.000940 1.343810
Travis -0.713544 -0.831154 -2.370232 -1.860761 -0.860757
In [38]: mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
....: 'd': 'blue', 'e': 'red', 'f' : 'orange'}
In [39]: by_column = people.groupby(mapping, axis=1)
In [40]: by_column.sum()
Out[40]:
blue red
Joe 0.503905 1.063885
Steve 1.297183 -1.553778
Wes -1.021228 -1.116829
Jim 0.524712 1.770545
Travis -4.230992 -2.405455
1.4 通过函数进行分组
比起使用字典或Series,使用Python函数是一种更原生地方法定义分组映射。任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称。
In [44]: people.groupby(len).sum()
Out[44]:
a b c d e
3 0.591569 -0.993608 0.798764 -0.791374 2.119639
5 0.886429 -2.001637 -0.371843 1.669025 -0.438570
6 -0.713544 -0.831154 -2.370232 -1.860761 -0.860757
In [45]: key_
list = ['one', 'one', 'one', 'two', 'two']
In [46]: people.groupby([len, key_
list]).min()
Out[46]:
a b c d e
3 one -0.539741 -1.296221 0.274992 -1.021228 -0.577087
two 0.124121 0.302614 0.523772 0.000940 1.343810
5 one 0.886429 -2.001637 -0.371843 1.669025 -0.438570
6 two -0.713544 -0.831154 -2.370232 -1.860761 -0.860757
1.5 根据索引级别分组
层次化索引数据集最方便的地方就在于它能够根据轴索引的一个级别进行聚合
In [47]: columns = pd.MultiIndex.from_arrays([['US', 'US', 'US','JP', 'JP'],
....: [1, 3, 5, 1, 3]],
....: names=['cty', 'tenor'])
In [48]: hier_df = pd.DataFrame(np.random.randn(4, 5), columns=c
olumns)
In [49]: hier_df
Out[49]:
cty US JP
tenor 1 3 5 1 3
0 0.560145 -1.265934 0.119827 -1.063512 0.332883
1 -2.359419 -0.199543 -1.541996 -0.970736 -1.307030
2 0.286350 0.377984 -0.753887 0.331286 1.349742
3 0.069877 0.246674 -0.011862 1.004812 1.327195
In [50]: hier_df.groupby(level='cty', axis=1).count()
Out[50]:
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3
2.数据聚合
聚合是指任何能够从数组产生标量值的数据转换过程。许多常见的聚合运算都有进行相关优化,除了这些方法,还可以使用其他的。
可以使用自定义的聚合运算,还可以调用分组对象上已经定义好的任何方法,例如quantile可以计算Series或DataFrame列的样本分位数。
In [51]: df
Out[51]:
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
3 -0.555730 0.769023 b two
4 1.965781 1.246435 a one
In [52]: grouped = df.groupby('key1')
In [53]: grouped['data1'].quantile(0.9)
Out[53]:
key1
a 1.668413
b -0.523068
Name: data1, dtype: float64
2.1 面向列的多函数应用
使用read_csv导入数据之后,可以添加一个百分比列:
In [57]: tips = pd.read
_
csv('examples/tips.csv')
# Add tip percentage of total bill
In [58]: tips['tip_pct'] = tips['tip'] / tips['total_bill']
In [59]: tips[:6]
Out[59]:
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808
5 25.29 4.71 No Sun Dinner 4 0.186240
2.2 以“没有行索引”的形式返回聚合数据
所有示例中的聚合数据都有唯一的分组键组成的索引,因此也可以像groupby传入as_index=False来禁用该功能。
In [73]: tips.groupby(['day', 'smoker'], as
_
index=False).mean()
Out[73]:
day smoker total_bill tip size tip_pct
0 Fri No 18.420000 2.812500 2.250000 0.151650
1 Fri Yes 16.813333 2.714000 2.066667 0.174783
2 Sat No 19.661778 3.102889 2.555556 0.158048
3 Sat Yes 21.276667 2.875476 2.476190 0.147906
4 Sun No 20.506667 3.167895 2.929825 0.160113
5 Sun Yes 24.120000 3.516842 2.578947 0.187250
6 Thur No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863
3 apply:一般性的“拆分-应用-合并”
最通用的GroupBy方法是apply,apply会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段组合到一起。
在之前的数据集中,假设根据分组选出最高的5个tip_pct值,需要首先编写一个选区指定列具有最大值的行的函数:
In [74]: def top(df, n=5, column='tip_pct'):
....: return df.sort_values(by=column)[-n:]
In [75]: top(tips, n=6)
Out[75]:
total_bill tip smoker day time size tip_pct
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
232 11.61 3.39 No Sat Dinner 2 0.291990
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
3.1 禁用分组键
从上面的额例子中可以看出,分组键会跟原始对象的索引共同后塍结果对象中的层次化索引,将group_keys=False传入GroupBy即可禁用fail效果:
In [81]: tips.groupby('smoker', group_
keys=False).apply(top)
Out[81]:
total_bill tip smoker day time size tip_pct
88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
3.2 分位数和桶分析
pandas有一些根据指定面元或样本分位数将数据拆分成多块的工具(比如cut和qcut)。将这些函数跟GroupBy结合一起,就能非常轻松地实现对数据集的桶或分位数分析。
In [82]: frame = pd.DataFrame({'data1': np.random.randn(1000),
....: 'data2': np.random.randn(1000)})
In [83]: quartiles = pd.cut(frame.data1, 4)
In [84]: quartiles[:10]
Out[84]:
0 (-1.23, 0.489]
1 (-2.956,
-1.23]
2 (-1.23, 0.489]
3 (0.489, 2.208]
4 (-1.23, 0.489]
5 (0.489, 2.208]
6 (-1.23, 0.489]
7 (-1.23, 0.489]
8 (0.489, 2.208]
9 (0.489, 2.208]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-2.956,-1.23] < (-1.23, 0.489] < (0.489, 2.208] < (2.208, 3.928]]
3.3 示例:用特定于分组的值填充缺失值
对于缺失数据的清理工作,通常会用drona将其替换掉,而有时用一个固定值或由数据集本身衍生出来的值来填充NA值,例如fillna工具。
In [91]: s = pd.Series(np.random.randn(6))
In [92]: s[::2] = np.nan
In [93]: s
Out[93]:
0 NaN
1 -0.125921
2 NaN
3 -0.884475
4 NaN
5 0.227290
dtype: float64
In [94]: s.fillna(s.mean())
Out[94]:
0 -0.261035
1 -0.125921
2 -0.261035
3 -0.884475
4 -0.261035
5 0.227290
dtype: float64
In [95]: states = ['Ohio', 'New York', 'Vermont', 'Florida',
....: 'Oregon', 'Nevada', 'California', 'Idaho']
In [96]: group_
key = ['East'] * 4 + ['West'] * 4
In [97]: data = pd.Series(np.random.randn(8), index=states)
In [98]: data
Out[98]:
Ohio 0.922264
New York -2.153545
Vermont -0.365757
Florida -0.375842
Oregon 0.329939
Nevada 0.981994
California 1.105913
Idaho -1.613716
dtype: float64
[‘East’] * 4产生了一个列表,包括了[‘East’]中元素的四个拷贝。将这些列表串联起来。将一些值设为缺失:
In [99]: data[['Vermont', 'Nevada', 'Idaho']] = np.nan
In [100]: data
Out[100]:
Ohio 0.922264
New York -2.153545
Vermont NaN
Florida -0.375842
Oregon 0.329939
Nevada NaN
California 1.105913
Idaho NaN
dtype: float64
In [101]: data.groupby(group_
key).mean()
Out[101]:
East -0.535707
West 0.717926
dtype: float64
用分组平均值来填充NA值:
In [102]: fill
_
mean = lambda g: g.fillna(g.mean())
In [103]: data.groupby(group_
key).apply(fill_mean)
Out[103]:
Ohio 0.922264
New York -2.153545
Vermont -0.535707
Florida -0.375842
Oregon 0.329939
Nevada 0.717926
California 1.105913
Idaho 0.717926
dtype: float64
3.4 示例:随机采样和排列
从一个大数据集随机抽取样本来进行蒙特卡罗模拟或者其他分析工作,“抽样”的方式有多种,一般使用的方法是对Series使用sample方法:
# Hearts, Spades, Clubs, Diamonds
suits = ['H', 'S', 'C', 'D']
card_val = (list(range(1, 11)) + [10] * 3) * 4
base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
cards = []
for suit in ['H', 'S', 'C', 'D']:
cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards)
将一个长度为52的Series,其索引包括牌名,值则是21点或其他游戏中用于计分的点数:
In [108]: deck[:13]
Out[108]:
AH 1
2H 2
3H 3
4H 4
5H 5
6H 6
7H 7
8H 8
9H 9
10H 10
JH 10
KH 10
QH 10
dtype: int64
In [109]: def draw(deck, n=5):
.....: return deck.sample(n)
In [110]: draw(deck)
Out[110]:
AD 1
8C 8
5H 5
KC 10
2C 2
dtype: int64
从每种花色中随机抽取两张牌。由于花色是牌名的最后一个字符,所以我们可以据此进行分组,并使用apply:
In [111]: get
_
suit = lambda card: card[-1] # last letter is suit
In [112]: deck.groupby(get
_
suit).apply(draw, n=2)
Out[112]:
C 2C 2
3C 3
D KD 10
8D 8
H KH 10
3H 3
S 2S 2
4S 4
dtype: int64
3.5 示例:分组加权平均数和相关系数
根据GroupBy的“拆分-应用-合并”范式,可以进行DataFrame的列与列之间或者两个Series之间的运算。
In [114]: df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
.....: 'b', 'b', 'b', 'b'],
.....: 'data': np.random.randn(8),
.....: 'weights': np.random.rand(8)})
In [115]: df
Out[115]:
category data weights
0 a 1.561587 0.957515
1 a 1.219984 0.347267
2 a -0.482239 0.581362
3 a 0.315667 0.217091
4 b -0.047852 0.894406
5 b -0.454145 0.918564
6 b -0.556774 0.277825
7 b 0.253321 0.955905
利用category计算分组加权平均数:
In [116]: grouped = df.groupby('category')
In [117]: get_wavg = lambda g: np.average(g['data'], weights=g['
weights'])
In [118]: grouped.apply(get_wavg)
Out[118]:
category
a 0.811643
b -0.122262
dtype: float64
3.6 示例:组级别的线性回归
可以用groupby执行更为复杂的分组统计分析,只要函数返回的是pandas对象或标量值即可。例如,定义下面这个regress函数(利
用statsmodels计量经济学库)对各数据块执行普通最小二乘法(Ordinary LeastSquares,OLS)回归:
import statsmodels.api as sm
def regress(data, yvar, xvars):
Y = data[yvar]
X = data[xvars]
X['intercept'] = 1.
result = sm.OLS(Y, X).fit()
return result.params
按年计算AAPL对SPX收益率的线性回归,执行
In [129]: by_year.apply(regress, 'AAPL', ['SPX'])
Out[129]:
SPX intercept
2003 1.195406 0.000710
2004 1.363463 0.004201
2005 1.766415 0.003246
2006 1.645496 0.000080
2007 1.198761 0.003438
2008 0.968016 -0.001110
2009 0.879103 0.002954
2010 1.052608 0.001261
2011 0.806605 0.001514
4. 透视表和交叉表
透视表是各种电子表格程序和其他数据分析软件中一种常见的额数据汇总工具,它根据一个或过个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域中。DataFrame有一个pivot_table方法,此外还有一个顶级的pandas.pivot_table函数。除能为GroupBy提供便利之外,pivot_table还可以添加分项小计,也叫做margins。
如果想要根据day和smoker来计算消费数据集的分组平均数,并将day和smoker放到行上:
In [130]: tips.pivot
_
table(index=['day', 'smoker'])
Out[130]:
size tip tip_pct total_bill
day smoker
Fri No 2.250000 2.812500 0.151650 18.420000
Yes 2.066667 2.714000 0.174783 16.813333
Sat No 2.555556 3.102889 0.158048 19.661778
Yes 2.476190 2.875476 0.147906 21.276667
Sun No 2.929825 3.167895 0.160113 20.506667
Yes 2.578947 3.516842 0.187250 24.120000
Thur No 2.488889 2.673778 0.160298 17.113111
Yes 2.352941 3.030000 0.163863 19.190588
In [131]: tips.pivot
_
table(['tip_pct', 'size'], index=['time', '
day'],
.....: columns='smoker')
Out[131]:
size tip_pct
smoker No Yes No Yes
time day
Dinner Fri 2.000000 2.222222 0.139622 0.165347
Sat 2.555556 2.476190 0.158048 0.147906
Sun 2.929825 2.578947 0.160113 0.187250
Thur 2.000000 NaN 0.159744 NaN
Lunch Fri 3.000000 1.833333 0.187735 0.188937
Thur 2.500000 2.352941 0.160311 0.163863
In [132]: tips.pivot
_
table(['tip_pct', 'size'], index=['time', '
day'],
.....: columns='smoker', margins=True)
Out[132]:
size tip_pct
smoker No Yes All No Yes
All
time day
Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347 0
.158916
Sat 2.555556 2.476190 2.517241 0.158048 0.147906 0
.153152
Sun 2.929825 2.578947 2.842105 0.160113 0.187250 0
.166897
Thur 2.000000 NaN 2.000000 0.159744 NaN 0
.159744
Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937 0
.188765
Thur 2.500000 2.352941 2.459016 0.160311 0.163863 0
.161301
All 2.668874 2.408602 2.569672 0.159328 0.163196 0
.160803
要使用其他的聚合函数,将其传给aggfunc即可。例如,使用count或len可以得到有关分组大小的交叉表(计数或频率):
In [133]: tips.pivot
_
table('tip_pct', index=['time', 'smoker'],
columns='day',
.....: aggfunc=len, margins=True)
Out[133]:
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
pivot_table的参数说明如下图:
4.1 交叉表:corsstable
交叉表(cross-tabulation,简称crosstab)是一种用于计算分组频率的特殊透视表。
In [138]: data
Out[138]:
Sample Nationality Handedness
0 1 USA Right-handed
1 2 Japan Left-handed
2 3 USA Right-handed
3 4 Japan Right-handed
4 5 Japan Left-handed
5 6 Japan Right-handed
6 7 USA Right-handed
7 8 USA Left-handed
8 9 Japan Right-handed
9 10 USA Right-handed
可以使用pivot_table来根据国际和用手习惯对这段数据进行统计汇总。
In [139]: pd.crosstab(data.Nationality, data.Handedness, margins=
True)
Out[139]:
Handedness Left-handed Right-handed All
Nationality
Japan 2 3 5
USA 1 4 5
All 3 7 10
crosstab的前两个参数可以是数组或Series,或是数组列表:
In [140]: pd.crosstab([tips.time, tips.day], tips.smoker, margin
s=True)
Out[140]:
smoker No Yes All
time day
Dinner Fri 3 9 12
Sat 45 42 87
Sun 57 19 76
Thur 1 0 1
Lunch Fri 1 6 7
Thur 44 17 61
All 151 93 244
掌握pandas数据分组工具有助于数据清理,也有助于建模或统计分析工作。
来源:CSDN
作者:LanceJerry
链接:https://blog.csdn.net/LanceJerry/article/details/103571123