《利用Python進行數據分析》第9章 分組級運算和轉換筆記

分組級運算和轉換

聚合是分組運算的其中一種。它是數據轉換的一個特例,它接受能夠將一維數組簡化為標量值的函數。

接下來將介紹transform和apply方法,它們能夠執行更多其他的分組運算。

如果要為一個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函數對兩個數據列進行轉換,我們將在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會將一個函數應用到各個分組,然后將結果放置到適當的位置上。如果各分組產生的是一個標量值,則該值就會被廣播出去。現在,假設你希望從各組中減去平均值。為此,我們先創建一個距平化函數(demeaning function),然后將其傳給transform

def demean(arr):
    return arr-arr.mean()
demeaned=people.groupby(key).transform(demean)
demeaned

檢查一下demeaned現在的分組平均值是否為0

demeaned.groupby(key).mean()

apply:一般性的“拆分-應用-合并”

跟aggregate一樣,transform也是一個有著嚴格條件的特殊函數:傳入的函數只能產生兩種結果,要么產生一個可以廣播的標量值(如np.mean),要么產生一個相同大小的結果數組。最一般化的GroupBy方法是apply。

回到之前那個小費數據集,假設你想要根據分組選出最高的5個tip_pct值。首先,編寫一個選取指定列具有最大值的行的函數

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,現在使用新的sort_values

如果對smoker分組并用該函數調用apply,就會得到

tips.groupby('smoker').apply(top)

top函數在DataFrame的各個片段上調用,然后結果由pandas.concat組裝到一起,并以分組名稱進行了標記。于是,最終結果就有了一個層次化索引,其內層索引值來自原DataFrame。

如果傳給apply的函數能夠接受其他參數或關鍵字,則可以將這些內容放在函數名后面一并傳入

tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill')

之前在GroupBy對象上調用過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中,當你調用諸如describe之類的方法時,實際上只是應用了下面兩條代碼的快捷方式而已

f=lambda x: x.describe()
grouped.apply(f)

禁止分組鍵

從上面的例子中可以看出,分組鍵會跟原始對象的索引共同構成結果對象中的層次化索引。將group_keys=False傳入groupby即可禁止該效果

tips.groupby('smoker',group_keys=False).apply(top)

分位數和桶分析

以下面這個簡單的隨機數據集為例,我們利用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做一些統計計算

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()

這些都是長度相等的桶。要根據樣本分位數得到大小相等的桶,使用qcut即可。傳入labels=False即可只獲取分位數的編號。

grouping=pd.qcut(frame.data1,10,labels=False)# 返回分位數編號
grouped=frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()

示例:用特定于分組的值填充缺失值

對于缺失數據的清理工作,有時你會用dropna將其濾除,而有時則可能會希望用一個固定值或由數據集本身所衍生出來的值去填充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

如果需要對不同的分組填充不同的值。只需將數據分組,并使用apply和一個能夠對各數據塊調用fillna的函數即可。下面是一些有關美國幾個州的示例數據,這些州又被分為東部和西部

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

也可以在代碼中預定義各組的填充值。由于分組具有一個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

示例:隨機采樣和排列

假設你想要從一個大數據集中隨機抽取樣本以進行蒙特卡羅模擬(Monte Carlo simulation)或其他分析工作。“抽取”的方式有很多,其中一些的效率會比其他的高很多。一個辦法是,選取np.random.permutation(N)的前K個元素,其中N為完整數據的大小,K為期望的樣本大小。作為一個更有趣的例子,下面是構造一副英語型撲克牌的一個方式

# 紅桃(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)

現在我有了一個長度為52的Series,其索引為牌名,值則是21點或其他游戲中用于計分的點數(為了簡單起見,我當A的點數為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

根據我上面所講的,從整副牌中抽出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

假設你想要從每種花色中隨機抽取兩張牌。由于花色是牌名的最后一個字符,所以我們可以據此進行分組,并使用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

示例:分組加權平均數和相關系數

根據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計算分組加權平均數

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的數據集,其中含有標準普爾500指數(SPX字段)和幾只股票的收盤價

close_px=pd.read_csv('pydata_book/ch09/stock_px.csv',parse_dates=True,index_col=0)
close_px[:10]
close_px[-4:]

做一個比較有趣的任務:計算一個由日收益率(通過百分數變化計算)與SPX之間的年度相關系數組成的DataFrame。下面是一個實現辦法

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)

還可以計算列與列之間的相關系數

by_year.apply(lambda g: g['AAPL'].corr(g['MSFT'])) # 蘋果和微軟的年度相關系數
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

示例:面向分組的線性回歸

順著上一個例子繼續,你可以用groupby執行更為復雜的分組統計分析,只要函數返回的是pandas對象或標量值即可。例如,我可以定義下面這個regress函數(利用statsmodels庫)對各數據塊執行普通最小二乘法(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

現在,為了按年計算AAPL對SPX收益率的線性回歸,進行執行下面的

by_year.apply(regress, 'AAPL', ['SPX'])

透視表和交叉表

透視表(pivot table)是各種電子表格程序和其他數據分析軟件中一種常見的數據匯總工具。它根據一個或多個鍵對數據進行聚合,并根據行和列上的分組鍵將數據分配到各個矩形區域中。在Python和pandas中,可以通過本章所介紹的groupby功能以及(能夠利用層次化索引的)重塑運算制作透視表。DataFrame有一個pivot_table方法,此外還有一個頂級的pandas.pivot_table函數。除能為groupby提供便利之外,pivot_table還可以添加分項小計(也叫做margins)。

回到小費數據集,假設我想要根據sex和smoker計算分組平均數(pivot_table的默認聚合類型),并將sex和smoker放到行上

 tips.pivot_table(index=['sex', 'smoker'])

現在,假設我們只想聚合tip_pct和size,而且想根據day進行分組。我將smoker放到列上,把day放到行上

注意:第一版的rows和cols報錯警告,需要修改為index和columns
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
                  columns='smoker')

傳入margins=True添加分項小計。這將會添加標簽為All的行和列,其值對應于單個等級中所有數據的分組統計。在下面這個例子中,All值為平均數:不單獨考慮煙民與非煙民(All列),不單獨考慮行分組兩個級別中的任何單項(All行)

tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
                   columns='smoker', margins=True)

要使用其他的聚合函數,將其傳給aggfunc即可。例如,使用count或len可以得到有關分組大小的交叉表

tips.pivot_table('tip_pct',index=['sex','smoker'],
                            columns='day',aggfunc=len,margins=True)

如果存在空的組合(也就是NA),你可能會希望設置一個fill_value

tips.pivot_table('size',index=['time','sex','smoker'],
                              columns='day',aggfunc=sum,fill_value=0)

交叉表:crosstab 缺少數據就沒有進行練習了,自己另外學習。

總結:本章知識比較多,不能很快的掌握,接下來需要復習再進行下一章的時間序列學習。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容