分組級運算和轉(zhuǎn)換
聚合是分組運算的其中一種。它是數(shù)據(jù)轉(zhuǎn)換的一個特例,它接受能夠?qū)⒁痪S數(shù)組簡化為標量值的函數(shù)。
接下來將介紹transform和apply方法,它們能夠執(zhí)行更多其他的分組運算。
如果要為一個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)})
df

k1_means=df.groupby('key1').mean().add_prefix('mean_')
k1_means

pd.merge(df,k1_means,left_on='key1',right_index=True)

該過程看做利用np.mean函數(shù)對兩個數(shù)據(jù)列進行轉(zhuǎn)換,我們將在GroupBy上使用transform方法
people=DataFrame(np.random.randn(5,5),columns=['a','b','c','d','e'],
index=['Joe','Steve','Wes','Jim','Travis'])
people.loc[2:3,['b','c']]=np.nan #添加NA值
people

key=['one','two','one','two','one']
people.groupby(key).mean()

people.groupby(key).transform(np.mean)

transform會將一個函數(shù)應(yīng)用到各個分組,然后將結(jié)果放置到適當?shù)奈恢蒙?。如果各分組產(chǎn)生的是一個標量值,則該值就會被廣播出去?,F(xiàn)在,假設(shè)你希望從各組中減去平均值。為此,我們先創(chuàng)建一個距平化函數(shù)(demeaning function),然后將其傳給transform
def demean(arr):
return arr-arr.mean()
demeaned=people.groupby(key).transform(demean)
demeaned

檢查一下demeaned現(xiàn)在的分組平均值是否為0
demeaned.groupby(key).mean()

apply:一般性的“拆分-應(yīng)用-合并”
跟aggregate一樣,transform也是一個有著嚴格條件的特殊函數(shù):傳入的函數(shù)只能產(chǎn)生兩種結(jié)果,要么產(chǎn)生一個可以廣播的標量值(如np.mean),要么產(chǎn)生一個相同大小的結(jié)果數(shù)組。最一般化的GroupBy方法是apply。
回到之前那個小費數(shù)據(jù)集,假設(shè)你想要根據(jù)分組選出最高的5個tip_pct值。首先,編寫一個選取指定列具有最大值的行的函數(shù)
def top(df, n=5, column='tip_pct'):
return df.sort_index(by=column)[-n:]
top(tips, n=6)
e:\python\lib\site-packages\ipykernel_launcher.py:2: FutureWarning: by argument to sort_index is deprecated, please use .sort_values(by=...)

sort_index is deprecated,現(xiàn)在使用新的sort_values

如果對smoker分組并用該函數(shù)調(diào)用apply,就會得到
tips.groupby('smoker').apply(top)

top函數(shù)在DataFrame的各個片段上調(diào)用,然后結(jié)果由pandas.concat組裝到一起,并以分組名稱進行了標記。于是,最終結(jié)果就有了一個層次化索引,其內(nèi)層索引值來自原DataFrame。
如果傳給apply的函數(shù)能夠接受其他參數(shù)或關(guān)鍵字,則可以將這些內(nèi)容放在函數(shù)名后面一并傳入
tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill')

之前在GroupBy對象上調(diào)用過describe
result=tips.groupby('smoker')['tip_pct'].describe()
result

result.unstack('smoker')
smoker
count No 151.000000
Yes 93.000000
mean No 0.159328
Yes 0.163196
std No 0.039910
Yes 0.085119
min No 0.056797
Yes 0.035638
25% No 0.136906
Yes 0.106771
50% No 0.155625
Yes 0.153846
75% No 0.185014
Yes 0.195059
max No 0.291990
Yes 0.710345
dtype: float64
在GroupBy中,當你調(diào)用諸如describe之類的方法時,實際上只是應(yīng)用了下面兩條代碼的快捷方式而已
f=lambda x: x.describe()
grouped.apply(f)

禁止分組鍵
從上面的例子中可以看出,分組鍵會跟原始對象的索引共同構(gòu)成結(jié)果對象中的層次化索引。將group_keys=False傳入groupby即可禁止該效果
tips.groupby('smoker',group_keys=False).apply(top)

