高性能 Pandas 方法:query 和 eval
为什么不喜欢写方括号
对于一个最开始从 R 语言的 Tidyverse 生态中转到 Pandas 中的人来说,除了管道操作符和链式调用的方式外,最不习惯的方式就是 Pandas 在对 DataFrame 做条件筛选或查询时,使用过多的方括号。
先看看在 R 语言的 Tidyverse 生态中筛选操作是怎样的(主要基于 dplyr
包),这里以 iris 数据集为例。
library(tidyverse)
# normal way
iris[which(iris$Sepal.Length > 5 & iris$Petal.Length <= 1.5), ]
# Tidyverse way
iris %>% filter(
Sepal.Length > 5 &
Petal.Length <= 1.5
)
虽然在 R 语言也可以用方括号加 which
的方式来进行行索引定位,但是相比于 filter
来说反而略显啰嗦。因此 filter
的方式反而更容易被记住且频繁使用。
在 Pandas 中,大部分人往往在初学时最先接触到的就是通过方括号的形式来进行筛选查询,像是这样:
# !pip install seaborn
import pandas as pd
import seaborn as sns
# Load iris dataset
iris = sns.load_dataset('iris')
# 等价于 iris[(iris.sepal_length > 5) & (iris.petal_length <= 1.5)]
iris[(iris['sepal_length'] > 5) & (iris['petal_length'] <= 1.5)]
尽管可以直接通过 iris.column
的形式来调用相应的字段,但是这很容易和方法混在一起,因此在实际项目中往往我更偏好用方括号和引号来表示字段,以增强可读性并同方法相区分。
可我们也很容易看到 Pandas 的筛选写法其实相当冗长,同时这样的形式即便是熟练使用 Pandas 的「老鸟」也会经常这么写,甚至在各种课程中也屡见不鲜。
但其实对于每个使用 Pandas 库的人来说,让代码更简洁才符合 Pythonic 的风格。这就是为什么本文所要谈论 query
和 eval
这两个方法的原因。
query 和 eval
query
和 eval
方法在 2014年 1 月 3 日的 0.13.0 版本中首次加入,它们允许使用者以传入字符串表达式(Expression)的形式来对 DataFrame 进行操作。
用法一:条件筛选与查询
前面分所述的写法其实可以变成这样:
# query
iris.query("sepal_length > 5 & petal_length <= 1.5")
# eval
cond = iris.eval("sepal_length > 5 & petal_length <= 1.5")
iris[cond]
相比于传统的括号写法,query
和eval
方法能有效减少在筛选查询时符号冗余的情况,尤其是 query
方法能够紧密地同链式调用相结合从而达到了 R 语言 Tidyverse 中的 filter
用法,类似于这样:
(iris
.query("sepal_length > 5 & petal_length <= 1.5")
.assign(sepal_len_cut = lambda df: df['sepal_length']
.pipe(pd.cut, bins=3),
petal_len_cut = lambda df: df['petal_length']
.pipe(pd.cut, bins=3))
.groupby(['sepal_len_cut', 'petal_len_cut'])
.sum()
.dropna()
)
当筛选的条件为多条件时,除了使用六个引号的长字符串之外,还需要注意的就是手动加上\
进行断行,就像这样:
iris.query(
"""
sepal_length > 5 & \
petal_length <= 1.5
"""
)
否则会导致报错,因为这段查询表达式的字符串中多出了不恰当的换行符,从而导致表达式不能够被正确解析。
同时需要注意的是,如果你的数据字段命存在一些空格或者下划线之类的特殊符号,那么可能也会同样导致报错。在 R 语言中你可以通过加上反引号「``」(键盘上 Esc 键下面的那个符号)来避免,就像这样:
library(tidyverse)
df <- data.frame(
A = seq(0,10,2),
B = seq(1,11,2)
)
df %>%
rename(`B B` = "B") %>%
filter(`B B` > 5)
当然,在 Python 中你也可以同样如此,Pandas 对此也已经进行了支持:
df = pd.DataFrame(np.arange(12).reshape(6,2), columns=["A", "B B"])
df.query("B B == 3") # error
df.query("`B B` == 3") # right
用法二:结合 DataFrame 之外的其他对象进行操作
query
和eval
方法还能有效将数据同非 DataFrame 之外的变量进行结合并操作,只需要在表达式字符串中加入@
符号,就像这样:
sepal_len, petal_len = 5, 1.5
iris.query(
"""
sepal_length > @sepal_len & \
petal_length <= @petal_len
"""
)
当条件过长时,对条件进行适当分解之后通过这样的用法,也能提高代码的可读性以及简洁程度。
除此之外,你也可以用来创建新字段、对字段作运算等(限于 eval
),以下例子源自于 Pandas 官方文档,详细的示例可以参考Pandas 官方的这部分内容。
你甚至也可以直接在表达式字符串中写入代码语句,但是我不太建议你这么操作,即便是可行的。因为一旦表达式在执行时出现错误,Debug 时很有可能无法定位到当中代码语句的错误。
other = iris.copy()
iris.query(
"""
species in @other.query("species=='setosa'")['species']
"""
)
尾巴
query
和 eval
方法应该是每个适用 Pandas 应该掌握的一种技巧,当进行多条件索引筛选或查询时,裹脚布般的方括号及当中的条件括号简直就是噩梦般的存在。
这两个方法都是基于 numexpr
这一个库实现,该库由 C/C++ 编写,在性能上就不用多说;同时,在官方文档中也可以看到,处理大数据集时使用 query
或 eval
方法是多么高效:
尽管官方文档中有说明,当数据中的记录小于 10000 行时,性能表现上 numexpr
略逊于 Python 原生的解析器;但是从图可以看到,在小数据集中使用二者的差别连一秒钟都不到,因此也不必有所顾虑。
虽然 query
和 eval
默认使用 numexpr
作为解析引擎,但在源码中可以发现,在你使用这两个方法时,都会预先检查你是否安装了 numexpr
库,如果不存在则会调用 Python 原生的解析器,这一点在文档中也有所说明。
如果不需要中间变量、步骤不需要分解且保证最后返回的就是 DataFrame 类型,那么就愉快地使用链式调用方法来完成你的数据流程吧!
作者:100gle,练习时长不到两年的非正经文科生一枚,喜欢敲代码、写写文章、捣鼓捣鼓各种新事物;现从事有关大数据分析与挖掘的相关工作。
赞 赏 作 者
Python中文社区作为一个去中心化的全球技术社区,以成为全球20万Python中文开发者的精神部落为愿景,目前覆盖各大主流媒体和协作平台,与阿里、腾讯、百度、微软、亚马逊、开源中国、CSDN等业界知名公司和技术社区建立了广泛的联系,拥有来自十多个国家和地区数万名登记会员,会员来自以工信部、清华大学、北京大学、北京邮电大学、中国人民银行、中科院、中金、华为、BAT、谷歌、微软等为代表的政府机关、科研单位、金融机构以及海内外知名公司,全平台近20万开发者关注。
▼点击成为社区会员 喜欢就点个在看吧