如何根据记录中其他 4 个字段的布尔运算符有效地更新数据框中的字段?
Posted
技术标签:
【中文标题】如何根据记录中其他 4 个字段的布尔运算符有效地更新数据框中的字段?【英文标题】:How do I efficiently update a field within a dataframe based on boolean operators on 4 other fields in the record? 【发布时间】:2019-09-02 22:14:31 【问题描述】:我正在分析和总结一个数据集(“报告”)作为 Python pandas 数据框。该表显示了 4 个不同数据集(“Inputs”)之间的匹配过程的结果,这些数据集都应该在同一个键上匹配。
在 报告 中,每个 输入 都有一个字段,其中包含与基本数据集匹配数 (>=0) 的计数器。我想更新报告中的一个字段以指示有多少数据集与基本数据匹配(“matchCounter”),因此对于任意数量的成功匹配(即 >0), matchCounter 应该以 1 递增,最大为 4(即所有四个数据集都与基础数据匹配)。
我在 Jupyter 笔记本中使用大约 100,000 条记录的小型数据集开发了该过程,虽然我成功地更新了 matchCounter 字段,但我怀疑它花费的时间比预期的要长。完整的数据集是 10'000'000 条记录,根据我的粗略计算,我当前的代码需要 8 个多小时才能完成(我认为这是一个非常简单的操作)。
我已经阅读了一些关于提高数据帧性能的文章 (Pandas DataFrame performance),但是由于我是按顺序迭代行,并且 if 语句是在行中的项目而不是数据帧上测试的,我不知道这是否适用。
这是代码的摘要版本。第一个 for 循环是导致瓶颈的循环:
import numpy as np
import pandas as pd
df = pd.read_csv(fileIn, header=0)
df['match_count']= 0
df['exclude']= False
# This for loop takes 300+ seconds to execute 100'000 times
for index, row in df.iterrows():
matchCounter = 0
if row['in_deeds'] > 0:
matchCounter += 1
if row['in_valuation'] > 0:
matchCounter += 1
if row['in_property'] > 0:
matchCounter += 1
if row['in_sg'] > 0:
matchCounter += 1
df.loc[index,'match_count'] = matchCounter
# This for loop takes only 11.75 seconds
i=0
for index, row in df.iterrows():
if "EXCL" in row['stat_deeds'].upper():
i=i+1
df.loc[index,'exclude']=True
elif "EXCL" in row['stat_valuation'].upper():
i=i+1
df.loc[index,'exclude']=True
elif "EXCL" in row['stat_property'].upper():
i=i+1
df.loc[index,'exclude']=True
elif "EXCL" in row['stat_sg'].upper():
i=i+1
df.loc[index,'exclude']=True
df = df.query('exclude == False')
这是我第一次使用 Pandas,而且我也是 Python 的初学者,所以我认为我犯了一个愚蠢的错误。但我也不确定我的期望是否错误,这只是我应该期待的表现。有没有更好的办法?即使有人能指出我正确的方向,我也会很感激!
【问题讨论】:
【参考方案1】:OP 评论后更新:
df['match_count']=(df[['in_deeds','in_valuation','in_property','in_sg']]>0).astype(int).sum(axis=1)
以下还将通过获取匹配计数的累积总和来提供每个点(每行)的匹配总数。
df['match_count']=(df[['in_deeds','in_valuation','in_property','in_sg']]>0).astype(int).sum(axis=1).cumsum()
一片一片:
我们首先检查(对于每一行)指定列中的值是否大于零。这将返回一个布尔值True
或False
,我们将其转换为整数.astype(int)
df[['in_deeds','in_valuation','in_property','in_sg']]>0).astype(int)
然后我们将每一行的这些值相加.sum(axis=1)
。
这将返回一个单列,在该列中,我们知道在每一行上满足了多少条件 (>0
)。
我们最终对各行求和以获得匹配的总数(每行)。
我们最终在原始数据框df
中创建一个新列df['match_count']=
,并将结果分配给该列。
【讨论】:
谢谢,Gio,我无法相信解决方案如此简单和高效。我必须做的唯一更改是删除.cumsum()
,因为我实际上想要每条记录的总匹配数,而不是所有记录的累积计数(对不起,如果我的问题不清楚)。
您可以(并且应该)更新我的答案。为遇到同样问题的下一个用户明确解决方案总是有用的
好的,谢谢 Gio - 我不确定,以后会记得的。【参考方案2】:
过去我在迭代数据帧时遇到过类似的问题 - df.iterrows()
乍一看似乎是正确的选择,因为它易于使用,但便利是有代价的。这是a helpful blog,它概述了 pandas 中更有效地迭代的方法。
结果是 - 不要使用iterrows
。通常,可以通过使用索引作为迭代器然后使用df.loc
或df.iloc
来访问数据帧的行,如下所示:
for i in df.index:
print(df.loc[i, :])
使用df.apply
apply
方法允许您将用户定义的函数应用于数据帧的所有列或行。虽然这里的使用可能有点不直观,但它是迄今为止最快的:
import numpy as np
import pandas as pd
def counter(row):
if np.any(row[row > 0]):
return np.sum(row[row > 0])
else:
return 0
N = 100000
df = pd.DataFrame('A': np.random.randint(0, 2, N),
'B': np.random.randint(0, 2, N),
'C': np.random.randint(0, 2, N),
'D': np.random.randint(0, 2, N))
df['match-count'] = df.apply(counter, axis=1, raw=True)
这里,该函数将检查数据框的每一行(由axis=1
指定); np.any
如果布尔选择 row[row > 0]
不为空,则返回 True
,此时布尔选择会用 np.sum
减少以获得最终计数。我们将raw
关键字参数设置为True
,以便传递原始的numpy
数组,该数组应用于减少操作(如求和)以提高性能(参见docs)。
在我的机器上运行大约需要 1.2 秒。
编辑
Gio 的回答显示了我认为在使用 pandas 时是一种很好的做法的原则 - 如果存在可以直接对数据帧进行操作的方法(例如 sum
、cumsum
),请尝试使用这些方法,因为它们总是会更快。
如果不存在这样的方法,df.apply
在指定要应用的更复杂的操作时会很有用 - 只是对未来的提示!
编辑二
上面的 apply 示例假定数据框中的所有列都用于布尔选择。如果只有特定列具有需要用于计数器的数值,请在 counter
方法中使用 Gio 的建议:
def counter(row):
selection = row[['in_deeds', 'in_valuation', 'in_property', 'in_sg']] > 0
if np.any(selection):
return np.sum(selection)
else:
return 0
【讨论】:
谢谢,blog post 确实帮助我更好地理解了一些事情! 谢谢,达戈罗迪尔。我也一直在尝试您的 df.apply() 解决方案,但我不断收到错误并且无法解决。错误是:TypeError: ("'>' not supported between instances of 'str' and 'int'", 'occurred at index 0')
。会不会是它试图将列标题与 0 进行比较?
可能是您在问题中指定的列之外的其他列属于字符串类型? apply 中的函数将根据其查看的特定行的所有列执行布尔选择,因此如果有字符串,它们将无法在row > 0
中进行比较。但是,您可以在 np.any()
和 np.sum()
方法中使用 Gio 在他的答案中包含的行 - 请参阅最近的编辑以上是关于如何根据记录中其他 4 个字段的布尔运算符有效地更新数据框中的字段?的主要内容,如果未能解决你的问题,请参考以下文章