如果你打算从事数据分析或数据挖掘等数据科学领域的工作,或者像我一样目前正在从事相关领域的工作,那么“链式调用”是必修课当然对我们来说。为什么要链式调用?链式调用,也称为方法链(MethodChaining),字面意思是将一系列操作或函数方法像链条一样放在一起。最早感受到链式调用的“美妙”之处,是从R语言的管道算子入手。库(tidyverse)mtcars%>%group_by(cyl)%>%summarise(meanmeanOfdisp=mean(disp))%>%ggplot(aes(x=as.factor(cyl),y=meanOfdisp,fill=as.factor(seq(1,3))))+geom_bar(stat='identity')+guides(fill=F)对于R用户来说,这段代码可以快速理解整个流程步骤是怎样的。这都是用符号%>%(管道运算符)来讨论的。使用管道运算符,我们可以将左边的东西传递给下一个东西。这里我将mtcars数据集传递给group_by函数,然后将得到的结果传递给summarize函数,最后传递给ggplot函数进行可视化绘制。如果我没学过链式调用,那我肯定是在刚学R语言的时候写了这个:library(tidyverse)cyl4<-mtcars[which(mtcars$cyl==4),]cyl6<-mtcars[which(mtcars$cyl==6),]cyl8<-mtcars[其中(mtcars$cyl==8),]data<-data.frame(ccyl=c(4,6,8),meanOfdisp=c(mean(cyl4$disp),mean(cyl6$disp),mean(cyl8$disp)))graph<-ggplot(datadata=data,aes(x=factor(cyl),y=meanOfdisp,fill=as.factor(seq(1,3))))graph<-graph+geom_bar(stat='identity')+guides(fill=F)graph如果你不使用pipelineoperator,那么我会做不必要的赋值并覆盖原来的数据对象,但实际上cyl#和里面生成的数据其实最后只是服务于graph的图片,所以造成的问题就是代码会变得冗余。链式调用在极大简化代码的同时,也提高了代码的可读性,可以快速理解每一步在做什么。这种方法在做数据分析或处理数据时非常有用,减少了不必要变量的创建,并且可以以快速简单的方式进行探索。你可以在很多地方看到链式调用或管道操作。这里我举两个R语言以外的典型例子。一种是shell语句:echo"`seq1100`"|grep-e"^[3-4].*"|tr"3""*"使用"|"shell语句中的管道操作符可以快速实现链式调用,这里我先打印1-100的所有整数,然后传入grep方法中,提取所有以3或4开头的部分,然后将这部分传入tr方法,并且包括3的部分用星号代替。结果如下:另一种是Scala语言:objectTest{defmain(args:Array[String]):Unit={valnumOfseq=(1to100).toListvalchain=numOfseq.filter(_%2==0).map(_*2).take(10)}}在这个例子中,首先numOfseq变量包含了1-100的所有整数,然后从chain部分,我首先调用了基于numOfseq的filter方法来过滤这些数字中的偶数中间,然后调用map方法将这些过滤后的数字乘以2,最后使用take方法从新形成的数字中取出前10个数字,并将这些数字一起赋值给chain变量。通过上面的描述,相信大家对链式调用已经有了初步的印象,但是一旦掌握了链式调用,除了改变自己的代码风格,你的编程思维也会有所不同。Python中的链调用通过构建类方法并返回对象本身或返回所属类(@classmethod)在Python中实现简单的链调用classChain:def__init__(self,name):self.name=namedefintroduce(self):print(“你好,我的名字是%s”%self.name)returnsselfdeftalk(self):print("Canwemakeafriend?")returnsselfdefgreet(self):print("Hey!Howareyou?")returnsselfif__name__=='__main__':chain=Chain(name="jobs")chain.introduce()print("-"*20)chain.introduce().talk()print("-"*20)chain.introduce().talk().greet()这里我们创建一个Chain类,需要传入一个name字符串参数来创建一个实例对象;本课一共有三种方法,分别是介绍、交谈和打招呼。由于每次返回的是self本身,我们可以不断调用对象所属类中的方法,结果如下:hello,mynameisjobs--------------------hello,mynameisjobsCanwemakeafriend?--------------------你好,mynameisjobsCanwemakeafriend?嘿!你好吗?在Pandas中使用链式调用铺垫了这么多,最后才讲Pandas链式调用部分Pandas中的大部分方法都适合用链式方法操作,因为API处理后的返回往往是Series类型或者DataFrame类型,所以我们可以直接调用相应的方法,这里我以今年2月左右给别人做案例展示时爬取的华农兄弟B站的视频数据为例。可以通过链接获取。数据字段信息如下,一共有300条数据,20个字段:字段信息但是在使用这部分数据之前,我们还需要对这部分数据进行初步清洗,这里我主要选择下面的fields:aid:视频对应的av号comment:评论数play:播放量title:标题video_review:创建的弹幕数:上传日期length:视频时长1.数据清洗各个字段对应的值如下:字段值我们从数据中可以看出To:title字段会一直有“华农兄弟”这四个字,需要在统计的时候提前去掉标题中的字数;created的上传日期看似显示为一长串值,但实际上是从1970年到现在的时间戳。我们需要将其处理成可读的年月日格式;length只显示分秒,但是小时不是用“00”补全的,所以这里我们一方面需要补全,另一方面要转换成对应的时间格式,链式调用操作为如下:importreimportpandasaspd#Definewordcountfunctiondefword_count(text):returnlen(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=lambdadf:df['title'].str.replace("华农兄弟:",""),title_count=lambdadf:df['title'].apply(word_count),created=lambdadf:df['created'].pipe(pd.to_datetime,unit='s'),created_date=lambdadf:df['created'].dt.date,length=lambdadf:"00:"+df['length'],video_length=lambdadf:df['length'].pipe(pd.to_timedelta).dt.seconds))这里先通过loc方法选择列,然后调用assign方法新建字段。如果新字段的字段名与原字段一致,则将被覆盖。从assign中我们可以清楚的看到其中字段的生成过程,和lambda表达式进行交互:1.title和title_count:原来的title字段可以直接方便的调用,因为它属于string类型str.*方法来处理,这里我会直接调用replace方法在清洗后的title字段的基础上清洗“华农兄弟:”字符,然后在这个字段上使用apply方法,传递给我们之前定义的实现计数的函数words,提取每条记录的标题中\u4e00到\u9fa5范围内的所有Unicode汉字,并计算长度2.created和created_date:为原来的created字段调用一个pipe方法,该方法将created字段传过去进入pd.to_datetime参数,这里需要设置单位时间单位为s秒才能显示正确的时间,否则还是会按照处理过的Unix时间错误样式显示创建的字段,我们can通过datetime64的性质获取对应的时间。这里Pandas为我们提供了一个非常方便的API方法,通过dt.*获取属性值3.length和video_length:原始长度字段我们直接将字符串00:与该字段拼接起来,进行下一步基于完整长度时间的转换string,我们再次调用pipe方法将这个字段作为参数隐式传递给pd.to_timedelta方法进行转换,然后同理获取对应的属性值,方法和create_date字段一样。这里我取秒数2,播放量趋势图是根据稍微清洗后得到的tidy_data数据。我们可以快速探索播放量趋势。这里我们需要使用createdfield,属于datetime64,作为X轴,playfield作为Y轴进行可视化展示。#播放量趋势%matplotlibinline%configInlineBackend.figure_format='retina'importmatplotlib.pyplotasplt(tidy_data[['created','play']].set_index('created').resample('1M').sum().plot(kind='line',figsize=(16,8),title='VideoPlayPrend(2018-2020)',grid=True,legend=False))plt.xlabel("")plt.ylabel('TheNumberOfPlaying')这里我们选择上传日期和播放量,我们需要先设置created作为索引,然后使用resample重采样的方式进行聚合操作。这里我们以月份为统计粒度,对每月的播放量进行汇总,然后调用plot接口实现可视化。链式调用的一个小技巧就是可以利用括号作用域的连续特性,这样整个链式调用的运行就不会报错。当然,如果你不喜欢这种方式,你可以在每次操作后手动添加一个\符号,这样上面的整个操作就会变成这样:tidy_data[['created','play']]\.set_index('创建')\.resample('1M').sum().plot(\kind='line',\figsize=(16,8),\title='VideoPlayPrend(2018-2020)',\grid=True,\legend=False\)但是相对于追加一对括号,这种在末尾追加\符号的方式不推荐,也不优雅。但是如果既没有附加括号作用域也没有附加\符号,Python解释器会在运行时报错。3.链式调用性能通过前两个案例我们可以看出,链式调用可以说是一个更优雅快速的过程,可以实现一组数据操作,但是链式调用也会因为写法不同而存在性能问题不同之处。这里我们继续在前面tidy_data的基础上进行操作。这里我们根据created_date对play、comment和video_review求和,然后将值以10为底进行对数。最后需要得到如下结果:统计表写法1:通用写法通用写法这种写法是基于tidy_data的复制和运算,运算得到的结果会不断覆盖原来的数据对象写法2:ChaincallwritingChaincallwriting可以看出,chaincall的写法比一般的写法要快,但是因为数据量比较小,所以两者的时间差不大;但是链式调用不需要额外的中间变量。覆盖写入步骤将有更少的内存开销。完结:链式调用的优缺点从本文的寥寥数语中可以体会到,链式调用极大地增强了代码的可读性,同时用尽可能少的代码实现了更多的操作。当然,链式调用并不完美,它也有一定的缺陷。例如,链式调用的方法超过10步或更多步时,出错的概率会大大增加,给调试或调试带来困难。例如:(data.method1(...).method2(...).method3(...).method4(...).method5(...).method6(...).method7(...)#SomethingError.method8(...).method9(...).method10(...).method11(...))你只能调用链中的方法体“从头到尾thebeginning”一步步重现问题发生的地方。因此,在使用链式调用时,必须考虑以下问题:是否需要中间变量?是否需要分解数据操作中的步骤?每次操作后的结果是否还是DataFrame类型?如果不需要中间变量,不需要分解步骤,最终返回的就是DataFrame类型,那么愉快的使用链式调用的方式来完成你的数据流转吧!
