原文:http://pandas.pydata.org/pandas-docs/stable/enhancingperf.html
校对:(虚位以待)
对于许多使用情况下,用纯python和numpy编写pandas就足够了。然而,在一些计算繁重的应用中,可以通过将工作转换到cython来实现可观的加速。
本教程假设您已在python中尽可能重构,例如尝试删除for循环并使用numpy向量化,它总是值得在python首先优化。
本教程将介绍一个“典型”的细化慢计算过程。我们使用cython文档中的示例,但是在pandas的上下文中。我们最终的cythonized解决方案比纯python大约快100倍。
我们有一个DataFrame,我们要对其应用一个行的方式。
In [1]: df = pd.DataFrame({'a': np.random.randn(1000),
...: 'b': np.random.randn(1000),
...: 'N': np.random.randint(100, 1000, (1000)),
...: 'x': 'x'})
...:
In [2]: df
Out[2]:
N a b x
0 585 0.469112 -0.218470 x
1 841 -0.282863 -0.061645 x
2 251 -1.509059 -0.723780 x
3 972 -1.135632 0.551225 x
4 181 1.212112 -0.497767 x
5 458 -0.173215 0.837519 x
6 159 0.119209 1.103245 x
.. ... ... ... ..
993 190 0.131892 0.290162 x
994 931 0.342097 0.215341 x
995 374 -1.512743 0.874737 x
996 246 0.933753 1.120790 x
997 157 -0.308013 0.198768 x
998 977 -0.079915 1.757555 x
999 770 -1.010589 -1.115680 x
[1000 rows x 4 columns]
这里是纯python中的函数:
In [3]: def f(x):
...: return x * (x - 1)
...:
In [4]: def integrate_f(a, b, N):
...: s = 0
...: dx = (b - a) / N
...: for i in range(N):
...: s += f(a + i * dx)
...: return s * dx
...:
我们通过使用apply
(逐行)来实现我们的结果:
In [7]: %timeit df.apply(lambda x: integrate_f(x['a'], x['b'], x['N']), axis=1)
10 loops, best of 3: 174 ms per loop
但显然这对我们来说不够快。让我们来看看,使用prun ipython magic function查看在此操作期间花费的时间(限于最耗时的四个调用):
In [5]: %prun -l 4 df.apply(lambda x: integrate_f(x['a'], x['b'], x['N']), axis=1)
671915 function calls (666906 primitive calls) in 0.379 seconds
Ordered by: internal time
List reduced from 128 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.193 0.000 0.290 0.000 <ipython-input-4-91e33489f136>:1(integrate_f)
552423 0.089 0.000 0.089 0.000 <ipython-input-3-bc41a25943f6>:1(f)
3000 0.011 0.000 0.060 0.000 base.py:2146(get_value)
1000 0.008 0.000 0.008 0.000 {range}
到目前为止,大部分时间是花费在integrate_f
或f
内,因此我们将集中力量对这两个函数进行cythonizing。
注意
在python 2中,用其生成器对(xrange
)替换range
将意味着range
线将消失。在python 3 range
已经是一个生成器。
First we’re going to need to import the cython magic function to ipython (for cython versions < 0.21 you can use %load_ext cythonmagic
):
In [6]: %load_ext Cython
现在,让我们简单地将我们的函数复制到cython as(后缀在这里区分功能版本):
In [7]: %%cython
...: def f_plain(x):
...: return x * (x - 1)
...: def integrate_f_plain(a, b, N):
...: s = 0
...: dx = (b - a) / N
...: for i in range(N):
...: s += f_plain(a + i * dx)
...: return s * dx
...:
注意
如果你无法将上面的内容粘贴到你的ipython中,你可能需要使用出血边缘的ipython来粘贴,以适应细胞魔法。
In [4]: %timeit df.apply(lambda x: integrate_f_plain(x['a'], x['b'], x['N']), axis=1)
10 loops, best of 3: 85.5 ms per loop
这已经刮了三分之一,不是太糟糕了一个简单的复制和粘贴。
我们通过提供类型信息获得另一个巨大的改进:
In [8]: %%cython
...: cdef double f_typed(double x) except? -2:
...: return x * (x - 1)
...: cpdef double integrate_f_typed(double a, double b, int N):
...: cdef int i
...: cdef double s, dx
...: s = 0
...: dx = (b - a) / N
...: for i in range(N):
...: s += f_typed(a + i * dx)
...: return s * dx
...:
In [4]: %timeit df.apply(lambda x: integrate_f_typed(x['a'], x['b'], x['N']), axis=1)
10 loops, best of 3: 20.3 ms per loop
现在,我们在说话!它现在比原来的python实现快十倍,我们没有真的修改代码。让我们再看看什么是吃饭时间:
In [9]: %prun -l 4 df.apply(lambda x: integrate_f_typed(x['a'], x['b'], x['N']), axis=1)
118490 function calls (113481 primitive calls) in 0.093 seconds
Ordered by: internal time
List reduced from 124 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
3000 0.011 0.000 0.064 0.000 base.py:2146(get_value)
3000 0.006 0.000 0.072 0.000 series.py:600(__getitem__)
3000 0.005 0.000 0.014 0.000 base.py:1131(_convert_scalar_indexer)
9024 0.005 0.000 0.012 0.000 {getattr}
这是电话系列...很多!它从每一行创建一个系列,并从索引和系列(每行三次)获取。函数调用在Python中很昂贵,所以也许我们可以通过应用部分的cythonizing最小化。
注意
我们现在将ndarrays传递给cython函数,幸运的是cython和numpy非常好。
In [10]: %%cython
....: cimport numpy as np
....: import numpy as np
....: cdef double f_typed(double x) except? -2:
....: return x * (x - 1)
....: cpdef double integrate_f_typed(double a, double b, int N):
....: cdef int i
....: cdef double s, dx
....: s = 0
....: dx = (b - a) / N
....: for i in range(N):
....: s += f_typed(a + i * dx)
....: return s * dx
....: cpdef np.ndarray[double] apply_integrate_f(np.ndarray col_a, np.ndarray col_b, np.ndarray col_N):
....: assert (col_a.dtype == np.float and col_b.dtype == np.float and col_N.dtype == np.int)
....: cdef Py_ssize_t i, n = len(col_N)
....: assert (len(col_a) == len(col_b) == n)
....: cdef np.ndarray[double] res = np.empty(n)
....: for i in range(len(col_a)):
....: res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
....: return res
....:
实现很简单,它创建一个零和循环的行数组,应用我们的integrate_f_typed
,并将其放在零的数组。
警告
In 0.13.0 since Series
has internaly been refactored to no longer sub-class ndarray
but instead subclass NDFrame
, you can not pass a Series
directly as a ndarray
typed parameter to a cython function. 而应使用系列的.values
属性传递实际的ndarray
。
0.13.0之前
apply_integrate_f(df['a'], df['b'], df['N'])
使用.values
来获取底层的ndarray
apply_integrate_f(df['a'].values, df['b'].values, df['N'].values)
注意
Loops like this would be extremely slow in python, but in Cython looping over numpy arrays is fast.
In [4]: %timeit apply_integrate_f(df['a'].values, df['b'].values, df['N'].values)
1000 loops, best of 3: 1.25 ms per loop
我们又有了一个很大的改进。让我们再次检查时间花费在哪里:
In [11]: %prun -l 4 apply_integrate_f(df['a'].values, df['b'].values, df['N'].values)
208 function calls in 0.002 seconds
Ordered by: internal time
List reduced from 53 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.002 0.002 0.002 0.002 {_cython_magic_40485b2751cb6bc085f3a7be0856f402.apply_integrate_f}
3 0.000 0.000 0.000 0.000 internals.py:4031(__init__)
9 0.000 0.000 0.000 0.000 generic.py:2746(__setattr__)
3 0.000 0.000 0.000 0.000 internals.py:3565(iget)
正如人们所期望的,大多数时间现在花费在apply_integrate_f
中,因此如果我们想提高效率,我们必须继续集中精力在这里。
仍有改善的希望。这里有一个使用一些更先进的cython技术的例子:
In [12]: %%cython
....: cimport cython
....: cimport numpy as np
....: import numpy as np
....: cdef double f_typed(double x) except? -2:
....: return x * (x - 1)
....: cpdef double integrate_f_typed(double a, double b, int N):
....: cdef int i
....: cdef double s, dx
....: s = 0
....: dx = (b - a) / N
....: for i in range(N):
....: s += f_typed(a + i * dx)
....: return s * dx
....: @cython.boundscheck(False)
....: @cython.wraparound(False)
....: cpdef np.ndarray[double] apply_integrate_f_wrap(np.ndarray[double] col_a, np.ndarray[double] col_b, np.ndarray[int] col_N):
....: cdef int i, n = len(col_N)
....: assert len(col_a) == len(col_b) == n
....: cdef np.ndarray[double] res = np.empty(n)
....: for i in range(n):
....: res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
....: return res
....:
In [4]: %timeit apply_integrate_f_wrap(df['a'].values, df['b'].values, df['N'].values)
1000 loops, best of 3: 987 us per loop
更快,但需要注意的是,我们的cython代码中的一个错误(例如,一个一个一个的错误)可能会导致segfault,因为内存访问未检查。
最近一种替代静态编译cython代码的方法是使用动态jit编译器,numba
。
Numba使您能够通过使用Python直接编写的高性能函数加快应用程序的速度。有了几个注释,面向数组和数学重的Python代码可以及时编译为本机机器指令,性能类似于C,C ++和Fortran,无需切换语言或Python解释器。
Numba通过在导入时间,运行时或静态(使用包含的pycc工具)使用LLVM编译器基础结构生成优化的机器代码。Numba支持编译Python以在CPU或GPU硬件上运行,并且旨在与Python科学软件堆栈集成。
注意
您需要安装numba
。This is easy with conda
, by using: conda install numba
, see installing using miniconda.
注意
从numba
版本0.20起,pandas对象不能直接传递到numba编译的函数。相反,必须将pandas
对象下面的numpy
数组传递到numba编译函数,如下所示。
使用numba
来及时编译代码。我们只需从上面的普通python代码,并用@jit
装饰器注释。
import numba
@numba.jit
def f_plain(x):
return x * (x - 1)
@numba.jit
def integrate_f_numba(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f_plain(a + i * dx)
return s * dx
@numba.jit
def apply_integrate_f_numba(col_a, col_b, col_N):
n = len(col_N)
result = np.empty(n, dtype='float64')
assert len(col_a) == len(col_b) == n
for i in range(n):
result[i] = integrate_f_numba(col_a[i], col_b[i], col_N[i])
return result
def compute_numba(df):
result = apply_integrate_f_numba(df['a'].values, df['b'].values, df['N'].values)
return pd.Series(result, index=df.index, name='result')
注意,我们直接将numpy
数组传递给numba函数。compute_numba
只是一个包装器,通过传递/返回pandas对象来提供更好的界面。
In [4]: %timeit compute_numba(df)
1000 loops, best of 3: 798 us per loop
numba
也可用于编写不需要用户明确循环向量观察的向量化函数;矢量化函数将自动应用于每行。考虑下面的玩具示例,将每个观察值加倍:
import numba
def double_every_value_nonumba(x):
return x*2
@numba.vectorize
def double_every_value_withnumba(x):
return x*2
# Custom function without numba
In [5]: %timeit df['col1_doubled'] = df.a.apply(double_every_value_nonumba)
1000 loops, best of 3: 797 us per loop
# Standard implementation (faster than a custom function)
In [6]: %timeit df['col1_doubled'] = df.a*2
1000 loops, best of 3: 233 us per loop
# Custom function with numba
In [7]: %timeit df['col1_doubled'] = double_every_value_withnumba(df.a.values)
1000 loops, best of 3: 145 us per loop
注意
numba
将对任何函数执行,但只能加速某些类的函数。
numba
最适合加速将数值函数应用于numpy数组的函数。当传递一个只使用操作的函数时,它知道如何加速,它将在nopython
模式下执行。
如果numba
传递的函数包含不知道如何使用的东西 - 当前包含集合,列表,字典或字符串函数的类别,它将还原为对象 模式
。在对象 模式
中,numba将执行,但您的代码不会显着加速。如果您希望numba
在无法以加快代码的方式编译函数时抛出错误,请将numba参数传递给nopython=True
(例如@numba.jit(nopython=True)
)。有关解决numba
模式问题的详情,请参阅numba疑难解答页。
请在numba docs中了解详情。
eval()
(Experimental)版本0.13中的新功能。
顶层函数pandas.eval()
实现Series
和DataFrame
对象的表达式求值。
注意
要受益于使用eval()
,您需要安装numexpr
。有关详细信息,请参阅recommended dependencies section。
使用eval()
来表达式求值而不是纯Python是两个方面:1)大的DataFrame
对象被更有效地计算,2)大的算术和布尔表达式由底层引擎一次性计算(默认情况下,numexpr
用于计算)。
注意
对于简单表达式或涉及小型DataFrames的表达式,不应使用eval()
。事实上,对于较小的表达式/对象,eval()
比纯粹的Python要慢许多个数量级。一个好的经验法则是,当您拥有超过10,000行的DataFrame
时,只使用eval()
。
eval()
支持引擎支持的所有算术表达式,除了一些仅在pandas中可用的扩展。
注意
帧越大,表达式越大,使用eval()
可以看到的加速越快。
这些操作由pandas.eval()
支持:
<<
) and right shift (>>
) operators, e.g., df + 2 * pi / s ** 4 % 42 - the_golden_ratio
2 df df2
df < df2 and df3 < df4 or not df_bool
list
and tuple
literals, e.g., [1, 2]
or (1, 2)
df.a
df[0]
pd.eval('df')
(这不是很有用)此Python语法为不允许:
eval()
Examplespandas.eval()
适用于包含大型数组的表达式。
首先,让我们创建一些大小合适的数组:
In [13]: nrows, ncols = 20000, 100
In [14]: df1, df2, df3, df4 = [pd.DataFrame(np.random.randn(nrows, ncols)) for _ in range(4)]
现在让我们比较使用纯粹的Python和eval()
将它们添加在一起:
In [15]: %timeit df1 + df2 + df3 + df4
10 loops, best of 3: 24.6 ms per loop
In [16]: %timeit pd.eval('df1 + df2 + df3 + df4')
100 loops, best of 3: 8.36 ms per loop
现在让我们做同样的事情,但比较:
In [17]: %timeit (df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)
10 loops, best of 3: 30.9 ms per loop
In [18]: %timeit pd.eval('(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)')
100 loops, best of 3: 16.4 ms per loop
eval()
也可以使用未对齐的pandas对象:
In [19]: s = pd.Series(np.random.randn(50))
In [20]: %timeit df1 + df2 + df3 + df4 + s
10 loops, best of 3: 38.4 ms per loop
In [21]: %timeit pd.eval('df1 + df2 + df3 + df4 + s')
100 loops, best of 3: 9.31 ms per loop
注意
操作如
1 and 2 # would parse to 1 & 2, but should evaluate to 2 3 or 4 # would parse to 3 | 4, but should evaluate to 3 ~1 # this is okay, but slower when using eval
应该在Python中执行。如果尝试使用非类型为bool
或np.bool_
的标量操作数执行任何布尔/逐位运算,则会引发异常。同样,你应该在纯Python中执行这些类型的操作。
DataFrame.eval
method (Experimental)版本0.13中的新功能。
除了顶层pandas.eval()
函数,您还可以评估DataFrame
的“上下文”中的表达式。
In [22]: df = pd.DataFrame(np.random.randn(5, 2), columns=['a', 'b'])
In [23]: df.eval('a + b')
Out[23]:
0 -0.246747
1 0.867786
2 -1.626063
3 -1.134978
4 -1.027798
dtype: float64
作为有效pandas.eval()
表达式的任何表达式也是有效的DataFrame.eval()
表达式,还有一个好处,到您想要评估的列的DataFrame
的名称。
此外,您可以在表达式中执行列的分配。这允许公式计算。分配目标可以是新的列名称或现有的列名称,它必须是有效的Python标识符。
版本0.18.0中的新功能。
inplace
关键字确定此分配是否对原始DataFrame
执行,或返回带有新列的副本。
警告
对于向后兼容性,如果未指定,inplace
默认为True
。这将在未来版本的pandas中改变 - 如果你的代码依赖于一个内部赋值,你应该更新来显式设置inplace=True
In [24]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [25]: df.eval('c = a + b', inplace=True)
In [26]: df.eval('d = a + b + c', inplace=True)
In [27]: df.eval('a = 1', inplace=True)
In [28]: df
Out[28]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
当inplace
设置为False
时,将返回带有新列或已修改列的DataFrame
的副本,原始帧不变。
In [29]: df
Out[29]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
In [30]: df.eval('e = a - c', inplace=False)
Out[30]:
a b c d e
0 1 5 5 10 -4
1 1 6 7 14 -6
2 1 7 9 18 -8
3 1 8 11 22 -10
4 1 9 13 26 -12
In [31]: df
Out[31]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
版本0.18.0中的新功能。
为了方便,可以通过使用多行字符串来执行多个分配。
In [32]: df.eval("""
....: c = a + b
....: d = a + b + c
....: a = 1""", inplace=False)
....:
Out[32]:
a b c d
0 1 5 6 12
1 1 6 7 14
2 1 7 8 16
3 1 8 9 18
4 1 9 10 20
在标准Python中的等价将是
In [33]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [34]: df['c'] = df.a + df.b
In [35]: df['d'] = df.a + df.b + df.c
In [36]: df['a'] = 1
In [37]: df
Out[37]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
版本0.18.0中的新功能。
query
方法获得了inplace
关键字,该关键字确定查询是否修改原始帧。
In [38]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [39]: df.query('a > 2')
Out[39]:
a b
3 3 8
4 4 9
In [40]: df.query('a > 2', inplace=True)
In [41]: df
Out[41]:
a b
3 3 8
4 4 9
警告
Unlike with eval
, the default value for inplace
for query
is False
. 这与以前版本的熊猫一致。
在pandas版本0.14中,本地变量API已更改。在pandas 0.13.x中,你可以像在标准Python中一样引用局部变量。例如,
df = pd.DataFrame(np.random.randn(5, 2), columns=['a', 'b'])
newcol = np.random.randn(len(df))
df.eval('b + newcol')
UndefinedVariableError: name 'newcol' is not defined
从生成的异常中可以看出,不再允许使用此语法。您必须通过将@
字符放在名称前,显式引用要在表达式中使用的任何局部变量。例如,
In [42]: df = pd.DataFrame(np.random.randn(5, 2), columns=list('ab'))
In [43]: newcol = np.random.randn(len(df))
In [44]: df.eval('b + @newcol')
Out[44]:
0 -0.173926
1 2.493083
2 -0.881831
3 -0.691045
4 1.334703
dtype: float64
In [45]: df.query('b < @newcol')
Out[45]:
a b
0 0.863987 -0.115998
2 -2.621419 -1.297879
如果你不用局部变量前缀@
,pandas将引发一个异常告诉你该变量是未定义的。
当使用DataFrame.eval()
和DataFrame.query()
时,这允许您有一个局部变量和一个DataFrame
表达式中的名称。
In [46]: a = np.random.randn()
In [47]: df.query('@a < a')
Out[47]:
a b
0 0.863987 -0.115998
In [48]: df.loc[a < df.a] # same as the previous expression
Out[48]:
a b
0 0.863987 -0.115998
With pandas.eval()
you cannot use the @
prefix at all, because it isn’t defined in that context. 如果您尝试在对pandas.eval()
的顶级调用中尝试使用@
,则pandas
会让您知道这一点。例如,
In [49]: a, b = 1, 2
In [50]: pd.eval('@a + b')
File "<string>", line unknown
SyntaxError: The '@' prefix is not allowed in top-level eval calls,
please refer to your variables by name without the '@' prefix
在这种情况下,你应该像在标准Python中那样引用变量。
In [51]: pd.eval('a + b')
Out[51]: 3
pandas.eval()
Parsers有两个不同的解析器和两个不同的引擎可以用作后端。
默认的'pandas'
解析器允许更直观的语法来表达类查询操作(比较,连接和析取)。特别地,使&
和|
运算符的优先级等于相应的布尔运算and
和or
。
例如,上述连接可以不用括号写。或者,您可以使用'python'
解析器强制执行严格的Python语义。
In [52]: expr = '(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)'
In [53]: x = pd.eval(expr, parser='python')
In [54]: expr_no_parens = 'df1 > 0 & df2 > 0 & df3 > 0 & df4 > 0'
In [55]: y = pd.eval(expr_no_parens, parser='pandas')
In [56]: np.all(x == y)
Out[56]: True
相同的表达式可以与字and
一起被“anded”:
In [57]: expr = '(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)'
In [58]: x = pd.eval(expr, parser='python')
In [59]: expr_with_ands = 'df1 > 0 and df2 > 0 and df3 > 0 and df4 > 0'
In [60]: y = pd.eval(expr_with_ands, parser='pandas')
In [61]: np.all(x == y)
Out[61]: True
这里的and
和or
运算符具有与在vanilla Python中相同的优先级。
pandas.eval()
Backends还有一个选项让eval()
操作与纯粹的Python相同。
注意
使用'python'
引擎通常不有用,除了测试其他评估引擎。您将使用eval()
和engine='python'
实现no性能优势,实际上可能会造成性能损失。
你可以通过使用pandas.eval()
和'python'
引擎来看到这一点。它比在Python中评估同一个表达式慢一点(不是太多)
In [62]: %timeit df1 + df2 + df3 + df4
10 loops, best of 3: 24.2 ms per loop
In [63]: %timeit pd.eval('df1 + df2 + df3 + df4', engine='python')
10 loops, best of 3: 25.2 ms per loop
pandas.eval()
Performanceeval()
旨在加速某些类型的操作。特别地,涉及具有大的DataFrame
/ Series
对象的复杂表达式的那些操作应当看到显着的性能益处。这里是一个图表,显示pandas.eval()
的运行时间作为计算中涉及的框架大小的函数。这两条线是两个不同的引擎。
注意
使用纯Python,较小对象(大约15k-20k行)的操作速度更快:
此图使用DataFrame
创建,每个列包含使用numpy.random.randn()
生成的浮点值。
必须在Python空间中评估导致对象dtype或涉及datetime操作(因为NaT
)的表达式。此行为的主要原因是保持与numpy版本的向后兼容性在numpy
的这些版本中,对ndarray.astype(str)
的调用将截断长度超过60个字符的任何字符串。第二,我们不能将object
数组传递到numexpr
,因此字符串比较必须在Python空间中求值。
结果是,这仅适用于object-dtype的表达式。所以,如果你有一个表达式 - 例如
In [64]: df = pd.DataFrame({'strings': np.repeat(list('cba'), 3),
....: 'nums': np.repeat(range(3), 3)})
....:
In [65]: df
Out[65]:
nums strings
0 0 c
1 0 c
2 0 c
3 1 b
4 1 b
5 1 b
6 2 a
7 2 a
8 2 a
In [66]: df.query('strings == "a" and nums == 1')
Out[66]:
Empty DataFrame
Columns: [nums, strings]
Index: []
比较的数字部分(nums == 1
)将由numexpr
In general, DataFrame.query()
/pandas.eval()
will evaluate the subexpressions that can be evaluated by numexpr
and those that must be evaluated in Python space transparently to the user. 这是通过从其参数和运算符推断表达式的结果类型来完成的。