同时获取最小值和最小值索引(或最大值和最大值索引)?

8
我想知道是否有可能在同一次调用/循环中同时调用idxminmin
假设以下数据框:
    id  option_1    option_2    option_3    option_4
0   0   10.0        NaN         NaN         110.0
1   1   NaN         20.0        200.0       NaN
2   2   NaN         300.0       30.0        NaN
3   3   400.0       NaN         NaN         40.0
4   4   600.0       700.0       50.0        50.0

我希望计算出 option_ 系列中的最小值 (min) 以及包含它的列 (idxmin)。
    id  option_1    option_2    option_3    option_4    min_column  min_value
0   0   10.0        NaN         NaN         110.0       option_1        10.0
1   1   NaN         20.0        200.0       NaN         option_2        20.0
2   2   NaN         300.0       30.0        NaN         option_3        30.0
3   3   400.0       NaN         NaN         40.0        option_4        40.0
4   4   600.0       700.0       50.0        50.0        option_3        50.0

显然,我可以单独调用idxminmin(一个接一个地,参见下面的例子),但有没有一种更有效的方法,不需要两次搜索矩阵(一次搜索值和另一次搜索索引)?


调用minidxmin的示例

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'id': [0,1,2,3,4], 
    'option_1': [10,     np.nan, np.nan, 400,    600], 
    'option_2': [np.nan, 20,     300,    np.nan, 700], 
    'option_3': [np.nan, 200,    30,     np.nan, 50],
    'option_4': [110,    np.nan, np.nan, 40,     50], 
})

df['min_column'] = df.filter(like='option').idxmin(1)
df['min_value'] = df.filter(like='option').min(1)

我预计这会使搜索效果不佳,因为搜索将被执行两次。

@smci,问题的前提是基于理论(而不是“直觉假设”),即通过索引(直接访问)在数组中直接访问值为O(1),搜索为O(n)。实际上,直接访问的k似乎非常大,矢量化搜索非常快。我假设直接访问也应该被矢量化了(我错了吗?),在这种情况下,我们比较的是矢量化的搜索直接访问,再次强调,我的假设可能是错误的。 - toto_tico
我会使用术语“值”或“这些行和列中的值”。pandas的.lookup()[]/.iloc()慢,因为它需要标签(而不是索引)。正如你的答案所示,列越多,速度越慢。此外,如果您说明您关心的典型或最大维度范围:10,000列或更多?1,000行或更多?矩阵是否稀疏,您是否关心NaN条目,NaN是否可以稀疏表示?请参见pandas Sparse data structures - smci
Pandas查找时间比较 - smci
1
你知道标签是如何转换为直接索引的吗?我本来以为它类似于字典(哈希表),那么更多的列不应该使它们变慢。等等,实际上,你说的并不是我的结果所显示的。目前的结果无法得出关于查找和列数的任何结论。(现在我测试了一下,对于1000行和100、1000、10000列,结果几乎相同,但对于10列,速度快了约5倍) - toto_tico
显示剩余7条评论
3个回答

5

Google Colab
GitHub

转置后进行agg

df.set_index('id').T.agg(['min', 'idxmin']).T

  min    idxmin
0  10  option_1
1  20  option_2
2  30  option_3
3  40  option_4
4  50  option_3

Numpy v1

d_ = df.set_index('id')
v = d_.values
pd.DataFrame(dict(
    Min=np.nanmin(v, axis=1),
    Idxmin=d_.columns[np.nanargmin(v, axis=1)]
), d_.index)

      Idxmin   Min
id                
0   option_1  10.0
1   option_2  20.0
2   option_3  30.0
3   option_4  40.0
4   option_3  50.0

Numpy v2

col_mask = df.columns.str.startswith('option')
options = df.columns[col_mask]
v = np.column_stack([*map(df.get, options)])
pd.DataFrame(dict(
    Min=np.nanmin(v, axis=1),
    IdxMin=options[np.nanargmin(v, axis=1)]
))

全面模拟

结论

Numpy解决方案最快。

结果

10列

         pir_agg_1  pir_agg_2  pir_agg_3  wen_agg_1  tot_agg_1  tot_agg_2
10       12.465358   1.272584        1.0   5.978435   2.168994   2.164858
30       26.538924   1.305721        1.0   5.331755   2.121342   2.193279
100      80.304708   1.277684        1.0   7.221127   2.215901   2.365835
300     230.009000   1.338177        1.0   5.869560   2.505447   2.576457
1000    661.432965   1.249847        1.0   8.931438   2.940030   3.002684
3000   1757.339186   1.349861        1.0  12.541915   4.656864   4.961188
10000  3342.701758   1.724972        1.0  15.287138   6.589233   6.782102

在此输入图片描述

