文章的數(shù)據(jù)和代碼都已上傳至我的github倉(cāng)庫(kù):https://github.com/CNFeffery/DataScienceStudyNotes
一、簡(jiǎn)介
pandas提供了很多方便簡(jiǎn)潔的方法,,用于對(duì)單列,、多列數(shù)據(jù)進(jìn)行批量運(yùn)算或分組聚合運(yùn)算,,熟悉這些方法后可極大地提升數(shù)據(jù)分析的效率,,也會(huì)使得你的代碼更加地優(yōu)雅簡(jiǎn)潔。
本文就將針對(duì)pandas中的map()
、apply()
、applymap()
,、groupby()
,、agg()
等方法展開(kāi)詳細(xì)介紹,并結(jié)合實(shí)際例子幫助大家更好地理解它們的使用技巧。
二,、非聚合類(lèi)方法
這里的非聚合指的是數(shù)據(jù)處理前后沒(méi)有進(jìn)行分組操作,,數(shù)據(jù)列的長(zhǎng)度沒(méi)有發(fā)生改變,,因此本章節(jié)中不涉及groupby()
,。
首先讀入數(shù)據(jù),這里使用到的全美嬰兒姓名數(shù)據(jù),包含了1880-2018年全美每年對(duì)應(yīng)每個(gè)姓名的新生兒數(shù)據(jù),在jupyterlab中讀入數(shù)據(jù)并打印數(shù)據(jù)集的一些基本信息以了解我們的數(shù)據(jù)集:
import pandas as pd
#讀入數(shù)據(jù)
data = pd.read_csv('data.csv')
data.head()
#查看各列數(shù)據(jù)類(lèi)型,、數(shù)據(jù)框行列數(shù)
print(data.dtypes)
print()
print(data.shape)
2.1 map()
類(lèi)似Python內(nèi)建的map()
方法,,pandas中的map()
方法將函數(shù),、字典索引或是一些需要接受單個(gè)輸入值的特別的對(duì)象與對(duì)應(yīng)的單個(gè)列的每一個(gè)元素建立聯(lián)系并串行得到結(jié)果,。
譬如這里我們想要得到gender列的F、M轉(zhuǎn)換為女性,、男性的新列,,可以有以下幾種實(shí)現(xiàn)方式:
這里我們編寫(xiě)F、M與女性,、男性之間一一映射的字典,,再利用map()
方法來(lái)得到映射列:
#定義F->女性,M->男性的映射字典
gender2xb = {'F': '女性', 'M': '男性'}
#利用map()方法得到對(duì)應(yīng)gender列的映射列
data.gender.map(gender2xb)
這里我們向map()
中傳入lambda函數(shù)來(lái)實(shí)現(xiàn)所需功能:
#因?yàn)橐呀?jīng)知道數(shù)據(jù)gender列性別中只有F和M所以編寫(xiě)如下lambda函數(shù)
data.gender.map(lambda x:'女性' if x is 'F' else '男性')
也可以傳入def
定義的常規(guī)函數(shù):
def gender_to_xb(x):
return '女性' if x is 'F' else '男性'
data.gender.map(gender_to_xb)
map()
可以傳入的內(nèi)容有時(shí)候可以很特殊,,如下面的例子:
一些接收單個(gè)輸入值且有輸出的對(duì)象也可以用map()
方法來(lái)處理:
data.gender.map('This kid's gender is {}'.format)
map()
還有一個(gè)參數(shù)na_action
,,類(lèi)似R中的na.action
,取值為None
或ingore
,,用于控制遇到缺失值的處理方式,,設(shè)置為ingore
時(shí)串行運(yùn)算過(guò)程中將忽略Nan值原樣返回。
2.2 apply()
apply()
堪稱pandas中最好用的方法,,其使用方式跟map()
很像,,主要傳入的主要參數(shù)都是接受輸入返回輸出。
但相較于map()
針對(duì)單列Series進(jìn)行處理,,一條apply()
語(yǔ)句可以對(duì)單列或多列進(jìn)行運(yùn)算,,覆蓋非常多的使用場(chǎng)景。
下面我們來(lái)分別介紹:
這里我們參照2.1向apply()
中傳入lambda
函數(shù):
data.gender.apply(lambda x:'女性' if x is 'F' else '男性')
可以看到這里實(shí)現(xiàn)了跟map()
一樣的功能,。
apply()
最特別的地方在于其可以同時(shí)處理多列數(shù)據(jù),我們先來(lái)了解一下如何處理多列數(shù)據(jù)輸入單列數(shù)據(jù)輸出的情況,。
譬如這里我們編寫(xiě)一個(gè)使用到多列數(shù)據(jù)的函數(shù)用于拼成對(duì)于每一行描述性的話,,并在apply()
用lambda函數(shù)傳遞多個(gè)值進(jìn)編寫(xiě)好的函數(shù)中(當(dāng)調(diào)用DataFrame.apply()
時(shí),apply()
在串行過(guò)程中實(shí)際處理的是每一行數(shù)據(jù),,而不是Series.apply()
那樣每次處理單個(gè)值),。
注意在處理多個(gè)值時(shí)要給apply()
添加參數(shù)axis=1
:
def generate_descriptive_statement(year, name, gender, count):
year, count = str(year), str(count)
gender = '女性' if gender is 'F' else '男性'
return '在{}年,叫做{}性別為{}的新生兒有{}個(gè)。'.format(year, name, gender, count)
data.apply(lambda row:generate_descriptive_statement(row['year'],
row['name'],
row['gender'],
row['count']),
axis = 1)
有些時(shí)候我們利用apply()
會(huì)遇到希望同時(shí)輸出多列數(shù)據(jù)的情況,,在apply()
中同時(shí)輸出多列時(shí)實(shí)際上返回的是一個(gè)Series,,這個(gè)Series中每個(gè)元素是與apply()
中傳入函數(shù)的返回值順序?qū)?yīng)的元組。
比如下面我們利用apply()來(lái)提取name列中的首字母和剩余部分字母:
data.apply(lambda row: (row['name'][0], row['name'][1:]), axis=1)
可以看到,,這里返回的是單列結(jié)果,,每個(gè)元素是返回值組成的元組,這時(shí)若想直接得到各列分開(kāi)的結(jié)果,,需要用到zip(*zipped)
來(lái)解開(kāi)元組序列,,從而得到分離的多列返回值:
a, b = zip(*data.apply(lambda row: (row['name'][0], row['name'][1:]), axis=1))
print(a[:10])
print(b[:10])
- 結(jié)合tqdm給
apply()
過(guò)程添加進(jìn)度條
我們知道apply()
在運(yùn)算時(shí)實(shí)際上仍然是一行一行遍歷的方式,因此在計(jì)算量很大時(shí)如果有一個(gè)進(jìn)度條來(lái)監(jiān)視運(yùn)行進(jìn)度就很舒服,。
tqdm:用于添加代碼進(jìn)度條的第三方庫(kù)
tqdm對(duì)pandas也是有著很好的支持,。
我們可以使用progress_apply()
代替apply()
,并在運(yùn)行progress_apply()
之前添加tqdm.tqdm.pandas(desc='')
來(lái)啟動(dòng)對(duì)apply
過(guò)程的監(jiān)視,。
其中desc
參數(shù)傳入對(duì)進(jìn)度進(jìn)行說(shuō)明的字符串,,下面我們?cè)谏弦恍〔糠质纠幕A(chǔ)上進(jìn)行改造來(lái)添加進(jìn)度條功能:
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 '在{}年,叫做{}性別為{}的新生兒有{}個(gè),。'.format(year, name, gender, count)
#啟動(dòng)對(duì)緊跟著的apply過(guò)程的監(jiān)視
tqdm.pandas(desc='apply')
data.progress_apply(lambda row:generate_descriptive_statement(row['year'],
row['name'],
row['gender'],
row['count']),
axis = 1)
可以看到在jupyter lab中運(yùn)行程序的過(guò)程中,,下方出現(xiàn)了監(jiān)視過(guò)程的進(jìn)度條,這樣就可以實(shí)時(shí)了解apply過(guò)程跑到什么地方了,。
- 結(jié)合
tqdm_notebook()
給apply()
過(guò)程添加美觀進(jìn)度條
熟悉tqdm的朋友都知道其針對(duì)jupyter notebook開(kāi)發(fā)了ui更加美觀的tqdm_notebook()
,。
而要想在jupyter notebook/jupyter lab平臺(tái)上為pandas的apply
過(guò)程添加美觀進(jìn)度條,可以參照如下示例:
from tqdm._tqdm_notebook import tqdm_notebook
tqdm_notebook.pandas(desc='apply')
data.progress_apply(lambda row:generate_descriptive_statement(row['year'],
row['name'],
row['gender'],
row['count']),
axis = 1)
這時(shí)所添加的進(jìn)度條就美觀了不少,。
2.3 applymap()
applymap()
是與map()
方法相對(duì)應(yīng)的專(zhuān)屬于DataFrame對(duì)象的方法,,類(lèi)似map()方法傳入函數(shù)、字典等,,傳入對(duì)應(yīng)的輸出結(jié)果,。
不同的是applymap()
將傳入的函數(shù)等作用于整個(gè)數(shù)據(jù)框中每一個(gè)位置的元素,因此其返回結(jié)果的形狀與原數(shù)據(jù)框一致,。
譬如下面的簡(jiǎn)單示例,,我們把嬰兒姓名數(shù)據(jù)中所有的字符型數(shù)據(jù)消息小寫(xiě)化處理,對(duì)其他類(lèi)型則原樣返回:
def lower_all_string(x):
if isinstance(x, str):
return x.lower()
else:
return x
data.applymap(lower_all_string)
其形狀沒(méi)有變化:
配合applymap()
,,可以簡(jiǎn)潔地完成很多數(shù)據(jù)處理操作,。
三、聚合類(lèi)方法
有些時(shí)候我們需要像SQL里的聚合操作那樣將原始數(shù)據(jù)按照某個(gè)或某些離散型的列進(jìn)行分組再求和,、平均數(shù)等聚合之后的值,,在pandas中分組運(yùn)算是一件非常優(yōu)雅的事。
3.1 利用groupby()
進(jìn)行分組
要進(jìn)行分組運(yùn)算第一步當(dāng)然就是分組,,在pandas中對(duì)數(shù)據(jù)框進(jìn)行分組使用到groupby()方法,。
其主要使用到的參數(shù)為by
,,這個(gè)參數(shù)用于傳入分組依據(jù)的變量名稱,當(dāng)變量為1個(gè)時(shí)傳入名稱字符串即可,。
當(dāng)為多個(gè)時(shí)傳入這些變量名稱列表,,DataFrame對(duì)象通過(guò)groupby()
之后返回一個(gè)生成器,需要將其列表化才能得到需要的分組后的子集,,如下面的示例:
#按照年份和性別對(duì)嬰兒姓名數(shù)據(jù)進(jìn)行分組
groups = data.groupby(by=['year','gender'])
#查看groups類(lèi)型
type(groups)
可以看到它此時(shí)是生成器,,下面我們用列表解析的方式提取出所有分組后的結(jié)果:
#利用列表解析提取分組結(jié)果
groups = [group for group in groups]
查看其中的一個(gè)元素:
可以看到每一個(gè)結(jié)果都是一個(gè)二元組,元組的第一個(gè)元素是對(duì)應(yīng)這個(gè)分組結(jié)果的分組組合方式,,第二個(gè)元素是分組出的子集數(shù)據(jù)框,,而對(duì)于DataFrame.groupby()
得到的結(jié)果。
主要可以進(jìn)行以下幾種操作:
譬如這里我們提取count列后直接調(diào)用max()
方法:
#求每個(gè)分組中最高頻次
data.groupby(by=['year','gender'])['count'].max()
注意這里的year,、gender列是以索引的形式存在的,,想要把它們還原回?cái)?shù)據(jù)框,使用reset_index(drop=False)
即可:
分組后的結(jié)果也可以直接調(diào)用apply()
,,這樣可以編寫(xiě)更加自由的函數(shù)來(lái)完成需求,譬如下面我們通過(guò)自編函數(shù)來(lái)求得每年每種性別出現(xiàn)頻次最高的名字及對(duì)應(yīng)頻次,。
要注意的是,,這里的apply傳入的對(duì)象是每個(gè)分組之后的子數(shù)據(jù)框,所以下面的自編函數(shù)中直接接收的df參數(shù)即為每個(gè)分組的子數(shù)據(jù)框:
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()
進(jìn)行更靈活的聚合
agg即aggregate,,聚合,,在pandas中可以利用agg()
對(duì)Series、DataFrame以及groupby()后的結(jié)果進(jìn)行聚合,。
其傳入的參數(shù)為字典,,鍵為變量名,值為對(duì)應(yīng)的聚合函數(shù)字符串,,譬如{'v1':['sum','mean'], 'v2':['median','max','min]}
就代表對(duì)數(shù)據(jù)框中的v1列進(jìn)行求和,、均值操作,對(duì)v2列進(jìn)行中位數(shù),、最大值,、最小值操作。
下面用幾個(gè)簡(jiǎn)單的例子演示其具體使用方式:
在對(duì)Series進(jìn)行聚合時(shí),,因?yàn)橹挥?列,,所以可以不使用字典的形式傳遞參數(shù),直接傳入函數(shù)名列表即可:
#求count列的最小值,、最大值以及中位數(shù)
data['count'].agg(['min','max','median'])
對(duì)數(shù)據(jù)框進(jìn)行聚合時(shí)因?yàn)橛卸嗔?,所以要使用字典的方式傳入聚合方案?/p>
data.agg({'year': ['max','min'], 'count': ['mean','std']})
值得注意的是,因?yàn)樯侠袑?duì)于不同變量的聚合方案不統(tǒng)一,,所以會(huì)出現(xiàn)NaN
的情況,。
data.groupby(['year','gender']).agg({'count':['min','max','median']}).reset_index(drop=False)
可以注意到雖然我們使用reset_index()
將索引列還原回變量,,但聚合結(jié)果的列名變成紅色框中奇怪的樣子,而在pandas 0.25.0以及之后的版本中,,可以使用pd.NamedAgg()
來(lái)為聚合后的每一列賦予新的名字:
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)
以上就是本文全部?jī)?nèi)容,,如有筆誤望指出!