分位數(shù)和桶分析
以下面這個簡單的隨機數(shù)據(jù)集為例,我們利用cut將其裝入長度相等的桶中
frame=DataFrame({'data1':np.random.randn(1000),'data2':np.random.randn(1000)})
factor=pd.cut(frame.data1,4)
factor[:8]
0 (-0.131, 1.515]
1 (-3.432, -1.778]
2 (1.515, 3.162]
3 (-0.131, 1.515]
4 (-0.131, 1.515]
5 (1.515, 3.162]
6 (-1.778, -0.131]
7 (-1.778, -0.131]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-3.432, -1.778] < (-1.778, -0.131] < (-0.131, 1.515] < (1.515, 3.162]]
由cut返回的Factor對象可直接用于groupby。因此,我們可以像下面這樣對data2做一些統(tǒng)計計算
def get_stats(group):
return {'min':group.min(),'max':group.max(),
'count':group.count(),'min':group.mean()}
grouped=frame.data2.groupby(factor)
grouped.apply(get_stats).unstack()

這些都是長度相等的桶。要根據(jù)樣本分位數(shù)得到大小相等的桶,使用qcut即可。傳入labels=False即可只獲取分位數(shù)的編號。
grouping=pd.qcut(frame.data1,10,labels=False)# 返回分位數(shù)編號
grouped=frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()

示例:用特定于分組的值填充缺失值
對于缺失數(shù)據(jù)的清理工作,有時你會用dropna將其濾除,而有時則可能會希望用一個固定值或由數(shù)據(jù)集本身所衍生出來的值去填充NA值。這時就得使用fillna這個工具了。在下面這個例子中,我用平均值去填充NA值
s=Series(np.random.randn(6))
s[::2]=np.nan #插入NA值
s
0 NaN
1 -1.522965
2 NaN
3 0.500331
4 NaN
5 -0.981807
dtype: float64
s.fillna(s.mean())
0 -0.668147
1 -1.522965
2 -0.668147
3 0.500331
4 -0.668147
5 -0.981807
dtype: float64
如果需要對不同的分組填充不同的值。只需將數(shù)據(jù)分組,并使用apply和一個能夠?qū)Ω鲾?shù)據(jù)塊調(diào)用fillna的函數(shù)即可。下面是一些有關(guān)美國幾個州的示例數(shù)據(jù),這些州又被分為東部和西部
states=['Ohio','New York','Vermont','Florida','Oregon','Nevada','California','Idaho']
group_key=['East']*4+['West']*4
data=Series(np.random.randn(8),index=states)
data[['Vermont','Nevada','Idaho']]=np.nan
data
Ohio 0.255096
New York 0.509371
Vermont NaN
Florida 0.658680
Oregon 0.475809
Nevada NaN
California 1.298450
Idaho NaN
dtype: float64
data.groupby(group_key).mean()
East 0.474382
West 0.887130
dtype: float64
用分組平均值去填充NA值
fill_mean=lambda g:g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
Ohio 0.255096
New York 0.509371
Vermont 0.474382
Florida 0.658680
Oregon 0.475809
Nevada 0.887130
California 1.298450
Idaho 0.887130
dtype: float64
也可以在代碼中預(yù)定義各組的填充值。由于分組具有一個name屬性
fill_values={'East':0.5,'West':-1}
fill_func=lambda g:g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)
Ohio 0.255096
New York 0.509371
Vermont 0.500000
Florida 0.658680
Oregon 0.475809
Nevada -1.000000
California 1.298450
Idaho -1.000000
dtype: float64
示例:隨機采樣和排列
假設(shè)你想要從一個大數(shù)據(jù)集中隨機抽取樣本以進行蒙特卡羅模擬(Monte Carlo simulation)或其他分析工作?!俺槿 钡姆绞接泻芏?,其中一些的效率會比其他的高很多。一個辦法是,選取np.random.permutation(N)的前K個元素,其中N為完整數(shù)據(jù)的大小,K為期望的樣本大小。作為一個更有趣的例子,下面是構(gòu)造一副英語型撲克牌的一個方式
# 紅桃(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 = Series(card_val, index=cards)
現(xiàn)在我有了一個長度為52的Series,其索引為牌名,值則是21點或其他游戲中用于計分的點數(shù)(為了簡單起見,我當A的點數(shù)為1)
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
根據(jù)我上面所講的,從整副牌中抽出5張,代碼如下:
def draw(deck,n=5):
return deck.take(np.random.permutation(len(deck))[:n])
draw(deck)
6S 6
2C 2
6D 6
6C 6
QC 10
dtype: int64
假設(shè)你想要從每種花色中隨機抽取兩張牌。由于花色是牌名的最后一個字符,所以我們可以據(jù)此進行分組,并使用apply
get_suit=lambda card: card[-1]# 只要最后一個字母就可以了
deck.groupby(get_suit).apply(draw,n=2)
C 4C 4
9C 9
D 4D 4
10D 10
H 9H 9
4H 4
S 10S 10
2S 2
dtype: int64
也可以這樣寫
deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
7C 7
6C 6
7D 7
9D 9
10H 10
6H 6
7S 7
5S 5
dtype: int64
示例:分組加權(quán)平均數(shù)和相關(guān)系數(shù)
根據(jù)groupby的“拆分-應(yīng)用-合并”范式,DataFrame的列與列之間或兩個Series之間的運算(比如分組加權(quán)平均)成為一種標準作業(yè)。以下面這個數(shù)據(jù)集為例,它含有分組鍵、值以及一些權(quán)重值
df = pd.DataFrame({'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
'data': np.random.randn(8),'weights': np.random.rand(8)})
df

