欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

如何遍历 DataFrame 的行(您应该怎么做?)

最编程 2024-04-18 16:04:47
...

关于pandas的最多的搜索(和讨论)问题之一是如何迭代DataFrame 中的行。对于那些已经将一些数据加载到DataFrame ,现在想用它做一些有用的事情的新用户来说,这个问题往往会马上出现。对大多数程序员来说,自然而然地想到接下来要做什么,就是建立一个循环。他们可能还不了解使用DataFrames 的 "正确 "方法,但即使是有经验的pandas和NumPy开发者也会考虑在DataFrame 的行上进行迭代,以解决一个问题。与其试图找到关于迭代的唯一正确答案,不如了解所涉及的问题并知道何时选择最佳解决方案,这才是更有意义的。

截至目前,Stack Overflow上以 "pandas "为标签的投票率最高的问题是关于如何对DataFrame 行进行迭代。事实证明,这个问题是整个网站上用代码块复制最多的答案。Stack Overflow的开发人员说,每周都有成千上万的人查看这个答案,并复制它来解决他们的问题。很明显,人们希望对DataFrame 行进行迭代!

使用*解决方案在DataFrame 行上进行迭代,也确实会有严重的后果。这个问题的其他答案(尤其是评分第二高的答案)在提供其他选项方面做得相当好,但是整个26个(还在继续!)答案的列表非常混乱。与其问_如何_在DataFrame 行上迭代,不如了解有哪些选项,它们的优点和缺点是什么,然后选择对你有意义的选项,这样更有意义。在某些情况下,投票最多的迭代答案可能是最好的选择!

但我听说迭代是错误的,是这样吗?

首先,选择在DataFrame 的行上进行迭代并不自动成为解决问题的错误方法。然而,在大多数情况下,初学者用迭代所要做的事情,用另一种方法做得更好。然而,任何人都不应该为写出第一个使用迭代而不是其他(也许更好的)方法的解决方案而感到难过。这往往是最好的学习方法,你可以把第一个解决方案看作是你文章的初稿,你可以通过一些编辑来改进它。

现在我们要做的是DataFrame

让我们从基本问题开始。如果我们看一下Stack Overflow上的原始问题,问题和答案只是打印了DataFrame 的内容。首先,让我们都同意,这不是一个看待DataFrame 的内容的好方法。DataFrame 的标准渲染,无论是用print 渲染,还是用 Jupyter 笔记本查看display ,或者作为单元格中的输出,都会比使用自定义格式打印的内容要好得多。

如果DataFrame 是大的,默认情况下可能只有一些列和行是可见的。使用headtail 来获得数据的感觉。如果你想只看一个DataFrame 的子集,而不是用一个循环来只显示那些行,使用pandas强大的索引功能。只要稍加练习,你可以选择任何行或列的组合来显示。先从这里开始。

现在不是一个琐碎的打印例子,让我们来看看如何在一个包含一些逻辑的DataFrame ,实际使用某一行的数据。

例子

让我们建立一个可以使用的例子DataFrame 。我将通过制作一些假数据(使用Faker)来完成注意,这些列是不同的数据类型(我们有一些字符串、一个整数和日期)。

from datetime import datetime, timedelta

import pandas as pd
import numpy as np
from faker import Faker

fake = Faker()

today = datetime.now()
next_month = today + timedelta(days=30)
df = pd.DataFrame([[fake.first_name(), fake.last_name(),
                    fake.date_this_decade(), fake.date_between_dates(today, next_month),
                    fake.city(), fake.state(), fake.zipcode(), fake.random_int(-100,1000)]
                  for r in range(100)],
                  columns=['first_name', 'last_name', 'start_date',                           'end_date', 'city', 'state', 'zipcode', 'balance'])


df['start_date'] = pd.to_datetime(df['start_date']) # convert to datetimes
df['end_date'] = pd.to_datetime(df['end_date'])

df.dtypes
first_name            object
last_name             object
start_date    datetime64[ns]
end_date      datetime64[ns]
city                  object
state                 object
zipcode               object
balance                int64
dtype: object
df.head()
  first_name last_name start_date   end_date               city      state  \
