提问人:stackoverflowuser2010 提问时间:10/6/2018 最后编辑:stackoverflowuser2010 更新时间:9/18/2020 访问量:88935
Pandas 的性能 应用与np.vectorize从现有列创建新列
Performance of Pandas apply vs np.vectorize to create new column from existing columns
问:
我正在使用 Pandas 数据帧,并希望创建一个新列作为现有列的函数。我还没有看到关于和之间的速度差异的良好讨论,所以我想我会在这里问。df.apply()
np.vectorize()
Pandas 功能很慢。从我的测量结果(如下图所示)来看,使用 比使用 DataFrame 函数快 25 倍(或更多),至少在我的 2016 MacBook Pro 上是这样。 这是预期的结果吗,为什么?apply()
np.vectorize()
apply()
例如,假设我有以下包含行的数据帧: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
如果我增加到 100 万或更多等实际大小,那么我观察到它比 快 25 倍或更多。N
np.vectorize()
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
如果一般总是比 快,那为什么不多提呢?我只看到过与 相关的 StackOverflow 帖子,例如:np.vectorize()
df.apply()
np.vectorize()
df.apply()
答:
你的功能越复杂(即,越少可以移动到它自己的内部),你就越能看到性能不会有那么大的不同。例如: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)
当您调用 时,Numpy 会尝试将 python 函数转换为 numpy 对象。它是如何做到这一点的,我实际上并不知道 - 你必须比我愿意 ATM 更深入地挖掘 numpy 的内部结构。也就是说,它似乎在简单的数值函数上比这里这个基于字符串的函数做得更好。ufunc
np.vectorize
将大小提高到 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
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)
评论
我首先要说的是,Pandas 和 NumPy 数组的强大功能来自于对数值数组的高性能矢量化计算。1 矢量化计算的全部意义在于通过将计算转移到高度优化的 C 代码并利用连续的内存块来避免 Python 级别的循环。阿拉伯数字
Python 级循环
现在我们可以看看一些时间。下面是所有 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
一些要点:
- 基于 - 的方法(前 4 个)比基于 - 的方法(后 3 个)更有效。
tuple
pd.Series
np.vectorize
,列表推导 + 和方法,即前 3 名,都具有大致相同的性能。这是因为它们使用并绕过了 .zip
map
tuple
pd.DataFrame.itertuples
- 与不使用相比,使用速度显着提高。此选项将 NumPy 数组馈送到自定义函数而不是对象。
raw=True
pd.DataFrame.apply
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)
输出:。相对于 NumPy 数组,创建、传递和查询 Pandas 系列对象会产生大量开销。这不足为奇:Pandas 系列包含相当数量的脚手架来容纳索引、值、属性等。<class 'pandas.core.series.Series'>
再次做同样的练习,你会看到。所有这些都在文档中进行了描述,但看到它更有说服力。raw=True
<class 'numpy.ndarray'>
np.vectorize
:假矢量化
np.vectorize
的文档有以下说明:
矢量化函数对 输入数组类似于 Python Map 函数,只不过它使用 numpy的广播规则。
pyfunc
“广播规则”在这里无关紧要,因为输入数组具有相同的维度。并行是有启发性的,因为上面的版本具有几乎相同的性能。源代码显示了正在发生的事情:通过 np.frompyfunc
将输入函数转换为通用函数(“ufunc”)。有一些优化,例如缓存,这可以带来一些性能改进。map
map
np.vectorize
简而言之,它执行了 Python 级别的循环应该做的事情,但增加了大量的开销。没有你用 numba
看到的 JIT 编译(见下文)。这只是一种方便。np.vectorize
pd.DataFrame.apply
真正的矢量化:你应该使用什么
为什么在任何地方都没有提到上述差异?因为真正矢量化计算的性能使它们变得无关紧要:
%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
:效率更高
当循环被认为是可行的时,它们通常通过底层 NumPy 数组进行优化,以尽可能多地移动到 C。numba
事实上,将性能提高到微秒级。如果没有一些繁琐的工作,将很难获得比这更高的效率。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)
1 数值类型包括:、、、、。它们不包括 dtype,可以保存在连续的内存块中。int
float
datetime
bool
category
object
2 与 Python 相比,NumPy 操作效率至少有两个原因:
- Python 中的所有内容都是一个对象。与 C 不同,这包括数字。因此,Python 类型具有原生 C 类型不存在的开销。
- NumPy 方法通常是基于 C 的。此外,优化的算法 在可能的情况下使用。
评论
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]
16.4 ms ± 192 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit [divide(a, b) for a, b in zip(df['A'].values, df['B'].values)]
34.8 ms ± 388 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
.values
apply
A
B
df['A']
df['B']
zip
tuple
评论
np.vectorize
for
apply
apply
np.vectorize
.str