pandas的iterrows有性能问题吗?

151 投票
9 回答
128235 浏览
提问于 2025-04-18 14:08

我发现使用pandas中的iterrows时性能非常差。

这是iterrows特有的问题吗?对于某些数据大小(我在处理200到300万行数据)应该避免使用这个函数吗?

在GitHub上的这个讨论让我觉得这是因为在数据框中混合了不同的数据类型,但下面这个简单的例子显示,即使只使用一种数据类型(float64),问题依然存在。在我的电脑上,这个操作花了36秒:

import pandas as pd
import numpy as np
import time

s1 = np.random.randn(2000000)
s2 = np.random.randn(2000000)
dfa = pd.DataFrame({'s1': s1, 's2': s2})

start = time.time()
i=0
for rowindex, row in dfa.iterrows():
    i+=1
end = time.time()
print end - start

为什么像apply这样的向量化操作要快得多?我想这其中也一定有逐行迭代的过程。

我现在还搞不清楚在我的情况下如何不使用iterrows(这个我会留到以后再问)。所以我很想知道你们是否能一直避免这种迭代。我是在根据不同数据框中的数据进行计算。

我想运行的简化版本是:

import pandas as pd
import numpy as np

#%% Create the original tables
t1 = {'letter':['a','b'],
      'number1':[50,-10]}

t2 = {'letter':['a','a','b','b'],
      'number2':[0.2,0.5,0.1,0.4]}

table1 = pd.DataFrame(t1)
table2 = pd.DataFrame(t2)

#%% Create the body of the new table
table3 = pd.DataFrame(np.nan, columns=['letter','number2'], index=[0])

#%% Iterate through filtering relevant data, optimizing, returning info
for row_index, row in table1.iterrows():
    t2info = table2[table2.letter == row['letter']].reset_index()
    table3.ix[row_index,] = optimize(t2info,row['number1'])

#%% Define optimization
def optimize(t2info, t1info):
    calculation = []
    for index, r in t2info.iterrows():
        calculation.append(r['number2']*t1info)
    maxrow = calculation.index(max(calculation))
    return t2info.ix[maxrow]

9 个回答

6

另一个选择是使用 to_records(),这个方法比 itertuplesiterrows 都要快。

不过在你的情况下,还有很多其他的改进空间。

这是我最终优化后的版本:

def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    t2info = table2.to_records()
    for index, letter, n1 in table1.to_records():
        t2 = t2info[grouped.groups[letter].values]
        # np.multiply is in general faster than "x * y"
        maxrow = np.multiply(t2.number2, n1).argmax()
        # `[1:]`  removes the index column
        ret.append(t2[maxrow].tolist()[1:])
    global table3
    table3 = pd.DataFrame(ret, columns=('letter', 'number2'))

基准测试:

-- iterrows() --
100 loops, best of 3: 12.7 ms per loop
  letter  number2
0      a      0.5
1      b      0.1
2      c      5.0
3      d      4.0

-- itertuple() --
100 loops, best of 3: 12.3 ms per loop

-- to_records() --
100 loops, best of 3: 7.29 ms per loop

-- Use group by --
100 loops, best of 3: 4.07 ms per loop
  letter  number2
1      a      0.5
2      b      0.1
4      c      5.0
5      d      4.0

-- Avoid multiplication --
1000 loops, best of 3: 1.39 ms per loop
  letter  number2
0      a      0.5
1      b      0.1
2      c      5.0
3      d      4.0

完整代码:

import pandas as pd
import numpy as np

#%% Create the original tables
t1 = {'letter':['a','b','c','d'],
      'number1':[50,-10,.5,3]}

t2 = {'letter':['a','a','b','b','c','d','c'],
      'number2':[0.2,0.5,0.1,0.4,5,4,1]}

table1 = pd.DataFrame(t1)
table2 = pd.DataFrame(t2)

#%% Create the body of the new table
table3 = pd.DataFrame(np.nan, columns=['letter','number2'], index=table1.index)


print('\n-- iterrows() --')

