在pandas DataFrame中找到(仅)满足给定条件的第一行

20

我有一个数据框 df,其中包含一列非常长的随机正整数:

df = pd.DataFrame({'n': np.random.randint(1, 10, size = 10000)})

我想确定列中第一个偶数的索引。一种方法是:

df[df.n % 2 == 0].iloc[0]

但这涉及到很多操作(生成指标f.n % 2 == 0,在这些指标上评估df,最后取第一项),速度非常慢。像这样的循环会快得多:

但是这需要执行许多操作(生成索引f.n % 2 == 0,在这些索引上求值df,最后取第一个项目),速度很慢。像这样的循环要快得多:

for j in range(len(df)):
    if df.n.iloc[j] % 2 == 0:
        break

还因为第一个结果可能会在前几行中出现。是否有任何类似性能的pandas方法可以做到这一点?谢谢。

注意:这个条件(即为偶数)只是一个例子。我正在寻找适用于值上任何类型条件的快速一行替代方案:

df[ conditions on df.n ].iloc[0]

1
为什么不直接使用那个循环呢? - R Nar
是的,这对于查找偶数是行不通的。我的意思是你可以计算数组的模数,但那是你要避免的。以下是一些相关讨论:12 - ayhan
1
如果像你所说的那样,条件通常在前几行中得到满足,那么您可以执行 df.iloc[:x,df.A > 3.5].iloc[0] 来仅搜索前 X 行。如果这样还是找不到,就继续搜索下一个 X 行,以此类推。根据您的数据和 X 的选择,这应该会很快。否则,我可能会尝试 ayhan 链接中的某个答案中的 numba 函数。 - JohnE
1
说到底,对于 df.n 的条件是一个非常广泛的要求,并且根据具体的条件有不同的操作。无论如何,很难避免对系列/列进行逐元素比较。.iloc[0] 或者你在末尾添加的任何其他内容都不是昂贵的部分。 - Brad Solomon
循环时需要注意的一点是:当访问数据框的单个值时,最好使用atiat而不是lociloc。来源:Pandas数据框中迭代行的不同方法-性能比较 - rocarvaj
显示剩余4条评论
5个回答

13

我决定为了好玩尝试一些可能性。我拿到一个数据框:

MAX = 10**7
df = pd.DataFrame({'n': range(MAX)})

(这次不是随机的。)我想要找出第一行,其中n >= N对于某个N的值成立。 我计时了以下四个版本:

def getfirst_pandas(condition, df):
    return df[condition(df)].iloc[0]

def getfirst_iterrows_loop(condition, df):
    for index, row in df.iterrows():
        if condition(row):
            return index, row
    return None

def getfirst_for_loop(condition, df):
    for j in range(len(df)):
        if condition(df.iloc[j]):
            break
    return j

def getfirst_numpy_argmax(condition, df):
    array = df.as_matrix()
    imax  = np.argmax(condition(array))
    return df.index[imax]

其中N表示10的幂次方。当然,numpy(优化的C语言)代码预计比Python中的for循环更快,但我想看看在哪些N值上,Python循环仍然能够运行良好。

我对这些行计时:

getfirst_pandas(lambda x: x.n >= N, df)
getfirst_iterrows_loop(lambda x: x.n >= N, df)
getfirst_for_loop(lambda x: x.n >= N, df)
getfirst_numpy_argmax(lambda x: x >= N, df.n)

对于N = 1, 10, 100, 1000, ...,这是性能的对数对数图:

图片

简单的for循环只有在"第一个True位置"预期位于开头时才有效,但之后就会变得糟糕。最安全的解决方案是np.argmax

从图中可以看出,pandasargmax的时间保持(几乎)不变,因为它们总是扫描整个数组。 最好有一个nppandas方法不需要扫描整个数组。


1
至少有人提到for循环的复杂性取决于期望结果所在的位置...是的,楼主的解决方案通常并不是最快的,正如得到最多赞的答案所声称的那样... - user59271

6

经过一些计时,使用生成器通常会使您更快地获得结果。

df = pd.DataFrame({'n': np.random.randint(1, 10, size = 10000)})

