如何在 Pandas 中遍历 DataFrame 中的行

How to iterate over rows in a DataFrame in Pandas

提问人:Roman 提问时间:5/10/2013 最后编辑:cottontailRoman 更新时间:11/6/2023 访问量:7030073

问:

我有一个 pandas 数据帧,:df

   c1   c2
0  10  100
1  11  110
2  12  120

如何遍历此 DataFrame 的行?对于每一行,我都希望通过列的名称访问其元素(单元格中的值)。例如:

for row in df.rows:
    print(row['c1'], row['c2'])

我发现了一个类似的问题,建议使用以下任一:

  • for date, row in df.T.iteritems():
    
  • for row in df.iterrows():
    

但我不明白这个对象是什么,以及我如何使用它。row

Python pandas 数据帧 循环

评论

170赞 oulenz 10/16/2019
与 cs95 所说的相反,想要迭代 DataFrame 是完全有充分理由的,因此新用户不应该感到气馁。例如,如果要使用每行的值作为输入来执行某些代码。此外,如果您的数据帧相当小(例如少于 1000 个项目),则性能并不是真正的问题。
10赞 cs95 4/18/2023
如果你是这个线程的初学者,并且不熟悉 pandas 库,那么值得退后一步,评估迭代是否确实是你的问题的解决方案。在某些情况下,确实如此。在大多数情况下,事实并非如此。通过让初学者了解矢量化的概念来向他们介绍库是很重要的,这样他们就知道编写“好代码”与“正常工作的代码”之间的区别,并且也知道何时使用哪个。
0赞 Gabriel Staples 10/5/2023
相关新闻: pandas 中的 for 循环真的很糟糕吗?我什么时候应该关心?
0赞 Gabriel Staples 11/24/2023
Смотритетакже: 使用 pandas 遍历数据帧的最有效方法是什么?.

答:

249赞 Wes McKinney 5/24/2012 #1

你应该使用 df.iterrows()。尽管逐行迭代并不是特别有效,因为必须创建对象。Series

评论

9赞 Richard Wong 12/16/2015
我对 df.iterrows()、df.itertuples() 和 zip(df['a'], df['b']) 的时间消耗进行了一些测试,并将结果发布在另一个问题的答案中:stackoverflow.com/a/34311080/2142098
5232赞 waitingkuo 5/10/2013 #2

DataFrame.iterrows 是一个生成器,它同时生成索引和行(作为 Series):

import pandas as pd

df = pd.DataFrame({'c1': [10, 11, 12], 'c2': [100, 110, 120]})
df = df.reset_index()  # make sure indexes pair with number of rows

for index, row in df.iterrows():
    print(row['c1'], row['c2'])
10 100
11 110
12 120

文档的强制性免责声明

遍历 pandas 对象通常很慢。在许多情况下,不需要手动迭代行,可以使用以下方法之一来避免:

  • 寻找矢量化解决方案:许多操作都可以使用内置方法或 NumPy 函数、(布尔)索引等来执行。
  • 当您的函数无法一次处理完整的 DataFrame/Series 时,最好使用 apply() 而不是遍历值。请参阅有关函数应用的文档。
  • 如果需要对值进行迭代操作,但性能很重要,请考虑使用 cython 或 numba 编写内部循环。有关此方法的一些示例,请参阅“增强性能”部分。

如果您有兴趣了解更多信息,本线程中的其他答案将更深入地探讨 iter* 函数的替代方案。

评论

0赞 Gabriel Staples 11/5/2023
虽然有效,但它比最快的技术慢 600 倍左右。在我的回答中,我快速测试并演示了 13 种迭代 Pandas DataFrame 的方法。 是第二慢的。13 种技术中有 11 种比 更快,一旦你看到如何做到这一点的示例,它们中的大多数仍然非常简单。.iterrows().iterrows().iterrows()
129赞 cheekybastard 6/1/2015 #3

还可以用于循环访问行和访问函数的多个列。df.apply()

文档:DataFrame.apply()

def valuation_formula(x, y):
    return x * y * 0.5

df['price'] = df.apply(lambda row: valuation_formula(row['x'], row['y']), axis=1)

请注意,此处与 相同,并且用于将函数应用于每一而不是每一列。如果未指定,则默认行为是将函数应用于每一列axis=1axis='columns'

评论

14赞 gented 4/4/2018
请注意,它不会对行进行“迭代”,而是按行应用函数。如果您确实需要迭代和 indeces,例如在比较不同行的值时,上述代码将不起作用(在这种情况下,您只能进行迭代)。apply
0赞 Gabriel Staples 9/19/2023
@gented,这并不完全正确。例如,要访问上一行中的值,只需添加一个包含上一行值的新列,如下所示: .然后,您可以使用此答案在给定行中访问此变量。dataframe["val_previous"] = dataframe["val"].shift(1)val_previous
183赞 e9t 9/20/2015 #4

虽然是一个不错的选择,但有时可能会更快:iterrows()itertuples()

df = pd.DataFrame({'a': randn(1000), 'b': randn(1000),'N': randint(100, 1000, (1000)), 'x': 'x'})

%timeit [row.a * 2 for idx, row in df.iterrows()]
# => 10 loops, best of 3: 50.3 ms per loop

%timeit [row[1] * 2 for row in df.itertuples()]
# => 1000 loops, best of 3: 541 µs per loop

评论

10赞 Alex 9/21/2015
在这两个示例中,大部分时间差异似乎是由于您似乎对 .iterrows() 命令使用了基于标签的索引,而对 .itertuples() 命令使用了基于整数的索引。
8赞 miradulo 2/14/2017
@AbeMiessler将每行数据装箱到一个系列中,而没有。iterrows()itertuples()
2赞 Ajasja 11/8/2018
i.stack.imgur.com/HBe9o.png 我也获得了 >50 倍的增长(在第二次运行中更改为 attr 访问器时)。
160赞 PJay 9/7/2016 #5

您可以按如下方式使用 df.iloc 函数:

for i in range(0, len(df)):
    print(df.iloc[i]['c1'], df.iloc[i]['c2'])

评论

22赞 Ken Williams 1/19/2018
这是我所知道的唯一有效的技术,如果你想保留数据类型,并按名称引用列。 保留数据类型,但删除任何它不喜欢的名称。 反其道而行之。itertuplesiterrows
568赞 viddik13 12/8/2016 #6

首先考虑是否真的需要循环访问 DataFrame 中的行。有关替代方案,请参阅 cs95 的答案

如果仍需要遍历行,可以使用以下方法。请注意一些重要的警告,这些警告在任何其他答案中都没有提到。

itertuples()应该比iterrows()

但请注意,根据文档(目前为 pandas 0.24.2):

  • iterrows:可能不匹配dtype

    由于 iterrows 为每一行返回一个 Series,因此它不会在行之间保留 dtypes(dtypes 在 DataFrame 的列之间保留)。为了在遍历行时保留 dtypes,最好使用 itertuples(),它返回值的命名元组,通常比 iterrows() 快得多

  • iterrows:不修改行

    你永远不应该修改你正在迭代的东西。这并不能保证在所有情况下都有效。根据数据类型,迭代器返回的是副本而不是视图,写入该视图将不起作用。

    请改用 DataFrame.apply():

    new_df = df.apply(lambda x: x * 2, axis=1)
    
  • 迭代:

    如果列名是无效的 Python 标识符、重复或以下划线开头,则列名将重命名为位置名称。对于大量列 (>255),将返回常规元组。

有关更多详细信息,请参阅有关迭代的 pandas 文档

评论

8赞 Brian Burns 6/29/2018
注意:您也可以说在行迭代器中仅包含某些列。for row in df[['c1','c2']].itertuples(index=True, name=None):
0赞 Muhammad Yasirroni 12/5/2021
我不知道为什么,但在我的用例中使用 make 速度提高了 50%。name=Noneitertuples
24赞 Pedro Lobito 3/12/2017 #7

要循环 中的所有行,您可以使用:dataframe

for x in range(len(date_example.index)):
    print date_example['Date'].iloc[x]

评论

0赞 cs95 4/19/2019
如果要实现此目的,请调用 df.columns.get_loc 以获取日期列的整数索引位置(在循环外部),然后在内部使用单个 iloc 索引调用。
23赞 Grag2015 11/2/2017 #8
 for ind in df.index:
     print df['c1'][ind], df['c2'][ind]

评论

6赞 cs95 4/19/2019
这就是链式索引。不要用这个!
27赞 piRSquared 11/7/2017 #9

