3.9 累計與分組
在對較大的數據進行分析時,一項基本工作就是進行有效的數據積累,計算積累指標,如和、平均值、中值、最值等,其中每個指標都呈現了大數據集的特徵。pd有累計功能。
3.9.1 行星數據
通過網上seaborn類提供的行星數據進行各種演示:
import numpy as np import pandas as pd class display(object): """Display HTML representation of multiple objects""" template = """<div style="float: left; padding: 10px;"> <p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1} </div>""" def __init__(self, *args): self.args = args def _repr_html_(self): return '\n'.join(self.template.format(a, eval(a)._repr_html_()) for a in self.args) def __repr__(self): return '\n\n'.join(a + '\n' + repr(eval(a)) for a in self.args)
import seaborn as sns planets = sns.load_dataset('planets') planets.shape
(1035, 6)
planets.head()
method | number | orbital_period | mass | distance | year | |
---|---|---|---|---|---|---|
0 | Radial Velocity | 1 | 269.300 | 7.10 | 77.40 | 2006 |
1 | Radial Velocity | 1 | 874.774 | 2.21 | 56.95 | 2008 |
2 | Radial Velocity | 1 | 763.000 | 2.60 | 19.84 | 2011 |
3 | Radial Velocity | 1 | 326.030 | 19.40 | 110.62 | 2007 |
4 | Radial Velocity | 1 | 516.220 | 10.50 | 119.47 | 2009 |
數據包括了1000多個外行星的數據。
3.9.2 Pandas的簡單累計功能
與一維np數組相同,pd的Series的累計函數也會返回一個統計值:
rng = np.random.RandomState(42) ser = pd.Series(rng.rand(5)) ser
0 0.374540 1 0.950714 2 0.731994 3 0.598658 4 0.156019 dtype: float64
ser.sum()
2.811925491708157
ser.mean()
0.5623850983416314
DF的累計函數默認對每列進行統計:
df = pd.DataFrame({'A': rng.rand(5), 'B': rng.rand(5)}) df
A | B | |
---|---|---|
0 | 0.155995 | 0.020584 |
1 | 0.058084 | 0.969910 |
2 | 0.866176 | 0.832443 |
3 | 0.601115 | 0.212339 |
4 | 0.708073 | 0.181825 |
df.mean()
A 0.477888 B 0.443420 dtype: float64
設置axis參數就可以對每一行進行統計:
df.mean(axis='columns')
0 0.088290 1 0.513997 2 0.849309 3 0.406727 4 0.444949 dtype: float64
pd的Series和DF支持所有2.4節中介紹的常用累計函數。另外,還有一個非常方便的describe方法可以計算每列的若干常用統計值。
對於行星數據,先丟棄有缺失值的行,再用describe:
planets.dropna().describe()
number | orbital_period | mass | distance | year | |
---|---|---|---|---|---|
count | 498.00000 | 498.000000 | 498.000000 | 498.000000 | 498.000000 |
mean | 1.73494 | 835.778671 | 2.509320 | 52.068213 | 2007.377510 |
std | 1.17572 | 1469.128259 | 3.636274 | 46.596041 | 4.167284 |
min | 1.00000 | 1.328300 | 0.003600 | 1.350000 | 1989.000000 |
25% | 1.00000 | 38.272250 | 0.212500 | 24.497500 | 2005.000000 |
50% | 1.00000 | 357.000000 | 1.245000 | 39.940000 | 2009.000000 |
75% | 2.00000 | 999.600000 | 2.867500 | 59.332500 | 2011.000000 |
max | 6.00000 | 17337.500000 | 25.000000 | 354.000000 | 2014.000000 |
這是一種理解數據集所有統計屬性的有效方法。如,從年份看,1989年首先發現外行星,而且一半的一隻外行星都是在2010年及以後發現的。
pd內置的一些累計方法如下:
方法 | 描述 |
---|---|
count() |
計數項 |
first() , last() |
第一項與最後一項 |
mean() , median() |
均值與中位數 |
min() , max() |
最小值與最大值 |
std() , var() |
標準差與方差 |
mad() |
均值絕對偏差 |
prod() |
所有項乘積 |
sum() |
所有項求和 |
Series和DF對象支持以上所有方法。但若想深入理解數據,僅僅依靠累計函數是不夠的,數據累計下一級別是groupby分組操作,其可快速有效地計算數據各個子集的累計值。
3.9.3 GroupBy:分割、應用和組合
分割、應用和組合
這一系列操作是:將數據分割爲小組——每組應用特定的操作進行數據處理——將結果組合爲最終輸出的值。而內部過程對用戶是透明的,只需要一行命令即可。
df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'], 'data': range(6)}, columns=['key', 'data']) df
key | data | |
---|---|---|
0 | A | 0 |
1 | B | 1 |
2 | C | 2 |
3 | A | 3 |
4 | B | 4 |
5 | C | 5 |
groupby方法可以完成絕大多數常見的分割應用組合操作,將需要分組的列名作爲參數傳入即可:
df.groupby('key')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x09AADAB0>
注意,這裏的返回值不是DF對象,而是DFGroupBy對象:其可看成特殊的DF,內部隱藏着若干組數據,但在沒有應用累計函數之前不會計算。這種封裝便於用戶使用。
爲得到結果,可對DFGB對象應用累計函數,算出結果:
df.groupby('key').sum()
data | |
---|---|
key | |
A | 3 |
B | 5 |
C | 7 |
df.groupby('key').sum().describe()
data | |
---|---|
count | 3.0 |
mean | 5.0 |
std | 2.0 |
min | 3.0 |
25% | 4.0 |
50% | 5.0 |
75% | 6.0 |
max | 7.0 |
df.groupby('key').describe()
data | ||||||||
---|---|---|---|---|---|---|---|---|
count | mean | std | min | 25% | 50% | 75% | max | |
key | ||||||||
A | 2.0 | 1.5 | 2.12132 | 0.0 | 0.75 | 1.5 | 2.25 | 3.0 |
B | 2.0 | 2.5 | 2.12132 | 1.0 | 1.75 | 2.5 | 3.25 | 4.0 |
C | 2.0 | 3.5 | 2.12132 | 2.0 | 2.75 | 3.5 | 4.25 | 5.0 |
GroupBy對象
是一種非常靈活的抽象類型。在大多數場景中,可將其看成是DF的集合,在底層結果所有難題。
用行星數據作爲演示GB對象的基本操作:
按列取值
GB對象與DF對象一樣,也支持按列取值,餅返回一個修改過的GB對象,如:
planets.groupby('method')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x09AAD050>
planets.groupby('method')['orbital_period']
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x09ACA9B0>
第一句命令將DF按照method數據列進行分組;第二句命令將分組的DF數據中取出名爲orbital_period的一列數據。所以返回的是SGB對象。
這裏從原來的DF中取某個列名作爲一個Series組。與GB對象一樣,直到運行累計函數,纔會開始計算:
planets.groupby('method')['orbital_period'].median()
method Astrometry 631.180000 Eclipse Timing Variations 4343.500000 Imaging 27500.000000 Microlensing 3300.000000 Orbital Brightness Modulation 0.342887 Pulsar Timing 66.541900 Pulsation Timing Variations 1170.000000 Radial Velocity 360.200000 Transit 5.714932 Transit Timing Variations 57.011000 Name: orbital_period, dtype: float64
這樣就可以獲得不同方法method下所有行星公轉週期orbital_period(按天計算)的中位數。
按組迭代
GB對象支持直接按組進行迭代,返回的每一組都是Series或DF:
for (method, group) in planets.groupby('method'): print("{0:30s} shape={1}".format(method, group.shape))
Astrometry shape=(2, 6) Eclipse Timing Variations shape=(9, 6) Imaging shape=(38, 6) Microlensing shape=(23, 6) Orbital Brightness Modulation shape=(3, 6) Pulsar Timing shape=(5, 6) Pulsation Timing Variations shape=(1, 6) Radial Velocity shape=(553, 6) Transit shape=(397, 6) Transit Timing Variations shape=(4, 6)
儘管通常還是使用內置的apply功能速度更快,但這種方法在手動處理某些問題時非常有用,見後。
調用方法
藉助Python類的魔力(@classmethod),可以讓任何不由GB對象直接實現的方法直接應用到每一組,無論是DF還是Series對象都同樣適用。例如可用DF的describe方法進行累計,對每一組數據進行描述性統計:
planets.groupby('method')['year'].describe()
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
method | ||||||||
Astrometry | 2.0 | 2011.500000 | 2.121320 | 2010.0 | 2010.75 | 2011.5 | 2012.25 | 2013.0 |
Eclipse Timing Variations | 9.0 | 2010.000000 | 1.414214 | 2008.0 | 2009.00 | 2010.0 | 2011.00 | 2012.0 |
Imaging | 38.0 | 2009.131579 | 2.781901 | 2004.0 | 2008.00 | 2009.0 | 2011.00 | 2013.0 |
Microlensing | 23.0 | 2009.782609 | 2.859697 | 2004.0 | 2008.00 | 2010.0 | 2012.00 | 2013.0 |
Orbital Brightness Modulation | 3.0 | 2011.666667 | 1.154701 | 2011.0 | 2011.00 | 2011.0 | 2012.00 | 2013.0 |
Pulsar Timing | 5.0 | 1998.400000 | 8.384510 | 1992.0 | 1992.00 | 1994.0 | 2003.00 | 2011.0 |
Pulsation Timing Variations | 1.0 | 2007.000000 | NaN | 2007.0 | 2007.00 | 2007.0 | 2007.00 | 2007.0 |
Radial Velocity | 553.0 | 2007.518987 | 4.249052 | 1989.0 | 2005.00 | 2009.0 | 2011.00 | 2014.0 |
Transit | 397.0 | 2011.236776 | 2.077867 | 2002.0 | 2010.00 | 2012.0 | 2013.00 | 2014.0 |
Transit Timing Variations | 4.0 | 2012.500000 | 1.290994 | 2011.0 | 2011.75 | 2012.5 | 2013.25 | 2014.0 |
這張表可幫助我們對數據有更深刻的認識,如大多數行星都是通過Radial Velocity 和 Transit 方法發現的,而且後者在近十年變得越來越普遍,最新的 Transit Timing Variation 和 Orbital Brightness Modulation 方法在2011年之後纔有新發現。
這只是演示pd調用方法的示例之一。方法首先會應用到每組數據上,然後結果由GroupBy組合後返回。另外,任意DF或Series的方法都可由GB方法調用,從而實現非常靈活強大的操作。
累計、過濾、轉換和應用
雖然前面的章節只重點介紹了組合操作,但是還有許多操作沒有介紹,尤其是 GroupBy 對象的 aggregate()、filter()、transform() 和 apply() 方法,在數據組合之前實現了大量高效的操作。 爲了方便後面內容的演示,使用下面這個 DataFrame:
rng = np.random.RandomState(0) df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'], 'data1': range(6), 'data2': rng.randint(0, 10, 6)}, columns = ['key', 'data1', 'data2']) df
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
(1) 累計。我們目前比較熟悉的 GroupBy 累計方法只有 sum() 和 median() 之類的簡單函數,但是 aggregate() 其實可以支持更復雜的操作,比如字符串、函數或者函數列表,並且能一次性計算所有累計值。下面來快速演示一個例子:
df.groupby('key').aggregate(['min', np.median, max])
data1 | data2 | |||||
---|---|---|---|---|---|---|
min | median | max | min | median | max | |
key | ||||||
A | 0 | 1.5 | 3 | 3 | 4.0 | 5 |
B | 1 | 2.5 | 4 | 0 | 3.5 | 7 |
C | 2 | 3.5 | 5 | 3 | 6.0 | 9 |
另一種用法就是通過 Python 字典指定不同列需要累計的函數:
df.groupby('key').aggregate({'data1': 'min', 'data2': 'max'})
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 7 |
C | 2 | 9 |
(2) 過濾。過濾操作可以讓你按照分組的屬性丟棄若干數據。例如,我們可能只需要保留標準差超過某個閾值的組:
def filter_func(x): return x['data2'].std() > 4 display('df', "df.groupby('key').std()", "df.groupby('key').filter(filter_func)")
df
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby('key').std()
data1 | data2 | |
---|---|---|
key | ||
A | 2.12132 | 1.414214 |
B | 2.12132 | 4.949747 |
C | 2.12132 | 4.242641 |
df.groupby('key').filter(filter_func)
key | data1 | data2 | |
---|---|---|---|
1 | B | 1 | 0 |
2 | C | 2 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
filter_func () 函數會返回一個布爾值,表示每個組是否通過過濾。由於 A 組 'data2' 列的標準差不大於 4,所以被丟棄了。
(3) 轉換。累計操作返回的是對組內全量數據縮減過的結果,而轉換操作會返回一個新的全量數據。數據經過轉換之後,其形狀與原來的輸入數據是一樣的。常見的例子就是將每一組的樣本數據減去各組的均值,實現數據標準化:
df.groupby('key').transform(lambda x: x - x.mean())
data1 | data2 | |
---|---|---|
0 | -1.5 | 1.0 |
1 | -1.5 | -3.5 |
2 | -1.5 | -3.0 |
3 | 1.5 | -1.0 |
4 | 1.5 | 3.5 |
5 | 1.5 | 3.0 |
(4) apply() 方法。apply() 方法讓你可以在每個組上應用任意方法。這個函數輸入一個 DataFrame,返回一個 Pandas 對象(DataFrame 或 Series)或一個標量(scalar,單個數值)。組合操作會適應返回結果類型。
下面的例子就是用 apply() 方法將第一列數據以第二列的和爲基數進行標準化:
def norm_by_data2(x): # x is a DataFrame of group values x['data1'] /= x['data2'].sum() return x display('df', "df.groupby('key').apply(norm_by_data2)")
df
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby('key').apply(norm_by_data2)
key | data1 | data2 | |
---|---|---|---|
0 | A | 0.000000 | 5 |
1 | B | 0.142857 | 0 |
2 | C | 0.166667 | 3 |
3 | A | 0.375000 | 3 |
4 | B | 0.571429 | 7 |
5 | C | 0.416667 | 9 |
GroupBy 裏的 apply() 方法非常靈活,唯一需要注意的地方是它總是輸入分組數據的 DataFrame,返回 Pandas 對象或標量。具體如何選擇需要視情況而定。
設置分割的鍵
前面的簡單例子一直在用列名分割 DataFrame。這只是衆多分組操作中的一種,下面將繼續介紹更多的分組方法。
(1) 將列表、數組、Series 或索引作爲分組鍵。
分組鍵可以是長度與 DataFrame 匹配的任意 Series 或列表,例如:
L = [0, 1, 0, 1, 2, 0] display('df', 'df.groupby(L).sum()')
df
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby(L).sum()
data1 | data2 | |
---|---|---|
0 | 7 | 17 |
1 | 4 | 3 |
2 | 4 | 7 |
因此,還有一種比前面直接用列名更囉嗦的表示df.groupby('key') 的方法:
display('df', "df.groupby(df['key']).sum()")
df
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby(df['key']).sum()
data1 | data2 | |
---|---|---|
key | ||
A | 3 | 8 |
B | 5 | 7 |
C | 7 | 12 |
(2) 用字典或 Series 將索引映射到分組名稱。
另一種方法是提供一個字典,將索引映射到分組鍵:
df2 = df.set_index('key') mapping = {'A': 'vowel', 'B': 'consonant', 'C': 'consonant'} display('df2', 'df2.groupby(mapping).sum()')
df2
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 0 |
C | 2 | 3 |
A | 3 | 3 |
B | 4 | 7 |
C | 5 | 9 |
df2.groupby(mapping).sum()
data1 | data2 | |
---|---|---|
consonant | 12 | 19 |
vowel | 3 | 8 |
(3) 任意 Python 函數。
與前面的字典映射類似,你可以將任意 Python 函數傳入 groupby,函數映射到索引,然後新的分組輸出:
display('df2', 'df2.groupby(str.lower).mean()')
df2
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 0 |
C | 2 | 3 |
A | 3 | 3 |
B | 4 | 7 |
C | 5 | 9 |
df2.groupby(str.lower).mean()
data1 | data2 | |
---|---|---|
a | 1.5 | 4.0 |
b | 2.5 | 3.5 |
c | 3.5 | 6.0 |
(4) 多個有效鍵構成的列表。
此外,任意之前有效的鍵都可以組合起來進行分組,從而返回一個多級索引的分組結果:
df2.groupby([str.lower, mapping]).mean()
data1 | data2 | ||
---|---|---|---|
a | vowel | 1.5 | 4.0 |
b | consonant | 2.5 | 3.5 |
c | consonant | 3.5 | 6.0 |
分組案例
通過下例中的幾行 Python 代碼,我們就可以運用上述知識,獲取不同方法和不同年份發現的行星數量:
decade = 10 * (planets['year'] // 10) decade = decade.astype(str) + 's' decade.name = 'decade' planets.groupby(['method', decade])['number'].sum().unstack().fillna(0)
decade | 1980s | 1990s | 2000s | 2010s |
---|---|---|---|---|
method | ||||
Astrometry | 0.0 | 0.0 | 0.0 | 2.0 |
Eclipse Timing Variations | 0.0 | 0.0 | 5.0 | 10.0 |
Imaging | 0.0 | 0.0 | 29.0 | 21.0 |
Microlensing | 0.0 | 0.0 | 12.0 | 15.0 |
Orbital Brightness Modulation | 0.0 | 0.0 | 0.0 | 5.0 |
Pulsar Timing | 0.0 | 9.0 | 1.0 | 1.0 |
Pulsation Timing Variations | 0.0 | 0.0 | 1.0 | 0.0 |
Radial Velocity | 1.0 | 52.0 | 475.0 | 424.0 |
Transit | 0.0 | 0.0 | 64.0 | 712.0 |
Transit Timing Variations | 0.0 | 0.0 | 0.0 | 9.0 |
此例足以展現 GroupBy 在探索真實數據集時快速組合多種操作的能力——只用寥寥幾行代碼,就可以讓我們立即對過去幾十年裏不同年代的行星發現方法有一個大概的瞭解。 我建議你花點時間分析這幾行代碼,確保自己真正理解了每一行代碼對結果產生了怎樣的影響。雖然這個例子的確有點兒複雜,但是理解這幾行代碼的含義可以幫你掌握分析類似數據的方法。