pandas的iterrows方法是否存在性能问题?

148

我注意到使用pandas中的iterrows函数性能很差。

这个问题是特定于iterrows函数的吗?在处理200-300万行数据时,应该避免使用这个函数吗?

这个GitHub上的讨论让我相信这是由于混合了不同的数据类型导致的,但是下面这个简单的例子显示即使使用了一个dtype(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]

11
apply 不支持向量化。iterrows 更糟,因为它会把所有东西都装箱处理(这就是与“apply”相比的性能差异所在)。你应该只在非常少的情况下使用 iterrows。在我看来,甚至从未使用。请展示一下你实际上是如何使用 iterrows 的。 - Jeff
3
您提供的链接与将“DatetimeIndex”转换为“Timestamps”的问题有关(在python空间中实现),而这个问题已经在主分支中得到了很大改进。 - Jeff
2
请参阅此问题以获取更全面的讨论:https://github.com/pydata/pandas/issues/7194。 - Jeff
以下是有关编程的内容,需要将其从英语翻译成中文。请仅返回翻译后的文本:链接到具体问题(此处为通用链接):https://dev59.com/dYHba4cB1Zd3GeqPVLxm - KieranPC
2
请不要推荐使用iterrows()。它是Pandas历史上最糟糕的反模式的明显助长者。 - cs95
还可以参考这个答案中关于.iterrows()性能的讨论这里我的答案 - undefined
8个回答

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。除非您真的需要这个功能,否则请使用其他方法。

  • 单行逐个更新空帧。我看到这种方法使用得太多了。它是迄今为止最慢的方法。它可能很常见(对于某些Python结构而言相对较快),但是DataFrame在索引上执行了大量检查,因此逐行更新将始终非常缓慢。更好的方法是创建新的结构并使用concat


  • 1
    是的,我使用了数字6(和5)。我还有一些学习要做。对于相对初学者来说,这似乎是一个显而易见的选择。 - KieranPC
    3
    根据我的经验,在不同的使用情境下,3、4和5之间的差异是有限的。 - IanS
    15
    我尝试检查了这个笔记本中的运行时间(https://github.com/dimgold/Datathon-TAU/blob/master/7.6%20Pandas%20iterations%20test/Pandas_Iterations.ipynb)。不知何故,“itertuples”比“apply”更快 :( - Dimgold
    3
    pd.DataFrame.applyitertuples通常更慢。此外,对于不可向量化的计算,值得考虑列表解析、map、名字不太合适的np.vectorize以及numba(没有特定顺序),例如请参见这个答案 - jpp
    2
    @Jeff,出于好奇,为什么你没有在这里添加列表推导式呢?虽然它们确实不能处理索引对齐或缺失数据(除非你使用带有try-catch的函数),但它们非常适用于许多情况(字符串/正则表达式等),在这些情况下,pandas方法没有向量化(在最真实的意义上)的实现。你认为值得提到LC是一种更快、更低开销的替代方案,可以替代pandas apply和许多pandas字符串函数吗? - cs95
    显示剩余2条评论

    23

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

    • 分摊类型查找:Python是一种动态类型语言,所以对于数组中的每个元素都需要运行时开销。然而,Numpy(以及pandas)在C中执行计算(通常通过Cython)。数组的类型仅在迭代开始时确定;仅此一项的节省已经是最大的收益之一。

    • 更好的缓存性能:对C数组进行迭代是友好的,因此非常快。pandas DataFrame是一个“列向表”,这意味着每列实际上只是一个数组。因此,在DataFrame上执行的本机操作(如对一列中所有元素求和)将具有较少的缓存未命中。

    • 更多并行化机会:可以通过SIMD指令对简单的C数组进行操作。根据您的CPU和安装过程,Numpy的某些部分可以启用SIMD。尽管并行化的好处不像静态类型和更好的缓存效果那么明显,但仍然是一个很好的优势。

    寓意:使用Numpy和pandas中的向量操作。它们比Python中的标量操作更快,因为这些操作恰好是C程序员手动编写的内容。(除了数组概念比具有嵌入式SIMD指令的显式循环更容易阅读之外。)


    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
    

    非常清晰的答案,谢谢。我会尝试合并,但我有疑虑,因为那样我将拥有50亿行(2.5百万*2000)。为了保持这个问题的一般性,我创建了一个具体的问题。如果您知道其他方法可以避免使用这个巨大的表格,我很乐意看到:在这里:https://dev59.com/dYHba4cB1Zd3GeqPVLxm - KieranPC
    1
    这并不会创建笛卡尔积,它是一个压缩的空间,非常节省内存。你正在解决一个非常标准的问题,试一下吧。(你链接的问题有一个非常相似的解决方案) - Jeff

    13

    不要使用iterrows!

    ...或者iteritems,或者itertuples。真的,不要这样做。在可能的情况下,尽量使用向量化你的代码。如果你不相信我,问问Jeff

    我承认在DataFrame上进行迭代有合理的用例,但是有比iter*系列函数更好的迭代替代方案,特别是

    很多初学者在使用pandas时会问涉及到iterrows的代码问题。由于这些新用户可能不熟悉向量化的概念,他们会将解决问题的代码想象为涉及循环或其他迭代过程的代码。由于不知道如何进行迭代,他们通常最终会找到这个问题,并学到了错误的东西。


    支持的论点

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

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

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

    基准测试代码,供参考iterrows 是其中最糟糕的方法,而且其他迭代方法也不好。

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


    总结

    始终追求向量化。有时,根据问题或数据的性质,这并不总是可能的,因此请寻找比iterrows更好的迭代方法。除了在处理极少量行时方便之外,几乎没有合理的使用情况,否则请准备好花费很长时间等待代码运行,可能需要几个小时。

    请查看下面的链接,确定解决代码的最佳方法/向量化例程。


    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。

    4

    有关详细信息,可参见此视频

    基准测试 enter image description here


    2

    如果您确实需要迭代并按名称访问行字段,只需将列名保存到列表中,并将数据帧转换为NumPy数组:

    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})
    columns = list(dfa.columns)
    dfa = dfa.values
    start = time.time()
    i=0
    for row in dfa:
        blablabla = row[columns.index('s1')]
        i+=1
    end = time.time()
    print (end - start)
    

    0.9485495090484619


    这个解决方案非常棒,因为它非常容易用它来替换现有的 .iterrows() 循环,并且速度比使用所有内存并崩溃要快得多。有人知道这种方法的缺点或限制吗?到目前为止,我发现 dfa.values 将自动选择与所有列的 dtype 兼容的 dtype,并将所有数据转换为该单一 dtype,这在我看来不是 DF 的最佳选择,但通常不会破坏任何东西。注意:https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.values.html 建议使用 .to_numpy() 而不是 .values - tylerl

    1

    是的,Pandas itertuples() 比 iterrows() 更快。

    您可以参考文档:pandas.DataFrame.iterrows

    为了在迭代行时保留dtype,最好使用itertuples(),它返回值的命名元组,并且通常比iterrows更快。


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