1. 程式人生 > 其它 >【Task 4】分組

【Task 4】分組

技術標籤:# Pandaspython

一、分組模式及其物件

1. 分組的一般模式

實現分組操作,必須明確三個要素

  • 分組依據
  • 資料來源
  • 操作及其返回結果

同時從充分性的角度來說,明確了這三方面,就能確定一個分組操作,從而分組程式碼的一般模式即:

# 引用方法
df.groupby(分組依據)[資料來源].使用操作
  • 單一維度進行分組
# 依據 性別 分組,統計全國人口 壽命 的 平均值
df.groupby('Gender')['Longevity'].mean()


# 在學生體測的資料集上,按照 性別 統計 身高中位數
df = pd.read_csv('data/learn_pandas.csv')

df.groupby('Gender')['Height'].median()
## 輸出 
Gender
Female    159.6
Male      173.4
Name: Height, dtype: float64

2. 分組依據的本質

根據多個維度進行分組,只需在groupby中傳入相應列名構成的列表即可。

# 根據學校和性別進行分組,統計身高的 均值
df.groupby(['School', 'Gender'])['Height'].mean()
## 輸出
School                         Gender
Fudan University               Female    158.776923
                               Male      174.212500
Peking University              Female    158.666667
                               Male      172.030000
Shanghai Jiao Tong University  Female    159.122500
                               Male      176.760000
Tsinghua University            Female    159.753333
                               Male      171.638889
Name: Height, dtype: float64

groupby的分組依據都是直接可以從列中按照名字獲取的。

如果希望通過一定的複雜邏輯來分組,首先先寫出分組條件,然後將其傳入groupby中:

# 根據學生體重是否超過總體均值來分組,計算身高的均值

# 首先先寫出分組條件
condition = df.Weight > df.Weight.mean()

# 然後將其傳入 groupby 中
df.groupby(condition)['Height'].mean()
## 輸出
Weight
False    159.034646
True     172.705357
Name: Height, dtype: float64

上面從索引可以看出,最後產生的結果就是按照條件列表中元素的值`

TrueFalse)`來分組。

# 在列表中 用隨機傳入字母序列
item = np.random.choice(list('abc'), df.shape[0])

df.groupby(item)['Height'].mean()
## 輸出
a    163.924242
b    162.928814
c    162.708621
Name: Height, dtype: float64

這裡的索引就是原先item中的元素。

如果傳入多個序列進入groupby,那麼最後分組的依據就是這兩個序列對應行唯一組合

df.groupby([condition, item])['Height'].mean()
## 輸出
Weight   
False   a    160.193617
        b    158.921951
        c    157.756410
True    a    173.152632
        b    172.055556
        c    172.873684
Name: Height, dtype: float64

由此,之前傳入列名只是一種簡便的記號,事實上等價於傳入的是一個或多個列,最後分組的依據來自於資料來源組合的unique值,通過drop_duplicates能知道具體的組類別:

df[['School', 'Gender']].drop_duplicates()
## 輸出
                           School  Gender
0   Shanghai Jiao Tong University  Female
1               Peking University    Male
2   Shanghai Jiao Tong University    Male
3                Fudan University  Female
4                Fudan University    Male
5             Tsinghua University  Female
9               Peking University  Female
16            Tsinghua University    Male


# 傳入多個列
df.groupby([df['School'], df['Gender']])['Height'].mean()
## 輸出
School                         Gender
Fudan University               Female    158.776923
                               Male      174.212500
Peking University              Female    158.666667
                               Male      172.030000
Shanghai Jiao Tong University  Female    159.122500
                               Male      176.760000
Tsinghua University            Female    159.753333
                               Male      171.638889
Name: Height, dtype: float64

3. Groupby物件

在具體做分組操作時,所呼叫的方法都來自於pandas中的groupby物件,這個物件上定義了許多方法,也具有一些方便的屬性,下面列出2個屬性2個方法。

gb = df.groupby(['School', 'Grade'])

gb
## 輸出
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000024372714408>
  • ngroups屬性,可以得到分組個數