100列

        pir_agg_1  pir_agg_2  pir_agg_3  wen_agg_1  tot_agg_1  tot_agg_2
10       8.008895   1.000000   1.977989   5.612195   1.727308   1.769866
30      18.798077   1.000000   1.855291   4.350982   1.618649   1.699162
100     56.725786   1.000000   1.877474   6.749006   1.780816   1.850991
300    132.306699   1.000000   1.535976   7.779359   1.707254   1.721859
1000   253.771648   1.000000   1.232238  12.224478   1.855549   1.639081
3000   346.999495   2.246106   1.000000  21.114310   1.893144   1.626650
10000  431.135940   2.095874   1.000000  32.588886   2.203617   1.793076

在此输入图片描述

功能

def pir_agg_1(df):
  return df.set_index('id').T.agg(['min', 'idxmin']).T

def pir_agg_2(df):
  d_ = df.set_index('id')
  v = d_.values
  return pd.DataFrame(dict(
      Min=np.nanmin(v, axis=1),
      IdxMin=d_.columns[np.nanargmin(v, axis=1)]
  ))

def pir_agg_3(df):
  col_mask = df.columns.str.startswith('option')
  options = df.columns[col_mask]
  v = np.column_stack([*map(df.get, options)])
  return pd.DataFrame(dict(
      Min=np.nanmin(v, axis=1),
      IdxMin=options[np.nanargmin(v, axis=1)]
  ))

def wen_agg_1(df):
  v = df.filter(like='option')
  d = v.stack().sort_values().groupby(level=0).head(1).reset_index(level=1)
  d.columns = ['IdxMin', 'Min']
  return d

def tot_agg_1(df):
  """I combined toto_tico's 2 filter calls into one"""
  d = df.filter(like='option')
  return df.assign(
      IdxMin=d.idxmin(1),
      Min=d.min(1)
  )

def tot_agg_2(df):
  d = df.filter(like='option')
  idxmin = d.idxmin(1)
  return df.assign(
      IdxMin=idxmin,
      Min=d.lookup(d.index, idxmin)
  )

仿真设置

def sim_df(n, m):
  return pd.DataFrame(
      np.random.randint(m, size=(n, m))
  ).rename_axis('id').add_prefix('option').reset_index()


fs = 'pir_agg_1 pir_agg_2 pir_agg_3 wen_agg_1 tot_agg_1 tot_agg_2'.split()
ix = [10, 30, 100, 300, 1000, 3000, 10000]

res_small_col = pd.DataFrame(index=ix, columns=fs, dtype=float)
res_large_col = pd.DataFrame(index=ix, columns=fs, dtype=float)

for i in ix:
  df = sim_df(i, 10)
  for j in fs:
    stmt = f"{j}(df)"
    setp = f"from __main__ import {j}, df"
    res_small_col.at[i, j] = timeit(stmt, setp, number=10)

for i in ix:
  df = sim_df(i, 100)
  for j in fs:
    stmt = f"{j}(df)"
    setp = f"from __main__ import {j}, df"
    res_large_col.at[i, j] = timeit(stmt, setp, number=10)

1
或者 df.agg(lambda x: x.agg(['min', 'idxmin']), axis=1),不要加 .T - rafaelc
1
顺便提一下,pandas 0.24.0 版本将允许直接执行 df.agg(['min', 'idxmin'], 1),请查看这里这里 ;} - rafaelc
1
谢谢@RafaelC,这是值得期待的事情。我本能地尝试了一下我正在使用的版本,但显然没有起作用。 - piRSquared
一般来说,这个答案的效率要低得多,特别是如果你没有很多列。请检查我的答案,其中包括时间。 - toto_tico
1
我本来就预料到了。我看你没有测试我的第二个解决方案,它已经存在了3个小时了。 - piRSquared
@piRSquared,非常棒的结果。我想加速代码中的瓶颈,numpy的答案肯定会有所帮助。您能否在开头复制numpy v2答案,并使用df.assign操作创建min_columnmin_value(这是为了与问题中的示例保持一致)?(请检查我的答案,看看我是什么意思)。之后,我会将其标记为答案。此外,您能否看一下test9,即pandas中numpy lookup等效的内容?我很难接受搜索比索引更快,您知道更好的方法吗? - toto_tico

2

更新 2:

在我看来,@piRSquared 的 numpy 解决方案是最常见的情况下的赢家。以下是他的答案,只进行了最少的修改以将列分配给原始数据帧(这是我在所有测试中都做的,以保持与原始问题示例的一致性)。

col_mask = df.columns.str.startswith('option')
options = df.columns[col_mask]
v = np.column_stack([*map(df.get, options)])
df.assign(min_value = np.nanmin(v, axis=1),
          min_column = options[np.nanargmin(v, axis=1)])

