Pandas apply 与 np.vectorize 从现有列创建新列的性能对比
- 2025-01-15 08:46:00
- admin 原创
- 101
问题描述:
我正在使用 Pandas 数据框,并希望根据现有列创建一个新列。我还没有看到关于df.apply()
和之间的速度差异的很好的讨论np.vectorize()
,所以我想在这里问一下。
Pandasapply()
函数很慢。根据我的测量结果(如下所示的一些实验),使用np.vectorize()
比使用 DataFrame 函数快 25 倍(或更多)apply()
,至少在我的 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
进一步假设我想创建一个新列作为两列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()
比 快 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 (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 的“应用”函数应用于多列?
如何将函数应用于 Pandas 数据框的两列
解决方案 1:
首先我想说的是,Pandas 和 NumPy 数组的强大功能源自对数值数组的高性能矢量化计算。1矢量化计算的全部意义在于通过将计算转移到高度优化的 C 代码并利用连续的内存块来避免 Python 级循环。2
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
、列表推导 +zip
和map
方法(即前 3 个)的性能大致相同。这是因为它们使用tuple
并绕过了 的一些 Pandas 开销pd.DataFrame.itertuples
。raw=True
与不使用相比,使用时速度有显著提升pd.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
像 python map 函数一样对输入数组的连续元组进行评估,但它使用 numpy 的广播规则。
“广播规则”在这里无关紧要,因为输入数组具有相同的维度。与的比较很有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数字类型包括:int
,float
,datetime
,bool
,category
。它们不包括 object
dtype 并且可以保存在连续的内存块中。
2
NumPy 操作比 Python 更高效至少有两个原因:
Python 中的一切都是对象。与 C 不同,这包括数字。因此,Python 类型具有原生 C 类型不存在的开销。
NumPy 方法通常基于 C。此外,尽可能使用优化算法。
解决方案 2:
您的函数越复杂(即,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)
进行一些计时:
使用 Apply
%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 函数转换为 numpyufunc
对象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)