您可以编写自己的迭代器来实现namedtuple

from collections import namedtuple

def myiter(d, cols=None):
    if cols is None:
        v = d.values.tolist()
        cols = d.columns.values.tolist()
    else:
        j = [d.columns.get_loc(c) for c in cols]
        v = d.values[:, j].tolist()

    n = namedtuple('MyTuple', cols)

    for line in iter(v):
        yield n(*line)

这直接相当于 .我的目标是以更高的效率执行相同的任务。pd.DataFrame.itertuples


对于带有我的函数的给定数据帧:

list(myiter(df))

[MyTuple(c1=10, c2=100), MyTuple(c1=11, c2=110), MyTuple(c1=12, c2=120)]

或者用:pd.DataFrame.itertuples

list(df.itertuples(index=False))

[Pandas(c1=10, c2=100), Pandas(c1=11, c2=110), Pandas(c1=12, c2=120)]

全面的测试 我们测试
使所有列都可用并子集列。

def iterfullA(d):
    return list(myiter(d))

def iterfullB(d):
    return list(d.itertuples(index=False))

def itersubA(d):
    return list(myiter(d, ['col3', 'col4', 'col5', 'col6', 'col7']))

def itersubB(d):
    return list(d[['col3', 'col4', 'col5', 'col6', 'col7']].itertuples(index=False))

res = pd.DataFrame(
    index=[10, 30, 100, 300, 1000, 3000, 10000, 30000],
    columns='iterfullA iterfullB itersubA itersubB'.split(),
    dtype=float
)

for i in res.index:
    d = pd.DataFrame(np.random.randint(10, size=(i, 10))).add_prefix('col')
    for j in res.columns:
        stmt = '{}(d)'.format(j)
        setp = 'from __main__ import d, {}'.format(j)
        res.at[i, j] = timeit(stmt, setp, number=100)

res.groupby(res.columns.str[4:-1], axis=1).plot(loglog=True);

enter image description here

enter image description here

评论

5赞 James L. 12/2/2017
对于不想阅读代码的人来说:蓝线是,橙线是通过收益块的迭代器列表。 没有比较。intertuplesinterrows
6赞 James L. 12/2/2017 #10

您还可以进行 NumPy 索引以获得更大的速度。它不是真正的迭代,但对于某些应用程序来说,它比迭代要好得多。

subset = row['c1'][0:5]
all = row['c1'][:]

您可能还想将其转换为数组。这些索引/选择应该已经像 NumPy 数组一样工作,但我遇到了问题并需要强制转换

np.asarray(all)
imgs[:] = cv2.resize(imgs[:], (224,224) ) # Resize every image in an hdf5 file
53赞 Lucas B 1/17/2018 #11

我一直在寻找如何迭代行,并到此结束:

for i, row in df.iterrows():
    for j, column in row.iteritems():
        print(column)
15赞 Herpes Free Engineer 4/23/2018 #12

为了循环 a 中的所有行并方便地使用每行的值,可以转换为 s。例如:dataframenamedtuplesndarray

df = pd.DataFrame({'col1': [1, 2], 'col2': [0.1, 0.2]}, index=['a', 'b'])

遍历这些行:

for row in df.itertuples(index=False, name='Pandas'):
    print np.asarray(row)

结果如下:

[ 1.   0.1]
[ 2.   0.2]

请注意,如果 ,则索引被添加为元组的第一个元素,这对于某些应用程序来说可能是不可取的。index=True

17赞 Zach 6/28/2018 #13

有时一个有用的模式是:

# Borrowing @KutalmisB df example
df = pd.DataFrame({'col1': [1, 2], 'col2': [0.1, 0.2]}, index=['a', 'b'])
# The to_dict call results in a list of dicts
# where each row_dict is a dictionary with k:v pairs of columns:value for that row
for row_dict in df.to_dict(orient='records'):
    print(row_dict)

其结果是:

{'col1':1.0, 'col2':0.1}
{'col1':2.0, 'col2':0.2}
10赞 shubham ranjan 1/19/2019 #14

有很多方法可以遍历 Pandas 数据帧中的行。一种非常简单直观的方法是:

df = pd.DataFrame({'A':[1, 2, 3], 'B':[4, 5, 6], 'C':[7, 8, 9]})
print(df)
for i in range(df.shape[0]):
    # For printing the second column
    print(df.iloc[i, 1])

    # For printing more than one columns
    print(df.iloc[i, [0, 2]])
12赞 Hossein Kalbasi 2/27/2019 #15

对于查看和修改值,我会使用 .在 for 循环中,通过使用元组解包(参见示例: ),我使用 for 仅查看值,并在想要修改值时与方法一起使用。如前面的答案所述,在这里你不应该修改你正在迭代的东西。iterrows()i, rowrowiloc

for i, row in df.iterrows():
    df_column_A = df.loc[i, 'A']
    if df_column_A == 'Old_Value':
        df_column_A = 'New_value'  

这里的 in the loop 是该行的副本,而不是它的视图。因此,您不应该编写类似 的东西,它不会修改 DataFrame。但是,您可以使用 和 并指定 DataFrame 来执行工作。rowrow['A'] = 'New_Value'iloc

4赞 mjr2000 3/17/2019 #16

此示例使用 iloc 隔离数据框中的每个数字。

import pandas as pd

 a = [1, 2, 3, 4]
 b = [5, 6, 7, 8]

 mjr = pd.DataFrame({'a':a, 'b':b})

 size = mjr.shape

 for i in range(size[0]):
     for j in range(size[1]):
         print(mjr.iloc[i, j])
2393赞 cs95 4/7/2019 #17

如何在 Pandas 中遍历 DataFrame 中的行

答:不要*

Pandas 中的迭代是一种反模式,只有在用尽所有其他选项时才应该这样做。您不应使用名称中带有“”的任何函数超过几千行,否则您将不得不习惯大量的等待。iter

是否要打印 DataFrame?使用 DataFrame.to_string()。

你想计算一些东西吗?在这种情况下,请按以下顺序搜索方法(从此处修改的列表):

  1. 矢 量化
  2. Cython 例程
  3. 列表推导式(香草循环)for
  4. DataFrame.apply(): i) 可以在 Cython 中执行的缩减,ii) Python 空间中的迭代
  5. items(iteritems() (自 v1.5.0 起已弃用)
  6. DataFrame.itertuples()
  7. DataFrame.iterrows()

iterrows和(在回答这个问题时都获得了许多票数)应该在非常罕见的情况下使用,例如生成用于顺序处理的行对象/名称元组,这实际上是这些函数唯一有用的东西。itertuples

诉诸权威

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

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

* 它实际上比“不要”要复杂一些。df.iterrows() 是这个问题的正确答案,但“矢量化你的操作”是更好的答案。我承认,在某些情况下,迭代是无法避免的(例如,某些操作的结果取决于为前一行计算的值)。但是,需要对库有一定的了解才能知道何时。如果您不确定是否需要迭代解决方案,您可能不需要。PS:要了解更多关于我写这个答案的理由,请跳到最底部。


比循环更快:矢量化Cython

大量的基本操作和计算被 pandas “矢量化”(通过 NumPy 或通过 Cythonized 函数)。这包括算术、比较、(大多数)约简、重塑(例如透视)、联接和分组操作。浏览有关基本功能的文档,为您的问题找到合适的矢量化方法。

如果不存在,请随意使用自定义 Cython 扩展编写自己的扩展。


下一个最好的事情:列表推导式*

如果 1) 没有可用的矢量化解决方案,2) 性能很重要,但还不够重要,不足以解决代码化问题的麻烦,以及 3) 您正在尝试对代码执行元素转换,则列表推导式应该是您的下一个停靠港。有大量证据表明,对于许多常见的 Pandas 任务来说,列表推导足够快(有时甚至更快)。

公式很简单,

# Iterating over one column - `f` is some function that processes your data
result = [f(x) for x in df['col']]

# Iterating over two columns, use `zip`
result = [f(x, y) for x, y in zip(df['col1'], df['col2'])]

# Iterating over multiple columns - same data type
result = [f(row[0], ..., row[n]) for row in df[['col1', ...,'coln']].to_numpy()]

# Iterating over multiple columns - differing data type
result = [f(row[0], ..., row[n]) for row in zip(df['col1'], ..., df['coln'])]

如果可以将业务逻辑封装到函数中,则可以使用调用该函数的列表推导式。您可以通过原始 Python 代码的简单性和速度使任意复杂的事情发挥作用。

警告

