根据上一篇文章对数据集的拆分-应用-联合,具体讲解了写拆分和联合用到的细则,这篇将围绕应用这块进行描述。
GroupBy方法最常见的目的是apply(应用),这将是本节的内容。apply将对象拆分成多块,然后在每一块上调用传递函数,之后尝试将每一块拼接到一起。
回到之前的小费数据集,假设你想要按组选出小费百分比(tip_pct)最高的五组。首先,写一个可以在特定列中选出最大值所在行的函数:
def top(df,n=5,column='tip_pct'):
return df.sort_values(by=column)[-n:]
a = top(tips,n=6)
print(a)
---------------------------------------------------------
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
现在,如果我们按照smoker进行分组,之后调用apply,我们会得到以下结果:
total_bill tip smoker day time size tip_pct
smoker
No 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
Yes 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
top函数在DataFrame的每一行分组上被调用,之后使用pandas.concat将函数结果粘贴在一起,并使用分组名作为各组的标签。因此结果包含一个分层索引,该分层索引的内部层级包含原DataFrame的索引值:
如果你除了向apply传递函数,还传递其他参数或关键字的话,你可以把这些放在函数后进行传递:
a = tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill')
print(a)
-------------------------------------------------------------------------
total_bill tip smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857
Sat 212 48.33 9.00 No Sat Dinner 4 0.186220
Sun 156 48.17 5.00 No Sun Dinner 6 0.103799
Thur 142 41.19 5.00 No Thur Lunch 5 0.121389
Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750
Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812
Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178
Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982
之前我们有在GroupBy对象上调用describe方法,在GroupBy对象的内部,当你调用像describe这样的方法时,实际上是以下代码的简写:
f = lambda x :x.describe()
grouped.apply(f)
在之前的例子中,你可以看到所得到的对象具有分组键所形成的分层索引以及每个原始对象的索引。你可以通过向groupby传递group_keys=False来禁用这个功能:
a = tips.groupby('smoker',group_keys=False).apply(top)
print(a)
------------------------------------------------------------------------
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
在之前的第七章中,pandas有一些工具,尤其是cut和qcut,用于将数据按照你选择的箱位或样本分位数进行分桶。与groupby方法一起使用这些函数可以对数据集更方便地进行分桶或分位分析。考虑一个简单的随机数据集和一个使用cut的等长桶分类:
frame = pd.DataFrame({'data1':np.random.randn(1000),
'data2':np.random.randn(1000)})
quartiles=pd.cut(frame.data1,4)
print(quartiles)
-----------------------------------------------------------------------------
0 (-1.487, 0.0725]
1 (-1.487, 0.0725]
2 (-1.487, 0.0725]
3 (0.0725, 1.632]
4 (-1.487, 0.0725]
...
995 (-3.053, -1.487]
996 (1.632, 3.192]
997 (0.0725, 1.632]
998 (-1.487, 0.0725]
999 (-1.487, 0.0725]
Name: data1, Length: 1000, dtype: category
Categories (4, interval[float64, right]): [(-3.053, -1.487] < (-1.487, 0.0725] < (0.0725, 1.632] <
(1.632, 3.192]]
cut返回的Categories对象可以直接传递给groupby。所以我们可以计算出data2列的一个统计值集合:
def get_stats(group):
return {'min': group.min(), 'max': group.max(),
'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(quartiles)
a = grouped.apply(get_stats).unstack()
print(a)
-------------------------------------------------------------
min max count mean
data1
(-3.053, -1.487] -1.929778 2.387990 58.0 -0.067484
(-1.487, 0.0725] -3.116310 3.202132 474.0 0.054767
(0.0725, 1.632] -3.019290 2.734140 415.0 -0.118471
(1.632, 3.192] -3.065552 2.069831 53.0 -0.246630
为了根据样本分位数计算出等大小的桶,则需要qcut。我们将传递labels=False来获得分位数:
grouping = pd.qcut(frame.data1,10,labels=False)
grouped = frame.data2.groupby(grouping)
a = grouped.apply(get_stats).unstack()
print(a)
---------------------------------------------------------------
min max count mean
data1
0 -2.452613 2.387990 100.0 0.009122
1 -2.711972 1.814960 100.0 0.012730
2 -2.489099 3.202132 100.0 0.032057
3 -3.116310 2.554327 100.0 0.094443
4 -1.919159 2.781821 100.0 0.071050
5 -2.370437 2.393702 100.0 -0.021028
6 -2.903778 2.734140 100.0 -0.327223
7 -2.969580 1.735026 100.0 -0.041452
8 -2.050720 2.268590 100.0 0.004044
9 -3.065552 2.069831 100.0 -0.235658
在清楚缺失值时,有时你会使用dronpa来去除缺失值,但是有时你可能想要使用修正值或来自于其他数据的值来输入(填充)到null值(NA)。fillna是一个可以使用的正确工具,例如这里我们使用平均值来填充NA值:
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
print(s)
a = s.fillna(s.mean())
print(a)
---------------------------------------------------------------------------
0 NaN
1 0.882770
2 NaN
3 -0.086084
4 NaN
5 -0.221206
dtype: float64
0 0.191826
1 0.882770
2 0.191826
3 -0.086084
4 0.191826
5 -0.221206
dtype: float64
假设你需要填充值按组变化。一个方法是对数据分组后使用apply和一个在每个数据块上都调用fillna的函数:
states = ['Ohio','New York','Vermont','Florida','Oregon','Nevada','California','Idaho']
group_key = ['East']*4+['West']*4
print(group_key)
s = pd.Series(np.random.randn(8),index=states)
print(s)
--------------------------------------------------------------------------------
['East', 'East', 'East', 'East', 'West', 'West', 'West', 'West']
Ohio -1.150053
New York 0.041887
Vermont 0.975530
Florida -0.153980
Oregon -0.531615
Nevada -0.720547
California 1.093888
Idaho 1.278055
dtype: float64
然后我们将数据中的一些值设置为缺失值:
s[['Vermont','Nevada','Idaho']]=np.nan
print(s)
a = s.groupby(group_key).mean()
print(a)
---------------------------------------------------------------
Ohio -1.150053
New York 0.041887
Vermont NaN
Florida -0.153980
Oregon -0.531615
Nevada NaN
California 1.093888
Idaho NaN
dtype: float64
East -0.420715
West 0.281137
dtype: float64
之后我们使用分组的平均值来填充NA值:
fill_mean = lambda g:g.fillna(g.mean())
a = s.groupby(group_key).apply(fill_mean)
print(a)
---------------------------------------------------------------
Ohio -1.150053
New York 0.041887
Vermont -0.420715
Florida -0.153980
Oregon -0.531615
Nevada 0.281137
California 1.093888
Idaho 0.281137
dtype: float64
如果你已经在代码中为每个分组预定义了填充值。由于每个分组都有一个内置的name属性,就可以这样使用 :
fill_values = {'East':0.5,'West':-1}
fill_func = lambda g: g.fillna(fill_values[g.name])
a = s.groupby(group_key).apply(fill_func)
print(a)
-------------------------------------------------------------
Ohio -1.150053
New York 0.041887
Vermont 0.500000
Florida -0.153980
Oregon -0.531615
Nevada -1.000000
California 1.093888
Idaho -1.000000
dtype: float64
假设你想从大数据集中抽取随机样本(有或没有替换)以用于蒙特卡罗模拟目的或某些其他应用程序。有很多方法来执行“抽取”,这里我们使用Series的sample方法:
为了演示,这里讲解一种构造一副英式扑克牌的方法:
# 红桃、黑桃、梅花、方块
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 suits:
cards.extend(str(num)+suit for num in base_names)
deck = pd.Series(card_val,index=cards)
print(deck[:13])
----------------------------------------------------------------------------
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
利用sample方法,从这副牌中拿出五张牌可以写成:
def draw(deck,n=5):
return deck.sample(n)
a = draw(deck)
print(a)
------------------------------------------------------
JD 10
7D 7
5C 5
9C 9
3D 3
dtype: int64
假设你想要从每个花色中随机抽取两张牌。由于花色是牌名的最后两个字符,我们可以基于这点进行分组,并使用apply:
get_suit = lambda card : card[-1]
a = deck.groupby(get_suit).apply(draw,n=2)
print(a)
------------------------------------------------------------------------
dtype: int64
C KC 10
5C 5
D 3D 3
AD 1
H 3H 3
9H 9
S QS 10
8S 8
dtype: int64
或者我们也可以写成:
a = deck.groupby(get_suit,group_keys=False).apply(draw,n=2)
print(a)
--------------------------------------------------------------------
8C 8
4C 4
5D 5
6D 6
7H 7
6H 6
9S 9
JS 10
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)})
print(df)
------------------------------------------------------------------------
category data weights
0 a 0.410634 0.040028
1 a -0.788315 0.213055
2 a -0.654367 0.570789
3 a -0.157310 0.199540
4 b 0.411850 0.656720
5 b 0.165850 0.358905
6 b 1.337180 0.206716
7 b -0.517227 0.329182
通过category进行分组加权平均如下:
grouped = df.groupby('category')
get_wavg = lambda g :np.average(g['data'],weights=g['weights'])
a = grouped.apply(get_wavg)
print(a)
-------------------------------------------------------
category
a -0.543685
b 0.281110
dtype: float64
作为另一个例子,是一个从雅虎财经上获得的数据集,该数据集包含一些标普500(SPX符号)和股票的收盘价:
close_px = pd.read_csv('D:\浏览器下载\pydata-book-2nd-edition\pydata-book-2nd-edition\examples/stock_px_2.csv',parse_dates=True,index_col=0)
close_px.info()
print(close_px[-4:])
--------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 AAPL 2214 non-null float64
1 MSFT 2214 non-null float64
2 XOM 2214 non-null float64
3 SPX 2214 non-null float64
dtypes: float64(4)
memory usage: 86.5 KB
Unnamed: 0 AAPL MSFT XOM SPX
2210 2011-10-11 00:00:00 400.29 27.00 76.27 1195.54
2211 2011-10-12 00:00:00 402.19 26.96 77.16 1207.25
2212 2011-10-13 00:00:00 408.43 27.18 76.37 1203.66
2213 2011-10-14 00:00:00 422.00 27.27 78.11 1224.58
计算一个DataFrame,它包含标普指数(spx)每日收益的年度相关性(通过百分比变化计算)。作为实现的一种方式,我们首先创建一个计算每列与’SPX’列成关联的函数:
spx_corr = lambda x:x.corrwith(x['SPX'])
之后,我们使用pct_change计算close_px百分比的变化:
rets = close_px.pct_change().dropna()
最后,我们按年对百分比进行分组,可以使用单行函数从每个标签中提取每个datetime标签的year属性:
get_year =lambda x:x.year
by_year =rets.groupby(get_year)
a = by_year.apply(spx_corr)
print(a)
------------------------------------------------------------------
AAPL MSFT XOM SPX
2003 0.541124 0.745174 0.661265 1.0
2004 0.374283 0.588531 0.557742 1.0
2005 0.467540 0.562374 0.631010 1.0
2006 0.428267 0.406126 0.518514 1.0
2007 0.508118 0.658770 0.786264 1.0
2008 0.681434 0.804626 0.828303 1.0
2009 0.707103 0.654902 0.797921 1.0
2010 0.710105 0.730118 0.839057 1.0
2011 0.691931 0.800996 0.859975 1.0
你也可以计算内部列相关性:
a = by_year.apply(lambda g:g['AAPL'].corr(g['MSFT']))
print(a)
-------------------------------------------------------------------
2003 0.480868
2004 0.259024
2005 0.300093
2006 0.161735
2007 0.417738
2008 0.611901
2009 0.432738
2010 0.571946
2011 0.581987
dtype: float64
该示例中,我们可以定义以下regress(回归)函数,该函数对每个数据块执行普通最小二乘(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回报上的年度线性回归,执行以下代码:
a = by_year.apply(regress,'AAPL',['SPX'])
print(a)
-----------------------------------------------------------------------
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
数据透视表是电子表格程序和其他数据分析软件中常见的数据汇总工具。它根据一个或多个键聚合一张表的数据,将数据在矩形格式中排列,其中一些分组键是沿着行的,另一些是沿着列的。Python中的pandas透视表是通过本章介绍的groupby工具以及使用分层索引的重塑操作实现。DataFrame拥有一个pivot_table方法,并且还有一个顶层的pandas.pivot_table函数。除了为groupby提供一个方便接口,pivot_table还可以添加部分设计,也称作边距。
回到小费数据集,假设你想要计算一张在行方向上按day和smoker排列的分组平均值(默认的pivot_table聚合类型)的表:
a = tips.pivot_table(index=['day','smoker'])
print(a)
-----------------------------------------------------------------------
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
现在,假设我们只想在tip_pct和size上进行聚合,并根据time分组。我将把smoker放入表列,而将day放入表的行:
a = tips.pivot_table(['tip_pct','size'],index=['time','day'],
columns='smoker')
print(a)
-----------------------------------------------------------------------------
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
我们可以通过传递margins=True来扩充这个表包含部分总计。这会添加ALL行和列标签,其中相应的值是单层中所有数据的分组统计值:
a = tips.pivot_table(['tip_pct','size'],index=['time','day'],
columns='smoker',margins=True)
print(a)
------------------------------------------------------------------------------
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
这里的ALL的值是均值,且该均值是不考虑吸烟者与非吸烟者(ALL列)或行分组中任何两级的(ALL行)。
要使用不同的聚合函数时,将函数传递给aggfunc。例如,’count‘或者len将给出一张分组大小的交叉表(计数或出现频率):
a = tips.pivot_table('tip_pct',index=['time','smoker'],
columns='day',aggfunc=len,margins=True)
print(a)
--------------------------------------------------------------------------------------------------
day Fri Sat Sun Thur All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106
Yes 9.0 42.0 19.0 NaN 70
Lunch No 1.0 NaN NaN 44.0 45
Yes 6.0 NaN NaN 17.0 23
All 19.0 87.0 76.0 62.0 244
如果某些情况下产生了空值(或者NA),你想要传递一个fill_value:
a = tips.pivot_table('tip_pct',index=['time','size','smoker'],
columns='day',aggfunc='mean',fill_value=0)
print(a)
---------------------------------------------------------------------------------------------------
day Fri Sat Sun Thur
time size smoker
Dinner 1 No 0.000000 0.137931 0.000000 0.000000
Yes 0.000000 0.325733 0.000000 0.000000
2 No 0.139622 0.162705 0.168859 0.159744
Yes 0.171297 0.148668 0.207893 0.000000
3 No 0.000000 0.154661 0.152663 0.000000
Yes 0.000000 0.144995 0.152660 0.000000
4 No 0.000000 0.150096 0.148143 0.000000
Yes 0.117750 0.124515 0.193370 0.000000
5 No 0.000000 0.000000 0.206928 0.000000
Yes 0.000000 0.106572 0.065660 0.000000
6 No 0.000000 0.000000 0.103799 0.000000
Lunch 1 No 0.000000 0.000000 0.000000 0.181728
Yes 0.223776 0.000000 0.000000 0.000000
2 No 0.000000 0.000000 0.000000 0.166005
Yes 0.181969 0.000000 0.000000 0.158843
3 No 0.187735 0.000000 0.000000 0.084246
Yes 0.000000 0.000000 0.000000 0.204952
4 No 0.000000 0.000000 0.000000 0.138919
Yes 0.000000 0.000000 0.000000 0.155410
5 No 0.000000 0.000000 0.000000 0.121389
6 No 0.000000 0.000000 0.000000 0.173706
下表是pivot_table选项表:
选项名 | 描述 |
---|---|
values | 需要聚合的列名;默认情况下聚合所有数值型的列 |
index | 在结果透视表的行上进行分组的列名或其他分组键 |
columns | 在结果透视表的列上进行分组的列名或其他分组键 |
aggfunc | 聚合函数或函数列表(默认情况下是‘mean’);可是groupby上下文的任意有效函数 |
fill_value | 在结果表中替换缺失值的值 |
dropna | 如果为True,将不含所有条目均为NA的列 |
margins | 添加行/列小计和总和(默认为False) |
交叉表是数据透视表的一个特殊情况,计算的是分组中的频率,如下:
df = pd.DataFrame({'Sample':np.arange(1,11),
'Nationality':['USA','Japan','USA','Japan','Japan','Japan','USA','USA','Japan','USA'],
'Handedness':['Right-handed','left-handed','Right-handed','Right-handed','left-handed','Right-handed','Right-handed','left-handed','Right-handed','Right-handed']})
print(df)
-------------------------------------------------------------------------------------------------
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来说实现这个功能,但是pandas.crosstab函数更为方便:
a = pd.crosstab(df.Nationality,df.Handedness,margins=True)
print(a)
----------------------------------------------------------------------------------------------------
Handedness Right-handed left-handed All
Nationality
Japan 3 2 5
USA 4 1 5
All 7 3 10
以上就是今天要讲的内容,本文如何充分利用apply函数对数据集进行应用:通用拆分-应用-联合并且列举了相关的示例供参考,还学习了数据透视表和交叉表。精通pandas的数据分组工具既可以帮助我们清洗数据,也对建模或统计分析工作有益。