gb.ngroups
## 輸出
16
  • groups屬性,可以返回從組名對映到組索引列表的字典
res = gb.groups

res.keys() # 字典的值是索引,元素個數太多,所以只展示字典的鍵
## 輸出
dict_keys([('Fudan University', 'Freshman'), ('Fudan University', 'Junior'), ('Fudan University', 'Senior'), ('Fudan University', 'Sophomore'), ('Peking University', 'Freshman'), ('Peking University', 'Junior'), ('Peking University', 'Senior'), ('Peking University', 'Sophomore'), ('Shanghai Jiao Tong University', 'Freshman'), ('Shanghai Jiao Tong University', 'Junior'), ('Shanghai Jiao Tong University', 'Senior'), ('Shanghai Jiao Tong University', 'Sophomore'), ('Tsinghua University', 'Freshman'), ('Tsinghua University', 'Junior'), ('Tsinghua University', 'Senior'), ('Tsinghua University', 'Sophomore')])
  • size作為DataFrame的屬性時,返回的是表長乘以表寬的大小
  • size方法在groupby物件上時,表示統計每個組元素個數
gb.size()
## 輸出
School                         Grade    
Fudan University               Freshman      9
                               Junior       12
                               Senior       11
                               Sophomore     8
Peking University              Freshman     13
                               Junior        8
                               Senior        8
                               Sophomore     5
Shanghai Jiao Tong University  Freshman     13
                               Junior       17
                               Senior       22
                               Sophomore     5
Tsinghua University            Freshman     17
                               Junior       22
                               Senior       14
                               Sophomore    16
dtype: int64
  • get_group方法直接獲取所在組對應的行,但是必須知道組的具體名字
gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3] # 展示一部分
## 輸出
              School     Grade             Name
15  Fudan University  Freshman  Changqiang Yang
28  Fudan University  Freshman     Gaoqiang Qin
63  Fudan University  Freshman     Gaofeng Zhao

meanmedian都是groupby物件上的方法,這些函式和許多其他函式的操作具有高度相似性。

4. 分組的三大操作

對例子進行分析:

  • 依據性別分組,統計全國人口壽命的平均值

  • 依據季節分組,對每一個季節的溫度進行組內標準化

  • 依據班級分組,篩選出組內數學分數的平均值超過80分的班級

這三種類型分組返回的資料型態並不一樣:

  • 對於第一個例子,每一個組返回一個標量值,可以是平均值、中位數、組容量size

  • 對於第二個例子,做了原序列的標準化處理,也就是說每組返回的是一個Series型別

  • 對於第三個例子,既不是標量也不是序列,返回的整個組所在行的本身,即返回了DataFrame型別

