前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据科学 IPython 笔记本 7.15 高性能 Pandas

数据科学 IPython 笔记本 7.15 高性能 Pandas

作者头像
ApacheCN_飞龙
发布2022-05-07 13:43:19
6560
发布2022-05-07 13:43:19
举报
文章被收录于专栏:信数据得永生

# 7.15 高性能 Pandas:eval()query()

原文:High-Performance Pandas: eval() and query() 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。

我们在前面的章节中已经看到,PyData 技术栈的力量,建立在 NumPy 和 Pandas 通过直观语法,将基本操作推送到 C 的能力的基础上:例如 NumPy 中的向量化/广播操作,以及 Pandas 的分组类型操作。虽然这些抽象对于许多常见用例是高效且有效的,但它们通常依赖于临时中间对象的创建,这可能产生计算时间和内存使用的开销。

从版本 0.13(2014 年 1 月发布)开始,Pandas 包含一些实验性工具,允许你直接访问速度和 C 一样的操作,而无需昂贵的中间数组分配。这些是eval()query()函数,它依赖于 Numexpr 包。在这个笔记本中,我们将逐步介绍它们的使用方法,并提供一些何时可以考虑使用它们的经验法则。

query()eval()的动机:复合表达式

我们以前见过 NumPy 和 Pandas 支持快速向量化操作;例如,相加两个数组的元素时:

代码语言:javascript
复制
import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(1000000)
y = rng.rand(1000000)
%timeit x + y

# 100 loops, best of 3: 3.39 ms per loop

正如“NumPy 数组的计算:通用函数”中所讨论的,这比通过 Python 循环或推导式执行加法要快得多:

代码语言:javascript
复制
%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))

# 1 loop, best of 3: 266 ms per loop

但是在计算复合表达式时,这种抽象可能变得不那么有效。例如,请考虑以下表达式:

代码语言:javascript
复制
mask = (x > 0.5) & (y < 0.5)

因为 NumPy 会计算每个子表达式,所以大致相当于以下内容:

代码语言:javascript
复制
tmp1 = (x > 0.5)
tmp2 = (y < 0.5)
mask = tmp1 & tmp2

换句话说,每个中间步骤都在内存中明确分配。如果xy数组非常大,这可能会产生大量内存和计算开销。Numexpr 库使你能够逐元素计算这种类型的复合表达式,而无需分配完整的中间数组。Numexpr 文档有更多细节,但暂时可以说,这个库接受字符串,它提供了你想要计算的 NumPy 风格的表达式:

代码语言:javascript
复制
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)

# True

这里的好处是,Numexpr 以不使用完整临时数组的方式计算表达式,因此可以比 NumPy 更有效,特别是对于大型数组。我们将在这里讨论的 Pandas eval()query()工具,在概念上是相似的,并且依赖于 Numexpr 包。

用于高效操作的pandas.eval()

Pandas 中的eval()函数接受字符串表达式,来使用DataFrame高效地计算操作。例如,考虑以下DataFrame

代码语言:javascript
复制
import pandas as pd
nrows, ncols = 100000, 100
rng = np.random.RandomState(42)
df1, df2, df3, df4 = (pd.DataFrame(rng.rand(nrows, ncols))
                      for i in range(4))

要使用典型的 Pandas 方法计算所有四个DataFrame的和,我们可以写出总和:

代码语言:javascript
复制
%timeit df1 + df2 + df3 + df4

# 10 loops, best of 3: 87.1 ms per loop

通过将表达式构造为字符串,可以通过pd.eval计算相同的结果:

代码语言:javascript
复制
%timeit pd.eval('df1 + df2 + df3 + df4')

# 10 loops, best of 3: 42.2 ms per loop

这个表达式的eval()版本速度提高了约 50%(并且使用的内存更少),同时给出了相同的结果:

代码语言:javascript
复制
np.allclose(df1 + df2 + df3 + df4,
            pd.eval('df1 + df2 + df3 + df4'))
            
# True

pd.eval()所支持的操作

从 Pandas v0.16 开始,pd.eval()支持广泛的操作。为了演示这些,我们将使用以下整数DataFrame

代码语言:javascript
复制
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))
                           for i in range(5))
算术运算符

pd.eval()支持所有算术运算符,例如:

代码语言:javascript
复制
result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)

# True
比较运算符

pd.eval()支持所有比较运算符,包括链式表达式:

代码语言:javascript
复制
result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)

# True
按位运算符

pd.eval()支持&|按位运算符:

代码语言:javascript
复制
result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)
result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')
np.allclose(result1, result2)

# True

另外,它支持在布尔表达式中使用字面andor

代码语言:javascript
复制
result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)

# True
对象属性和索引

pd.eval()支持通过obj.attr语法访问对象属性,和通过obj[index]语法进行索引:

代码语言:javascript
复制
result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)

# True
其它运算符

其他操作,如函数调用,条件语句,循环和其他更复杂的结构,目前都没有在pd.eval()中实现。如果你想执行这些更复杂的表达式,可以使用 Numexpr 库本身。

用于逐列运算的DataFrame.eval()

就像 Pandas 有顶级的pd.eval()函数一样,DataFrameeval()方法,它的工作方式类似。eval()方法的好处是列可以通过名称引用。我们将使用这个带标签的数组作为示例:

代码语言:javascript
复制
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
df.head()

A

B

C

0

0.375506

0.406939

0.069938

1

0.069087

0.235615

0.154374

2

0.677945

0.433839

