总结了pandas各种进阶操作与使用技巧,并且对各方法间的效率进行比较。
创建一个pandas的dataframe对象作为下文样例:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"A":np.arange(0,5),
"B":np.arange(5,10),
"C":np.arange(15,20),
"D":np.arange(25,30),
"E":np.arange(35,40),
})
1 通过向量化筛选
通过使用pandas表达式,pandas会返回一个包含了布尔数据的series对象:
print(df['A']>2)
type(df['A']>2)
这时,我们只需将布尔型的series再次输入给df,就能筛选出我们想要的数据。这种方法也称为向量化(Vectorization),即使用series类型的布尔向量进行取数:
df[df['A']>2]
当存在多个条件时,用户要使用 括号 分割各条件,并使用 ‘&’ 和 ‘|’ 逻辑运算符,注意不能使用 and 或者 or:
df[df['A'] > 1 and df['A'] < 4] # 错误写法
df[(df['A'] > 1) & (df['A'] < 4)] # 正确写法
上面我们是直接使用大于号和小于号进行比较,也可以使用pandas的数值函数:
df.eq() # 等于相等 ==
df.ne() # 不等于 !=
df.le() # 小于等于 >=
df.lt() # 小于 <
df.ge() # 大于等于 >=
df.gt() # 大于 >
# 具体用法如下
df[df.A.ge(3)]
对于字符串型的内容,我们可以使用pandas.str里的方法。这里我们先将第一列数改为字符串,再取出第一列包含字符串‘2’所在的行:
df['A'] = df['A'].astype('str') # 将第一列数字改为‘str’类型
df[df['A'].str.contains('2')] # 取出字符串‘2’所在的行
类似的,除了str.contains外,还有str.startswith与str.endswith,这里不作过多展示。
2 通过query()、eval()、filter()筛选
前面使用向量法进行取数操作,本质上是通过pandas表达式构造series型的布尔向量进行取数。
接下来我们使用pandas提供的更高效的取数函数:query()和eval()。二者语法是一致的,区别在于前者取出所在的行,后者是返回布尔值。与向量取数不同的是,query和eval函数并不要求多个条件使用括号隔开,也不限定逻辑表达式的形式。样例如下:
df.query('A >= 1 and B <= 8')
df.eval('A >= 1 and B <= 8')
当需要传入外部对象时,需要使用@符号:
a = 1
b = 8
df.query('A >= @a and B <= @b')
除此以外,pandas还有以下取数方法:
- 过滤器filter(),筛选需要的行和列;
- 取值函数where(),返回符合要求的数据,不符合的返回NaN;
- 取反函数mask(),语法与where()一致,但返回的是不满足要求的数据。
上述的取数方法比向量化的方法更高效,是因为在处理复合表达式时,在上述案例中:
df[(df['A'] > 1) & (df['A'] < 4)]
实际上是等价于:
a = (df['A'] > 1)
b = (df['A'] < 4)
df[a & b]
每个中间步骤都存在内存显示分配,如果某个复合表达式特别大,会导致很大的内存和计算开销。因此催生了Numexpr 程序库,提供了元素到元素的复合代数式运算,不需要为临时数组分配全部内存,可以实现c级别的速度。
Pandas 的 eval() 和 query() 工具是基于于 Numexpr 来实现的,因此效率很高,使用时建议优先考虑。
3 通过定位取数
最简单的取数方式是使用索引和切片。不过二者在pandas中都不常用
df[df.index == 1] # 取出第二行的数据
df[start : stop : step] # 起始位置start,结束位置stop,步长step
切片的使用方式与python定义的模式相同;当步长为负数时,pandas会按倒叙取数。
除此以外,pandas支持的通过定位取数的方法有:
- loc
- iloc
- at
- iat
- any
- all
这些作为常见的方法,这里不进行过多的介绍,使用中注意以下几点即可:
- 在Dataframe中,如果只有一个中括号(如df.loc[0])返回的是series对象,如果是两个中括号(如df.loc[[0]])才会返回dataframe对象。
- at 和 iat 与 loc 和 iloc 的区别在于前者只会返回一个值,并且效率稍高。
-
any()是至少有一个为True则为True;all()是所有为True才会为True。
-
any()和all()中,当传入的axis=1,会按照行进行查询;axis=0表示按照列查询。
4 通过迭代器取数
前面我们是通过定位取数,但是通常要通过for循环来实现迭代,pandas中迭代取数的方法主要包括:
iteritems()
- iteritems()是按列遍历,返回一对键值对,其中键是列标签,值是列内容(值是一个series对象)。
iterrows()
- iterrows()可以看成iteritems()的转置,是按行遍历,返回的键是行标签,值是行内容(值是一个series对象)。
itertuples()
- itertuples()与iterrows()是类似的,都是按行返回结构,区别在于它将iterrows()的键和值都放入一个元组中,一起返回。
for循环 + zip()
- zip()可以将pandas中的两列缝合在一起,再通过for循环一起输出,例如:
-
[a+b for a, b in zip(df['A'], df['B'])]
apply函数
- pandas对象具有apply方法,通常需要和lambda函数结合使用,例如:
-
df.apply(lambda x: x.A + x.B, axis=1)
numpy矩阵
- 通过pandas对象的 .values 或者 .to_numpy 方法,将dataframe对象转变为numpy矩阵,再进行相关操作。
-
df['A'].values + df['B'].values
5 处理缺失数据
最后讲一下处理缺失值‘NaN’的方法。我们将案例中的第一列添加两个空值:
df['A'][0] = np.nan
df['A'][1] = np.nan
df
我们可以使用 .isnull() 或者 .isna() 查看空值,这俩效果相同:
df.isna()
但是这样还不够方便,我们可以用any()函数查看存在缺失值的列:
df.isnull().any()
我们还可以将pandas转换为numpy矩阵来查看缺失值所在的行:
df[df.isnull().to_numpy() == True] # df[df.isnull().values == True] 也可以
我们还可以对各行列的缺失值,缺失率进行统计:
isnull().sum(axis=0) # 列缺失统计
isnull().sum(axis=1) # 行缺失统计
isnull().mean() # 缺失率
这里有必要解释一下,为什么要使用isnull,不能直接判断。在python中,NaN类型不等于任何值,连自己都不相等。例如:
df['A'][0] == np.nan
找出缺失数据后,用户可能需要删除缺失值所在的行或列,使用drop()方法即可。也可以使用以下方法:
- .dropna()会删除缺失值所在的行或列;
- .fillna()会填充指定行列的缺失值;
6 各取数方法的速度比较
直接说结论:numpy完胜
在上述介绍的方法中,速度由快至慢分别是:
numpy矩阵、iteritems、pandas向量化操作、for+zip、itertuples、apply、ilat、iloc、iterrows。
(其中query和eval方法 > pandas向量化操作)
由于numpy矩阵运算是基于c语言实现的,效率上远优于其他迭代方法。如下图所示,numpy的向量化操作,其效率完胜于pandas的向量化操作,而后者的效率也远胜于普通的for循环。在内存空间充足的情况下,numpy和pandas向量化操作应当作为首选!
参考链接:
dataframe遍历效率对比_dataframe 遍历效率_Takoony的博客-CSDN博客
https://towardsdatascience.com/how-to-make-your-pandas-loop-71-803-times-faster-805030df4f06