def optimize(t2info, t1info):
    calculation = []
    for index, r in t2info.iterrows():
        calculation.append(r['number2'] * t1info)
    maxrow_in_t2 = calculation.index(max(calculation))
    return t2info.loc[maxrow_in_t2]

#%% Iterate through filtering relevant data, optimizing, returning info
def iterthrough():
    for row_index, row in table1.iterrows():   
        t2info = table2[table2.letter == row['letter']].reset_index()
        table3.iloc[row_index,:] = optimize(t2info, row['number1'])

%timeit iterthrough()
print(table3)

print('\n-- itertuple() --')
def optimize(t2info, n1):
    calculation = []
    for index, letter, n2 in t2info.itertuples():
        calculation.append(n2 * n1)
    maxrow = calculation.index(max(calculation))
    return t2info.iloc[maxrow]

def iterthrough():
    for row_index, letter, n1 in table1.itertuples():   
        t2info = table2[table2.letter == letter]
        table3.iloc[row_index,:] = optimize(t2info, n1)

%timeit iterthrough()


print('\n-- to_records() --')
def optimize(t2info, n1):
    calculation = []
    for index, letter, n2 in t2info.to_records():
        calculation.append(n2 * n1)
    maxrow = calculation.index(max(calculation))
    return t2info.iloc[maxrow]

def iterthrough():
    for row_index, letter, n1 in table1.to_records():   
        t2info = table2[table2.letter == letter]
        table3.iloc[row_index,:] = optimize(t2info, n1)

%timeit iterthrough()

print('\n-- Use group by --')

def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    for index, letter, n1 in table1.to_records():
        t2 = table2.iloc[grouped.groups[letter]]
        calculation = t2.number2 * n1
        maxrow = calculation.argsort().iloc[-1]
        ret.append(t2.iloc[maxrow])
    global table3
    table3 = pd.DataFrame(ret)

%timeit iterthrough()
print(table3)

print('\n-- Even Faster --')
def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    t2info = table2.to_records()
    for index, letter, n1 in table1.to_records():
        t2 = t2info[grouped.groups[letter].values]
        maxrow = np.multiply(t2.number2, n1).argmax()
        # `[1:]`  removes the index column
        ret.append(t2[maxrow].tolist()[1:])
    global table3
    table3 = pd.DataFrame(ret, columns=('letter', 'number2'))

%timeit iterthrough()
print(table3)

最终版本的速度几乎比原始代码快了10倍。这个策略是:

  1. 使用 groupby 来避免重复比较值。
  2. 使用 to_records 来访问原始的 numpy.records 对象。
  3. 在收集完所有数据之前,不要对 DataFrame 进行操作。
13

不要使用 iterrows!

...或者 iteritems,或者 itertuples。真的,不要。尽量让你的代码实现 向量化。如果你不相信我,可以问问 Jeff

我承认在某些情况下,遍历一个数据框是有其合理性的,但相比于 iter* 这类函数,还有更好的选择,主要有:

很多初学 pandas 的人常常会问与 iterrows 相关的问题。由于这些新用户可能不太了解向量化的概念,他们会想当然地认为解决问题的代码需要用到循环或其他迭代方式。因为他们也不知道怎么迭代,通常会找到这个问题,结果学到的都是错误的东西。


支持论据

文档页面上关于迭代的部分有一个巨大的红色警告框,上面写着:

遍历 pandas 对象通常很慢。在很多情况下,手动遍历行并不是必要的 [...]。

如果这还不能说服你,可以看看我在这里发的关于向量化和非向量化技术在添加两列 "A + B" 时的性能比较。

基准测试代码,供你参考iterrows 是最糟糕的选择,值得一提的是,其他迭代方法也没好到哪里去。

底部的那一行测量的是用 numpandas 写的一个函数,这是一种与 NumPy 深度结合的 Pandas 风格,以获得最佳性能。除非你知道自己在做什么,否则应该避免写 numpandas 代码。尽量使用 API(也就是说,优先选择 vec 而不是 vec_numpy)。


总结

