Pandas 的性能 应用与np.vectorize从现有列创建新列

Performance of Pandas apply vs np.vectorize to create new column from existing columns

提问人:stackoverflowuser2010 提问时间:10/6/2018 最后编辑:stackoverflowuser2010 更新时间:9/18/2020 访问量:88935

问:

我正在使用 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

进一步假设我想创建一个新列作为两列和 的函数。在下面的示例中,我将使用一个简单的函数。要应用该函数,我可以使用以下任一方法:ABdivide()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 倍或更多。Nnp.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()

pandas 根据其他列的值创建新列

如何将 Pandas 的“apply”功能用于多个列?

如何将函数应用于 Pandas 数据帧的两列

python 数组 pandas 性能 numpy

评论

0赞 roganjosh 10/6/2018
我没有深入研究你问题的细节,但基本上是一个 python 循环(这是一种方便的方法),并且带有 lambda 也在 python 时间np.vectorizeforapply
0赞 PMende 10/6/2018
“如果 np.vectorize() 通常总是比 df.apply() 快,那么为什么 np.vectorize() 没有被更多提及?”因为除非必要,否则您不应该逐行使用,而且显然矢量化函数的性能将优于非矢量化函数。apply
3赞 roganjosh 10/6/2018
@PMende但未矢量化。这是一个众所周知的用词不当np.vectorize
1赞 jpp 10/6/2018
@PMende,当然,我没有暗示其他。你不应该从时间安排中得出你对实施的意见。是的,他们很有见地。但它们会让你假设一些不真实的事情。
3赞 roganjosh 10/6/2018
@PMende玩 pandas 访问器。在很多情况下,它们比列表推导式慢。我们假设得太多了。.str

答:

12赞 PMende 10/6/2018 #1

你的功能越复杂(即,越少可以移动到它自己的内部),你就越能看到性能不会有那么大的不同。例如: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 的内部结构。也就是说,它似乎在简单的数值函数上比这里这个基于字符串的函数做得更好。ufuncnp.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)

评论

2赞 roganjosh 10/6/2018
我很确定你在这里的断言是不正确的。我现在不能用代码来支持该声明,希望其他人可以
245赞 jpp 10/6/2018 #2

首先要说的是,Pandas 和 NumPy 数组的强大功能来自于对数值数组的高性能矢量化计算。1 矢量化计算的全部意义在于通过将计算转移到高度优化的 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. 基于 - 的方法(前 4 个)比基于 - 的方法(后 3 个)更有效。tuplepd.Series
  2. np.vectorize,列表推导 + 和方法,即前 3 名,都具有大致相同的性能。这是因为它们使用绕过了 .zipmaptuplepd.DataFrame.itertuples
  3. 与不使用相比,使用速度显着提高。此选项将 NumPy 数组馈送到自定义函数而不是对象。raw=Truepd.DataFrame.applypd.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”)。有一些优化,例如缓存,这可以带来一些性能改进。mapmapnp.vectorize

简而言之,它执行了 Python 级别的循环应该做的事情,但增加了大量的开销。没有你用 numba 看到的 JIT 编译(见下文)。这只是一种方便np.vectorizepd.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,可以保存在连续的内存块中。intfloatdatetimeboolcategoryobject

2 与 Python 相比,NumPy 操作效率至少有两个原因:

  • Python 中的所有内容都是一个对象。与 C 不同,这包括数字。因此,Python 类型具有原生 C 类型不存在的开销。
  • NumPy 方法通常是基于 C 的。此外,优化的算法 在可能的情况下使用。

评论

0赞 PMende 10/6/2018
作为对“相对于 NumPy 数组,创建、传递和查询 Pandas 系列对象会产生大量开销”的评论。比较:结果:与:结果。即使将属性的访问从循环中拉出,这种差异也是一致的。%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
0赞 jpp 10/6/2018
@PMende,您错过了这里的重点,创建的系列是按行创建的,即每个系列都有一个元素和一个元素。在列表推导式中,/ 是唯一的 2 个系列,它们在任何意义上都不是“创建”的,它们已经存在。 可以比作生产一个,便宜得多。applyABdf['A']df['B']ziptuple
1赞 max9111 10/7/2018
您仔细检查了 b[i] != 0。正常的 Python 和 Numba 行为是检查 0 并抛出错误。这可能会破坏任何 SIMD 矢量化,并且通常会对执行速度产生很大影响。但是您可以在 Numba 中将其更改为 @njit(error_model='numpy'),以避免重复检查除以 0。还建议使用 np.empty 分配内存,并在 else 语句中将结果设置为 0。
1赞 max9111 10/8/2018
error_model numpy 使用处理器给出的除以 0 -> NaN 给出的内容。至少在 Numba 0.41dev 中,两个版本都使用 SIMD 矢量化。您可以按照此处的说明进行检查,numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3.为什么我的循环没有矢量化?我只需将 else 语句添加到您的函数 (res[i]=0.) 并使用 np.empty 分配内存。这应该与 error_model='numpy' 相结合,将性能提高约 20%。在较旧的 Numba 版本上,对性能的影响更大......
3赞 jpp 10/11/2018
@stackoverflowuser2010,没有“任意函数”的通用答案。您必须为正确的工作选择正确的工具,这是理解编程/算法的一部分。