0.652324

3

0.264038

0.808055

0.347197

4

0.589161

0.252418

0.557789

使用上面的pd.eval(),我们可以像这样使用三列来计算表达式:

代码语言:javascript
复制
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
np.allclose(result1, result2)

# True

DataFrame.eval()方法允许使用列来更简洁地求解表达式:

代码语言:javascript
复制
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)

# True

请注意,我们将列名称视为要求解的表达式中的变量,结果是我们希望的结果。

DataFrame.eval()中的赋值

除了刚才讨论的选项之外,DataFrame.eval()还允许赋值给任何列。让我们使用之前的DataFrame,它有列ABC

代码语言:javascript
复制
df.head()

A

B

C

0

0.375506

0.406939

0.069938

1

0.069087

0.235615

0.154374

2

0.677945

0.433839

0.652324

3

0.264038

0.808055

0.347197

4

0.589161

0.252418

0.557789

我们可以使用df.eval()创建一个新列'D'并为其赋一个从其他列计算的值:

代码语言:javascript
复制
df.eval('D = (A + B) / C', inplace=True)
df.head()

A

B

C

D

0

0.375506

0.406939

0.069938

11.187620

1

0.069087

0.235615

0.154374

1.973796

2

0.677945

0.433839

0.652324

1.704344

3

0.264038

0.808055

0.347197

3.087857

4

0.589161

0.252418

0.557789

1.508776

以同样的方式,可以修改任何现有列:

代码语言:javascript
复制
df.eval('D = (A - B) / C', inplace=True)
df.head()

A

B

C

D

0

0.375506

0.406939

0.069938

-0.449425

1

0.069087

0.235615

0.154374

-1.078728

2

0.677945

0.433839

0.652324

0.374209

3

0.264038

0.808055

0.347197

-1.566886

4

0.589161

0.252418

0.557789

0.603708

DataFrame.eval()中的局部变量

DataFrame.eval()方法支持一种额外的语法,可以使用 Python 局部变量。考虑以下:

代码语言:javascript
复制
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)

# True

这里的@字符标记变量名而不是列名,并允许你高效计算涉及两个“名称空间”的表达式:列的名称空间和 Python 对象的名称空间。请注意,这个@字符仅由DataFrame.eval()方法支持,不由pandas.eval()函数支持,因为pandas.eval ()函数只能访问一个(Python)命名空间。

DataFrame.query()方法

DataFrame有另一种基于字符串的求值方法,称为query()方法。考虑以下:

代码语言:javascript
复制
result1 = df[(df.A < 0.5) & (df.B < 0.5)]
result2 = pd.eval('df[(df.A < 0.5) & (df.B < 0.5)]')
np.allclose(result1, result2)

# True

与我们讨论DataFrame.eval()时使用的示例一样,这是一个涉及DataFrame列的表达式。但是,无法使用DataFrame.eval()语法表达它!相反,对于这种类型的过滤操作,你可以使用query()方法:

代码语言:javascript
复制
result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)

# True

除了作为更有效的计算之外,与掩码表达式相比,这更容易阅读和理解。注意query()方法也接受@标志来标记局部变量:

代码语言:javascript
复制
Cmean = df['C'].mean()
result1 = df[(df.A < Cmean) & (df.B < Cmean)]
result2 = df.query('A < @Cmean and B < @Cmean')
np.allclose(result1, result2)

# True

性能:什么时候使用这些函数

在考虑是否使用这些函数时,有两个注意事项:计算时间和内存使用。内存使用是最可预测的方面。 如前所述,涉及 NumPy 数组或 Pandas DataFrame的每个复合表达式,都会产生隐式创建的临时数组:例如,这个:

代码语言:javascript
复制
x = df[(df.A < 0.5) & (df.B < 0.5)]

大致相当于这个:

代码语言:javascript
复制
tmp1 = df.A < 0.5
tmp2 = df.B < 0.5
tmp3 = tmp1 & tmp2
x = df[tmp3]

如果临时DataFrame的大小与可用的系统内存(通常是几千兆字节)相比很大,那么使用eval()query()表达式是个好主意。你可以使用以下方法检查数组的大致大小(以字节为单位):

代码语言:javascript
复制
df.values.nbytes

# 32000

在性能方面,即使你没有超出你的系统内存,eval()也会更快。问题是你的临时DataFrame与系统上的 L1 或 L2 CPU 缓存的大小相比(2016 年通常为几兆字节)如何;如果它们更大,那么eval()可以避免不同内存缓存之间的某些值移动,它们可能很慢。

在实践中,我发现传统方法和eval/query方法之间的计算时间差异,通常不大 - 如果有的话,传统方法对于较小的数组来说更快!eval/query的好处主要在于节省的内存,以及它们提供的有时更清晰的语法。

我们已经涵盖了eval()query()的大部分细节;对于这些的更多信息,你可以参考 Pandas 文档。特别是,可以指定执行这些查询的不同解析器和引擎;详细信息请参阅“提升性能”部分中的讨论。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • query()和eval()的动机:复合表达式
  • 用于高效操作的pandas.eval()
    • pd.eval()所支持的操作
      • 算术运算符
      • 比较运算符
      • 按位运算符
      • 对象属性和索引
      • 其它运算符
  • 用于逐列运算的DataFrame.eval()
    • DataFrame.eval()中的赋值
      • DataFrame.eval()中的局部变量
      • DataFrame.query()方法
      • 性能:什么时候使用这些函数
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档