0  Katherine     Moody 2020-02-04 2021-06-28           Longberg   Maryland   
1      Sarah   Merritt 2021-03-02 2021-05-30  South Maryborough  Tennessee   
2      Karen   Hensley 2020-02-29 2021-06-23          Brentside   Missouri   
3      David  Ferguson 2020-02-02 2021-06-14         Judithport   Virginia   
4    Phillip     Davis 2020-07-17 2021-06-04          Louisberg  Minnesota   

  zipcode  balance  
0   20496      493  
1   18495      680  
2   63702      427  
3   66787      587  
4   98616      211  

第一次尝试

假设我们的DataFrame 包含客户数据,我们有一个客户评分函数,该函数使用多个客户属性来给他们打出'A'和'F'之间的分数。任何有负余额的客户都被打成 "F",高于500分的为 "A",之后的逻辑取决于客户是否是 "传统 "客户以及他们居住在哪个州。

请注意,我为这个功能做了测试,关于如何在Jupyter中进行单元测试的更多细节,请看我的Jupyter单元测试的帖子

from dataclasses import dataclass

@dataclass
class Customer:
    first_name: str
    last_name: str
    start_date: datetime
    end_date: datetime
    city: str
    state: str
    zipcode: str
    balance: int


def score_customer(customer:Customer) -> str:
    """Give a customer a credit score.
    >>> score_customer(Customer("Joe", "Smith", datetime(2020, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, -5))
    'F'
    >>> score_customer(Customer("Joe", "Smith", datetime(2020, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 50))
    'C'
    >>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 50))
    'D'
    >>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 150))
    'C'
    >>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 250))
    'B'
    >>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Chicago", "Illinois", 66666, 350))
    'B'
    >>> score_customer(Customer("Joe", "Smith", datetime(2021, 1, 1), datetime(2023,1,1), "Santa Fe", "California", 88888, 350))
    'A'
    >>> score_customer(Customer("Joe", "Smith", datetime(2020, 1, 1), datetime(2023,1,1), "Santa Fe", "California", 88888, 50))
    'C'
    """
    if customer.balance < 0:
        return 'F'
    if customer.balance > 500:
        return 'A'
    # legacy vs. non-legacy
    if customer.start_date > datetime(2020, 1, 1):
        if customer.balance < 100:
            return 'D'
        elif customer.balance < 200:
            return 'C'
        elif customer.balance < 300:
            return 'B'
        else:
            if customer.state in ['Illinois', 'Indiana']:
                return 'B'
            else:
                return 'A'
    else:
        if customer.balance < 100:
            return 'C'
        else:
            return 'A'


import doctest
doctest.testmod()
TestResults(failed=0, attempted=8)

给我们的客户打分

好了,现在我们有了一个具体的例子,我们如何获得所有客户的分数?让我们直接进入Stack Overflow问题的最高答案,DataFrame.iterrows 。这是一个生成器,它将某一行的索引与该行一起返回,作为Series 。如果你不熟悉什么是生成器,你可以把它想象成一个可以迭代的函数。因此,对它调用next ,将得到第一个元素。

next(df.iterrows())
(0,
 first_name              Katherine
 last_name                   Moody
 start_date    2020-02-04 00:00:00
 end_date      2021-06-28 00:00:00
 city                     Longberg
 state                    Maryland
 zipcode                     20496
 balance                       493
 Name: 0, dtype: object)

这看起来很有希望!这是一个元组,包含第一行的索引和行数据本身。也许我们可以把它直接传入我们的函数。让我们试一下,看看会发生什么。尽管该行是一个Series ,但其列与我们的Customer 类的属性是一样的,所以我们也许可以直接将其传入我们的评分函数。

score_customer(next(df.iterrows())[1])
'A'

哇,这似乎很有效。我们可以直接对整个表格进行评分吗?

df['score'] = [score_customer(c[1]) for c in df.iterrows()]

这是我们最好的选择吗?

哇,这似乎太简单了。你可以看到为什么这是投票最多的答案,因为它似乎正是我们想要的。为什么对这个答案会有争议呢?

就像通常熊猫的情况一样(实际上也是任何软件工程问题的情况),挑选一个理想的解决方案取决于输入。让我们总结一下各种设计选择可能存在的问题。如果提出的问题不适合你的特定用例,那么使用iterrows 迭代可能是一个完全可以接受的解决方案!我不会评判你。我使用它的次数很多,并将在最后总结如何对可能的解决方案做出决定。

