函数式编程,包括函数式编程思维,当然是一个很复杂的话题,但对今天介绍的apply() 函数,只需要理解:函数作为一个对象,能作为参数传递给其它参数,并且能作为函数的返回值。
函数作为对象能带来代码风格巨大的改变。举一个例子,有一个包含 1 到 10 的 list,从其中找出能被 3 整除的数字。用传统的方法:
def can_divide_by_three(number):
if number % 3 == 0:
return True
else:
return False
selected_numbers = []
for number in range(1, 11):
if can_divide_by_three(number):
selected_numbers.append(number)
循环是不可少的,因为== can_divide_by_three() 函数==只用一次,可以用 lambda 表达式简化:
divide_by_three = lambda x : True if x % 3 == 0 else False
selected_numbers = []
for number in range(1, 11):
if divide_by_three(item):
selected_numbers.append(item)
这个就是传统编程思维方式,如果用函数式的编程方法呢,我们可以这样想:从一个 list 中取出特定规则的数字,能不能只关注和设置这个规则,循环这种事情交给编程语言去处理呢?当然可以。当编程人员只关心规则(规则可能是一个条件,或者由某一个 function 来定义),代码将大大简化,可读性也更强。
Python 语言提供 filter() 函数,语法如下:
filter(function, sequence)
filter() 函数的功能:对 sequence 中的 item 依次执行 function(item),将结果为 True 的 item 组成一个 List/String/Tuple(取决于 sequence 的类型)并返回。有了这个函数,上面的代码可以简化为:
divide_by_three = lambda x : True if x % 3 == 0 else False
selected_numbers = filter(divide_by_three, range(1, 11))
#将 lambda 表达式放在语句中,代码简化到只需要一句话就够了
selected_numbers = filter(lambda x: x % 3 == 0, range(1, 11))
回到主题, pandas 的 apply() 函数可以作用于 Series 或者整个DataFrame,功能也是自动遍历整个 Series 或者 DataFrame, 对每一个元素运行指定的函数。
举一个例子,现在有这样一组数据,学生的考试成绩:
Name Nationality Score
张 汉 400
李 回 450
王 汉 460
如果民族不是汉族,则总分在考试分数上再加 5 分,现在需要用 pandas 来做这种计算,采取增加 pandas 列的方法。当然如果只是为了得到结果, numpy.where() 函数更简单,这里主要为了演示 Series.apply() 函数的用法。
import pandas as pd
df = pd.read_csv("studuent-score.csv")
df['ExtraScore'] = df['Nationality'].apply(lambda x : 5 if x != '汉' else 0)
df['TotalScore'] = df['Score'] + df['ExtraScore']
对于 Nationality 这一列, pandas 遍历每一个值,并且对这个值执行 lambda 匿名函数,将计算结果存储在一个新的 Series 中返回。上面代码在 jupyter notebook 中显示的结果如下:
Name Nationality Score ExtraScore TotalScore
0 张 汉 400 0 400
1 李 回 450 5 455
2 王 汉 460 0 460
apply() 函数当然也可执行 python 内置的函数,比如我们想得到 Name 这一列字符的个数,如果用 apply() 的话:
df['NameLength'] = df['Name'].apply(len)
apply函数是pandas
里面所有函数中自由度最高的函数。该函数如下:
DataFrame.apply(func, axis=0, broadcast=False, raw=False, reduce=None,
args=(), **kwds)
该函数最有用的是第一个参数,这个参数是函数,相当于C/C++的函数指针。
这个函数需要自己实现,函数的传入参数根据axis来定,比如axis = 1,就会把一行数据作为Series的数据结构传入给自己实现的函数中,我们在函数中实现对Series不同属性之间的计算,返回一个结果,则apply函数会自动遍历每一行DataFrame的数据,最后将所有结果组合成一个Series数据结构并返回。
DataFrame.apply() 函数则会遍历每一个元素,对元素运行指定的 function。比如下面的示例:
import pandas as pd
import numpy as np
matrix = [
[1,2,3],
[4,5,6],
[7,8,9]
]
df = pd.DataFrame(matrix, columns=list('xyz'), index=list('abc'))
df.apply(np.square)
对 df 执行 square() 函数后,所有的元素都执行平方运算:
x y z
a 1 4 9
b 16 25 36
c 49 64 81
如果只想 apply() 作用于指定的行和列,可以用行或者列的 name 属性进行限定。比如下面的示例将 x 列进行平方运算:
df.apply(lambda x : np.square(x) if x.name=='x' else x)
x y z
a 1 2 3
b 16 5 6
c 49 8 9
下面的示例对 x 和 y 列进行平方运算:
df.apply(lambda x : np.square(x) if x.name in ['x', 'y'] else x)
x y z
a 1 4 3
b 16 25 6
c 49 64 9
下面的示例对第一行 (a 标签所在行)进行平方运算:
df.apply(lambda x : np.square(x) if x.name == 'a' else x, axis=1)
默认情况下axis=0 表示按列,axis=1 表示按行。
平时我们会经常用到日期的计算,比如要计算两个日期的间隔,比如下面的一组关于 wbs 起止日期的数据:
wbs date_from date_to
job1 2019-04-01 2019-05-01
job2 2019-04-07 2019-05-17
job3 2019-05-16 2019-05-31
job4 2019-05-20 2019-06-11
假定要计算起止日期间隔的天数。比较简单的方法就是两列相减(datetime 类型):
import pandas as pd
import datetime as dt
wbs = {
"wbs": ["job1", "job2", "job3", "job4"],
"date_from": ["2019-04-01", "2019-04-07", "2019-05-16","2019-05-20"],
"date_to": ["2019-05-01", "2019-05-17", "2019-05-31", "2019-06-11"]
}
df = pd.DataFrame(wbs)
df['elpased'] = df['date_to'].apply(pd.to_datetime) -
df['date_from'].apply(pd.to_datetime)
apply() 函数将 date_from 和 date_to 两列转换成 datetime 类型。我们 print 一下 df:
wbs date_from date_to elapsed
0 job1 2019-04-01 2019-05-01 30 days
1 job2 2019-04-07 2019-05-17 40 days
2 job3 2019-05-16 2019-05-31 15 days
3 job4 2019-05-20 2019-06-11 22 days
日期间隔已经计算出来,但后面带有一个单位 days,这是因为两个 datetime 类型相减,得到的数据类型是 timedelta64,如果只要数字,还需要使用 timedelta 的 days 属性转换一下。
df['elpased']= df['date_to'].apply(pd.to_datetime) -
df['date_from'].apply(pd.to_datetime)
df['elapsed'] = df['elpased'].apply(lambda x : x.days)
使用 DataFrame.apply() 函数也能达到同样的效果,我们需要先定义一个函数 get_interval_days() 这个函数的第一列是一个 Series 类型的变量,执行的时候,依次接收 DataFrame 的每一行。
import pandas as pd
import datetime as dt
def get_interval_days(arrLike, start, end):
start_date = dt.datetime.strptime(arrLike[start], '%Y-%m-%d')
end_date = dt.datetime.strptime(arrLike[end], '%Y-%m-%d')
return (end_date - start_date).days
wbs = {
"wbs": ["job1", "job2", "job3", "job4"],
"date_from": ["2019-04-01", "2019-04-07", "2019-05-16","2019-05-20"],
"date_to": ["2019-05-01", "2019-05-17", "2019-05-31", "2019-06-11"]
}
df = pd.DataFrame(wbs)
df['elapsed'] = df.apply(
get_interval_days, axis=1, args=('date_from', 'date_to'))
读取表格
假如我们想要得到表格中的PublishedTime和ReceivedTime属性之间的时间差数据,就可以使用下面的函数来实现:
import pandas as pd
import datetime #用来计算日期差的包
def dataInterval(data1,data2):
d1 = datetime.datetime.strptime(data1, '%Y-%m-%d')
d2 = datetime.datetime.strptime(data2, '%Y-%m-%d')
delta = d1 - d2
return delta.days
def getInterval(arrLike): #用来计算日期间隔天数的调用的函数
PublishedTime = arrLike['PublishedTime']
ReceivedTime = arrLike['ReceivedTime']
# print(PublishedTime.strip(),ReceivedTime.strip())
days = dataInterval(PublishedTime.strip(),ReceivedTime.strip()) #注意去掉两端空白
return days
if __name__ == '__main__':
fileName = "NS_new.xls";
df = pd.read_excel(fileName)
df['TimeInterval'] = df.apply(getInterval , axis = 1)
有时候,我们想给自己实现的函数传递参数,就可以用的apply函数的*args和**kwds参数,比如同样的时间差函数,我希望自己传递时间差的标签,这样每次标签更改就不用修改自己实现的函数了,实现代码如下:
import pandas as pd
import datetime #用来计算日期差的包
def dataInterval(data1,data2):
d1 = datetime.datetime.strptime(data1, '%Y-%m-%d')
d2 = datetime.datetime.strptime(data2, '%Y-%m-%d')
delta = d1 - d2
return delta.days
def getInterval_new(arrLike,before,after): #用来计算日期间隔天数的调用的函数
before = arrLike[before]
after = arrLike[after]
# print(PublishedTime.strip(),ReceivedTime.strip())
days = dataInterval(after.strip(),before.strip()) #注意去掉两端空白
return days
if __name__ == '__main__':
fileName = "NS_new.xls";
df = pd.read_excel(fileName)
df['TimeInterval'] = df.apply(getInterval_new ,
axis = 1, args = ('ReceivedTime','PublishedTime')) #调用方式一
#下面的调用方式等价于上面的调用方式
df['TimeInterval'] = df.apply(getInterval_new ,
axis = 1, **{'before':'ReceivedTime','after':'PublishedTime'}) #调用方式二
#下面的调用方式等价于上面的调用方式
df['TimeInterval'] = df.apply(getInterval_new ,
axis = 1, before='ReceivedTime',after='PublishedTime') #调用方式三