为什么在pandas中列表推导比apply函数更快

12
使用列表推导式比普通的for循环快得多。给出的理由是列表推导式中不需要使用append,这一点是可以理解的。 但我发现在各个地方比较列表还是比apply更快。我也有这样的经历。但不明白内部工作原理为什么会比apply快得多?
我知道这与numpy中的向量化有关,而pandas数据框架的基础实现就是numpy。但是为什么列表推导式比apply更好,就不太容易理解了。因为在列表推导式中,我们在列表内部使用for循环,而在apply中甚至没有for循环(我认为这里也进行了向量化)。
编辑: 添加代码: 这是在“泰坦尼克号”数据集上运行的,从名字中提取标题: https://www.kaggle.com/c/titanic/data
%timeit train['NameTitle'] = train['Name'].apply(lambda x: 'Mrs.' if 'Mrs' in x else \
                                         ('Mr' if 'Mr' in x else ('Miss' if 'Miss' in x else\
                                                ('Master' if 'Master' in x else 'None'))))

%timeit train['NameTitle'] = ['Mrs.' if 'Mrs' in x else 'Mr' if 'Mr' in x else ('Miss' if 'Miss' in x else ('Master' if 'Master' in x else 'None')) for x in train['Name']]

结果: 782微秒 ± 6.36微秒每次循环(7次运行的平均值±标准差,每次1000次循环)

499微秒 ± 5.76微秒每次循环(7次运行的平均值±标准差,每次1000次循环)

Edit2: 为了在SO上添加代码,我创建了一个简单的代码,令人惊讶的是,对于下面的代码,结果相反:

import pandas as pd
import timeit
df_test = pd.DataFrame()
tlist = []
tlist2 = []
for i in range (0,5000000):
  tlist.append(i)
  tlist2.append(i+5)
df_test['A'] = tlist
df_test['B'] = tlist2

display(df_test.head(5))


%timeit df_test['C'] = df_test['B'].apply(lambda x: x*2 if x%5==0 else x)
display(df_test.head(5))
%timeit df_test['C'] = [ x*2 if x%5==0 else x for x in df_test['B']]

display(df_test.head(5))

1次循环,3次中最好的时间为每次2.14秒。

1次循环,3次中最好的时间为每次2.24秒。

编辑3: 正如一些人所建议的那样,apply本质上是一个for循环,但实际情况并非如此,因为如果我使用for循环运行此代码,它几乎永远不会结束,我不得不手动在3-4分钟后停止它,而且在此期间它从未完成。

for row in df_test.itertuples():
  x = row.B
  if x%5==0:
    df_test.at[row.Index,'B'] = x*2

运行上述代码需要大约23秒,但应用程序只需要1.8秒。那么在itertuples中使用物理循环和apply之间有什么区别呢?


3
"apply" 本质上不就是一个 "for" 循环吗? - Quang Hoang
1
你需要展示一些代码以配合你的基准测试。 - Chris_Rands
1
.apply 基本上就是一个 for 循环。它不使用向量化。请注意,列表推导式仅比 for 循环略快,如果您缓存 .append 方法的解析结果,两者都可以实现基本相同的性能,这实际上就是列表推导式所做的事情(请注意它仍然使用 append)。 - juanpa.arrivillaga
@QuangHoang向juanpa解释说,由于apply比for循环慢得多,因此添加了问题,因为apply不是for循环,所以必须有其他方法。 - Tushar Seth
1
@TusharSeth 因为你使用的循环是最慢的方式永远不要使用 x = df_test.loc[i,'B'],尝试使用 df.itertuples()。这是一个循环。你可以自己检查源代码 - juanpa.arrivillaga
显示剩余10条评论
1个回答

3

有几个原因导致了使用 apply 和列表推导式之间的性能差异。

首先,您代码中的列表推导式在每次迭代时不会进行函数调用,而 apply 会。这造成了很大的性能差异:

map_function = lambda x: 'Mrs.' if 'Mrs' in x else \
                 ('Mr' if 'Mr' in x else ('Miss' if 'Miss' in x else \
                 ('Master' if 'Master' in x else 'None')))

%timeit train['NameTitle'] = [map_function(x) for x in train['Name']]
# 581 µs ± 21.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit train['NameTitle'] = ['Mrs.' if 'Mrs' in x else \
                 ('Mr' if 'Mr' in x else ('Miss' if 'Miss' in x else \
                 ('Master' if 'Master' in x else 'None'))) for x in train['Name']]
# 482 µs ± 14.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

其次,apply函数不仅可以进行列表推导的操作。例如,它会尝试找到适合结果的dtype类型。通过禁用该行为,您可以看到它所产生的影响:

%timeit train['NameTitle'] = train['Name'].apply(map_function)
# 660 µs ± 2.57 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit train['NameTitle'] = train['Name'].apply(map_function, convert_dtype=False)
# 626 µs ± 4.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

apply函数中还有很多其他的操作,所以在这个例子中,你应该使用map函数:

%timeit train['NameTitle'] = train['Name'].map(map_function)
# 545 µs ± 4.02 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

这种方法要比在其中调用函数的列表推导更有效。

那么你可能会问,既然如此,为什么还要使用apply呢?我知道至少有一个例子,它比其他所有方法都更优 -- 当你想应用的操作是向量化通用函数时。这是因为applymap和列表推导不同,它允许函数在整个序列上运行,而不是在其中的单个对象上运行。 让我们来看一个例子:

%timeit train['AgeExp'] = train['Age'].apply(lambda x: np.exp(x))
# 1.44 ms ± 41.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit train['AgeExp'] = train['Age'].apply(np.exp)
# 256 µs ± 12.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit train['AgeExp'] = train['Age'].map(np.exp)
# 1.01 ms ± 8.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit train['AgeExp'] = [np.exp(x) for x in train['Age']]
# 1.21 ms ± 28.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接