數(shù)據(jù)分析比較常見的步驟是將對數(shù)據(jù)集進(jìn)行分組然后應(yīng)用函數(shù)低千,這步也可以稱之為分組運(yùn)算。Hadley Wickham大神為此創(chuàng)造了一個專用術(shù)語“split-apply-combine"箱吕,即拆分-應(yīng)用-合并。那么當(dāng)我們談?wù)?strong>分組運(yùn)算的時候柿冲,我們其實在談?wù)撌裁茨兀?/p>
- Splitting:根據(jù)標(biāo)準(zhǔn)對數(shù)據(jù)進(jìn)行拆分分組
- Applying: 對每組都分別應(yīng)用一個函數(shù)
- Combining: 將結(jié)果合并新的數(shù)據(jù)結(jié)構(gòu)
分組運(yùn)算一般要求的數(shù)據(jù)存放格式為“長格式”茬高,所以先介紹“長格式”和“寬格式”的轉(zhuǎn)換,然后是分組運(yùn)算的具體操作假抄。
“長格式“ VS”寬格式“
數(shù)據(jù)的常見保存方式有兩種怎栽,長格式(long format)或是寬格式(wide format)
- wide format
基因 | 分生組織 | 根 | 花 |
---|---|---|---|
gene1 | 582 | 91 | 495 |
gene2 | 305 | 3505 | 33 |
在寬格式下,類別型變量單獨成列宿饱。如上的植物的不同部位分為3列熏瞄,列中的數(shù)據(jù)表示為表達(dá)量∶裕看起來就非常的直觀强饮,而且日常生活中也是按照如此方法記錄數(shù)據(jù)。
- long format
基因 | 組織 | 表達(dá)量 |
---|---|---|
gene1 | 分生組織 | 582 |
gene2 | 分生組織 | 305 |
gene1 | 根 | 91 |
gene2 | 根 | 3503 |
gene1 | 花 | 492 |
gene2 | 花 | 33 |
而長結(jié)構(gòu)的數(shù)據(jù)則是類別型變量定義為專門的一列为黎。通常情況下邮丰,關(guān)系型數(shù)據(jù)庫(如MySQL)通常以長格式存儲數(shù)據(jù),這是因為固定架構(gòu)下隨著表中數(shù)據(jù)的增加或刪除铭乾, 類別行(如組織)能夠增加或減少剪廉。
后續(xù)的分組操作其實更傾向于數(shù)據(jù)是以長格式進(jìn)行保存,所以這里先介紹pandas是如何進(jìn)行長格式和寬格式之間的轉(zhuǎn)換的炕檩。
首先創(chuàng)建一個用于測試的長格式數(shù)據(jù)斗蒋,方法如下:
import pandas.util.testing as tm; tm.N = 3
def unpivot(frame):
N, K = frame.shape
data = {'value' : frame.values.ravel('F'),
'variable' : np.asarray(frame.columns).repeat(N),
'date' : np.tile(np.asarray(frame.index), K)}
return pd.DataFrame(data, columns=['date', 'variable', 'value'])
ldata = unpivot(tm.makeTimeDataFrame())
然后把長格式變成寬格式,可認(rèn)為是把數(shù)據(jù)的列“旋轉(zhuǎn)” 為行
# 第一種方法: pivot
# pd.pivot(index, columns, values), 對應(yīng)索引笛质,類別列和數(shù)值列
pivoted = ldata.pivot('date','variable','value')
# 第二種方法: unstack
## 先用set_index建立層次化索引
unstacked = ldata.set_index(['date','varibale']).unstack('variable')
再把寬格式變?yōu)殚L格式泉沾,也可以認(rèn)為是把數(shù)據(jù)的行“旋轉(zhuǎn)”成列。
# 先把unstacked數(shù)據(jù)還原成普通的DataFrame
wdata = nstacked.reset_index()
wdata.columns = ['date','A','B','C','D']
# 第一種方法:melt
pd.melt(wdata, id_vars=['date'])
# 第二種方法: stack
# 如果原始數(shù)據(jù)沒有索引妇押,需要用set_index重建
wdata.set_index('date').stack()
GroupBy
第一步是將數(shù)據(jù)集根據(jù)一定標(biāo)準(zhǔn)跷究,即分組鍵,拆分多個組舆吮,pandas提供了grouby
方法揭朝,能夠根據(jù)如下形式的分組鍵工作:
- 列表和數(shù)組(Numpy array)队贱,其長度與待分組的軸一致
- DataFrame的某個列名的值
- 字典或Series色冀,提供了待分組軸上的值與分組名(label -> group name)之間的對應(yīng)關(guān)系
- 根據(jù)多層次的軸索引
- Python函數(shù)潭袱,用于軸索引或索引中的各個標(biāo)簽
下面舉例說明:
形式一:根據(jù)某一列的值,一般是類別型數(shù)值
df = DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo'],
'B' : ['one', 'one', 'two', 'three',
'two', 'two', 'one', 'three'],
'C' : np.random.randn(8),
'D' : np.random.randn(8)})
# 根據(jù)A列進(jìn)行拆分
df.groupby('A')
# 根據(jù)A和B列進(jìn)行拆分
df.groupby(['A','B'])
這會產(chǎn)生一個pandas.core.groupby.DataFrameGroupBy object
對象锋恬,沒有進(jìn)行實質(zhì)性的運(yùn)算操作屯换,只產(chǎn)生了中間信息。你可以查看分組之后每一組的大小
df.groupby(['A','B']).size()
形式二: 根據(jù)函數(shù). 下面定義了一個函數(shù)根據(jù)列名是否屬于元音進(jìn)行拆分
def get_letter_type(letter):
if letter.lower() in 'aeiou':
return 'vowel'
else:
return 'consonant'
grouped = df.groupby(get_letter_type, axis=1).
形式三: 根據(jù)層次索引与学。 不同于R語言data.frame只有一個行或列名彤悔,pandas的DataFrame允許多個層次結(jié)構(gòu)的行或列名。
# 構(gòu)建多層次索引的數(shù)據(jù)
arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'],
['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]
index = pd.MultiIndex.from_arrays(arrays, names=['first', 'second'])
s = pd.Series(np.random.randn(8), index=index)
# 根據(jù)不同層次的索引進(jìn)行分組
level1 = s.groupby(level='first') # 第一層
level2 = s.groupby(level=1) # 第二層
形式四: 根據(jù)字典或Series進(jìn)行分組
可以通過字典定義分組信息的映射索守。
mapping = {'A':'str','B':'str','C':'values','D':'values','E':'values'}
df.groupby(mapping, axis=1)
形式四: 同時根據(jù)行索引和列數(shù)值進(jìn)行分組
df.index = index
df.groupby([pd.Grouper(level='first'),'A']).mean()
分組對象迭代: 上面產(chǎn)生的GroupBy分組對象都是可以進(jìn)行迭代晕窑,會產(chǎn)生一個一組二元元組。
# 對于單鍵值
for name, group in df.groupby(['A']):
print(name)
print(group)
# 對于多個鍵值
for (k1,k2), group in df.groupby(['A']):
print(k1,k2)
print(group)
選擇分組:我們可以在分組結(jié)束后用get_group()
提取某個特定的組
df.groupby('A').get_group('foo')
# 下面是結(jié)果
A B C D E
first second
bar one foo one 0.709603 -1.023657 -0.321730
baz one foo two 1.495328 -0.279928 -0.297983
foo one foo two 0.495288 -0.563845 -1.774453
qux one foo one 1.649135 -0.274208 1.194536
two foo three -2.194127 3.440418 -0.023144
# 多個組
df.groupby(['A', 'B']).get_group(('bar', 'one'))
Apply
分組步驟較為直接卵佛,而應(yīng)用函數(shù)步驟則變化較多杨赤。這一步,我們可能會做如下操作:
- 數(shù)據(jù)聚合(Aggregation):分別計算各組的統(tǒng)計信息截汪,如均值疾牲,大小等
- 轉(zhuǎn)換(Transformation): 對每一組進(jìn)行特異性的計算,如標(biāo)準(zhǔn)化衙解,缺失值插值等
- 過濾(Filtration): 根據(jù)統(tǒng)計信息進(jìn)行篩選阳柔,舍棄部分組。
數(shù)據(jù)聚合:Aggregation
所謂的聚合蚓峦,也就是數(shù)組產(chǎn)生標(biāo)量值的數(shù)據(jù)轉(zhuǎn)換過程舌剂。我們可以使用mean, count, min, sum
等一般性方法,也可以用GroupBy對象創(chuàng)建后的aggregate
或等價的agg
方法暑椰。
grouped = df.groupby('A')
grouped.mean()
# 等價于
grouped.agg(np.mean)
對于多個分組而言架诞,會產(chǎn)生層次索引的結(jié)果,如果希望層次索引成為單獨一列的話干茉,需要在groupby
用到as_index
選項谴忧。或者最后使用reset_index
方法
# as_index
grouped = df.groupby(['A','B'], as_index=False)
grouped.agg(np.mean)
# reset_index
df.groupby(['A','B']).sum().reset_index()
aggregate
或等價的agg
還允許對每一列使用多個統(tǒng)計函數(shù)
df.groupby('A').agg([np.sum,np.mean,np.std])
# 語法糖 df.groupby('A').C 等價于
# grouped=df.groupby(['A'])
# grouped['C']
df.groupby('A').C.agg([np.sum,np.mean,np.std])
或者是不同列使用不同函數(shù)角虫,比如說對D列計算標(biāo)準(zhǔn)差沾谓,對列求平均
grouped=df.groupby(['A'])
grouped.agg({'D': np.std , 'C': np.mean})
注:目前sum, mean, std和sem方法對Cython進(jìn)行了優(yōu)化。
轉(zhuǎn)換: Transformation
transform
返回的對象和原來分組數(shù)據(jù)具有相同的大小戳鹅。轉(zhuǎn)換所用的函數(shù)必須滿足如下條件:
- 返回的結(jié)果大小要與原先的組塊(group chunk)一致
- 在組塊中逐列操作
- 并非在組塊上原位運(yùn)算均驶。原先的組塊被認(rèn)為是不可修改的,任何對原來組塊的修改可能會導(dǎo)致意想不到的結(jié)果枫虏。如果你用到了
fillna
妇穴,必須是grouped.transform(lambda x: x.fillna(inplace=False))
舉例說明爬虱,比如說我們相對每一組數(shù)據(jù)進(jìn)行標(biāo)準(zhǔn)化。當(dāng)然我們先得有一組數(shù)據(jù), 隨機(jī)生成一組從1999年到2002年腾它,然后以100天為一個滑窗(window)跑筝,計算每一個滑窗的均值(rolling)。
# date_range產(chǎn)生日期索引
index = pd.date_range('10/1/1999', periods=1100)
ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
ts = ts.rolling(window=100,min_periods=100).mean().dropna()
然后要根據(jù)年份進(jìn)行分組瞒滴,對各組中的數(shù)據(jù)進(jìn)行標(biāo)準(zhǔn)化曲梗,計算zscore.
# 兩個匿名函數(shù),用于分組和計算zscore
key = lambda x : x.year
zscore = lambda x : (x - x.mean())/x.std()
transformed = ts.groupby(key).transform(zscore)
應(yīng)用zscore的過程中使用了廣播(broadcast)技術(shù)妓忍。讓我們可視化一下轉(zhuǎn)換前后數(shù)據(jù)形狀
compare = pd.DataFrame({'Original': ts, 'Transformed': transformed})
compare.plot()
過濾:Filtration
過濾返回原先數(shù)據(jù)的子集虏两。比如說上面時間周期數(shù)據(jù),過濾掉均值小于0.55的年份世剖。
ts.groupby(key).filter(lambda x : x.mean() > 0.55)
Apply: 更加靈活的“拆分-應(yīng)用-合并”
上述的aggregate
和transform
都有一定的局限性定罢,傳入的函數(shù)只能有兩個結(jié)果,要么是產(chǎn)生一個可以廣播的標(biāo)量值旁瘫,要么是產(chǎn)生相同大小的數(shù)組祖凫。apply是一個更加一般化的函數(shù),能完成上面所描述的所有任務(wù)境蜕,還能做得更好蝙场。
- 分組返回描述性統(tǒng)計結(jié)果
df.groupby('A').apply(lambda x : x.describe())
# 效果等價于df.groupby('A').describe()
- 修改返回的分組名
def f(group):
return pd.DataFrame({'original' : group,
'demeaned' : group - group.mean()})
df.groupby('A')['C'].apply(f)
demeaned original
first second
bar one 0.278558 0.709603
two 0.280707 -0.140075
baz one 1.064283 1.495328
two -0.848109 -1.268891
foo one 0.064243 0.495288
two 0.567402 0.146620
qux one 1.218089 1.649135
two -2.625172 -2.194127
更多有用的技巧如獲取每一組的第n行,見官方文檔