支持和反对使用iterrows 的论点可以归纳为以下几类。

  1. 效率(速度和内存
  2. 混合类型在一行中造成的问题
  3. 可读性和可维护性

速度和内存

一般来说,如果你希望在pandas(或Numpy,或任何提供矢量计算的框架)中的事情是快速的,你将不希望迭代元素,而是选择一个矢量化的解决方案。然而,即使解决方案_可以_被矢量化,对于程序员来说,尤其是初学者,这样做可能会很费劲。Stack Overflow上关于这个问题的其他答案提出了大量其他的解决方案。它们大多都属于以下几类,按速度优先的顺序排列。

  1. 矢量化
  2. Cython例程
  3. 列表理解(vanilla for loop)。
  4. DataFrame.apply()
  5. DataFrame.itertuples() 和 iteritems()
  6. DataFrame.iterrows()

矢量化

总是告诉人们要把所有东西都矢量化的主要问题是,有时矢量化的解决方案在编写、调试和维护时可能是一件非常麻烦的事情。为了证明矢量化是首选,所给出的例子往往显示了一些微不足道的操作,比如简单的乘法。但由于我在这篇文章中开始的例子不只是一个单一的计算,我决定写一个可能的矢量化解决方案来解决这个问题。

def vectorized_score(df):
    return np.select([df['balance'] < 0,
                      df['balance'] > 500, # technically not needed, would fall through
                      ((df['start_date'] > datetime(2020,1,1)) &
                       (df['balance'] < 100)),
                      ((df['start_date'] > datetime(2020,1,1)) &
                       (df['balance'] >= 100) &
                       (df['balance'] < 200)),
                      ((df['start_date'] > datetime(2020,1,1)) &
                       (df['balance'] >= 200) &
                       (df['balance'] < 300)),
                      ((df['start_date'] > datetime(2020,1,1)) &
                       (df['balance'] >= 300) &
                       df['state'].isin(['Illinois', 'Indiana'])),
                      ((df['start_date'] >= datetime(2020,1,1)) &
                       (df['balance'] < 100)),
                     ], # conditions
                     ['F',
                      'A',
                      'D',
                      'C',
                      'B',
                      'B',
                      'C'], # choices
                     'A') # default score


assert (df['score'] == vectorized_score(df)).all()

当然,有不止一种方法可以做到这一点。我选择了使用np.select (你可以在我关于使用where 和mask的文章中阅读更多关于它和其他各种更新DataFrames 的方法) 。当你有这样的多个条件时,我有点喜欢使用np.select ,尽管它的可读性不是很强。我们也可以用更多的代码来完成这个任务,每一步都用矢量更新的方式,使其更具有可读性。在速度上可能会差不多。

我个人认为这很难读懂,但也许通过一些好的评论,可以向未来的维护者(或我未来的自己)清楚地解释。但我们之所以要做矢量代码,是为了让它更快。对于我们的样本DataFrame ,性能如何?

%timeit vectorized_score(df)
2.75 ms ± 489 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

让我们也为我们的原始解决方案计时。

%timeit [score_customer(c[1]) for c in df.iterrows()] 
13.5 ms ± 911 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

好的,所以我们几乎快了5倍,只是用我们的小数据集。这个速度对于小数据集来说并不重要,但是对于大数据集来说,简单的重写就可以获得这么大的速度。我相信,只要稍加思考和分析,就可以写出一个更快的矢量版本。但请坚持到最后,看看大数据集的性能如何。

Cython

Cython是一个项目,它使得使用(大部分)Python语法为Python编写C语言扩展变得容易。我承认,我远不是Cython专家,但我发现,即使只是在Cython中做一点努力,也能使Python代码热点更快。在这种情况下,我们已经表明我们可以做出一个矢量化的解决方案,所以在非矢量化的解决方案中使用Cython可能不值得作为首选来追求。然而,我确实在这里写了一个简单的Cython版本,在较小尺寸的输入中,它是非矢量化解决方案中最快的,甚至只用了一点点的努力。特别是对于那些每一行都有大量计算而又不能被矢量化的情况,使用Cython可能是一个很好的选择,但需要投入一定的时间。

列表理解法

现在,下一个选择有点不同。我承认,我不认为我经常使用这种技术。这里的想法是使用一个列表理解,用你的DataFrame 中的每个元素调用你的函数。请注意,在我们的第一个解决方案中,我已经使用了一个列表理解,但它是与iterrows 。这一次没有使用iterrows ,而是直接从DataFrame 的每一列中提取数据,然后进行迭代。在这种情况下,没有创建Series 。如果你的函数有多个参数,你可以使用zip 来制作参数的图元,在你的DataFrame 中传入列以匹配参数的顺序。现在要做到这一点,我需要一个修改过的评分函数,因为我的DataFrame 中没有已经构建好的Customer 对象,而创建它们只是为了调用这个函数会增加另一个层次。我只使用客户的三个属性,所以这里是一个简单的重写。

def score_customer_attributes(balance:int, start_date:datetime, state:str) -> str:
    if balance < 0:
        return 'F'
    if balance > 500:
        return 'A'
    # legacy vs. non-legacy
    if start_date > datetime(2020, 1, 1):
        if balance < 100:
            return 'D'
        elif balance < 200:
            return 'C'
        elif balance < 300:
            return 'B'
        else:
            if state in ['Illinois', 'Indiana']:
                return 'B'
            else:
                return 'A'
    else:
        if balance < 100:
            return 'C'
        else:
            return 'A'

这里是调用函数时,列表理解的第一个循环的样子。

next(zip(df['balance'], df['start_date'], df['state']))
(493, Timestamp('2020-02-04 00:00:00'), 'Maryland')

我们现在将为整个DataFrame ,建立一个所有分数的列表。

df['score3'] = [score_customer_attributes(*a) for a in zip(df['balance'], df['start_date'], df['state'])]
assert (df['score'] == df['score3']).all()

现在这有多快呢?

%timeit [score_customer_attributes(*a) for a in zip(df['balance'], df['start_date'], df['state'])]
171 µs ± 11.2 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

哇,这可快多了,比原来对这个数据的处理快了70多倍。仅仅通过获取原始数据并调用一个简单的Python函数,分数就在Python空间中被快速计算出来。不需要将行转换为Series

请注意,我们也可以调用我们的原始函数,我们只需要做一个Customer 对象传进去。这就有点难看了,但还是相当快。

%timeit [score_customer(Customer(first_name='', last_name='', end_date=None, city=None, zipcode=None, balance=a[0], start_date=a[1], state=a[2])) for a in zip(df['balance'], df['start_date'], df['state'])]
254 µs ± 2.59 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

DataFrame.apply

我们也可以使用DataFrame.apply 。注意,要将其应用于行,你需要传入正确的轴,因为它默认为应用于每一列。这里的轴参数是指定你想在传递给你的函数的对象中拥有哪个索引。我们希望每个对象是一个客户行,列作为索引。

assert (df.apply(score_customer, axis=1) == df['score']).all()
%timeit df.apply(score_customer, axis=1)
3.57 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这里的性能比我们原来的要好,快3倍以上。这也是非常可读的,并允许我们使用我们的易于阅读和维护的原始函数。但它仍然比列表理解要慢,因为它为每一行构造一个Series 对象。

DataFrame.iteritems和DataFrame.itertuples

现在我们将更详细地看一下常规的迭代方法。对于DataFrames来说,有三个iter 函数:iteritems,itertuples, 和iterrowsDataFrames 也直接支持迭代,但是这些函数并不都是对相同的东西进行迭代。由于仅仅看到这些方法的名字就能理解它们的作用,会让人非常困惑,所以我们在这里回顾一下它们。

  • iter(df) (调用DataFrame.__iter__ 方法)。遍历信息轴,对于DataFrames ,它是列名,而不是值。
next(iter(df)) # 'first_name'
'first_name'
  • iteritems.遍历列,返回一个列名和列的元组,作为Series
next(df.iteritems())
next(df.items())       # these two are equivalent
('first_name',
 0       Katherine
 1           Sarah
 2           Karen
 3           David
 4         Phillip
          ...     
 95         Robert
 96    Christopher
 97        Kristen
 98       Nicholas
 99       Caroline
 Name: first_name, Length: 100, dtype: object)
  • items 。这和上面一样。iteritems 实际上只是调用了items
next(df.iterrows())
(0,
 first_name              Katherine
 last_name                   Moody
 start_date    2020-02-04 00:00:00
 end_date      2021-06-28 00:00:00
 city                     Longberg
 state                    Maryland
 zipcode                     20496
 balance                       493
 score                           A
 score3                          A
 Name: 0, dtype: object)
  • iterrows 。我们已经看到了这个,它遍历了行,但是以索引和行的元组形式返回,作为Series
  • itertuples.遍历行,为每行返回一个namedtuple 。你可以选择改变元组的名称,并禁用被返回的索引。
next(df.itertuples())
Pandas(Index=0, first_name='Katherine', last_name='Moody', start_date=Timestamp('2020-02-04 00:00:00'), end_date=Timestamp('2021-06-28 00:00:00'), city='Longberg', state='Maryland', zipcode='20496', balance=493, score='A', score3='A')

使用 itertuples

由于我们已经看了iterrows ,我们只需要看一下itertuples 。正如你所看到的,返回的值,一个namedtuple ,可以在我们的原始函数中使用。

assert ([score_customer(t) for t in df.itertuples()]  == df['score']).all()
%timeit [score_customer(t) for t in df.itertuples()] 
858 µs ± 5.23 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

这里的性能相当好,超过12倍的速度。为每一行构建一个namedtuple ,比构建一个Series ,要快得多。

一行中的混合类型

现在是提出iterrowsitertuples 的另一个区别的好时机。一个namedtuple ,可以在一行中适当地表示任何类型。在我们的例子中,我们有字符串、日期类型和整数。然而,一个pandasSeries ,在整个Series ,必须只有一种数据类型。因为我们的数据类型足够多样化,它们都被表示为object ,最后保留了它们的类型,对我们来说没有任何功能问题。但是,情况并不总是这样的!

例如,如果你的列有不同的数字类型,他们最终将成为可以代表所有的类型。这可能导致你的itertuplesiterrows ,这两种方法返回的数据略有不同,所以要注意。

dfmixed = pd.DataFrame({'integer_column': [1,2,3], 'float_column': [1.1, 2.2, 3.3]})
dfmixed.dtypes
integer_column      int64
float_column      float64
dtype: object
next(dfmixed.itertuples())
Pandas(Index=0, integer_column=1, float_column=1.1)
next(dfmixed.iterrows())
(0,
 integer_column    1.0
 float_column      1.1
 Name: 0, dtype: float64)

列名

还有一句警告的话。如果你的DataFrame 有不能用 Python 变量名表示的列,你将不能用点语法访问它们。因此,如果你有一个名为2bMy Column 的列,那么你将不得不使用位置名来访问它们 (例如,第一列将被称为_1)。对于iterrows ,该行将是一个Series ,所以你必须使用["2b"]["My Column"] 来访问该列。

其他选择

当然,还有其他的迭代选择。例如,你可以递增一个整数偏移量,并使用iloc 索引器在DataFrame ,以选择任何行。当然,这其实和其他的迭代没有什么区别,同时也是不规范的,所以其他人在阅读你的代码时可能会觉得难以阅读和理解。我在下面总结的性能比较代码中建立了一个天真的版本,如果你想看的话(性能很糟糕)。

选择好

选择正确的解决方案基本上取决于两个因素。

  1. 你的数据集有多大?
  2. 你可以轻松地编写(和维护)什么?

在下面的图片中,你可以看到我们所考虑的解决方案的运行时间(生成这个的代码在这里)。正如你所看到的,只有矢量化的解决方案能在较大的数据中保持良好的状态。如果你的数据集很大,矢量化的解决方案可能是你唯一合理的选择。

各种方法在我们的DataFrame上的运行时间的比较。

然而,根据你需要执行代码的次数,你需要花费多长时间来正确编写代码,以及你对代码的维护能力,你可能会选择其他任何一种解决方案,并且会很好。事实上,对于这些解决方案来说,它们都会随着数据的增加而线性增长。

也许思考这个问题的一种方式不仅仅是big-O符号,而是 "big-U "符号。换句话说,你要花多长时间才能写出一个正确的解决方案?如果它少于你的代码的运行时间,一个迭代的解决方案可能是完全没问题的。但是,如果你在写生产代码,请花时间学习如何矢量化。

还有一点;有时在一个较小的集合上写迭代解决方案是很容易的,你可能想先这样做,然后再写矢量化的版本。用迭代解决方案验证你的结果,以确保你做得正确,然后在更大的完整数据集上使用矢量化版本。

我希望你觉得这次对DataFrame 迭代的深入研究很有趣。我知道我在这一路上学到了一些有用的东西。

The postHow to iterate over DataFrame rows (and should you? )appeared first onwrighters.io.