列表推导式假设您的数据易于使用 - 这意味着您的数据类型是一致的,并且您没有 NaN,但这并不总是可以保证的。

  1. 第一个更明显,但在处理 NaN 时,如果存在内置的 pandas 方法,请首选它们(因为它们具有更好的极端情况处理逻辑),或者确保您的业务逻辑包含适当的 NaN 处理逻辑。
  2. 在处理混合数据类型时,应进行循环访问,而不是因为后者将数据隐式上调到最常见的类型。例如,如果 A 是数字而 B 是字符串,则会将整个数组转换为字符串,这可能不是您想要的。幸运的是,将列 ping 在一起是最直接的解决方法。zip(df['A'], df['B'], ...)df[['A', 'B']].to_numpy()to_numpy()zip

*您的里程可能因上述注意事项部分所述的原因而有所不同。


一个明显的例子

让我们用一个添加两个 pandas 列的简单示例来演示差异。这是一个可矢量化的操作,因此很容易对比上面讨论的方法的性能。A + B

基准测试代码,供您参考。底部的线测量的是用 numpandas 编写的函数,这是一种 Pandas 风格,它与 NumPy 大量混合以挤出最大性能。除非您知道自己在做什么,否则应避免编写 numpandas 代码。尽可能坚持使用 API(即,首选 )。vecvec_numpy

然而,我应该提一下,它并不总是如此简单。有时,“什么是最佳操作方法”的答案是“这取决于您的数据”。我的建议是在确定一种方法之前,先对你的数据测试不同的方法。


我的个人意见*

对ITER系列的各种替代品进行的大多数分析都是从性能的角度进行的。但是,在大多数情况下,您通常会处理一个大小合理的数据集(不超过几千行或 100K 行),并且性能将仅次于解决方案的简单性/可读性。

这是我个人在选择用于解决问题的方法时的偏好。

对于新手:

矢量化(如果可能);apply();列表推导式;itertuples()/iteritems();iterrows();Cython

对于更有经验的人:

矢量化(如果可能);apply();列表推导式;Cython;itertuples()/iteritems();iterrows()

矢量化是解决任何可以矢量化的问题的最惯用的方法。始终寻求矢量化!如有疑问,请查阅文档,或在 Stack Overflow 上查找有关您特定任务的现有问题。

我确实倾向于在我的很多帖子中继续谈论它有多糟糕,但我确实承认,对于初学者来说,更容易理解它正在做的事情。此外,在我的这篇文章中还解释了相当多的用例。applyapply

Cython 在列表中排名较低,因为它需要更多的时间和精力才能正确完成。通常,您永远不需要使用 pandas 编写代码,这些代码需要这种性能水平,即使是列表理解也无法满足。

*与任何个人意见一样,请随身携带盐!


延伸阅读

* Pandas 字符串方法是“矢量化”的,因为它们在系列中指定,但对每个元素进行操作。底层机制仍然是迭代的,因为字符串操作本质上很难矢量化。


我为什么写这个答案

我从新用户那里注意到的一个常见趋势是提出“我怎样才能迭代我的 df 来做 X?显示在循环中执行某些操作时调用的代码。原因如下。尚未了解矢量化概念的库新用户可能会将解决其问题的代码设想为遍历其数据以执行某些操作。不知道如何迭代 DataFrame,他们做的第一件事就是谷歌一下,最终来到这个问题。然后,他们看到公认的答案告诉他们如何做,他们闭上眼睛运行这段代码,而没有首先质疑迭代是否是正确的做法。iterrows()for

这个答案的目的是帮助新用户理解迭代不一定是每个问题的解决方案,并且可能存在更好、更快和更惯用的解决方案,并且值得投入时间去探索它们。我不是要发起一场迭代与矢量化的战争,但我希望新用户在使用这个库为他们的问题开发解决方案时得到通知。

最后......一个 TLDR 来总结这篇文章

enter image description here

评论

0赞 b_dev 9/20/2023
迭代可以成为调试过程中提供帮助的工具,例如使用 ipdb,尤其是在初始开发和理解边缘情况期间。
0赞 Gabriel Staples 9/22/2023
这里的双括号语法是什么?这是什么意思,它在哪里正式记录?df[['col1', ...,'coln']].to_numpy()
1赞 Timus 9/22/2023
@GabrielStaples 看这里:“......您可以将列列表传递给 [],以按该顺序选择列。..."
0赞 Gabriel Staples 10/11/2023
我用了很多你说的话,然后用它跑了。看看我想出的这 13 种技术以及显示它们速度差异的图。纯矢量化速度提高了 1400 倍。列表理解也相当不错!
15赞 Zeitgeist 10/17/2019 #18

有一种方法可以迭代抛出行,同时获取 DataFrame 作为回报,而不是 Series。我没有看到有人提到您可以将索引作为列表传递,以便将行作为 DataFrame 返回:

for i in range(len(df)):
    row = df.iloc[[i]]

请注意双括号的用法。这将返回一个包含单行的 DataFrame。

3赞 morganics 12/10/2019 #19

某些库(例如我使用的 Java 互操作库)要求一次在一行中传递值,例如,如果流式传输数据。为了复制流式处理的性质,我逐个“流式处理”我的数据帧值,我写了以下内容,不时派上用场。

class DataFrameReader:
  def __init__(self, df):
    self._df = df
    self._row = None
    self._columns = df.columns.tolist()
    self.reset()
    self.row_index = 0

  def __getattr__(self, key):
    return self.__getitem__(key)

  def read(self) -> bool:
    self._row = next(self._iterator, None)
    self.row_index += 1
    return self._row is not None

  def columns(self):
    return self._columns

  def reset(self) -> None:
    self._iterator = self._df.itertuples()

  def get_index(self):
    return self._row[0]

  def index(self):
    return self._row[0]

  def to_dict(self, columns: List[str] = None):
    return self.row(columns=columns)

  def tolist(self, cols) -> List[object]:
    return [self.__getitem__(c) for c in cols]

  def row(self, columns: List[str] = None) -> Dict[str, object]:
    cols = set(self._columns if columns is None else columns)
    return {c : self.__getitem__(c) for c in self._columns if c in cols}

  def __getitem__(self, key) -> object:
    # the df index of the row is at index 0
    try:
        if type(key) is list:
            ix = [self._columns.index(key) + 1 for k in key]
        else:
            ix = self._columns.index(key) + 1
        return self._row[ix]
    except BaseException as e:
        return None

  def __next__(self) -> 'DataFrameReader':
    if self.read():
        return self
    else:
        raise StopIteration

  def __iter__(self) -> 'DataFrameReader':
    return self

可以使用:

for row in DataFrameReader(df):
  print(row.my_column_name)
  print(row.to_dict())
  print(row['my_column_name'])
  print(row.tolist())

并保留正在迭代的行的值/名称映射。显然,它比使用如上所述的 apply 和 Cython 慢得多,但在某些情况下是必要的。

72赞 Romain Capron 12/20/2019 #20

如何高效迭代

如果你真的必须迭代一个 Pandas ,你可能希望避免使用 iterrows()。有不同的方法,通常的方法远非最好的。'itertuples()'' 可以快 100 倍。DataFrameiterrows()

总之:

  • 作为一般规则,使用 .特别是,当您的列数固定且列数少于 255 时。请参阅下面的项目符号 (3)。df.itertuples(name=None)
  • 否则,请使用 ,除非列具有特殊字符(如空格或 )。请参阅下面的项目符号 (2)。df.itertuples()-
  • 即使您的数据帧有奇怪的列,也可以使用下面的最后一个示例。请参阅下面的项目符号 (4)。itertuples()
  • 仅当无法使用上述任何解决方案时才使用。请参阅下面的项目符号 (1)。iterrows()

在 Pandas 中迭代行的不同方法:DataFrame

首先,为了在下面的所有示例中使用,生成一个包含 100 万行和 4 列的随机数据帧,如下所示:

df = pd.DataFrame(np.random.randint(0, 100, size=(1000000, 4)), columns=list('ABCD'))
print(df)