总是要尽量实现向量化。有时候,根据你的问题或数据的性质,这并不总是可能,所以要寻找比 iterrows 更好的迭代方法。除了在处理极少行时的方便外,几乎没有合理的使用场景,否则你可能会面临代码运行几个小时的等待。

查看下面的链接,找出解决你代码的最佳方法/向量化例程。

15

这是解决你问题的方法。这个方法是完全向量化的。

In [58]: df = table1.merge(table2,on='letter')

In [59]: df['calc'] = df['number1']*df['number2']

In [60]: df
Out[60]: 
  letter  number1  number2  calc
0      a       50      0.2    10
1      a       50      0.5    25
2      b      -10      0.1    -1
3      b      -10      0.4    -4

In [61]: df.groupby('letter')['calc'].max()
Out[61]: 
letter
a         25
b         -1
Name: calc, dtype: float64

In [62]: df.groupby('letter')['calc'].idxmax()
Out[62]: 
letter
a         1
b         2
Name: calc, dtype: int64

In [63]: df.loc[df.groupby('letter')['calc'].idxmax()]
Out[63]: 
  letter  number1  number2  calc
1      a       50      0.5    25
2      b      -10      0.1    -1
23

Numpy和pandas中的向量操作比普通Python中的标量操作要快得多,原因有几个:

  • 类型查找的节省:Python是一种动态类型的语言,这意味着在处理数组中的每个元素时都会有额外的时间开销。但是,Numpy(以及pandas)是在C语言中进行计算的(通常通过Cython)。数组的类型只在开始迭代时确定;光这一点就能节省不少时间。

  • 更好的缓存利用:在C数组上进行迭代时,缓存的使用效率很高,因此速度非常快。pandas的DataFrame是一个“列导向的表”,这意味着每一列实际上就是一个数组。所以你在DataFrame上可以进行的原生操作(比如对某一列的所有元素求和)几乎不会出现缓存失效的情况。

  • 更多的并行处理机会:简单的C数组可以通过SIMD指令进行操作。Numpy的某些部分支持SIMD,这取决于你的CPU和安装方式。虽然并行处理的好处没有静态类型和更好缓存的效果那么明显,但仍然是一个不错的提升。

总结一下:使用Numpy和pandas中的向量操作。它们比Python中的标量操作快,原因很简单,这些操作本来就是C程序员手动编写的(而且数组的写法比嵌入SIMD指令的显式循环要容易得多)。

266

一般来说,iterrows 只应该在非常特定的情况下使用。以下是不同操作性能的优先顺序:

  1. 向量化
  2. 使用自定义的 Cython 例程
  3. 应用 apply
    • 可以在 Cython 中执行的简化操作
    • 在 Python 中的迭代
  4. itertuples
  5. iterrows
  6. 逐行更新一个空的框架(例如,使用 loc 一次更新一行)

使用自定义的 Cython 例程通常太复杂,所以我们暂时不讨论这个。

  1. 向量化 总是 是首选和最佳选择。不过,有一小部分情况(通常涉及递归)是无法明显地进行向量化的。此外,在较小的 DataFrame 上,使用其他方法可能会更快。

  2. apply 通常 可以通过 Cython 中的迭代器来处理。这是由 pandas 内部处理的,不过具体情况取决于 apply 表达式内部的操作。例如,df.apply(lambda x: np.sum(x)) 执行得相当快,当然,df.sum(1) 更好。但是像 df.apply(lambda x: x['b'] + 1) 这样的操作会在 Python 中执行,因此会慢得多。

  3. itertuples 不会把数据装入 Series 中。它只是以元组的形式返回数据。

  4. iterrows 把数据装入 Series 中。除非你真的需要这样做,否则建议使用其他方法。

  5. 逐行更新一个空的框架。我见过这种方法用得太多了。这是最慢的方式。虽然在某些 Python 结构中可能比较常见(而且速度还算快),但 DataFrame 在索引时会进行很多检查,因此逐行更新总是非常慢。更好的方法是创建新的结构并使用 concat

撰写回答