由此,就分別對應了分組的三大操作

  • 聚合(agg
  • 變換(transform
  • 過濾(filter

二、聚合函式

1. 內建聚合函式

  • agg:聚合

在此之前,先要了解一些直接定義在groupby物件的聚合函式,因為它的速度基本都會經過內部的優化,使用功能時應當優先考慮。根據返回標量值的原則,包括如下函式:

  • max
  • min
  • mean
  • median
  • count
  • all:表示分組後每一組中所有bool值都為True則返回True , 有一個False就返回False。
  • any:表示bool值列中只要有一個True則返回True , 只有全為False才會返回False,如果非0,則全為True。
  • idxmax
  • idxmin
  • mad:mad(mean absolute deviation)平均絕對離差 , 用於統計學中對分組後的每組資料做離散程度分析的指標之一,公式為:
  • nunique
  • skew:skew(skewness)偏度 , 用來反映分組後每組資料分佈的偏態程度 , 正值為右偏 , 絕對值越大 , 偏度越高,公式為:
  • quantile
  • sum
  • std
  • var
  • sem:sem(standard error of mean)均值標準誤差 , 描述的是多個均值樣本的標準差,體現均值抽樣分佈的離散程度,反映樣本均值之間的差異。設樣本無偏估計標準差為s , 樣本大小為N , 則分組後每組的sem可表示為:
  • size
  • prod:prod(product)連乘 , 每組prod表示為:

聚合函式當傳入的資料來源包含多個列時,將按照列進行迭代計算:

gb = df.groupby('Gender')[['Height', 'Weight']]


gb.max()
## 輸出
        Height  Weight
Gender                
Female   170.2    63.0
Male     193.9    89.0

2. agg方法

groupby物件上定義了許多方便的函式,仍有的不足:

  • 無法同時使用多個函式

  • 無法對特定的列使用特定的聚合函式

  • 無法使用自定義的聚合函式

  • 無法直接對結果的列名在聚合前進行自定義命名

通過agg函式解決這四類問題:

  1. 使用多個函式
  2. 對特定的列使用特定的聚合函式
  3. 使用自定義函式
  4. 聚合結果重新命名

1. 使用多個函式

當使用多個聚合函式時,需要用列表的形式把內建聚合函式對應的字串傳入,先前提到的所有字串都是合法的。

gb.agg(['sum', 'idxmax', 'skew'])
## 輸出 
         Height                   Weight                 
            sum idxmax      skew     sum idxmax      skew
Gender                                                   
Female  21014.0     28 -0.219253  6469.0     28 -0.268482
Male     8854.9    193  0.437535  3929.0      2 -0.332393

此時的列索引為多級索引,第一層為資料來源,第二層為使用的聚合方法,分別逐一使用聚合,因此結果為6列。

2.對特定的列使用特定的聚合函式

對於方法和列的特殊對應,可以通過構造字典傳入agg中實現,其中字典以列名為鍵,以聚合字串或字串列表為值。

gb.agg({'Height':['mean','max'], 'Weight':'count'})
## 輸出
           Height        Weight
             mean    max  count
Gender                         
Female  159.19697  170.2    135
Male    173.62549  193.9     54

3.使用自定義函式

agg中可以使用具體的自定義函式。

注意:傳入函式的引數是之前資料來源中的,逐列進行計算。

# 分組計算身高和體重的 極差
gb.agg(lambda x: x.mean()-x.min())
## 輸出
          Height     Weight
Gender                     
Female  13.79697  13.918519
Male    17.92549  21.759259

傳入的是序列,因此序列上的方法和屬性都是可以在函式中使用的,但是需保證返回值是標量

# 表示如果組的指標均值,超過該指標的總體均值,返回High,否則返回Low
def my_func(s):
     res = 'High'
     if s.mean() <= df[s.name].mean():
         res = 'Low'
     return res 

gb.agg(my_func)
## 輸出
       Height Weight
Gender              
Female    Low    Low
Male     High   High

4.聚合結果重新命名

對聚合結果的列名進行重新命名,只需要將上述函式的位置改寫成元組。

  • 元組的第一個元素為新的名字
  • 元組的第二個位置為原來的函式,包括聚合字串和自定義函式
gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')]) # 第一個元素為新的名字,第二個位置為原來的函式
## 輸出
       Height          Weight        
        range   my_sum  range  my_sum
Gender                               
Female   24.8  21014.0   29.0  6469.0
Male     38.2   8854.9   38.0  3929.0



 
gb.agg({'Height': [('my_func', my_func), 'sum'],'Weight': lambda x:x.max()})  # 第一個元素為新的名字,第二個位置為原來的函式
 
## 輸出 
        Height            Weight
       my_func      sum <lambda>
Gender                          
Female     Low  21014.0     63.0
Male      High   8854.9     89.0

注意:

使用對一個或者多個列使用單個聚合的時候,重新命名需要加方括號,否則就不知道是新的名字還是手誤輸錯的內建函式字串。

gb.agg([('my_sum', 'sum')])
## 輸出
         Height  Weight
         my_sum  my_sum
Gender                 
Female  21014.0  6469.0
Male     8854.9  3929.0


gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})  # 加 方括號

## 輸出
        Height          Weight
       my_func      sum  range
Gender                        
Female     Low  21014.0   63.0
Male      High   8854.9   89.0

三、變換和過濾

1. 變換函式與transform方法

變換函式的返回值為同長度的序列,最常用的內建變換函式是累計函式cumcount/cumsum/cumprod/cummax/cummin,它們的使用方式和聚合函式類似,只不過完成的是組內累計操作。

補充:groupby物件上還定義了填充類和滑窗類的變換函式。

gb.cummax().head()
## 輸出 
   Height  Weight
0   158.9    46.0
1   166.5    70.0
2   188.9    89.0
3     NaN    46.0
4   188.9    89.0

用自定義變換需要使用transform方法,被呼叫的自定義函式,其傳入值資料來源的序列,與agg傳入型別是一致的,其最後的返回結果是行列索引與資料來源一致的DataFrame

# 對身高和體重進行分組標準化 即 減去 組均值後 除以 組的標準差
gb.transform(lambda x: (x-x.mean())/x.std()).head()
Out[35]: 
     Height    Weight
0 -0.058760 -0.354888
1 -1.010925 -0.355000
2  2.167063  2.089498
3       NaN -1.279789
4  0.053133  0.159631

transform只能返回同長度的序列,但其實還可以返回一個標量,這會使得結果被廣播到其所在的整個組,這種標量廣播的技巧在特徵工程中是非常常見的。

# 構造兩列新特徵來分別表示樣本所在性別組的 身高均值 和 體重均值
gb.transform('mean').head() # 傳入返回標量的函式也是可以的
## 輸出
      Height     Weight
0  159.19697  47.918519
1  173.62549  72.759259
2  173.62549  72.759259
3  159.19697  47.918519
4  173.62549  72.759259

2. 組索引與過濾

索引和過濾的區別:

過濾在分組中是對於組的過濾,而索引是對於行的過濾,在返回值中,無論是布林列表還是元素列表或者位置列表,本質上都是對於行的篩選,即如果符合篩選條件的則選入結果表,否則不選入。

組過濾作為行過濾的推廣,指的是如果對一個組的全體所在行進行統計的結果返回True則會被保留,False則該組會被過濾,最後把所有未被過濾的組其對應的所在行拼接起來作為DataFrame返回。

# 在原表中通過過濾得到所有容量大於100的組
gb.filter(lambda x: x.shape[0] > 100).head()
# 輸出
   Height  Weight
0   158.9    46.0
3     NaN    41.0
5   158.0    51.0
6   162.5    52.0
7   161.9    50.0

四、跨列分組

1. apply的引入

除了三大分組操作,還有一種常見的分組場景,這種情況用之前的方法是無法處理的。

例如:使用 apply 函式解決 身體質量的指數BMI,需要分組計算組BMI的均值:

BMI = \frac{Height}{Weight^{2}}

首先,這不是過濾操作,filter不符合要求;其次,返回的均值是標量而不是序列,因此transform不符合要求;最後,似乎使用agg函式能夠處理,但是之前強調過聚合函式是逐列處理的,而不能夠多列資料同時處理。

2. apply的使用

注意:

apply的自定義函式傳入引數與filter完全一致,只不過後者只允許返回布林值。

def BMI(x):
     Height = x['Height']/100
     Weight = x['Weight']
     BMI_value = Weight/Height**2
     return BMI_value.mean() 

gb.apply(BMI)
## 輸出
Gender
Female    18.860930
Male      24.318654
dtype: float64

除了返回標量之外,apply方法還可以返回一維Series和二維DataFrame,但它們產生的資料框維數和多級索引的層數有三種變化:

  • 標量情況:結果得到的是Series,索引與agg的結果一致
  • Series情況:得到的是DataFrame,行索引與標量情況一致,列索引為Series的索引
  • DataFrame情況:得到的是DataFrame,行索引最內層在每個組原先agg的結果索引上,再加一層返回的DataFrame行索引,同時分組結果DataFrame的列索引和返回的DataFrame列索引一致。

補充:

apply函式的靈活性是以犧牲一定效能為代價換得的,除非需要使用跨列處理的分組處理,否則應當使用其他專門設計的groupby物件方法,否則在效能上會存在較大的差距。同時,在使用聚合函式變換函式時,也應當優先使用內建函式,它們經過了高度的效能優化,一般而言在速度上都會快於用自定義函式來實現。