%timeit df[df.n % 2 == 0].iloc[0]
%timeit df.iloc[next(k for k,v in df.iterrows() if v.n % 2 == 0)]
%timeit df.iloc[next(t[0] for t in df.itertuples() if t.n % 2 == 0)]

我理解为:

1000 loops, best of 3: 1.09 ms per loop
1000 loops, best of 3: 619 µs per loop # <-- iterrows generator
1000 loops, best of 3: 1.1 ms per loop
10000 loops, best of 3: 25 µs per loop # <--- your solution

然而,当你将其扩大规模时:
df = pd.DataFrame({'n': np.random.randint(1, 10, size = 1000000)})

差异消失:
10 loops, best of 3: 40.5 ms per loop 
10 loops, best of 3: 40.7 ms per loop # <--- iterrows
10 loops, best of 3: 56.9 ms per loop

你的解决方案最快,为什么不使用它呢?
for j in range(len(df)):
    if df.n.iloc[j] % 2 == 0:
        break

我同意。我认为,在命中目标行时跳出循环,从而跳过下面的行,比寻找迭代所有行的最快方法节省更多时间。(特别是在大型数据框上) - Thomas Fauskanger
谢谢Anton,我想最终会接受在我的代码中编写循环,你展示的是最快的选项。 - peter
我认为你的比较不公平,因为在你的一行代码中,你正在访问满足 n % 2 == 0 条件的数据帧的行,而对于 for 循环,你并没有这样做。为了进行公正的比较,你可以将 df.iloc[j] 添加到这三行代码中,或者删除 next 语句周围的 df.iloc - EdG

1

如果你想要迭代行并在满意时停止,可以使用DataFrame.iterrows,它是pandas的行迭代器。

在这种情况下,你可以像这样实现:

def get_first_row_with(condition, df):
    for index, row in df.iterrows():
        if condition(row):
            return index, row
    return None # Condition not met on any row in entire DataFrame

然后,给定一个DataFrame,例如:

df = pd.DataFrame({
                    'cats': [1,2,3,4], 
                    'dogs': [2,4,6,8]
                  }, 
                  index=['Alice', 'Bob', 'Charlie', 'Eve'])

你可以将它用作:
def some_condition(row):
    return row.cats + row.dogs >= 7

index, row = get_first_row_with(some_condition, df)

# Use results however you like, e.g.:
print('{} is the first person to have at least 7 pets.'.format(index))
print('They have {} cats and {} dogs!'.format(row.cats, row.dogs))

将输出:

Charlie is the first person to have at least 7 pets.
They have 3 cats and 6 dogs!

谢谢Thomas,我很喜欢这个解决方案的风格。如果我找不到其他替代for循环的方法,我很快就会接受你的答案。 - peter
我已经测试了这个for循环与原始的pandas版本,如果条件在数组开头得到满足,它似乎具有类似的性能,然后它变得不那么高效(我的答案中有图表)。 - peter

1

Zip同时压缩索引和列,然后循环遍历以提高循环速度。 Zip提供了最快的循环性能,比iterrows()itertuples()更快。

for j in zip(df.index,df.n):
        if j[1] % 2 == 0:
                index_position = j[0]
                break

0
TLDR:您可以使用 next(j for j in range(len(df)) if df.at[j, "n"] % 2 == 0)

我认为完全可以用一行代码完成你的编程任务。让我们定义一个DataFrame来证明这一点:

df = pd.DataFrame({'n': np.random.randint(1, 10, size = 100000)})

首先,你的代码输出:

for j in range(len(df)):
    if df.n.iloc[j] % 2 == 0:
        break
% 22.1 µs ± 1.5 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

将其转换为一行代码如下:
next(j for j in range(len(df)) if df["n"].iloc[j] % 2 == 0)
% 20.6 µs ± 1.26 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)

为了进一步加快计算速度,我们可以使用at而不是iloc,因为在访问单个值时这样做会更快:
next(j for j in range(len(df)) if df.at[j, "n"] % 2 == 0)
% 8.88 µs ± 617 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

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