Pandas的性能适用于vs.np.vectorize从现有列创建新列


81

我正在使用Pandas数据框,并希望根据现有列创建一个新列。对于df.apply()和之间的速度差异,我还没有很好的讨论np.vectorize(),所以我想在这里问一下。

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

如果我N将现实世界的大小增加到一百万或更多,那么我发现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']))

结果如下所示:

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()不多提?我只看过与相关的StackOverflow帖子df.apply(),例如:

熊猫根据其他列中的值创建新列

如何在多个列中使用Pandas的“应用”功能?

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


我没有深入研究您问题的细节,但np.vectorize基本上是pythonfor循环(这是一种便捷方法),apply并且lambda也在python时间
roganjosh

“如果np.vectorize()通常总是比df.apply()快,那么为什么不多提及np.vectorize()?” 因为apply除非有必要,否则不应该逐行使用,显然矢量化函数的性能要优于非矢量化函数。
PMende '18 -10-5

1
@PMende,但未np.vectorize向量化。这是一个众所周知的用词不当
roganjosh

1
@PMende,当然,我没有暗示。您不应该从时间安排中得出有关实施的意见。是的,他们很有见识。但是它们可以使您推测不正确的事情。
jpp

3
@PMende与熊猫.str访问者玩耍。在很多情况下,它们比列表理解要慢。我们假设太多。
roganjosh

Answers:


115

首先,我要说的是Pandas和NumPy数组的功能是从对数字数组的高性能矢量化计算中获得的。1向量化计算的全部目的是通过将计算移至高度优化的C代码并利用连续的内存块来避免Python级循环。2

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方法(即前3名)的性能大致相同。这是因为它们使用tuple 绕过了熊猫的一些开销pd.DataFrame.itertuples
  3. 使用raw=Truewithpd.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)

输出:<class 'pandas.core.series.Series'>。相对于NumPy数组,创建,传递和查询Pandas系列对象会带来大量开销。这不足为奇:Pandas系列包含相当数量的脚手架,用于存放索引,值,属性等。

再次进行相同的练习raw=True,您会看到<class 'numpy.ndarray'>。所有这些都在文档中进行了描述,但是看到它更具说服力。

np.vectorize:假向量化

的文档np.vectorize有以下注意事项:

pyfunc除了使用numpy的广播规则外,矢量化函数像python map函数一样对输入数组的连续元组求值。

这里的“广播规则”是无关紧要的,因为输入数组具有相同的尺寸。与之并行map是有启发性的,因为上述map版本具有几乎相同的性能。该源代码显示发生的事情:np.vectorize你的输入函数转换成通用的功能通过(“ufunc”) np.frompyfunc。有些优化,例如缓存,可以带来一些性能改进。

总之,np.vectorize做一个Python级环路什么应该做,但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

是的,这比上述循环式解决方案中最快的速度快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

使用@njit(parallel=True)可以为更大的阵列提供进一步的提升。


1种数字类型包括:intfloatdatetimeboolcategory。它们不包含 objectdtype,可以保存在连续的内存块中。

2 NumPy操作相对于Python高效的原因至少有两个:

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

1
@jpp:将修饰符与parallel参数配合使用@njit(parallel=True)可为我提供优于just的进一步改进@njit。也许您也可以添加它。
谢尔多雷

1
您需要仔细检查b [i]!=0。正常的Python和Numba行为是检查0并引发错误。这可能会中断任何SIMD向量化,并且通常会对执行速度产生很大影响。但是,您可以在Numba中将其更改为@njit(error_model ='numpy'),以避免这种双重检查以0除法的情况。建议您使用np.empty分配内存,并在else语句中将结果设置为0。
max9111

1
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版本上,对性能的影响更大……
max9111

2
@ stackoverflowuser2010,没有针对“任意函数”的通用答案。您必须为正确的工作选择正确的工具,这是理解编程/算法的一部分。
jpp

1
节日快乐!
cs95

5

函数变得越复杂(即,越少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

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)

如果您将数字提高到size=1000000(100万)怎么办?
stackoverflowuser2010

2
我很确定您在这里的主张是不正确的。我现在无法用代码来支持该语句,希望其他人可以做到
roganjosh

@ stackoverflowuser2010我已经更新了它,以及一种实际的矢量化方法。
PMende '18 -10-5

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))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.