如果你有很多列(超过10000列),你应该小心谨慎,因为在这些极端情况下,结果可能会显著改变。

更新1:

根据我的测试,单独调用minidxmin是基于所有建议答案中最快的方法。


尽管不是同时进行(请参见下面的直接答案),但最好使用DataFrame.lookup在列索引(min_column列)上,以避免搜索值(min_values)。

因此,您只需遍历结果min_column系列 - 这是O(n),而不是遍历整个矩阵 - 这是O(n*m):

df = pd.DataFrame({
    'id': [0,1,2,3,4], 
    'option_1': [10,     np.nan, np.nan, 400,    600], 
    'option_2': [np.nan, 20,     300,    np.nan, 700], 
    'option_3': [np.nan, 200,    30,     np.nan, 50],
    'option_4': [110,    np.nan, np.nan, 40,     50], 
})

df['min_column'] = df.filter(like='option').idxmin(1)
df['min_value'] = df.lookup(df.index, df['min_column'])

直接回答(不够高效

由于您询问了如何“在同一次调用中计算值”(假设是因为您为问题简化了示例),您可以尝试使用lambda表达式:

def min_idxmin(x):
    _idx = x.idxmin()
    return _idx, x[_idx]

df['min_column'], df['min_value'] = zip(*df.filter(like='option').apply(
    lambda x: min_idxmin(x), axis=1))

要明确的是,虽然这里删除了第二个搜索(用x[_idx]直接访问),但这很可能需要更长的时间,因为您没有利用pandas/numpy的向量化属性。
总之,pandas/numpy向量化操作非常快速。

摘要总结:

使用df.lookup似乎没有任何优势,单独调用minidxmin比使用查找更好,这是令人惊叹的,并值得自己提出问题

时间摘要:

我测试了一个具有10000行和10列的数据框(初始示例中的option_序列)。由于我得到了一些意外的结果,所以我还进行了1000x1000和100x10000的测试。根据结果:

  1. 使用 numpy,就像@piRSquared(test8)建议的一样是最好的选择,只有在列数很多时(100,10000),它开始表现较差,但这并不能证明通常使用numpy不好。Test9试图在numpy中使用索引,但总体上表现更差。

  2. 分别调用minidxmin对于10000x10的情况表现最佳,甚至好于Dataframe.lookup(虽然在100x10000的情况下,Dataframe.lookup的结果表现更好)。尽管数据的形状影响结果,但我认为有10000列有点不切实际。

  3. @Wen提供的解决方案在性能上跟进,虽然它不比分别调用idxminmin或使用Dataframe.lookup更好。我进行了额外的测试(见test7()),因为我觉得操作(reset_indexzip)的添加可能会干扰结果。即使它不执行赋值(我无法想出如何使用head(1)进行赋值),它仍比test1test2差。@Wen,您能帮我吗?

  4. @Wen的解决方案在有更多列时(1000x1000或100x10000),性能会下降,这是有道理的,因为排序比搜索慢。在这种情况下,我建议使用的lambda表达式表现更好。

  5. 任何其他使用lambda表达式或使用转置(T)的解决方案都落后于前面提到的几种方法。我建议使用的lambda表达式需要大约1秒钟,比@piRSquared和@RafaelC建议的transpose T所需的~11秒快得多。

使用10000行x 10列的TimeIt结果(pandas 0.23.4):

使用以下10000行和10列的数据框:

import pandas as pd
import numpy as np

df = pd.DataFrame(np.random.randint(0,100,size=(10000, 10)), columns=[f'option_{x}' for x in range(1,11)]).reset_index()
  1. Calling the two columns twice separatedly:

    def test1():
        df['min_column'] = df.filter(like='option').idxmin(1)
        df['min_value'] = df.filter(like='option').min(1)
    %timeit -n 100 test1()
    13 ms ± 580 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
  2. Calling the lookup (it is slower for this case!):

    def test2():
        df['min_column'] = df.filter(like='option').idxmin(1)
        df['min_value'] = df.lookup(df.index, df['min_column'])    
    %timeit -n 100 test2()
    # 15.7 ms ± 399 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
  3. Using apply and min_idxmin(x):

    def min_idxmin(x):
        _idx = x.idxmin()
        return _idx, x[_idx]
    
    def test3():
        df['min_column'], df['min_value'] = zip(*df.filter(like='option').apply(
            lambda x: min_idxmin(x), axis=1))
    %timeit -n 10 test3()
    # 968 ms ± 32.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
  4. Using agg['min', 'idxmin'] by @piRSquared:

    def test4():
        df['min_column'], df['min_value'] = zip(*df.set_index('index').filter(like='option').T.agg(['min', 'idxmin']).T.values)
    
    %timeit -n 1 test4()
    # 11.2 s ± 850 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
  5. Using agg['min', 'idxmin'] by @RafaelC:

    def test5():
    
        df['min_column'], df['min_value'] = zip(*df.filter(like='option').agg(lambda x: x.agg(['min', 'idxmin']), axis=1).values)
        %timeit -n 1 test5()
        # 11.7 s ± 597 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
  6. Sorting values by @Wen:

    def test6():
        df['min_column'], df['min_value'] = zip(*df.filter(like='option').stack().sort_values().groupby(level=[0]).head(1).reset_index(level=1).values)
    
    %timeit -n 100 test6()
    # 33.6 ms ± 1.72 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
  7. Sorting values by @Wen modified by me to make the comparison fairer due to overload of assigment operation (I explained why in the summary at the beginning):

    def test7():
        df.filter(like='option').stack().sort_values().groupby(level=[0]).head(1)
    
    %timeit -n 100 test7()
    # 25 ms ± 937 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
  8. Using numpy:

    def test8():
        col_mask = df.columns.str.startswith('option')
        options = df.columns[col_mask]
        v = np.column_stack([*map(df.get, options)])
        df.assign(min_value = np.nanmin(v, axis=1),
                  min_column = options[np.nanargmin(v, axis=1)])
    
    %timeit -n 100 test8()
    # 2.76 ms ± 248 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
  9. Using numpy but avoid the search (indexing instead):

    def test9():
        col_mask = df.columns.str.startswith('option')
        options = df.columns[col_mask]
        v = np.column_stack([*map(df.get, options)])
        idxmin = np.nanargmin(v, axis=1)
        # instead of looking for the answer, indexes are used
        df.assign(min_value = v[range(v.shape[0]), idxmin],
                  min_column = options[idxmin])
    
    %timeit -n 100 test9()
    # 3.96 ms ± 267 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

1000行x1000列的TimeIt结果:

我进行了更多的测试,使用了1000x1000的形状:

df = pd.DataFrame(np.random.randint(0,100,size=(1000, 1000)), columns=[f'option_{x}' for x in range(1,1001)]).reset_index()

尽管结果会发生改变:
test1    ~27.6ms
test2    ~29.4ms
test3    ~135ms
test4    ~1.18s
test5    ~1.29s
test6    ~287ms
test7    ~290ms
test8    ~25.7
test9    ~26.1

100行 x 10000列的TimeIt结果:

我进行了一个100x10000的形状的更多测试:

df = pd.DataFrame(np.random.randint(0,100,size=(100, 10000)), columns=[f'option_{x}' for x in range(1,10001)]).reset_index()

虽然结果会改变:

test1    ~46.8ms
test2    ~25.6ms
test3    ~101ms
test4    ~289ms
test5    ~276ms
test6    ~349ms
test7    ~301ms
test8    ~121ms
test9    ~122ms

我很好奇你有没有计时? - roganjosh
这只是一个建议 :) 我现在无法测试,但我可以看到你问题的逻辑,我只是好奇解决方案是否能够实现相当的加速。 - roganjosh
@roganjosh,我添加了一些计时。令人惊讶的是,min似乎比lookup快得多 - toto_tico
你的第一个函数版本并不代表这个函数。你使用了过滤器、压缩和对现有数据框进行赋值。这些都不在我的函数中。我没有问题让别人测试我的函数运行时间,但是如果试图展示不真实的运行时间,我就有问题了。这可能会误导他人。 - piRSquared
1
可能是正确的(我怀疑你是对的),但它不一致。此外,您从未测试过我的第二个建议,这是为了提高性能而设计的。我的第一个建议旨在简洁明了。另外,当您提出问题时,应声明性能很重要。这对答案的方向有重大影响。 - piRSquared
显示剩余4条评论

2
也许可以使用 stackgroupby 结合使用。
v=df.filter(like='option')
v.stack().sort_values().groupby(level=[0]).head(1).reset_index(level=1)
Out[313]:
    level_1     0
0  option_1  10.0
1  option_2  20.0
2  option_3  30.0
3  option_4  40.0
4  option_3  50.0

如果只有几列,那么这个答案已经足够好了,但是分别调用 minidxmin 更好(请参见我的时间测试结果)。我建议的查找是第二好的,尽管从理论上讲,它应该更好,但由于某种原因似乎 lookupmin - toto_tico
@toto_tico,只是为了确保一下,这不是我干的,我实际上认为你的答案非常有趣(我认为在需要使用掩码进行多次搜索时可能是最好的),而且是的,我讨厌没有解释与负投票相关联... - toto_tico
@toto_tico 我知道啊,我注意到你分享了很多信息,谢谢伙计!! - BENY

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