我正在使用Pandas数据框,并希望根据现有列创建一个新列。对于df.apply()
和np.vectorize()
之间的速度差异,我还没有很好的讨论,所以我想在这里问一下。
熊猫apply()
功能很慢。根据我的测量(在某些实验中显示如下),至少在我的2016 MacBook Pro上,使用np.vectorize()
比使用DataFrame函数apply()
快25倍(或更多)。 这是预期的结果吗?为什么?
例如,假设我有以下带有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()
:
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
增加到现实世界的大小(例如100万或更多),那么我发现np.vectorize()
比df.apply()
快25倍或更多。
下面是一些完整的基准测试代码:
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()
相关的StackOverflow帖子,例如:
pandas create new column based on values from other columns
答案 0 :(得分:26)
首先,我将说起Pandas和NumPy数组的功能源自对数字数组进行高性能的 vectorized 计算。 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
一些要点:
tuple
的方法(前4种)比基于pd.Series
的方法(后3种)效率更高。np.vectorize
,列表理解+ zip
和map
方法(即前3名)的性能大致相同。这是因为他们使用tuple
并且绕过pd.DataFrame.itertuples
的一些熊猫开销。raw=True
与pd.DataFrame.apply
一起使用与不使用pd.Series
相比,可以显着提高速度。此选项将NumPy数组而不是pd.DataFrame.apply
对象提供给自定义函数。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'>
输出:raw=True
。相对于NumPy数组,创建,传递和查询Pandas系列对象会带来大量开销。这不足为奇:Pandas系列包含相当数量的脚手架,用于存放索引,值,属性等。
再次使用<class 'numpy.ndarray'>
做同样的练习,您将看到np.vectorize
。所有这些都在文档中进行了描述,但是看到它更令人信服。
pyfunc
:伪造向量化 np.vectorize
的文档具有以下注释:
向量化函数对{}的连续元组求
map
输入数组类似于python map函数,除了它使用 numpy的广播规则。
此处的“广播规则”无关紧要,因为输入数组的维数相同。与map
的并行具有指导意义,因为上述np.vectorize
版本具有几乎相同的性能。 source code显示正在发生的事情:np.vectorize
通过Universal function将您的输入函数转换为np.frompyfunc
(“ ufunc”)。有一些优化,例如缓存,可以提高性能。
简而言之,pd.DataFrame.apply
做了Python级循环 应该做的事情,但是%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
增加了庞大的开销。使用numba
不会看到JIT编译(请参见下文)。是just a convenience。
为什么在任何地方都没有提到上述差异?因为真正矢量化计算的性能使它们无关紧要:
numba
是的,这比上述循环式解决方案中最快的速度快40倍。这些都可以接受。我认为,第一个是简洁,可读和高效的。仅查看其他方法,例如如果性能至关重要,请参见下面的numba.njit
,这是您的瓶颈之一。
numba
:更高的效率当循环 被认为可行时,通常可以通过numba
使用底层的NumPy数组对其进行优化,以尽可能多地移动到C。
实际上,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)
使用int
可以为更大的阵列提供进一步的提升。
1 数值类型包括:float
,datetime
,bool
,category
,object
。它们排除 {{1}} dtype,并且可以保存在连续的内存块中。
2 NumPy操作相对于Python高效的原因至少有两个:
答案 1 :(得分:3)
您的函数越复杂(即,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)
做一些计时:
使用应用
%timeit name_series.apply(parse_name)
结果:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
使用np.vectorize
%timeit parse_name_vec(name_series)
结果:
77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
当您调用ufunc
时,Numpy尝试将python函数转换为numpy np.vectorize
个对象。我实际上并不知道它是如何做到的-您必须比我更愿意对ATM进行更多的numpy内部研究。也就是说,似乎在简单的数字函数上比在这里的基于字符串的函数做得更好。
将大小提高到1,000,000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
apply
%timeit name_series.apply(parse_name)
结果:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
%timeit parse_name_vec(name_series)
结果:
794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
使用np.select
的更好( vectorized )方法:
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()
时间:
%timeit np.select(cases, replacements, default=name_series)
结果:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
答案 2 :(得分:0)
我是python的新手。但是在下面的示例中,“应用”似乎比“矢量化”工作更快,或者我错过了什么。
import numpy as np
import pandas as pd
B = np.random.rand(1000,1000)
fn = np.vectorize(lambda l: 1/(1-np.exp(-l)))
print(fn(B))
B = pd.DataFrame(np.random.rand(1000,1000))
fn = lambda l: 1/(1-np.exp(-l))
print(B.apply(fn))