可以利用category計算分組加權(quán)平均數(shù)
grouped=df.groupby('category')
get_wavg=lambda g:np.average(g['data'],weights=g['weights'])
grouped.apply(get_wavg)
category
a -0.548882
b -0.748385
dtype: float64
看一個稍微實際點的例子——來自Yahoo!Finance的數(shù)據(jù)集,其中含有標準普爾500指數(shù)(SPX字段)和幾只股票的收盤價
close_px=pd.read_csv('pydata_book/ch09/stock_px.csv',parse_dates=True,index_col=0)
close_px[:10]

close_px[-4:]

做一個比較有趣的任務(wù):計算一個由日收益率(通過百分數(shù)變化計算)與SPX之間的年度相關(guān)系數(shù)組成的DataFrame。下面是一個實現(xiàn)辦法
rets = close_px.pct_change().dropna()
spx_corr = lambda x: x.corrwith(x['SPX'])
by_year = rets.groupby(lambda x: x.year)
by_year.apply(spx_corr)

還可以計算列與列之間的相關(guān)系數(shù)
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT'])) # 蘋果和微軟的年度相關(guān)系數(shù)
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
示例:面向分組的線性回歸
順著上一個例子繼續(xù),你可以用groupby執(zhí)行更為復(fù)雜的分組統(tǒng)計分析,只要函數(shù)返回的是pandas對象或標量值即可。例如,我可以定義下面這個regress函數(shù)(利用statsmodels庫)對各數(shù)據(jù)塊執(zhí)行普通最小二乘法(Ordinary Least Squares,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
現(xiàn)在,為了按年計算AAPL對SPX收益率的線性回歸,進行執(zhí)行下面的
by_year.apply(regress, 'AAPL', ['SPX'])

透視表和交叉表
透視表(pivot table)是各種電子表格程序和其他數(shù)據(jù)分析軟件中一種常見的數(shù)據(jù)匯總工具。它根據(jù)一個或多個鍵對數(shù)據(jù)進行聚合,并根據(jù)行和列上的分組鍵將數(shù)據(jù)分配到各個矩形區(qū)域中。在Python和pandas中,可以通過本章所介紹的groupby功能以及(能夠利用層次化索引的)重塑運算制作透視表。DataFrame有一個pivot_table方法,此外還有一個頂級的pandas.pivot_table函數(shù)。除能為groupby提供便利之外,pivot_table還可以添加分項小計(也叫做margins)。
回到小費數(shù)據(jù)集,假設(shè)我想要根據(jù)sex和smoker計算分組平均數(shù)(pivot_table的默認聚合類型),并將sex和smoker放到行上
tips.pivot_table(index=['sex', 'smoker'])

現(xiàn)在,假設(shè)我們只想聚合tip_pct和size,而且想根據(jù)day進行分組。我將smoker放到列上,把day放到行上
注意:第一版的rows和cols報錯警告,需要修改為index和columns
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
columns='smoker')

傳入margins=True添加分項小計。這將會添加標簽為All的行和列,其值對應(yīng)于單個等級中所有數(shù)據(jù)的分組統(tǒng)計。在下面這個例子中,All值為平均數(shù):不單獨考慮煙民與非煙民(All列),不單獨考慮行分組兩個級別中的任何單項(All行)
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
columns='smoker', margins=True)

要使用其他的聚合函數(shù),將其傳給aggfunc即可。例如,使用count或len可以得到有關(guān)分組大小的交叉表
tips.pivot_table('tip_pct',index=['sex','smoker'],
columns='day',aggfunc=len,margins=True)

如果存在空的組合(也就是NA),你可能會希望設(shè)置一個fill_value
tips.pivot_table('size',index=['time','sex','smoker'],
columns='day',aggfunc=sum,fill_value=0)


交叉表:crosstab 缺少數(shù)據(jù)就沒有進行練習(xí)了,自己另外學(xué)習(xí)。