使用 Pandas 的性能与 np.vectorize 相比,可以从现有列创建新列

我使用熊猫数据框架,并希望创建一个新的列作为一个函数的现有列。我还没有看到一个很好的讨论之间的速度差异 df.apply()np.vectorize(),所以我想我会在这里问。

熊猫的 apply()功能是缓慢的。根据我的测量(在下面的一些实验中显示) ,使用 np.vectorize()比使用 DataFrame 功能 apply()快25倍(或更多) ,至少在我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

进一步假设我想创建一个新列,作为两列 AB的函数。在下面的例子中,我将使用一个简单的函数 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 文章,比如:

熊猫基于其他列的值创建新列

我如何使用熊猫应用函数的多列?

如何将一个函数应用到熊猫数据框的两列

73655 次浏览

你的函数越复杂(也就是说,越少的 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 尝试在调用 np.vectorize时将 python 函数转换为 Numpy ufunc对象。如何做到这一点,我实际上不知道-你必须挖掘更多内在的麻木不仁比我愿意自动取款机。也就是说,它似乎在简单的数值函数上比这里的基于字符串的函数做得更好。

将大小调整到100万:

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)

我将 开始说熊猫和 NumPy 阵列的能力来源于数字阵列上的高性能 矢量化计算。向量化计算的全部意义在于通过将计算移动到高度优化的 C 代码和使用连续的内存块来避免 Python 级别的循环

Python 级别的循环

现在我们来看看时间。下面是 所有 Python 级别的循环,它们生成包含相同值的 pd.Seriesnp.ndarraylist对象。为了在数据框架内对序列进行赋值,结果是可比较的。

# 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

一些外卖:

  1. 基于 tuple的方法(前4种)比基于 pd.Series的方法(后3种)更有效。
  2. np.vectorize、列表内涵 + zipmap方法,即排名前三的方法,其性能大致相同。这是因为他们使用 tuple 还有绕过一些熊猫从 pd.DataFrame.itertuples开销。
  3. 与使用带有 pd.DataFrame.applyraw=True和不带 pd.DataFrame.applyraw=True相比,有显著的速度提高。此选项将 NumPy 数组提供给自定义函数而不是 pd.Series对象。

又一个循环

为了看到熊猫传递的物体 没错,你可以修改你的功能:

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 系列对象的开销很大。这并不令人惊讶: 熊猫系列包含了大量的脚手架来保存索引、值、属性等。

raw=True再做同样的练习,你会看到 <class 'numpy.ndarray'>。所有这些都在文件中描述了,但是看到它更有说服力。

虚假矢量化

np.vectorize的文件有以下说明:

向量化函数对连续的元组计算 pyfunc 类似于 python 映射函数的输入数组,但它使用 麻木的广播规则。

“广播规则”在这里是不相关的,因为输入数组具有相同的维度。与 map的并行是有益的,因为上面的 map版本具有几乎相同的性能。源代码显示了正在发生的事情: np.vectorize通过 np.frompyfunc将你的输入函数转换成 通用功能(“ ufunc”)。有一些优化,例如缓存,可以导致一些性能改进。

简而言之,np.vectorize做 Python 级别的循环 应该所做的事情,但是 pd.DataFrame.apply增加了大量的开销。在 numba中没有看到 JIT 编译(见下文)。是 只是为了方便

真矢量化: 你 应该使用什么

为什么没有提到上述差异? 因为真正向量化计算的性能使它们变得无关紧要:

%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

效率更高

当循环 被认为是可行的时候,它们通常通过带有底层 NumPy 数组的 numba进行优化,以尽可能地移动到 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)可以为更大的阵列提供进一步的增强。


数字类型包括: intfloatdatetimeboolcategory。它们是 排除 object dtype,可以保存在连续的存储器块中。

2 NumPy 操作之所以比 Python 高效,至少有两个原因:

  • Python 中的一切都是对象。与 C 不同,这包括数字。因此,Python 类型具有本机 C 类型不存在的开销。
  • NumPy 方法通常是基于 C 的 在可能的情况下使用。