我正在使用Pandas数据框,并希望根据现有列创建一个新列。我还没有看到之间的速度差的一个很好的讨论df.apply()和np.vectorize(),所以我想我会问这里。
df.apply()
np.vectorize()
熊猫apply()功能很慢。根据我的测量(在某些实验中显示如下),至少在我的2016 MacBook Pro上,使用np.vectorize()速度比使用DataFrame函数快25倍(或更多)apply()。 这是预期的结果吗?为什么?
apply()
例如,假设我具有带N行的以下数据框:
N
N = 10 A_list = np.random.randint(1, 100, N) B_list = np.random.randint(1, 100, N) df = pd.DataFrame({'A': A_list, 'B': B_list}) df.head() # A B # 0 78 50 # 1 23 91 # 2 55 62 # 3 82 64 # 4 99 80
进一步假设我想根据这两列A和创建一个新列B。在下面的示例中,我将使用一个简单的函数divide()。要应用此功能,我可以使用df.apply()或np.vectorize():
A
B
divide()
def divide(a, b): if b == 0: return 0.0 return float(a)/b df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1) df['result2'] = np.vectorize(divide)(df['A'], df['B']) df.head() # A B result result2 # 0 78 50 1.560000 1.560000 # 1 23 91 0.252747 0.252747 # 2 55 62 0.887097 0.887097 # 3 82 64 1.281250 1.281250 # 4 99 80 1.237500 1.237500
如果我N将现实世界的大小增加到一百万或更多,那么我发现np.vectorize()它快25倍甚至更多df.apply()。
以下是一些完整的基准测试代码:
import pandas as pd import numpy as np import time def divide(a, b): if b == 0: return 0.0 return float(a)/b for N in [1000, 10000, 100000, 1000000, 10000000]: print '' A_list = np.random.randint(1, 100, N) B_list = np.random.randint(1, 100, N) df = pd.DataFrame({'A': A_list, 'B': B_list}) start_epoch_sec = int(time.time()) df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1) end_epoch_sec = int(time.time()) result_apply = end_epoch_sec - start_epoch_sec start_epoch_sec = int(time.time()) df['result2'] = np.vectorize(divide)(df['A'], df['B']) end_epoch_sec = int(time.time()) result_vectorize = end_epoch_sec - start_epoch_sec print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \ (N, result_apply, result_vectorize) # Make sure results from df.apply and np.vectorize match. assert(df['result'].equals(df['result2']))
结果如下所示:
N=1000, df.apply: 0 sec, np.vectorize: 0 sec N=10000, df.apply: 1 sec, np.vectorize: 0 sec N=100000, df.apply: 2 sec, np.vectorize: 0 sec N=1000000, df.apply: 24 sec, np.vectorize: 1 sec N=10000000, df.apply: 262 sec, np.vectorize: 4 sec
如果np.vectorize()总的来说总是比快df.apply(),那么为什么np.vectorize()不多提?我只看过与相关的帖子df.apply(),
首先 ,我要说的是Pandas和NumPy数组的功能是从对数字数组的高性能 矢量化 计算中获得的。1向量化计算的全部目的是通过将计算移至高度优化的C代码并利用连续的内存块来避免Python级循环。2
现在我们来看一些时间。以下是 所有 的Python级环,其任一产生pd.Series,np.ndarray或list包含相同值的对象。为了分配给数据框内的序列,结果是可比较的。
pd.Series
np.ndarray
list
# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0 np.random.seed(0) N = 10**5 %timeit list(map(divide, df['A'], df['B'])) # 43.9 ms %timeit np.vectorize(divide)(df['A'], df['B']) # 48.1 ms %timeit [divide(a, b) for a, b in zip(df['A'], df['B'])] # 49.4 ms %timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)] # 112 ms %timeit df.apply(lambda row: divide(*row), axis=1, raw=True) # 760 ms %timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1) # 4.83 s %timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()] # 11.6 s
一些要点:
tuple
np.vectorize
zip
map
pd.DataFrame.itertuples
raw=True
pd.DataFrame.apply
要 确切地 查看Pandas传递的对象,可以对函数进行微不足道的修改:
def foo(row): print(type(row)) assert False # because you only need to see this once df.apply(lambda row: foo(row), axis=1)
输出:<class 'pandas.core.series.Series'>。相对于NumPy数组,创建,传递和查询Pandas系列对象会带来大量开销。这不足为奇:Pandas系列包含相当数量的脚手架,用于存放索引,值,属性等。
<class 'pandas.core.series.Series'>
再次进行相同的练习raw=True,您会看到<class 'numpy.ndarray'>。所有这些都在文档中进行了描述,但是看到它更具说服力。
<class 'numpy.ndarray'>
的文档np.vectorize有以下注意事项:
pyfunc除了使用numpy的广播规则外,矢量化函数像python map函数一样对输入数组的连续元组求值。
pyfunc
这里的“广播规则”是无关紧要的,因为输入数组具有相同的尺寸。与之并行map是有启发性的,因为上述map版本具有几乎相同的性能。该源代码显示发生的事情:np.vectorize你的输入函数转换成通用的功能通过(“ufunc”) np.frompyfunc。有些优化,例如缓存,可以带来一些性能改进。
np.frompyfunc
总之,np.vectorize做一个Python级环路什么 应该 做,但pd.DataFrame.apply增加了一个矮胖的开销。您不会看到任何JIT编译numba(请参见下文)。这只是一种方便。
numba
为什么在任何地方都没有提到上述差异?因为真正矢量化计算的性能使它们无关紧要:
%timeit np.where(df['B'] == 0, 0, df['A'] / df['B']) # 1.17 ms %timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0) # 1.96 ms
是的,这比上述循环式解决方案中最快的速度快40倍。这些都可以接受。我认为,第一个是简洁,可读和高效的。numba如果性能至关重要,这只是瓶颈,请仅查看其他方法,例如下面的方法。
numba.njit
当循环 被 认为可行时,通常可以通过numba底层的NumPy数组对其进行优化,以尽可能多地移至C。
实际上,将numba性能提高到了 微秒 。没有一些繁琐的工作,将很难获得比这更高的效率。
from numba import njit @njit def divide(a, b): res = np.empty(a.shape) for i in range(len(a)): if b[i] != 0: res[i] = a[i] / b[i] else: res[i] = 0 return res %timeit divide(df['A'].values, df['B'].values) # 717 µs
使用@njit(parallel=True)可以为更大的阵列提供进一步的提升。
@njit(parallel=True)
1种数字类型包括:int,float,datetime,bool,category。它们 不包含 objectdtype,可以保存在连续的内存块中。
int
float
datetime
bool
category
object
2 NumPy操作相对于Python高效的原因至少有两个: