1. 程式人生 > >(資料科學學習手札69)詳解pandas中的map、apply、applymap、groupby、agg

(資料科學學習手札69)詳解pandas中的map、apply、applymap、groupby、agg

*從本篇開始所有文章的資料和程式碼都已上傳至我的github倉庫:https://github.com/CNFeffery/DataScienceStudyNotes

一、簡介

  pandas提供了很多方便簡潔的方法,用於對單列、多列資料進行批量運算或分組聚合運算,熟悉這些方法後可極大地提升資料分析的效率,也會使得你的程式碼更加地優雅簡潔,本文就將針對pandas中的map()、apply()、applymap()、groupby()、agg()等方法展開詳細介紹,並結合實際例子幫助大家更好地理解它們的使用技巧(本文使用到的所有程式碼及資料均儲存在我的github倉庫:https://github.com/CNFeffery/DataScienceStudyNotes對應本文的資料夾下)。

 

二、非聚合類方法

  這裡的非聚合指的是資料處理前後沒有進行分組操作,資料列的長度沒有發生改變,因此本章節中不涉及groupby(),首先讀入資料,這裡使用到的全美嬰兒姓名資料,包含了1880-2018年全美每年對應每個姓名的新生兒資料,在jupyterlab中讀入資料並列印資料集的一些基本資訊以瞭解我們的資料集:

import pandas as pd

#讀入資料
data = pd.read_csv('data.csv')
data.head()

#檢視各列資料型別、資料框行列數
print(data.dtypes)
print()
print(data.shape)

2.1 map()

  類似Python內建的map()方法,pandas中的map()方法將函式、字典索引或是一些需要接受單個輸入值的特別的物件與對應的單個列的每一個元素建立聯絡並序列得到結果,譬如這裡我們想要得到gender列的F、M轉換為女性、男性的新列,可以有以下幾種實現方式:

● 字典對映

  這裡我們編寫F、M與女性、男性之間一一對映的字典,再利用map()方法來得到對映列:

#定義F->女性,M->男性的對映字典
gender2xb = {'F': '女性', 'M': '男性'}
#利用map()方法得到對應gender列的對映列
data.gender.map(gender2xb)

 ● lambda函式

  這裡我們向map()中傳入lambda函式來實現所需功能:

#因為已經知道資料gender列性別中只有F和M所以編寫如下lambda函式
data.gender.map(lambda x:'女性' if x is 'F' else '男性')

  ● 常規函式

  也可以傳入def定義的常規函式:

def gender_to_xb(x):

    return '女性' if x is 'F' else '男性'

data.gender.map(gender_to_xb) 

   map()可以傳入的內容有時候可以很特殊,如下面的例子:

● 特殊物件

  一些接收單個輸入值且有輸出的物件也可以用map()方法來處理:

data.gender.map("This kid's gender is {}".format)

   map()還有一個引數na_action,類似R中的na.action,取值為'None'或'ingore',用於控制遇到缺失值的處理方式,設定為'ingore'時序列運算過程中將忽略Nan值原樣返回。

 

2.2 apply()

  apply()堪稱pandas中最好用的方法,其使用方式跟map()很像,主要傳入的主要引數都是接受輸入返回輸出,但相較於map()針對單列Series進行處理,一條apply()語句可以對單列或多列進行運算,覆蓋非常多的使用場景,下面我們來分別介紹:

● 單列資料

  這裡我們參照2.1向apply()中傳入lambda函式:

data.gender.apply(lambda x:'女性' if x is 'F' else '男性')

   可以看到這裡實現了跟map()一樣的功能。

● 多列資料

  apply()最特別的地方在於其可以同時處理多列資料,譬如這裡我們編寫一個使用到多列資料的函式用於拼成對於每一行描述性的話,並在apply()用lambda函式傳遞多個值進編寫好的函式中(當呼叫DataFrame.apply()時,apply()在序列過程中實際處理的是每一行資料而不是Series.apply()那樣每次處理單個值),注意在處理多個值時要給apply()新增引數axis=1:

def generate_descriptive_statement(year, name, gender, count):
    year, count = str(year), str(count)
    gender = '女性' if gender is 'F' else '男性'
    
    return '在{}年,叫做{}性別為{}的新生兒有{}個。'.format(year, name, gender, count)

data.apply(lambda row:generate_descriptive_statement(row['year'],
                                                      row['name'],
                                                      row['gender'],
                                                      row['count']),
           axis = 1)

 ● 結合tqdm給apply()過程新增進度條

  我們知道apply()在運算時實際上仍然是一行一行遍歷的方式,因此在計算量很大時如果有一個進度條來監視執行進度就很舒服,在(資料科學學習手札53)Python中tqdm模組的用法中,我對基於tqdm為程式新增進度條做了介紹,而tqdm對pandas也是有著很好的支援,我們可以使用progress_apply()代替apply(),並在執行progress_apply()之前新增tqdm.tqdm.pandas(desc='')來啟動對apply過程的監視,其中desc引數傳入對進度進行說明的字串,下面我們在上一小部分示例的基礎上進行改造來新增進度條功能:

from tqdm import tqdm

def generate_descriptive_statement(year, name, gender, count):
    year, count = str(year), str(count)
    gender = '女性' if gender is 'F' else '男性'
    
    return '在{}年,叫做{}性別為{}的新生兒有{}個。'.format(year, name, gender, count)

#啟動對緊跟著的apply過程的監視
tqdm.pandas(desc='apply')
data.progress_apply(lambda row:generate_descriptive_statement(row['year'],
                                                      row['name'],
                                                      row['gender'],
                                                      row['count']),
          axis = 1)

   可以看到在jupyter lab中執行程式的過程中,下方出現了監視過程的進度條,這樣就可以實時瞭解apply過程跑到什麼地方了。

 

2.3  applymap()

  applymap()是與map()方法相對應的專屬於DataFrame物件的方法,類似map()方法傳入函式、字典等,傳入對應的輸出結果,不同的是applymap()將傳入的函式等作用於整個資料框中每一個位置的元素,因此其返回結果的形狀與原資料框一致,譬如下面的簡單示例,我們把嬰兒姓名資料中所有的字元型資料訊息小寫化處理,對其他型別則原樣返回:

def lower_all_string(x):
    if isinstance(x, str):
        return x.lower()
    else:
        return x

data.applymap(lower_all_string)

   其形狀沒有變化:

 

   配合applymap(),可以簡潔地完成很多資料處理操作。

 

三、聚合類方法

  有些時候我們需要像SQL裡的聚合操作那樣將原始資料按照某個或某些離散型的列進行分組再求和、平均數等聚合之後的值,在pandas中分組運算是一件非常優雅的事。

3.1 利用groupby()進行分組

  要進行分組運算第一步當然就是分組,在pandas中對資料框進行分組使用到groupby()方法,其主要使用到的引數為by,這個引數用於傳入分組依據的變數名稱,當變數為1個時傳入名稱字串即可,當為多個時傳入這些變數名稱列表,DataFrame物件通過groupby()之後返回一個生成器,需要將其列表化才能得到需要的分組後的子集,如下面的示例:

#按照年份和性別對嬰兒姓名資料進行分組
groups = data.groupby(by=['year','gender'])
#檢視groups型別
type(groups)

   可以看到它此時是生成器,下面我們用列表解析的方式提取出所有分組後的結果:

#利用列表解析提取分組結果
groups = [group for group in groups]

  檢視其中的一個元素:

 

   可以看到每一個結果都是一個二元組,元組的第一個元素是對應這個分組結果的分組組合方式,第二個元素是分組出的子集資料框,而對於DataFrame.groupby()得到的結果,主要可以進行以下幾種操作:

● 直接呼叫聚合函式

  譬如這裡我們提取count列後直接呼叫max()方法:

#求每個分組中最高頻次
data.groupby(by=['year','gender'])['count'].max()

 

   注意這裡的year、gender列是以索引的形式存在的,想要把它們還原回資料框,使用reset_index(drop=False)即可:

 

 ● 結合apply()

  分組後的結果也可以直接呼叫apply(),這樣可以編寫更加自由的函式來完成需求,譬如下面我們通過自編函式來求得每年每種性別出現頻次最高的名字及對應頻次,要注意的是,這裡的apply傳入的物件是每個分組之後的子資料框,所以下面的自編函式中直接接收的df引數即為每個分組的子資料框:

import numpy as np

def find_most_name(df):
    return str(np.max(df['count']))+'-'+df['name'][np.argmax(df['count'])]

data.groupby(['year','gender']).apply(find_most_name).reset_index(drop=False)

 

3.2 利用agg()進行更靈活的聚合

  agg即aggregate,聚合,在pandas中可以利用agg()對Series、DataFrame以及groupby()後的結果進行聚合,其傳入的引數為字典,鍵為變數名,值為對應的聚合函式字串,譬如{'v1':['sum','mean'], 'v2':['median','max','min]}就代表對資料框中的v1列進行求和、均值操作,對v2列進行中位數、最大值、最小值操作,下面用幾個簡單的例子演示其具體使用方式:

 ● 聚合Series

  在對Series進行聚合時,因為只有1列,所以可以不使用字典的形式傳遞引數,直接傳入函式名列表即可:

#求count列的最小值、最大值以及中位數
data['count'].agg(['min','max','median'])

  ● 聚合資料框

  對資料框進行聚合時因為有多列,所以要使用字典的方式傳入聚合方案:

data.agg({'year': ['max','min'], 'count': ['mean','std']})

   值得注意的是,因為上例中對於不同變數的聚合方案不統一,所以會出現NaN的情況。

● 聚合groupby()結果

data.groupby(['year','gender']).agg({'count':['min','max','median']}).reset_index(drop=False)

   可以注意到雖然我們使用reset_index()將索引列還原回變數,但聚合結果的列名變成紅色框中奇怪的樣子,而在pandas 0.25.0以及之後的版本中,可以使用pd.NamedAgg()來為聚合後的每一列賦予新的名字:

data.groupby(['year','gender']).agg(
    min_count=pd.NamedAgg(column='count', aggfunc='min'),
    max_count=pd.NamedAgg(column='count', aggfunc='max'),
    median=pd.NamedAgg(column='count', aggfunc='median')).reset_index(drop=False)

   

  以上就是本文全部內容,如有筆誤望指出!&n