使用pandas循环遍历数据框的最有效方法是什么?

400

我希望以连续的方式对数据框中的金融数据执行自己的复杂操作。

例如,我正在使用从Yahoo Finance获取的以下 MSFT CSV 文件:

Date,Open,High,Low,Close,Volume,Adj Close
2011-10-19,27.37,27.47,27.01,27.13,42880000,27.13
2011-10-18,26.94,27.40,26.80,27.31,52487900,27.31
2011-10-17,27.11,27.42,26.85,26.98,39433400,26.98
2011-10-14,27.31,27.50,27.02,27.27,50947700,27.27

....

接下来我会执行以下步骤:

#!/usr/bin/env python
from pandas import *

df = read_csv('table.csv')

for i, row in enumerate(df.values):
    date = df.index[i]
    open, high, low, close, adjclose = row
    #now perform analysis on open/close based on date, etc..

那是最有效的方式吗?鉴于pandas对速度的关注,我认为一定有一些特殊的函数可以以一种同时检索索引的方式迭代值(可能通过生成器进行内存效率)?df.iteritems不幸的是只能逐列迭代。


6
你尝试过编写一个函数并将其传递给 df.apply() 吗? - naught101
如果您想要更高的内存效率,您应该考虑使用向量化操作(使用矩阵和向量)。但是我不了解pandas,所以我无法告诉您是否可能进行这样的操作。 - mike
3
引用“unutbu”的话,NumPy似乎支持向量化操作(使用NumPy数组的关键在于一次性对整个数组执行操作,以获得更快的速度)。 - mike
1
问题特定于顺序迭代,这在金融领域非常常见,而向量化通常不可行。尼克·克劳福德的回答回答了这个问题,并额外提醒在可能的情况下使用向量化。 - Muppet
我通常不会要求提问者更改他们的接受投票,但这是一个罕见的情况,我会要求您考虑将其移动到我的答案。目前被接受的答案推荐使用iterrows(),这比最快的技术慢600倍,或者使用itertuples(),这比最快的技术慢15倍。因此,请考虑将接受的答案移动到我的答案,我在其中介绍了1倍和其他技术,并对它们进行了详细的速度测试。 - undefined
另请参阅:如何在Pandas中迭代DataFrame的行。我在那里进行了13种以上的速度测试 - undefined
13个回答

423

最新版本的pandas现在包括了一个用于迭代行的内置函数。

for index, row in df.iterrows():

    # do some logic here

或者,如果你想更快地完成,可以使用itertuples()

但是,unutbu的建议是使用numpy函数来避免对行进行迭代,这样可以生成最快的代码。


