sta*_*010 44 python arrays performance numpy pandas
我正在使用Pandas数据帧,并希望创建一个新列作为现有列的函数.我还没有看到之间的速度差的一个很好的讨论df.apply()和np.vectorize(),所以我想我会问这里.
熊猫apply()功能很慢.根据我的测量结果(如下面的一些实验所示),使用np.vectorize()比使用DataFrame功能快25倍(或更多)apply(),至少在我的2016 MacBook Pro上使用.这是预期的结果,为什么?
例如,假设我有以下带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
Run Code Online (Sandbox Code Playgroud)
进一步假设我想创建一个新列作为两列的函数A和B.在下面的例子中,我将使用一个简单的函数divide().要应用该功能,我可以使用df.apply()或np.vectorize():
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
Run Code Online (Sandbox Code Playgroud)
如果我增加到N现实世界的大小,如100万或更多,那么我发现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']))
Run Code Online (Sandbox Code Playgroud)
结果如下所示:
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
Run Code Online (Sandbox Code Playgroud)
如果np.vectorize()一般总是快于df.apply(),那么为什么np.vectorize()没有提到更多?我只看到与StackOverflow相关的帖子df.apply(),例如:
jpp*_*jpp 57
我首先要说的是Pandas和NumPy数组的强大功能来源于数字数组的高性能矢量化计算.1矢量化计算的全部要点是通过将计算移动到高度优化的C代码并利用连续的内存块来避免Python级循环.2
现在我们可以看看一些时间.下面是生成它们的所有 Python级循环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
Run Code Online (Sandbox Code Playgroud)
一些要点:
tuple基的方法(第一4)是一个因素比更有效的pd.Series基于方法(最后3).np.vectorize列表理解+ zip和map方法,即前3个,都具有大致相同的性能.这是因为他们使用tuple 并绕过了一些Pandas开销pd.DataFrame.itertuples.raw=True时速度显着提高pd.DataFrame.apply.此选项将NumPy数组提供给自定义函数而不是pd.Series对象.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)
Run Code Online (Sandbox Code Playgroud)
输出:<class 'pandas.core.series.Series'>.创建,传递和查询Pandas系列对象相对于NumPy数组会带来很大的开销.这应该不足为奇:熊猫系列包括一个相当数量的脚手架来保存索引,值,属性等.
再次进行相同的练习raw=True,你会看到<class 'numpy.ndarray'>.所有这些都在文档中描述,但看到它更有说服力.
np.vectorize:假矢量化文档np.vectorize有以下注释:
向量化函数评估
pyfunc输入数组的连续元组,如python map函数,除了它使用numpy的广播规则.
"广播规则"在这里是无关紧要的,因为输入数组具有相同的尺寸.并行map是有益的,因为map上面的版本具有几乎相同的性能.该源代码显示了发生的事情:np.vectorize你的输入函数转换成通用的功能通过("ufunc") np.frompyfunc.有一些优化,例如缓存,可以带来一些性能提升.
简而言之,np.vectorizePython级循环应该做什么,但pd.DataFrame.apply增加了一个庞大的开销.您没有看到JIT编译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
Run Code Online (Sandbox Code Playgroud)
是的,这比上述循环解决方案中最快的速度快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
Run Code Online (Sandbox Code Playgroud)
使用@njit(parallel=True)可以为更大的阵列提供进一步的推动.
1种数字类型包括:int,float,datetime,bool,category.它们排除了 object dtype,可以保存在连续的内存块中.
2 与Python相比,NumPy操作的效率至少有两个原因:
您的函数越复杂(即,numpy移至其内部的可能性越小),您就越会发现性能不会有太大差异。例如:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))
def parse_name(name):
if name.lower().startswith('a'):
return 'A'
elif name.lower().startswith('e'):
return 'E'
elif name.lower().startswith('i'):
return 'I'
elif name.lower().startswith('o'):
return 'O'
elif name.lower().startswith('u'):
return 'U'
return name
parse_name_vec = np.vectorize(parse_name)
Run Code Online (Sandbox Code Playgroud)
做一些计时:
使用应用
%timeit name_series.apply(parse_name)
Run Code Online (Sandbox Code Playgroud)
结果:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)
使用 np.vectorize
%timeit parse_name_vec(name_series)
Run Code Online (Sandbox Code Playgroud)
结果:
77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)
NumPy的试图扭转蟒蛇功能为numpy的ufunc对象,当你调用np.vectorize。它是如何做到这一点的,我实际上并不知道 - 您必须比我愿意 ATM 更深入地了解 numpy 的内部结构。也就是说,与这里的基于字符串的函数相比,它似乎在简单的数字函数上做得更好。
将大小设置为 1,000,000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
Run Code Online (Sandbox Code Playgroud)
apply
%timeit name_series.apply(parse_name)
Run Code Online (Sandbox Code Playgroud)
结果:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Run Code Online (Sandbox Code Playgroud)
np.vectorize
%timeit parse_name_vec(name_series)
Run Code Online (Sandbox Code Playgroud)
结果:
794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Run Code Online (Sandbox Code Playgroud)
更好的(矢量化)方式np.select:
cases = [
name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()
Run Code Online (Sandbox Code Playgroud)
时间:
%timeit np.select(cases, replacements, default=name_series)
Run Code Online (Sandbox Code Playgroud)
结果:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
11505 次 |
| 最近记录: |