如果你是有打算從事有關(guān)數(shù)據(jù)分析或者數(shù)據(jù)挖掘的等數(shù)據(jù)科學(xué)領(lǐng)域的工作,,或者和我一樣目前就是從事相關(guān)領(lǐng)域的工作,那么「鏈?zhǔn)秸{(diào)用」對(duì)我們而言是一門必修課,。
為什么是鏈?zhǔn)秸{(diào)用,? 鏈?zhǔn)秸{(diào)用,或者也可以稱為方法鏈(Method Chaining),,從字面意思上來說就是將一些列的操作或函數(shù)方法像鏈子一樣穿起來的 Code 方式,。
我最開始感知鏈?zhǔn)秸{(diào)用的「美」,還要從使用 R 語言的管道操作符開始,。
library (tidyverse) mtcars %>% group_by(cyl) %>% summarise(meanOfdisp = mean(disp)) %>% ggplot(aes(x=as.factor(cyl), y=meanOfdisp, fill=as.factor(seq(1 ,3 ))))+ geom_bar(stat = 'identity' ) + guides(fill=F )
對(duì)于 R user 來說,,對(duì)于這一段代碼很快就能明白整個(gè)流程步驟是怎樣的。這一切都是通過符號(hào)%>%
(管道操作符)談起,。
通過管道操作符,,我們可以將左邊事物傳遞給下一個(gè)事物。這里我將mtcars
數(shù)據(jù)集傳遞到group_by
函數(shù)中,,然后將得到后的結(jié)果再傳遞到summarize
函數(shù),,最后傳遞到ggplot
函數(shù)中進(jìn)行可視化繪制。
如果我沒有學(xué)會(huì)鏈?zhǔn)秸{(diào)用,,那么最開始學(xué)習(xí) R 語言的我一定是這樣寫:
library (tidyverse) cyl4 <- mtcars[which(mtcars$cyl==4 ), ] cyl6 <- mtcars[which(mtcars$cyl==6 ), ] cyl8 <- mtcars[which(mtcars$cyl==8 ), ] data <- data.frame( cyl = c(4 , 6 , 8 ), meanOfdisp = c(mean(cyl4$disp), mean(cyl6$disp), mean(cyl8$disp)) ) graph <- ggplot(data=data, aes(x=factor(cyl), y=meanOfdisp, fill = as.factor(seq(1 ,3 )))) graph <- graph + geom_bar(stat = 'identity' ) + guides(fill=F ) graph
如果不使用管道操作符,,那么我將會(huì)進(jìn)行不必要的賦值,并且覆蓋原有的數(shù)據(jù)對(duì)象,,但其實(shí)當(dāng)中產(chǎn)生的cyl#
,、data
其實(shí)最后都只是為graph
這一張圖片所服務(wù)的,因此導(dǎo)致的問題就是代碼會(huì)變得冗余,。
鏈?zhǔn)秸{(diào)用在極大程度簡(jiǎn)潔代碼的同時(shí),,也提高了代碼的可讀性,能夠很快速地了解到每一步都是在做什么,。這種方式對(duì)于做數(shù)據(jù)分析或處理數(shù)據(jù)時(shí)是十分有用,減少創(chuàng)建不必要的變量時(shí),,能夠以快速,、簡(jiǎn)單的方式進(jìn)行探索。
你能在很多地方見到鏈?zhǔn)秸{(diào)用或者管道操作的身影,,這里我舉除了 R 語言以外的兩個(gè)典型例子,。
一個(gè)是 Shell 語句:
echo '`seq 1 100`' | grep -e '^[3-4].*' | tr '3' '*'
在 shell 語句中使用「|」管道操作符能夠快速地實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,這里我首先是打印1-100的所有整數(shù),然后將其傳入到grep
方法中,,提取由 3 或 4 開頭的所有部分,,再將這部分傳入到tr
方法中,并對(duì)數(shù)字包含 3 的部分用星號(hào)替換,。結(jié)果如下:
另外一個(gè)是 Scala 語言:
object Test { def main(args: Array[String]): Unit = { val numOfseq = (1 to 100).toList val chain = numOfseq.filter(_%2==0) .map(_*2) .take(10) } }
在這段示例中,,首先numOfseq
這個(gè)變量包含了從 1-100 的所有整數(shù),然后從chain
部分開始,,我首先在numOfseq
的基礎(chǔ)上調(diào)用了filter
方法,,用以篩選這些數(shù)字中為偶數(shù)的部分,其次在調(diào)用map
方法,,將這些被篩選出來的數(shù)乘以 2,,最后使用take
方法從新構(gòu)成的數(shù)字中取出前 10 個(gè)數(shù),這些數(shù)共同賦值給了chain
變量,。
通過以上的敘述,,相信你能對(duì)鏈?zhǔn)秸{(diào)用有一個(gè)初步的印象,但是一旦你掌握了鏈?zhǔn)秸{(diào)用,,那么除了會(huì)讓你的代碼風(fēng)格有所改變以外,,你的編程思維也會(huì)有不一樣的提升。
Python 中的鏈?zhǔn)秸{(diào)用 在 Python 中實(shí)現(xiàn)一個(gè)簡(jiǎn)單的鏈?zhǔn)秸{(diào)用就是通過構(gòu)建類方法并返回對(duì)象自身或返回歸屬類(@classmethod
)
class Chain : def __init__ (self, name) : self.name = name def introduce (self) : print('hello, my name is %s' % self.name) return self def talk (self) : print('Can we make a friend?' ) return self def greet (self) : print('Hey! How are you?' ) return selfif __name__ == '__main__' : chain = Chain(name = 'jobs' ) chain.introduce() print('-' *20 ) chain.introduce().talk() print('-' *20 ) chain.introduce().talk().greet()
在這里我們創(chuàng)建一個(gè)Chain
類,,需要傳遞一個(gè)name
字符串參數(shù)進(jìn)行實(shí)例對(duì)象的創(chuàng)建,;當(dāng)中這個(gè)類里有三個(gè)方法,分別是introduce
,、talk
以及greet
,。
由于每次返回的是self
自身,那么我們就可以源源不斷地調(diào)用對(duì)象歸屬類中的方法,,結(jié)果如下:
hello, my name is jobs -------------------- hello, my name is jobs Can we make a friend? -------------------- hello, my name is jobs Can we make a friend? Hey! How are you?
在 Pandas 中使用鏈?zhǔn)秸{(diào)用 前面鋪墊了這么多終于談到有關(guān)于 Pandas 鏈?zhǔn)秸{(diào)用部分 Pandas 中的大部分方法都很適合使用鏈?zhǔn)椒椒ㄟM(jìn)行操作,,因?yàn)榻?jīng)過 API 處理后返回的往往還是 Series 類型或 DataFrame 類型,所以我們可以直接就調(diào)用相應(yīng)的方法,,這里我以我在今年 2 月份左右給別人做案例演示時(shí)爬取到的華農(nóng)兄弟 B 站視頻數(shù)據(jù)為例,。可以通過鏈接進(jìn)行獲取,。
數(shù)據(jù)字段信息如下所示,,里面有 300 條數(shù)據(jù),并且 20 個(gè)字段:
字段信息 但在使用這部分?jǐn)?shù)據(jù)之前,,我們還需要對(duì)這部分?jǐn)?shù)據(jù)進(jìn)行初步的清洗,,這里我主要選取了以下字段:
1、數(shù)據(jù)清洗 各字段對(duì)應(yīng)的值如下所示:
字段值 從數(shù)據(jù)中我們可以看到:
title
字段前面都會(huì)帶有「華農(nóng)兄弟」四個(gè)字,,如果對(duì)標(biāo)題字?jǐn)?shù)進(jìn)行統(tǒng)計(jì)時(shí)需要預(yù)先去除,;
created
上傳日期似乎顯示成了一長(zhǎng)串的數(shù)值,,但其實(shí)是從 1970 至今的時(shí)間戳,我們需要處理成可讀懂的年月日形式,;
length
播放量長(zhǎng)度只顯示了分秒,,但是小時(shí)并未用「00」來進(jìn)行補(bǔ)全,因此這里我們一方面需要將其補(bǔ)全,,另一方面要將其轉(zhuǎn)換成對(duì)應(yīng)的時(shí)間格式
鏈?zhǔn)秸{(diào)用操作如下:
import reimport pandas as pd# 定義字?jǐn)?shù)統(tǒng)計(jì)函數(shù) def word_count (text) : return len(re.findall(r'[\u4e00-\u9fa5]' , text)) tidy_data = ( pd.read_csv('~/Desktop/huanong.csv' ) .loc[:, ['aid' , 'title' , 'created' , 'length' , 'play' , 'comment' , 'video_review' ]] .assign(title = lambda df: df['title' ].str.replace('華農(nóng)兄弟:' , '' ), title_count = lambda df: df['title' ].apply(word_count), created = lambda df: df['created' ].pipe(pd.to_datetime, unit='s' ), created_date = lambda df: df['created' ].dt.date, length = lambda df: '00:' + df['length' ], video_length = lambda df: df['length' ].pipe(pd.to_timedelta).dt.seconds ) )
這里首先是通過loc
方法挑出其中的列,,然后調(diào)用assign
方法來創(chuàng)建新的字段,新的字段其字段名如果和原來的字段相一致,,那么就會(huì)進(jìn)行覆蓋,,從assign
中我們可以很清楚地看到當(dāng)中字段的產(chǎn)生過程,同lambda
表達(dá)式進(jìn)行交互:
1. title
和 title_count
:
原有的 title
字段因?yàn)閷儆谧址愋?,可以直接很方便的調(diào)用 str.*
方法來進(jìn)行處理,,這里我就直接調(diào)用當(dāng)中的 replace
方法將「華農(nóng)兄弟:」字符進(jìn)行清洗
基于清洗好的 title
字段,再對(duì)該字段使用 apply
方法,,該方法傳遞我們前面實(shí)現(xiàn)定義好的字?jǐn)?shù)統(tǒng)計(jì)的函數(shù),,對(duì)每一條記錄的標(biāo)題中,對(duì)屬于 \u4e00
到 \u9fa5
這一區(qū)間內(nèi)的所有 Unicode 中文字符進(jìn)行提取,,并進(jìn)行長(zhǎng)度計(jì)算
2. created
和 created_date
:
對(duì)原有的 created
字段調(diào)用一個(gè) pipe
方法,,該方法會(huì)將 created
字段傳遞進(jìn) pd.to_datetime
參數(shù)中,這里需要將 unit
時(shí)間單位設(shè)置成 s
秒才能顯示出正確的時(shí)間,,否則仍以 Unix 時(shí)間錯(cuò)的樣式顯示
基于處理好的 created
字段,,我們可以通過其屬于 datetime64
的性質(zhì)來獲取其對(duì)應(yīng)的時(shí)間,這里 Pandas 給我們提供了一個(gè)很方便的 API 方法,,通過 dt.*
來拿到當(dāng)中的屬性值
3. length
和 video_length
:
原有的 length
字段我們直接讓字符串 00:
和該字段進(jìn)行直接拼接,,用以做下一步轉(zhuǎn)換
基于完整的 length
時(shí)間字符串,我們?cè)俅握{(diào)用 pipe
方法將該字段作為參數(shù)隱式傳遞到 pd.to_timedelta
方法中轉(zhuǎn)化,,然后同理和 create_date
字段一樣獲取到相應(yīng)的屬性值,,這里我取的是秒數(shù)。
2,、播放量趨勢(shì)圖 基于前面稍作清洗后得到的tidy_data
數(shù)據(jù),,我們可以快速地做一個(gè)播放量走勢(shì)的探索。這里我們需要用到created
這個(gè)屬于datetime64
的字段為 X 軸,,播放量play
字段為 Y 軸做可視化展示,。
# 播放量走勢(shì) %matplotlib inline %config InlineBackend.figure_format = 'retina' import matplotlib.pyplot as plt (tidy_data[['created' , 'play' ]] .set_index('created' ) .resample('1M' ) .sum() .plot( kind='line' , figsize=(16 , 8 ), title='Video Play Prend(2018-2020)' , grid=True , legend=False ) ) plt.xlabel('' ) plt.ylabel('The Number Of Playing' )
這里我們將上傳日期和播放量?jī)蓚€(gè)選出來后,需要先將created
設(shè)定為索引,,才能接著使用resample
重采樣的方法進(jìn)行聚合操作,,這里我們以月為統(tǒng)計(jì)顆粒度,對(duì)每個(gè)月播放量進(jìn)行加總,,之后再調(diào)用plot
接口實(shí)現(xiàn)可視化,。
鏈?zhǔn)秸{(diào)用的一個(gè)小技巧就是,可以利用括號(hào)作用域連續(xù)的特性使整個(gè)鏈?zhǔn)秸{(diào)用的操作不會(huì)報(bào)錯(cuò),,當(dāng)然如果不喜歡這種方式也可以手動(dòng)在每條操作后面追加一個(gè)\
符號(hào),,所以上面的整個(gè)操作就會(huì)變成這樣:
tidy_data[['created' , 'play' ]] \ .set_index('created' ) \ .resample('1M' ) .sum() .plot( \ kind='line' , \ figsize=(16 , 8 ), \ title='Video Play Prend(2018-2020)' , \ grid=True , \ legend=False \ )
但是相比于追加一對(duì)括號(hào)來說,這種尾部追加\
符號(hào)的方式并不推薦,,也不優(yōu)雅,。
但是如果既沒有在括號(hào)作用域或未追加\
符號(hào),那么在運(yùn)行時(shí) Python 解釋器就會(huì)報(bào)錯(cuò),。
3,、鏈?zhǔn)秸{(diào)用性能 通過前兩個(gè)案例我們可以看出鏈?zhǔn)秸{(diào)用可以說是比較優(yōu)雅且快速地能實(shí)現(xiàn)一套數(shù)據(jù)操作的流程,但是鏈?zhǔn)秸{(diào)用也會(huì)因?yàn)椴煌膶懛ǘ嬖谛阅苌系牟町悺?/p>
這里我們繼續(xù)基于前面的tidy_data
操作,,這里我們基于created_date
來對(duì)play
,、comment
和video_review
進(jìn)行求和后的數(shù)值進(jìn)一步以 10 為底作對(duì)數(shù)化。最后需要得到以下結(jié)果:
統(tǒng)計(jì)表格 寫法一:一般寫法
一般寫法 這種寫法就是基于tidy_data
拷貝后進(jìn)行操作,,操作得到的結(jié)果會(huì)不斷地覆蓋原有的數(shù)據(jù)對(duì)象
寫法二:鏈?zhǔn)秸{(diào)用寫法
鏈?zhǔn)秸{(diào)用寫法 可以看到,,鏈?zhǔn)秸{(diào)用的寫法相比于一般寫法而言會(huì)快上一點(diǎn),不過由于數(shù)據(jù)量比較小,,因此二者時(shí)間的差異并不大,;但鏈?zhǔn)秸{(diào)用由于不需要額外的中間變量已經(jīng)覆蓋寫入步驟,在內(nèi)存開銷上會(huì)少一些,。
結(jié)尾:鏈?zhǔn)秸{(diào)用的優(yōu)劣 從本文的只言片語中,,你能領(lǐng)略到鏈?zhǔn)秸{(diào)用使得代碼在可讀性上大大的增強(qiáng),同時(shí)以盡肯能少的代碼量去實(shí)現(xiàn)更多操作,。
當(dāng)然,,鏈?zhǔn)秸{(diào)用并不算是完美的,它也存在著一定缺陷,。比如說當(dāng)鏈?zhǔn)秸{(diào)用的方法超過 10 步以上時(shí),,那么出錯(cuò)的幾率就會(huì)大幅度提高,從而造成調(diào)試或 Debug 的困難,。比如這樣:
(data .method1(...) .method2(...) .method3(...) .method4(...) .method5(...) .method6(...) .method7(...) # Something Error .method8(...) .method9(...) .method10(...) .method11(...) )
你只能從鏈?zhǔn)秸{(diào)用方法體中「從尾倒頭」一步一步地去重現(xiàn)發(fā)生問題的地方在哪,。
因此使用鏈?zhǔn)秸{(diào)用時(shí),一定必須要考慮以下問題:
是否需要中間變量
操作數(shù)據(jù)中的步驟是否需要分解
每次操作后的結(jié)果是否仍為 DataFrame 類型
如果不需要中間變量,、步驟不需要分解且保證最后返回的就是 DataFrame 類型,,那么就愉快地使用鏈?zhǔn)秸{(diào)用方法來完成你的數(shù)據(jù)流程吧!
作者:100gle,,練習(xí)時(shí)長(zhǎng)不到兩年的非正經(jīng)文科生一枚,,喜歡敲代碼、寫寫文章,、搗鼓搗鼓各種新事物,;現(xiàn)從事有關(guān)大數(shù)據(jù)分析與挖掘的相關(guān)工作,。