70
请注意,iterrows很慢(它将每一行转换为一个序列,可能会影响您的数据类型)。当您需要迭代器时,最好使用 itertuples - joris
14
顺便说一下,itertuples 返回命名元组(https://docs.python.org/3/library/collections.html#collections.namedtuple),因此您可以通过 row.high 或 getattr(row,'high') 按名称访问每个列。 - seanv507
9
注意,根据当前的文档: "在迭代过程中不应该修改任何内容。这种方法不能保证在所有情况下都能正常工作。取决于数据类型,迭代器返回的是一个副本而不是视图,对其进行写入操作将没有效果。" - viddik13
6
@joris,我非常同意你的观点,“itertuples”比“iterrows”快大约100倍。 - GoingMyWay
5
itertuples(name = None)更快,因为它会生成普通元组而不是命名元组。请参阅这篇有趣的文章:https://medium.com/swlh/why-pandas-itertuples-is-faster-than-iterrows-and-how-to-make-it-even-faster-bc50c0edd30d - Ismael EL ATIFI
显示剩余3条评论

168

Pandas基于NumPy数组。在NumPy数组中提高速度的关键是一次性对整个数组执行操作,而不是逐行或逐项执行。

例如,如果close是一个一维数组,并且你想要计算逐日百分比变化:

pct_change = close[1:]/close[:-1]

这个语句一次性计算了整个百分比变化的数组,而不是

pct_change = []
for row in close:
    pct_change.append(...)
因此,请尽量避免完全使用Python循环for i, row in enumerate(...),并思考如何通过对整个数组(或数据帧)进行操作来执行计算,而不是逐行进行。

41
我同意这是最好的方法,并且对于简单的操作我通常也是这样做。然而,在这种情况下,这是不可能的,因为产生的操作可能会变得非常复杂。具体来说,我正在尝试回测交易策略。例如,如果价格在30天的时间段内达到了新低,那么我们可能想要买入股票,并在满足某些条件时退出,这需要在现场模拟。这个简单的例子仍然可以通过向量化来完成,然而,交易策略越复杂,就越难以使用向量化。 - Muppet
3
请详细解释您试图执行的精确计算。先尽可能编写代码,然后进行性能分析和优化。 - unutbu
7
顺便说一下,对于某些计算(特别是那些不能表示为整个数组操作的计算),使用Python列表编写的代码比使用等效的numpy数组的代码更快。 - unutbu
39
我认为向量化是可能的情况下正确的解决方案,但有时迭代算法是唯一的方法。 - Wes McKinney
7
虽然留言晚了一些,但我发现有时候尝试对整列进行完整计算很难编写和调试。考虑使用中间计算列,可以更容易地调试和理解计算。我发现即使是最复杂的逻辑也可以像这样实现,同时避免循环。 - Joop
@Joop同意,对你的评论晚了。我曾经有一个项目,在其中将我需要的所有计算的中间步骤都放在列中。由于我不得不为过去x年的时间序列点重复计算,所以表格变得很大,>1000列。但这是值得的,因为可以始终追溯到表格中所有内容的来源。您可以为每个更大的步骤制作一个表格。然而,我总是将更改后的时间序列列附加到表格的右侧,并在其中添加一个虚拟列,这样可以减少表格关系的工作量。您需要好的前缀。 - questionto42

138

正如之前所提到的,pandas对象在一次性处理整个数组时最为高效。然而对于那些确实需要循环遍历pandas DataFrame来执行某些操作的人,比如我,我至少发现了三种方法来实现这一点。我进行了一个简短的测试,看看这三种方法中哪一种耗费的时间最少。

t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
B = []
C = []
A = time.time()
for i,r in t.iterrows():
    C.append((r['a'], r['b']))
B.append(time.time()-A)

C = []
A = time.time()
for ir in t.itertuples():
    C.append((ir[1], ir[2]))    
B.append(time.time()-A)

C = []
A = time.time()
for r in zip(t['a'], t['b']):
    C.append((r[0], r[1]))
B.append(time.time()-A)

print B

结果:

[0.5639059543609619, 0.017839908599853516, 0.005645036697387695]

这可能不是衡量时间消耗的最佳方法,但对我来说很快。

以下是我的一些优缺点:

  • .iterrows():将索引和行项目分别返回到变量中,但速度显著较慢
  • .itertuples():比.iterrows()更快,但将索引与行项目一起返回,ir[0]为索引
  • zip:最快,但无法访问行的索引

编辑2020/11/10

值得一提的是,这里有一份更新后的基准测试,其中包含其他一些替代方案(在MacBookPro 2.4 GHz Intel Core i9 8核32 Go 2667 MHz DDR4上进行性能测试)。

import sys
import tqdm
import time
import pandas as pd

B = []
t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
for _ in tqdm.tqdm(range(10)):
    C = []
    A = time.time()
    for i,r in t.iterrows():
        C.append((r['a'], r['b']))
    B.append({"method": "iterrows", "time": time.time()-A})

    C = []
    A = time.time()
    for ir in t.itertuples():
        C.append((ir[1], ir[2]))
    B.append({"method": "itertuples", "time": time.time()-A})

    C = []
    A = time.time()
    for r in zip(t['a'], t['b']):
        C.append((r[0], r[1]))
    B.append({"method": "zip", "time": time.time()-A})

    C = []
    A = time.time()
    for r in zip(*t.to_dict("list").values()):
        C.append((r[0], r[1]))
    B.append({"method": "zip + to_dict('list')", "time": time.time()-A})

    C = []
    A = time.time()
    for r in t.to_dict("records"):
        C.append((r["a"], r["b"]))
    B.append({"method": "to_dict('records')", "time": time.time()-A})

    A = time.time()
    t.agg(tuple, axis=1).tolist()
    B.append({"method": "agg", "time": time.time()-A})

    A = time.time()
    t.apply(tuple, axis=1).tolist()
    B.append({"method": "apply", "time": time.time()-A})

print(f'Python {sys.version} on {sys.platform}')
print(f"Pandas version {pd.__version__}")
print(
    pd.DataFrame(B).groupby("method").agg(["mean", "std"]).xs("time", axis=1).sort_values("mean")
)

## Output

Python 3.7.9 (default, Oct 13 2020, 10:58:24) 
[Clang 12.0.0 (clang-1200.0.32.2)] on darwin
Pandas version 1.1.4
                           mean       std
method                                   
zip + to_dict('list')  0.002353  0.000168
zip                    0.003381  0.000250
itertuples             0.007659  0.000728
to_dict('records')     0.025838  0.001458
agg                    0.066391  0.007044
apply                  0.067753  0.006997
iterrows               0.647215  0.019600

3
在Python 3中,zip()函数会返回一个迭代器,如果需要将其转换为列表,则需使用list(zip()) - Louis Maddox
4
你能否不使用t.index来循环索引? - elPastor
2
太棒了,感謝Richard。這個方法在Python 3.7+仍然適用。從使用iterrows花費的286秒,到使用zip僅需3.62秒。非常感謝。 - pacta_sunt_servanda
1
我已经使用 pandas.version == 1.1.4,Python 3.7.9 和全新的 MacBookPro 2.4 GHz 英特尔 Core i9 8 核心 32 Go 2667 MHz DDR4 重新运行了这个基准测试,并且 iterrows() 的结果甚至更差: [0.6970570087432861, 0.008062124252319336, 0.0036787986755371094] - ClementWalter
1
需要注意的是,zip + to_dict 方法比 itertuples 方法的内存效率要低得多。 - deadlock
显示剩余2条评论

75

你可以通过转置并调用iteritems来遍历行:

for date, row in df.T.iteritems():
   # do some logic here

对于这种情况我不确定效率如何。为了在迭代算法中获得最佳性能,您可能希望探索使用Cython编写它,以便您可以这样做:

def my_algo(ndarray[object] dates, ndarray[float64_t] open,
            ndarray[float64_t] low, ndarray[float64_t] high,
            ndarray[float64_t] close, ndarray[float64_t] volume):
    cdef:
        Py_ssize_t i, n
        float64_t foo
    n = len(dates)

    for i from 0 <= i < n:
        foo = close[i] - open[i] # will be extremely fast

我建议先用纯Python编写算法,确保其可行并查看其速度 - 如果速度不够快,则将一些内容转换为Cython,以最小的工作量得到与手写C/C++代码速度相当的东西。


10
我也推荐使用Cython;我曾经在构建我的回测引擎时遇到类似的问题,使用Cython后速度提升了1000倍。然后我又结合了multiprocessing库,这是一个非常好的组合。 - vgoklani
6
需要更新这个答案,包括新的df.iterrows()方法,正如@NickCrawford的回答所述。 - LondonRob
1
如果你想迭代特定列,那么df.T.iteritems()是一个比使用df.iterrows()更好的解决方案。+1 - Alireza
给出错误:def my_algo(ndarray[object] dates, ndarray[float64_t] opn,                        ^ 语法错误:无效的语法 - BhishanPoudel

62

您有三个选项:

通过 index(最简单):

>>> for index in df.index:
...     print ("df[" + str(index) + "]['B']=" + str(df['B'][index]))

使用iterrows(最常用):

>>> for index, row in df.iterrows():
...     print ("df[" + str(index) + "]['B']=" + str(row['B']))

使用itertuples(最快):

>>> for row in df.itertuples():
...     print ("df[" + str(row.Index) + "]['B']=" + str(row.B))

三个选项将显示类似以下内容:

df[0]['B']=125
df[1]['B']=415
df[2]['B']=23
df[3]['B']=456
df[4]['B']=189
df[5]['B']=456
df[6]['B']=12

来源:alphons.io


有一种方法比itertuples还要快三倍,可以参考上面的答案。这个答案是在三年前回答的。尽管如此,在这里仅使用索引还是一种新的方法(虽然不应该使用,但思路很简单)。 - questionto42

26
我注意到Nick Crawford的回答后查看了iterrows,但发现它产生(index, Series)元组。不确定哪个对您最好,但我最终使用itertuples方法解决了我的问题,它产生(index, row_value1...)元组。
还有iterkv,它遍历(column, series)元组。

你可以使用类似 dict(row) 的方法将行转换为可搜索列的集合。 - Carst
4
在我的使用案例中,我发现itertuples要快得多(10倍),因为它不会创建Series对象。 - Kamil Sindi
FYI:iterkv自0.13.1版本起已被弃用。 - JS.
iterrows(): Iterate over the rows of a DataFrame as (index, Series) pairs.... itertuples(): Iterate over the rows of a DataFrame as tuples of the values. This is a lot faster as iterrows(), and is in most cases preferable to use to iterate over the values of a DataFrame. - Nate Anderson

21

1
可能 x 是列名和行变量的混淆名称,尽管我同意 apply 是最简单的方法 :) - Andy Hayden
8
补充一下,apply 也可以用于多列:df['c'] = df[['a','b']].apply(lambda x: 对 x[0] 和 x[1] 进行操作, axis=1) - fantabolous
可以在代码中定义的其他地方使用apply函数吗?这样我们就可以引入一个更复杂的函数。 - user308827
1
我将 x 重命名为 col。更好的名称。 - smci
@user308827,您提供给apply()的函数不一定是lambda表达式,可以是任何可调用对象——比如在其他地方定义的函数或者定义了__call__方法的类等。 - Egor Kraev
显示剩余2条评论

15

正如@joris所指出的那样,iterrowsitertuples慢得多,而且itertuples的速度大约是iterrows的100倍,在一个包含500万条记录的DataFrame中测试了这两种方法的速度,结果是iterrows为1200it/s,itertuples为120000it/s。

如果使用itertuples,请注意for循环中的每个元素都是一个namedtuple,因此要获取每列的值,您可以参考以下示例代码:

>>> df = pd.DataFrame({'col1': [1, 2], 'col2': [0.1, 0.2]},
                      index=['a', 'b'])
>>> df
   col1  col2
a     1   0.1
b     2   0.2
>>> for row in df.itertuples():
...     print(row.col1, row.col2)
...
1, 0.1
2, 0.2

12

当然,迭代DataFrame最快的方法是通过访问底层的numpy ndarray,可以使用df.values(就像你现在做的一样)或者通过分别访问每个列df.column_name.values。如果您还想访问索引,可以使用df.index.values

index = df.index.values
column_of_interest1 = df.column_name1.values
...
column_of_interestk = df.column_namek.values

for i in range(df.shape[0]):
   index_value = index[i]
   ...
   column_value_k = column_of_interest_k[i]

不符合 Pythonic 风格?没错。但速度快。

如果想要从循环中挤出更多性能,您可以考虑使用Cython。Cython 可以让您获得巨大的速度提升(想象一下10倍-100倍)。为了最大化性能,请查看Cython 的内存视图


5
另一个建议是,如果行的子集具有使您能够这样做的共同特征,则将groupby与向量化计算相结合。

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