所有这些示例的输出都显示在底部。

  1. 通常很方便,但该死的慢:iterrows()

    start_time = time.clock()
    result = 0
    for _, row in df.iterrows():
        result += max(row['B'], row['C'])
    
    total_elapsed_time = round(time.clock() - start_time, 2)
    print("1. Iterrows done in {} seconds, result = {}".format(total_elapsed_time, result))
    
  2. 使用默认名称已经快得多,但它不适用于列名,例如(如果您的列是重复的,或者如果列名不能简单地转换为 Python 变量名,则应避免使用此方法)。itertuples()My Col-Name is very Strange

    start_time = time.clock()
    result = 0
    for row in df.itertuples(index=False):
        result += max(row.B, row.C)
    
    total_elapsed_time = round(time.clock() - start_time, 2)
    print("2. Named Itertuples done in {} seconds, result = {}".format(total_elapsed_time, result))
    
  3. 通过设置使用无名甚至更快,但不是很方便,因为您必须为每列定义一个变量。itertuples()name=None

    start_time = time.clock()
    result = 0
    for(_, col1, col2, col3, col4) in df.itertuples(name=None):
        result += max(col2, col3)
    
    total_elapsed_time = round(time.clock() - start_time, 2)
    print("3. Itertuples done in {} seconds, result = {}".format(total_elapsed_time, result))
    
  4. 最后,使用 polyvalent 比前面的示例慢,但您不必为每列定义一个变量,它适用于列名,例如 .itertuples()My Col-Name is very Strange

    start_time = time.clock()
    result = 0
    for row in df.itertuples(index=False):
        result += max(row[df.columns.get_loc('B')], row[df.columns.get_loc('C')])
    
    total_elapsed_time = round(time.clock() - start_time, 2)
    print("4. Polyvalent Itertuples working even with special characters in the column name done in {} seconds, result = {}".format(total_elapsed_time, result))
    

上述所有代码和示例的输出:

         A   B   C   D
0       41  63  42  23
1       54   9  24  65
2       15  34  10   9
3       39  94  82  97
4        4  88  79  54
...     ..  ..  ..  ..
999995  48  27   4  25
999996  16  51  34  28
999997   1  39  61  14
999998  66  51  27  70
999999  51  53  47  99

[1000000 rows x 4 columns]

1. Iterrows done in 104.96 seconds, result = 66151519
2. Named Itertuples done in 1.26 seconds, result = 66151519
3. Itertuples done in 0.94 seconds, result = 66151519
4. Polyvalent Itertuples working even with special characters in the column name done in 2.94 seconds, result = 66151519

@Gabriel斯台普斯在他的回答中绘制了这些结果:

enter image description here

另请参阅

  1. 这篇文章是 iterrows() 和 itertuples() 之间非常有趣的比较
17赞 bug_spray 3/25/2020 #21

更新:cs95 更新了他的答案,包括普通的 numpy 矢量化。你可以简单地参考他的回答。


cs95 表明 Pandas 矢量化远远优于其他 Pandas 方法,用于计算数据帧的内容。

我想补充一点,如果您首先将数据帧转换为 NumPy 数组,然后使用矢量化,它甚至比 Pandas 数据帧矢量化更快(这包括将其转换回数据帧系列的时间)。

如果将以下函数添加到 cs95 的基准测试代码中,这将变得非常明显:

def np_vectorization(df):
    np_arr = df.to_numpy()
    return pd.Series(np_arr[:,0] + np_arr[:,1], index=df.index)

def just_np_vectorization(df):
    np_arr = df.to_numpy()
    return np_arr[:,0] + np_arr[:,1]

Enter image description here

评论

15赞 artoby 6/2/2020 #22

总之

  • 如果可能,使用矢量化
  • 如果操作无法矢量化 - 使用列表推导式
  • 如果您需要一个表示整行的对象 - 请使用 itertuples
  • 如果以上操作太慢 - 请尝试 swifter.apply
  • 如果它仍然太慢 - 尝试 Cython 例程

基准

Benchmark of iteration over rows in a Pandas DataFrame

评论

0赞 JohnE 1/12/2023
Cython 将帮助 ofc,但 numpy/numba 对大多数人来说可能更容易访问
2赞 imanzabet 10/3/2020 #23

除了这篇文章中的伟大答案外,我将提出分而治之的方法,我写这个答案不是为了废除其他伟大的答案,而是用另一种对我有效有效的方法来实现它们。它有两个步骤和 pandas 数据帧:splittingmerging

分而治之的优点:

  • 无需使用矢量化或任何其他方法将 DataFrame 的类型转换为另一种类型
  • 你不需要对你的代码进行Cythonize,这通常会花费你额外的时间
  • 两者,就我而言,在整个数据帧上具有相同的性能iterrows()itertuples()
  • 取决于你对切片的选择,你将能够成倍地加快迭代速度。越高,迭代过程越快。indexindex

分而治之的缺点:

  • 不应依赖同一数据帧和不同切片的迭代过程。这意味着如果你想从其他切片读取或写入,可能很难做到这一点。

===================分而治之的方法=================

第 1 步:分割/切片

在此步骤中,我们将在整个数据帧上划分迭代。假设您要将 CSV 文件读入 pandas df 中,然后对其进行迭代。在可能的情况下,我有 5,000,000 条记录,我将将其拆分为 100,000 条记录。

注意:我需要重申,正如本页其他解决方案中解释的其他运行时分析一样,“记录数”在 df 上的搜索中与“运行时”的比例呈指数级。根据我的数据基准,以下是结果:

Number of records | Iteration rate [per second]
========================================
100,000           | 500
500,000           | 200
1,000,000         | 50
5,000,000         | 20

第 2 步:合并

这将是一个简单的步骤,只需将所有写入的 CSV 文件合并到一个数据帧中,然后将其写入更大的 CSV 文件即可。

示例代码如下:

# Step 1 (Splitting/Slicing)
import pandas as pd
df_all = pd.read_csv('C:/KtV.csv')
df_index = 100000
df_len = len(df)
for i in range(df_len // df_index + 1):
    lower_bound = i * df_index
    higher_bound = min(lower_bound + df_index, df_len)
    # Splitting/slicing df (make sure to copy() otherwise it will be a view
    df = df_all[lower_bound:higher_bound].copy()
    '''
    Write your iteration over the sliced df here
    using iterrows() or intertuples() or ...
    '''
    # Writing into CSV files
    df.to_csv('C:/KtV_prep_' + str(i) + '.csv')



# Step 2 (Merging)
filename = 'C:/KtV_prep_'
df = (pd.read_csv(f) for f in [filename + str(i) + '.csv' for i in range(ktv_len // ktv_index + 1)])
df_prep_all = pd.concat(df)
df_prep_all.to_csv('C:/KtV_prep_all.csv')

参考:

对 datafreame 进行高效迭代的方式

将 CSV 文件连接成一个 Pandas DataFrame

评论

0赞 tbrugere 11/8/2023
是什么让您认为这种方法比直接对整个 DataFrame 执行 IterRows 更快?它具有相同的内存和时间复杂度,并且具有更多的操作(尤其是 CSV IO 应该非常慢)。此外,这不是严格意义上的分而治之的方法。分而治之是一种递归范式。您只需将数据帧切割成df_index大小的子表并使用这些子表即可。
9赞 François B. 11/3/2020 #24

最简单的方法,使用该功能apply

def print_row(row):
   print row['c1'], row['c2']

df.apply(lambda row: print_row(row), axis=1)
13赞 JohnE 12/22/2020 #25

有时循环确实比矢量化代码更好

正如这里的许多答案正确指出的那样,你在 Pandas 中的默认计划应该是编写矢量化代码(带有隐式循环),而不是自己尝试显式循环。但问题仍然存在,你是否应该在 Pandas 中编写循环,如果是这样,在这种情况下循环的最佳方式是什么。

我相信至少在一种一般情况下,循环是合适的:当您需要以某种复杂的方式计算依赖于其他行中的值的函数时。在这种情况下,循环代码通常比矢量化代码更简单、更易读且更不容易出错。

循环代码甚至可能更快,正如您将在下面看到的那样,因此在速度至关重要的情况下,循环可能很有意义。但实际上,这些只是你可能一开始就应该使用 numpy/numba(而不是 Pandas)的情况的子集,因为优化的 numpy/numba 几乎总是比 Pandas 更快。

让我们用一个例子来说明这一点。假设您要获取一列的累积总和,但每当其他列等于零时重置它:

import pandas as pd
import numpy as np

df = pd.DataFrame( { 'x':[1,2,3,4,5,6], 'y':[1,1,1,0,1,1]  } )

#   x  y  desired_result
#0  1  1               1
#1  2  1               3
#2  3  1               6
#3  4  0               4
#4  5  1               9
#5  6  1              15

这是一个很好的例子,你当然可以写一行 Pandas 来实现这一点,尽管它不是特别可读,特别是如果你对 Pandas 还没有相当的经验:

df.groupby( (df.y==0).cumsum() )['x'].cumsum()

对于大多数情况来说,这将足够快,尽管您也可以通过避免 来编写更快的代码,但它的可读性可能会更低。groupby

或者,如果我们把它写成一个循环呢?您可以使用 NumPy 执行如下操作:

import numba as nb

@nb.jit(nopython=True)  # Optional
def custom_sum(x,y):
    x_sum = x.copy()
    for i in range(1,len(df)):
        if y[i] > 0: x_sum[i] = x_sum[i-1] + x[i]
    return x_sum

df['desired_result'] = custom_sum( df.x.to_numpy(), df.y.to_numpy() )

诚然,将 DataFrame 列转换为 NumPy 数组需要一些开销,但核心代码只是一行代码,即使您对 Pandas 或 NumPy 一无所知,您也可以阅读:

if y[i] > 0: x_sum[i] = x_sum[i-1] + x[i]

而且这段代码实际上比矢量化代码更快。在一些 100,000 行的快速测试中,上述方法比 groupby 方法快 10 倍左右。请注意,速度的一个键是 numba,这是可选的。如果没有 “@nb.jit” 行,循环代码实际上比 groupby 方法慢 10 倍左右。

显然,这个例子很简单,你可能更喜欢一行熊猫,而不是写一个循环及其相关的开销。然而,这个问题还有更复杂的版本,NumPy/numba 循环方法的可读性或速度可能是有意义的。

0赞 dna-data 3/5/2021 #26

用。例如,使用数据帧“rows_df”:df.iloc[]

Enter image description here

若要从特定行获取值,可以将 DataFrame 转换为 ndarray。

然后选择行和列值,如下所示:

Enter image description here

评论

14赞 Scratte 3/7/2021
请考虑不要在图像中发布代码,而是在代码块中作为文本发布代码。
2赞 tbrugere 5/26/2021 #27

正如公认的答案所述,将函数应用于行的最快方法是使用矢量化函数,即所谓的 NumPy(通用函数)。ufuncs

但是,当您要应用的函数尚未在 NumPy 中实现时,您应该怎么做?

好吧,使用 中的装饰器,您可以轻松地直接在 Python 中创建 ufunc,如下所示:vectorizenumba

from numba import vectorize, float64

@vectorize([float64(float64)])
def f(x):
    #x is your line, do something with it, and return a float

此函数的文档如下:创建 NumPy 通用函数

5赞 Ashvani Jaiswal 7/3/2021 #28

df.iterrows()返回 其中 是 和 是 。tuple(a, b)aindexbrow

8赞 Ernesto Elsäßer 7/28/2021 #29

可能是最优雅的解决方案(但肯定不是最有效的):

for row in df.values:
    c2 = row[1]
    print(row)
    # ...

for c1, c2 in df.values:
    # ...

请注意:

  • 该文档明确建议改用.to_numpy()
  • 在最坏的情况下,生成的 NumPy 数组将具有适合所有列的 dtypeobject
  • 首先有充分的理由不使用循环

尽管如此,我认为这个选项应该包括在这里,作为(人们应该认为的)微不足道问题的直接解决方案。

42赞 Sachin 11/24/2021 #30

我们有多种选择可以做同样的事情,很多人都分享了他们的答案。

我发现以下两种方法既简单又有效:

  1. DataFrame.iterrows()
  2. DataFrame.itertuples()

例:

 import pandas as pd
 inp = [{'c1':10, 'c2':100}, {'c1':11,'c2':110}, {'c1':12,'c2':120}]
 df = pd.DataFrame(inp)
 print (df)

 # With the iterrows method

 for index, row in df.iterrows():
     print(row["c1"], row["c2"])

 # With the itertuples method

 for row in df.itertuples(index=True, name='Pandas'):
     print(row.c1, row.c2)

注意:itertuples() 应该比 iterrows() 快

4赞 gru 2/23/2022 #31

免責聲明:尽管这里有很多答案建议不要使用迭代(循环)方法(我基本上同意),但我仍然认为它是以下情况的合理方法:

使用来自 API 的数据扩展数据帧

假设您有一个包含不完整用户数据的大型数据帧。现在,您必须使用其他列(例如,用户的 和 .agegender

这两个值都必须从后端 API 获取。我假设 API 不提供“批处理”端点(它将一次接受多个用户 ID)。否则,您应该只调用一次 API。

网络请求的成本(等待时间)远远超过了数据帧的迭代。我们谈论的是数百毫秒的网络往返时间,而使用替代迭代方法的收益可以忽略不计。

每行一个昂贵的网络请求

因此,在这种情况下,我绝对更喜欢使用迭代方法。尽管网络请求的开销很高,但可以保证仅针对数据帧中的每一行触发一次。下面是使用 DataFrame.iterrows 的示例:

for index, row in users_df.iterrows():
  user_id = row['user_id']

  # Trigger expensive network request once for each row
  response_dict = backend_api.get(f'/api/user-data/{user_id}')

  # Extend dataframe with multiple data from response
  users_df.at[index, 'age'] = response_dict.get('age')
  users_df.at[index, 'gender'] = response_dict.get('gender')
13赞 Jordy 2/21/2023 #32

我建议使用(source)来迭代所有pandas单元格。df.at[row, column]

例如:

for row in range(len(df)):
  print(df.at[row, 'c1'], df.at[row, 'c2'])

输出将是:

10 100
11 110
12 120

奖金

您还可以使用 修改单元格的值。df.at[row, column] = newValue

for row in range(len(df)):
  df.at[row, 'c1'] = 'data-' + str(df.at[row, 'c1'])
  print(df.at[row, 'c1'], df.at[row, 'c2']) 

输出将是:

data-10 100
data-11 110
data-12 120
5赞 cottontail 4/12/2023 #33

1. 迭代和访问df.indexat[]

一种非常可读的方法是遍历索引(如@Grag2015所建议的那样)。但是,为了提高效率,而不是在那里使用链式索引:at

for ind in df.index:
    print(df.at[ind, 'col A'])

这种方法的优点是,即使索引不是 。请参阅以下示例:for i in range(len(df))RangeIndex

df = pd.DataFrame({'col A': list('ABCDE'), 'col B': range(5)}, index=list('abcde'))

for ind in df.index:
    print(df.at[ind, 'col A'], df.at[ind, 'col B'])    # <---- OK
    df.at[ind, 'col C'] = df.at[ind, 'col B'] * 2      # <---- can assign values
        
for ind in range(len(df)):
    print(df.at[ind, 'col A'], df.at[ind, 'col B'])    # <---- KeyError

如果需要一行的整数位置(例如,获取上一行的值),请将其包装为:enumerate()

for i, ind in enumerate(df.index):
    prev_row_ind = df.index[i-1] if i > 0 else df.index[i]
    df.at[ind, 'col C'] = df.at[prev_row_ind, 'col B'] * 2

2.与get_locitertuples()

尽管它比 快得多,但其主要缺点是,如果列标签中包含空格,它会破坏列标签(例如 becomes etc.),这使得在迭代中很难访问值。iterrows()itertuples()'col C'_1

可用于获取列标签的整数位置,并使用它来为命名元组编制索引。请注意,每个命名元组的第一个元素是索引标签,因此要按整数位置正确访问列,您必须将 1 添加到返回的任何内容中,或者在开始时解压缩元组。df.columns.get_loc()get_loc

df = pd.DataFrame({'col A': list('ABCDE'), 'col B': range(5)}, index=list('abcde'))

for row in df.itertuples(name=None):
    pos = df.columns.get_loc('col B') + 1              # <---- add 1 here
    print(row[pos])


for ind, *row in df.itertuples(name=None):
#   ^^^^^^^^^    <---- unpacked here
    pos = df.columns.get_loc('col B')                  # <---- already unpacked
    df.at[ind, 'col C'] = row[pos] * 2
    print(row[pos])

3. 转换为字典并遍历dict_items

循环遍历 DataFrame 的另一种方法是将其转换为字典并遍历 or 。orient='index'dict_itemsdict_values

df = pd.DataFrame({'col A': list('ABCDE'), 'col B': range(5)})

for row in df.to_dict('index').values():
#                             ^^^^^^^^^         <--- iterate over dict_values
    print(row['col A'], row['col B'])


for index, row in df.to_dict('index').items():
#                                    ^^^^^^^^   <--- iterate over dict_items
    df.at[index, 'col A'] = row['col A'] + str(row['col B'])

这不会破坏 dtypes like ,不会破坏列标签 like 和 不可知论列的数量(如果有很多列,很快就会变得麻烦)。iterrowsitertupleszip(df['col A'], df['col B'], ...)


最后,正如@cs95提到的,尽可能避免循环。特别是如果你的数据是数字的,如果你稍微挖掘一下,库中就会有一个优化的方法来执行你的任务。

也就是说,在某些情况下,迭代比矢量化操作更有效。一个常见的此类任务是将 pandas 数据帧转储到嵌套的 json 中。至少从 pandas 1.5.3 开始,在这种情况下,循环比任何涉及方法的矢量化操作都要快得多。itertuples()groupby.apply

12赞 Gabriel Staples 10/11/2023 #34

关键要点:

  1. 使用矢量化。
  2. 快速分析您的代码!不要因为你认为它更快而认为它更快;速度分析并证明它更快。结果可能会让你大吃一惊。

如何在不迭代的情况下迭代 Pandas sDataFrame

经过几个星期的思考,我想出了以下几个结论:

以下是迭代 Pandas DataFrame的 13 种技术。如您所见,所需的时间差异很大。最快的技术比最慢的技术快 ~1363 倍正如@cs95在这里所说,关键的一点是不要迭代!请改用矢量化(“数组编程”)。这实际上意味着您应该直接在数学公式中使用数组,而不是尝试手动遍历数组。当然,底层对象必须支持这一点,但 Numpy 和 Pandas 都支持

在 Pandas 中使用矢量化的方法有很多种,您可以在图和我下面的示例代码中看到这些方法。当直接使用数组时,底层循环仍然会发生,但(我认为)在非常优化的底层 C 代码中,而不是通过原始 Python。

结果

测试了 13 种技术,编号为 1 至 13。技术编号和名称位于每个条形图的下方。总计算时间位于每根柱线上方。下面是一个乘数,显示它比最右边最快的技术花了多长时间:

来自我的eRCaGuy_hello_world存储库中的pandas_dataframe_iteration_vs_vectorization_vs_list_comprehension_speed_tests.svg(由此代码生成)。

enter image description here

总结

列表推导矢量化(可能使用布尔索引)是您真正需要的。

使用列表推导(良好)和矢量化(最佳)。我认为纯矢量化总是可能的,但在复杂的计算中可能需要额外的工作。在这个答案中搜索“布尔索引”、“布尔数组”“布尔掩码”(这三者都是一回事),看看一些更复杂的情况,在这些情况下,可以使用纯矢量化。

以下是 13 种技术,按最快到最慢的顺序列出。我建议永远不要使用最后(最慢)的 3 到 4 种技术。

  1. 技术8:8_pure_vectorization__with_df.loc[]_boolean_array_indexing_for_if_statment_corner_case
  2. 技术 6:6_vectorization__with_apply_for_if_statement_corner_case
  3. 技术 7:7_vectorization__with_list_comprehension_for_if_statment_corner_case
  4. 技术 11:11_list_comprehension_w_zip_and_direct_variable_assignment_calculated_in_place
  5. 技术 10:10_list_comprehension_w_zip_and_direct_variable_assignment_passed_to_func
  6. 技术 12:12_list_comprehension_w_zip_and_row_tuple_passed_to_func
  7. 技术 5:5_itertuples_in_for_loop
  8. 技术 13:13_list_comprehension_w__to_numpy__and_direct_variable_assignment_passed_to_func
  9. 技术 9:9_apply_function_with_lambda
  10. 技术1:1_raw_for_loop_using_regular_df_indexing
  11. 技术 2:2_raw_for_loop_using_df.loc[]_indexing
  12. 技术 4:4_iterrows_in_for_loop
  13. 技术 3:3_raw_for_loop_using_df.iloc[]_indexing

经验法则:

  1. 切勿使用技术 3、4 和 2。它们非常慢,没有任何优势。但请记住:不是索引技术,例如 OR,使这些技术变得糟糕,而是它们所处的 for 循环使它们变得糟糕!例如,我使用最快的(纯矢量化)方法!因此,这里有 3 种最慢的技术,永远不应该使用:.loc[].iloc[].loc[]
    1. 3_raw_for_loop_using_df.iloc[]_indexing
    2. 4_iterrows_in_for_loop
    3. 2_raw_for_loop_using_df.loc[]_indexing
  2. 也不应该使用技术,但如果你要使用原始的for循环,它比其他循环更快。1_raw_for_loop_using_regular_df_indexing
  3. .apply() 函数 () 是可以的,但一般来说,我也会避免它。然而,技术确实比 表现得更好,这很有趣。9_apply_function_with_lambda6_vectorization__with_apply_for_if_statement_corner_case7_vectorization__with_list_comprehension_for_if_statment_corner_case
  4. 列表理解很棒!它不是最快的,但它易于使用且非常快!
    1. 它的好处是它可以与任何旨在处理单个值或数组值的函数一起使用。这意味着你可以在函数中拥有非常复杂的语句和内容。因此,这里的权衡是,通过使用外部计算函数,它为您提供了非常多才多艺的代码,具有真正可读性和可重用性的代码,同时仍然为您提供了极快的速度!if
  5. 矢量化是最快和最好的,只要方程简单,就应该使用什么。您可以选择仅对方程式中较复杂的部分使用类似或列出理解的内容,同时仍可轻松地对其余部分使用矢量化。.apply()
  6. 纯矢量化绝对是最快和最好的,如果你愿意付出努力让它工作,你应该使用什么。
    1. 对于简单的情况,这是您应该使用的。
    2. 对于复杂的情况、语句等,也可以通过布尔索引使纯矢量化起作用,但会增加额外的工作,并可能降低可读性。因此,您可以选择使用列表推导式(通常是最好的)或 .apply()(通常较慢,但并非总是如此)来代替这些边缘情况,同时仍然使用矢量化进行其余的计算。例如:请参阅技术和 .if7_vectorization__with_list_comprehension_for_if_statment_corner_case6_vectorization__with_apply_for_if_statement_corner_case

测试数据

假设我们有以下 Pandas DataFrame。它有 200 万行,有 4 列 (, , , 和 ),每列的随机值为 :ABCD-10001000

df =
           A    B    C    D
0       -365  842  284 -942
1        532  416 -102  888
2        397  321 -296 -616
3       -215  879  557  895
4        857  701 -157  480
...      ...  ...  ...  ...
1999995 -101 -233 -377 -939
1999996 -989  380  917  145
1999997 -879  333 -372 -970
1999998  738  982 -743  312
1999999 -306 -103  459  745

我像这样生成了这个 DataFrame:

import numpy as np
import pandas as pd

# Create an array (numpy list of lists) of fake data
MIN_VAL = -1000
MAX_VAL = 1000
# NUM_ROWS = 10_000_000
NUM_ROWS = 2_000_000  # default for final tests
# NUM_ROWS = 1_000_000
# NUM_ROWS = 100_000
# NUM_ROWS = 10_000  # default for rapid development & initial tests
NUM_COLS = 4
data = np.random.randint(MIN_VAL, MAX_VAL, size=(NUM_ROWS, NUM_COLS))

# Now convert it to a Pandas DataFrame with columns named "A", "B", "C", and "D"
df_original = pd.DataFrame(data, columns=["A", "B", "C", "D"])
print(f"df = \n{df_original}")

测试方程/计算

我想证明所有这些技术在非平凡的函数或方程上都是可能的,所以我特意让它们计算的方程需要:

  1. if语句
  2. 来自 DataFrame 中多个列的数据
  3. 来自 DataFrame 中多行的数据

我们将为每一行计算的方程式是这样的。我随意编造了它,但我认为它包含足够的复杂性,您将能够扩展我所做的工作,以在具有完全矢量化的 Pandas 中执行您想要的任何方程:

enter image description here

在 Python 中,上面的等式可以写成这样:

# Calculate and return a new value, `val`, by performing the following equation:
val = (
    2 * A_i_minus_2
    + 3 * A_i_minus_1
    + 4 * A
    + 5 * A_i_plus_1
    # Python ternary operator; don't forget parentheses around the entire 
    # ternary expression!
    + ((6 * B) if B > 0 else (60 * B))
    + 7 * C
    - 8 * D
)

或者,你可以这样写:

# Calculate and return a new value, `val`, by performing the following equation:

if B > 0:
    B_new = 6 * B
else:
    B_new = 60 * B

val = (
    2 * A_i_minus_2
    + 3 * A_i_minus_1
    + 4 * A
    + 5 * A_i_plus_1
    + B_new
    + 7 * C
    - 8 * D
)

其中任何一个都可以包装到函数中。前任:

def calculate_val(
        A_i_minus_2,
        A_i_minus_1,
        A,
        A_i_plus_1,
        B,
        C,
        D):
    val = (
        2 * A_i_minus_2
        + 3 * A_i_minus_1
        + 4 * A
        + 5 * A_i_plus_1
        # Python ternary operator; don't forget parentheses around the 
        # entire ternary expression!
        + ((6 * B) if B > 0 else (60 * B))
        + 7 * C
        - 8 * D
    )
    return val

技术

完整的代码可以在我的 eRCaGuy_hello_world 存储库中的 python/pandas_dataframe_iteration_vs_vectorization_vs_list_comprehension_speed_tests.py 文件中下载和运行。

以下是所有 13 种技术的代码:

  1. 技术1: 1_raw_for_loop_using_regular_df_indexing

    val = [np.NAN]*len(df)
    for i in range(len(df)):
        if i < 2 or i > len(df)-2:
            continue
    
        val[i] = calculate_val(
            df["A"][i-2],
            df["A"][i-1],
            df["A"][i],
            df["A"][i+1],
            df["B"][i],
            df["C"][i],
            df["D"][i],
        )
    df["val"] = val  # put this column back into the dataframe
    
  2. 技术 2: 2_raw_for_loop_using_df.loc[]_indexing

    val = [np.NAN]*len(df)
    for i in range(len(df)):
        if i < 2 or i > len(df)-2:
            continue
    
        val[i] = calculate_val(
            df.loc[i-2, "A"],
            df.loc[i-1, "A"],
            df.loc[i,   "A"],
            df.loc[i+1, "A"],
            df.loc[i,   "B"],
            df.loc[i,   "C"],
            df.loc[i,   "D"],
        )
    
    df["val"] = val  # put this column back into the dataframe
    
  3. 技术 3: 3_raw_for_loop_using_df.iloc[]_indexing

    # column indices
    i_A = 0
    i_B = 1
    i_C = 2
    i_D = 3
    
    val = [np.NAN]*len(df)
    for i in range(len(df)):
        if i < 2 or i > len(df)-2:
            continue
    
        val[i] = calculate_val(
            df.iloc[i-2, i_A],
            df.iloc[i-1, i_A],
            df.iloc[i,   i_A],
            df.iloc[i+1, i_A],
            df.iloc[i,   i_B],
            df.iloc[i,   i_C],
            df.iloc[i,   i_D],
        )
    
    df["val"] = val  # put this column back into the dataframe
    
  4. 技术 4: 4_iterrows_in_for_loop

    val = [np.NAN]*len(df)
    for index, row in df.iterrows():
        if index < 2 or index > len(df)-2:
            continue
    
        val[index] = calculate_val(
            df["A"][index-2],
            df["A"][index-1],
            row["A"],
            df["A"][index+1],
            row["B"],
            row["C"],
            row["D"],
        )
    
    df["val"] = val  # put this column back into the dataframe
    

对于接下来的所有示例,我们必须首先通过添加具有上一个和下一个值的列来准备数据帧:、 和 。DataFrame 中的这些列将分别命名为 、 和 。A_(i-2)A_(i-1)A_(i+1)A_i_minus_2A_i_minus_1A_i_plus_1

df_original["A_i_minus_2"] = df_original["A"].shift(2)  # val at index i-2
df_original["A_i_minus_1"] = df_original["A"].shift(1)  # val at index i-1
df_original["A_i_plus_1"] = df_original["A"].shift(-1)  # val at index i+1

# Note: to ensure that no partial calculations are ever done with rows which
# have NaN values due to the shifting, we can either drop such rows with
# `.dropna()`, or set all values in these rows to NaN. I'll choose the latter
# so that the stats that will be generated with the techniques below will end
# up matching the stats which were produced by the prior techniques above. ie:
# the number of rows will be identical to before. 
#
# df_original = df_original.dropna()
df_original.iloc[:2, :] = np.NAN   # slicing operators: first two rows, 
                                   # all columns
df_original.iloc[-1:, :] = np.NAN  # slicing operators: last row, all columns

运行上面的矢量化代码以生成这 3 个新列总共需要 0.044961 秒

现在谈谈其余的技术:

  1. 技术 5: 5_itertuples_in_for_loop

    val = [np.NAN]*len(df)
    for row in df.itertuples():
        val[row.Index] = calculate_val(
            row.A_i_minus_2,
            row.A_i_minus_1,
            row.A,
            row.A_i_plus_1,
            row.B,
            row.C,
            row.D,
        )
    
    df["val"] = val  # put this column back into the dataframe
    
  2. 技术 6: 6_vectorization__with_apply_for_if_statement_corner_case

    def calculate_new_column_b_value(b_value):
        # Python ternary operator
        b_value_new = (6 * b_value) if b_value > 0 else (60 * b_value)  
        return b_value_new
    
    # In this particular example, since we have an embedded `if-else` statement
    # for the `B` column, pure vectorization is less intuitive. So, first we'll
    # calculate a new `B` column using
    # **`apply()`**, then we'll use vectorization for the rest.
    df["B_new"] = df["B"].apply(calculate_new_column_b_value)
    # OR (same thing, but with a lambda function instead)
    # df["B_new"] = df["B"].apply(lambda x: (6 * x) if x > 0 else (60 * x))
    
    # Now we can use vectorization for the rest. "Vectorization" in this case
    # means to simply use the column series variables in equations directly,
    # without manually iterating over them. Pandas DataFrames will handle the
    # underlying iteration automatically for you. You just focus on the math.
    df["val"] = (
        2 * df["A_i_minus_2"]
        + 3 * df["A_i_minus_1"]
        + 4 * df["A"]
        + 5 * df["A_i_plus_1"]
        + df["B_new"]
        + 7 * df["C"]
        - 8 * df["D"]
    )
    
  3. 技术 7: 7_vectorization__with_list_comprehension_for_if_statment_corner_case

    # In this particular example, since we have an embedded `if-else` statement
    # for the `B` column, pure vectorization is less intuitive. So, first we'll
    # calculate a new `B` column using **list comprehension**, then we'll use
    # vectorization for the rest.
    df["B_new"] = [
        calculate_new_column_b_value(b_value) for b_value in df["B"]
    ]
    
    # Now we can use vectorization for the rest. "Vectorization" in this case
    # means to simply use the column series variables in equations directly,
    # without manually iterating over them. Pandas DataFrames will handle the
    # underlying iteration automatically for you. You just focus on the math.
    df["val"] = (
        2 * df["A_i_minus_2"]
        + 3 * df["A_i_minus_1"]
        + 4 * df["A"]
        + 5 * df["A_i_plus_1"]
        + df["B_new"]
        + 7 * df["C"]
        - 8 * df["D"]
    )
    
  4. 技术8: 8_pure_vectorization__with_df.loc[]_boolean_array_indexing_for_if_statment_corner_case

    这使用布尔索引(又名:布尔掩码)来完成等式中语句的等效项。这样,纯矢量化可以用于整个方程式,从而最大限度地提高性能和速度。if

    # If statement to evaluate:
    #
    #     if B > 0:
    #         B_new = 6 * B
    #     else:
    #         B_new = 60 * B
    #
    # In this particular example, since we have an embedded `if-else` statement
    # for the `B` column, we can use some boolean array indexing through
    # `df.loc[]` for some pure vectorization magic.
    #
    # Explanation:
    #
    # Long:
    #
    # The format is: `df.loc[rows, columns]`, except in this case, the rows are
    # specified by a "boolean array" (AKA: a boolean expression, list of
    # booleans, or "boolean mask"), specifying all rows where `B` is > 0. Then,
    # only in that `B` column for those rows, set the value accordingly. After
    # we do this for where `B` is > 0, we do the same thing for where `B` 
    # is <= 0, except with the other equation.
    #
    # Short:
    #
    # For all rows where the boolean expression applies, set the column value
    # accordingly.
    #
    # GitHub CoPilot first showed me this `.loc[]` technique.
    # See also the official documentation:
    # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html
    #
    # ===========================
    # 1st: handle the > 0 case
    # ===========================
    df["B_new"] = df.loc[df["B"] > 0, "B"] * 6
    #
    # ===========================
    # 2nd: handle the <= 0 case, merging the results into the
    # previously-created "B_new" column
    # ===========================
    # - NB: this does NOT work; it overwrites and replaces the whole "B_new"
    #   column instead:
    #
    #       df["B_new"] = df.loc[df["B"] <= 0, "B"] * 60
    #
    # This works:
    df.loc[df["B"] <= 0, "B_new"] = df.loc[df["B"] <= 0, "B"] * 60
    
    # Now use normal vectorization for the rest.
    df["val"] = (
        2 * df["A_i_minus_2"]
        + 3 * df["A_i_minus_1"]
        + 4 * df["A"]
        + 5 * df["A_i_plus_1"]
        + df["B_new"]
        + 7 * df["C"]
        - 8 * df["D"]
    )
    
  5. 技术 9: 9_apply_function_with_lambda

    df["val"] = df.apply(
        lambda row: calculate_val(
            row["A_i_minus_2"],
            row["A_i_minus_1"],
            row["A"],
            row["A_i_plus_1"],
            row["B"],
            row["C"],
            row["D"]
        ),
        axis='columns' # same as `axis=1`: "apply function to each row", 
                       # rather than to each column
    )
    
  6. 技术 10: 10_list_comprehension_w_zip_and_direct_variable_assignment_passed_to_func

    df["val"] = [
        # Note: you *could* do the calculations directly here instead of using a
        # function call, so long as you don't have indented code blocks such as
        # sub-routines or multi-line if statements.
        #
        # I'm using a function call.
        calculate_val(
            A_i_minus_2,
            A_i_minus_1,
            A,
            A_i_plus_1,
            B,
            C,
            D
        ) for A_i_minus_2, A_i_minus_1, A, A_i_plus_1, B, C, D
        in zip(
            df["A_i_minus_2"],
            df["A_i_minus_1"],
            df["A"],
            df["A_i_plus_1"],
            df["B"],
            df["C"],
            df["D"]
        )
    ]
    
  7. 技术 11: 11_list_comprehension_w_zip_and_direct_variable_assignment_calculated_in_place

    df["val"] = [
        2 * A_i_minus_2
        + 3 * A_i_minus_1
        + 4 * A
        + 5 * A_i_plus_1
        # Python ternary operator; don't forget parentheses around the entire
        # ternary expression!
        + ((6 * B) if B > 0 else (60 * B))
        + 7 * C
        - 8 * D
        for A_i_minus_2, A_i_minus_1, A, A_i_plus_1, B, C, D
        in zip(
            df["A_i_minus_2"],
            df["A_i_minus_1"],
            df["A"],
            df["A_i_plus_1"],
            df["B"],
            df["C"],
            df["D"]
        )
    ]
    
  8. 技术 12: 12_list_comprehension_w_zip_and_row_tuple_passed_to_func

    df["val"] = [
        calculate_val(
            row[0],
            row[1],
            row[2],
            row[3],
            row[4],
            row[5],
            row[6],
        ) for row
        in zip(
            df["A_i_minus_2"],
            df["A_i_minus_1"],
            df["A"],
            df["A_i_plus_1"],
            df["B"],
            df["C"],
            df["D"]
        )
    ]
    
  9. 技术 13: 13_list_comprehension_w__to_numpy__and_direct_variable_assignment_passed_to_func

    df["val"] = [
        # Note: you *could* do the calculations directly here instead of using a
        # function call, so long as you don't have indented code blocks such as
        # sub-routines or multi-line if statements.
        #
        # I'm using a function call.
        calculate_val(
            A_i_minus_2,
            A_i_minus_1,
            A,
            A_i_plus_1,
            B,
            C,
            D
        ) for A_i_minus_2, A_i_minus_1, A, A_i_plus_1, B, C, D
            # Note: this `[[...]]` double-bracket indexing is used to select a
            # subset of columns from the dataframe. The inner `[]` brackets
            # create a list from the column names within them, and the outer 
            # `[]` brackets accept this list to index into the dataframe and
            # select just this list of columns, in that order.
            # - See the official documentation on it here:
            #   https://pandas.pydata.org/docs/user_guide/indexing.html#basics
            #   - Search for the phrase "You can pass a list of columns to [] to
            #     select columns in that order."
            #   - I learned this from this comment here:
            #     https://stackoverflow.com/questions/16476924/how-to-iterate-over-rows-in-a-dataframe-in-pandas/55557758#comment136020567_55557758
            # - One of the **list comprehension** examples in this answer here
            #   uses `.to_numpy()` like this:
            #   https://stackoverflow.com/a/55557758/4561887
        in df[[
            "A_i_minus_2",
            "A_i_minus_1",
            "A",
            "A_i_plus_1",
            "B",
            "C",
            "D"
        ]].to_numpy()  # NB: `.values` works here too, but is deprecated. See:
                       # https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.values.html
    ]
    

结果如下:

enter image description here

在 4 循环技术中使用预移位行for

我想看看删除此检查并在 4 个循环技术中使用预移位行是否会产生很大影响:iffor

if i < 2 or i > len(df)-2:
    continue

...所以我用这些修改创建了这个文件:pandas_dataframe_iteration_vs_vectorization_vs_list_comprehension_speed_tests_mod.py。在文件中搜索“MOD:”以查找 4 种新的修改技术。

它只有轻微的改进。以下是这 17 种技术的结果,其中 4 种新技术在其名称开头附近,紧跟在它们的编号后面。这次是超过 500k 行,而不是 2M:_MOD_

enter image description here

更多信息.iterrtuples()

使用时实际上有更多的细微差别。要深入研究其中的一些问题,请阅读 @Romain Capron 的答案。这是我根据他的结果绘制的条形图:.itertuples()

enter image description here

我为他的结果绘制的代码在我的eRCaGuy_hello_world存储库中以 python/pandas_plot_bar_chart_better_GREAT_AUTOLABEL_DATA.py 格式编写。

未来工作

使用 Cython(Python 编译成 C 代码),或者只是由 Python 调用的原始 C 函数,可能会更快,但我不打算在这些测试中这样做。我只会研究并快速测试这些选项以进行重大优化。

我目前不了解Cython,也觉得没有必要学习它。如上所述,简单地正确使用纯矢量化已经运行得非常快,只需 0.1 秒即可处理 200 万行,即每秒 2000 万行。

引用

  1. 一堆 Pandas 官方文档,尤其是这里的文档:https://pandas.pydata.org/pandas-docs/stable/reference/frame.htmlDataFrame

  2. @cs95 的这个出色的答案 - 这是我特别学习如何使用列表推导来遍历 DataFrame 的地方。

  3. 这个关于 itertuples() 的答案,由 @Romain Capron 撰写 - 我仔细研究了它并对其进行了编辑/格式化。

  4. 所有这些都是我自己的代码,但我想指出,我与 GitHub Copilot(主要是)、Bing AI 和 ChatGPT 进行了数十次聊天,以便弄清楚其中的许多技术并随时调试我的代码。

  5. Bing Chat 为我制作了漂亮的 LaTeX 方程式,并带有以下提示。当然,我验证了输出:

    将此 Python 代码转换为一个漂亮的方程式,我可以粘贴到 Stack Overflow 上:

        val = (
            2 * A_i_minus_2
            + 3 * A_i_minus_1
            + 4 * A
            + 5 * A_i_plus_1
            # Python ternary operator; don't forget parentheses around the entire ternary expression!
            + ((6 * B) if B > 0 else (60 * B))
            + 7 * C
            - 8 * D
        )
    

另请参阅

  1. 这个答案也发布在我的个人网站上:https://gabrielstaples.com/python_iterate_over_pandas_dataframe/

  2. https://en.wikipedia.org/wiki/Array_programming - 数组编程,或“矢量化”:

    在计算机科学中,数组编程是指允许一次将运算应用于整个值集的解决方案。此类解决方案通常用于科学和工程环境。

    支持数组编程的现代编程语言(也称为向量或多维语言)专门设计用于泛化标量运算,以透明地应用于向量、矩阵和高维数组。其中包括 APL、J、Fortran、MATLAB、Analytica、Octave、R、Cilk Plus、Julia、Perl 数据语言 (PDL)。在这些语言中,对整个数组进行操作的操作可以称为矢量化操作,1 无论它是否在实现向量指令的向量处理器上执行。

  3. pandas 中的 for 循环真的不好吗?我什么时候应该关心?

    1. 我的回答
  4. pandas iterrows 有性能问题吗?

    1. 这个答案
      1. 我在下面的评论

        ...然而,根据我的结果,我想说,这些是最好的方法,按照最佳优先的顺序:

        1. 矢 量化
        2. 列表推导,
        3. .itertuples(),
        4. .apply(),
        5. 原始循环,for
        6. .iterrows().

        我没有测试Cython。