3.9 累計與分組

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 在探索真實數據集時快速組合多種操作的能力——只用寥寥幾行代碼,就可以讓我們立即對過去幾十年裏不同年代的行星發現方法有一個大概的瞭解。 我建議你花點時間分析這幾行代碼,確保自己真正理解了每一行代碼對結果產生了怎樣的影響。雖然這個例子的確有點兒複雜,但是理解這幾行代碼的含義可以幫你掌握分析類似數據的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章