深入浅出Pandas--Pandas高级操作--函数应用
对应书本第二部分第5章Pandas高级操作第7节
我们知道,函可以让复杂的常用操作模块化,既能在需要使用时直接调用,达到复用的目的,也能简化代码。Pandas提供了几个常用的调用数函数的方法。
- pipe():应用在整个DataFrame或Series上。
- apply():应用在DataFrame的行或列中,默认为列。
- applymap():应用在DataFrame的每个元素中。
- map():应用在Series或DataFrame的一列的每个元素中。
pipe()
Pandas提供的pipe()叫作管道方法,它可以让我们写的分析过程标准化、流水线化,达到复用目标,它也是最近非常流行的链式方法的重要代表。DataFrame和Series都支持pipe()方法。
pipe()的语法结构为df.pipe(<函数名>, <传给函数的参数列表或字典>)。它将DataFrame或Series作为函数的第一个参数(见图5-1),可以根据需求返回自己定义的任意类型数据。
pipe()可以将复杂的调用简化,看下面的例子:
# 对df多重应用多个函数
f(g(h(df), arg1=a), arg2=b, arg3=c)
# 用pipe可以把它们连接起来
(df.pipe(h)
.pipe(g, arg1=a)
.pipe(f, arg2=b, arg3=c)
)
# 以下是将'arg2'参数传给函数f,然后作为函数整体接受后面的参数
(df.pipe(h)
.pipe(g, arg1=a)
.pipe((f, 'arg2'), arg1=a, arg3=c)
)
函数h传入df的值返回的结果作为函数g的第一个参数值,g同时还传入了参数arg1;再将返回结果作为函数f的第一个参数,最终得到计算结果,这个调用过程显得异常复杂。使用pipe改造后代码逻辑复杂度大大降低,通过链式调用pipe()方法,对数据进行层层处理,大大提高代码的可读性。
接下来我们看一下实际案例:
# 定义一个函数,给所有季度的成绩加n,然后增加平均数
# 其中n中要加的值为必传参数
def add_mean(rdf, n):
df = rdf.copy()
df = df.loc[:,'Q1':'Q4'].applymap(lambda x: x+n)
df['avg'] = df.loc[:,'Q1':'Q4'].mean(1)
return df
# 调用
df.pipe(add_mean, 100)
函数部分可以使用lambda。下例完成了一个数据筛选需求,lambda的第一个参数为self,即使用前的数据本身,后面的参数可以在逻辑代码中使用。
# 筛选出Q1大于等于80且Q2大于等于90的数据
df.pipe(lambda df_, x, y: df_[(df_.Q1 >= x) & (df_.Q2 >= y)], 80, 90)
apply()
apply()可以对DataFrame按行和列(默认)进行函数处理,也支持Series。如果是Series,逐个传入具体值,DataFrame逐行或逐列传入,如图5-2所示。
# 将name全部变为小写
df.name.apply(lambda x: x.lower())
下面看一个DataFrame的例子。我们需要计算每个季度的平均成绩,计算方法为去掉一个最高分和一个最低分,剩余成绩的平均值为最终的平均分。
# 去掉一个最高分和一个最低分再算出平均分
def my_mean(s):
max_min_ser = pd.Series([-s.max(), -s.min()])
return s.append(max_min_ser).sum()/(s.count()-2)
# 对数字列应用函数
df.select_dtypes(include='number').apply(my_mean)
分析一下代码:函数my_mean接收一个Series,从此Series中取出最大值和最小值的负值组成一个需要减去的负值Series;传入的Series追加此负值Series,最后对Series求和,求和过程中就减去了两个极值;由于去掉了两个值,分母不能取Series的长度,需要减去2,最终计算出结果。这是函数的代码逻辑。
应用函数时,我们只选择数字类型的列,再使用apply调用函数my_mean,执行后,结果返回了每个季度的平均分。
希望以此算法计算每个学生的平均成绩,在apply中传入axis=1则每行的数据组成一个Series传入自定义函数中。
# 同样的算法以学生为维度计算
(
df.set_index('name') # 设定name为索引
.select_dtypes(include='number')
.apply(my_mean, axis=1) # 横向计算
)
由上面的案例可见,直接调用lambda函数非常方便。在今后的数据处理中,我们会经常使用这种操作。
以下是一个判断一列数据是否包含在另一列数据中的案例。
# 判断一个值是否在另一个类似列表的列中
df.apply(lambda d: d.s in d.s_list, axis=1) # 布尔序列
df.apply(lambda d: d.s in d.s_list, axis=1).astype(int) # 0 和 1 序列
它常被用来与NumPy库中的np.where()方法配合使用,如下例:
# 函数,将大于90分数标记为good
fun = lambda x: np.where(x.team=='A' and x.Q1>90, 'good' ,'other')
df.apply(fun, axis=1)
# 同上效果
(df.apply(lambda x: x.team=='A' and x.Q1>90, axis=1)
.map({True:'good', False:'other'})
)
df.apply(lambda x: 'good' if x.team=='A' and x.Q1>90 else '', axis=1)
总结一下,apply()可以应用的函数类型如下:
df.apply(fun) # 自定义
df.apply(max) # Python内置函数
df.apply(lambda x: x*2) # lambda
df.apply(np.mean) # NumPy等其他库的函数
df.apply(pd.Series.first_valid_index) # Pandas自己的函数
后面介绍到的其他调用函数的方法也适用这个规则。
applymap()
df.applymap()可实现元素级函数应用,即对DataFrame中所有的元素(不包含索引)应用函数处理,如图5-3所示。
使用lambda时,变量是指每一个具体的值
# 计算数据的长度
def mylen(x):
return len(str(x))
df.applymap(lambda x:mylen(x)) # 应用函数
df.applymap(mylen) # 效果同上
map()
map()根据输入对应关系映射值返回最终数据,用于Series对象或DataFrame对象的一列。传入的值可以是一个字典,键为原数据值,值为替换后的值。可以传入一个函数(参数为Series的每个值),还可以传入一个字符格式化表达式来格式化数据内容。
df.team.map({'A':'一班', 'B':'二班','C':'三班', 'D':'四班',}) # 枚举替换
df.team.map('I am a {}'.format)
df.team.map('I am a {}'.format, na_action='ignore')
t = pd.Series({'six': 6., 'seven': 7.})
s.map(t)
# 应用函数
def f(x):
return len(str(x))
df['name'].map(f)
agg()
agg()一般用于使用指定轴上的一项或多项操作进行汇总,可以传入一个函数或函数的字符,还可以用列表的形式传入多个函数。
# 每列的最大值
df.agg('max')
# 将所有列聚合产生sum和min两行
df.agg(['sum', 'min'])
# 序列多个聚合
df.agg({'Q1' : ['sum', 'min'], 'Q2' : ['min', 'max']})
# 分组后聚合
df.groupby('team').agg('max')
df.Q1.agg(['sum', 'mean'])
def mymean(x):
return x.mean()
df.Q2.agg(['sum', mymean])
另外,agg()还支持传入函数的位置参数和关键字参数,支持每个列分别用不同的方法聚合,支持指定轴的方向。
# 每列使用不同的方法进行聚合
df.agg(
a=('Q1', max),
b=('Q2', 'min'),
c=('Q3', np.mean),
d=('Q4', lambda s:s.sum()+1)
)
# 按行聚合
df.loc[:,'Q1':].agg("mean", axis="columns")
# 利用pd.Series.add方法对所有数据加分,other是add方法的参数
df.loc[:,'Q1':].agg(pd.Series.add, other=10)
agg()的用法整体上与apply()极为相似。
transform()
DataFrame或Series自身调用函数并返回一个与自身长度相同的数据。
df.transform(lambda x: x*2) # 应用匿名函数
df.transform([np.sqrt, np.exp]) # 调用多个函数
df.transform([np.abs, lambda x: x + 1])
df.transform({'A': np.abs, 'B': lambda x: x + 1})
df.transform('abs')
df.transform(lambda x: x.abs())
可以对比下面两个操作:
df.groupby('team').sum()
df.groupby('team').transform(sum)
分组后,直接使用计算函数并按分组显示合计数据。使用transform()调用计算函数,返回的是原数据的结构,但在指定位置上显示聚合计算后的结果,这样方便我们了解数据所在组的情况。
copy()
类似于Python中copy()函数,df.copy()方法可以返回一个新对象,这个新对象与原对象没有关系。
当deep = True(默认)时,将创建一个新对象,其中包含调用对象的数据和索引的副本。对副本数据或索引的修改不会反映在原始对象中。当deep = False时,将创建一个新对象而不复制调用对象的数据或索引(仅复制对数据和索引的引用)。原始数据的任何更改都将对浅拷贝的副本进行同步更改,反之亦然。
s = pd.Series([1, 2], index=["a", "b"])
s_1 = s
s_copy = s.copy()
s_1 is s # True
s_copy is s # False
熟练使用函数可以帮助我们抽象问题,复用解决方案,同时大大减少代码量。