同时(“同时”)获取“ min”和“ idxmin”(或“ max”和“ idxmax”)?

时间:2018-08-20 14:01:17

标签: python pandas performance

我想知道是否有可能同时(在同一调用/循环中)调用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

我想计算min系列的最小值(idxmin)和包含最小值的列(option_):

    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)

(我希望这将是次优的,因为执行了两次搜索。)

3 个答案:

答案 0 :(得分: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

enter image description here

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

enter image description here

功能

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 :(得分: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是最快的操作。


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

因此,您无需遍历整个矩阵(即O(n * m)),而只需遍历所得的min_column系列-即O(n):

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的矢量化属性。

底线是熊猫/ numpy向量化操作非常快。



摘要摘要:

使用df.lookup 似乎没有任何优势,分别调用minidxmin会更好,而不是使用令人发指并值得a question in itself的查找。

时间摘要:

我测试了一个具有10000行和10列的数据帧(在初始示例中为option_序列)。既然得到了一些意想不到的结果,然后我也分别使用1000x1000和100x10000进行了测试。根据结果​​:

  1. 使用numpy作为@piRSquared(test8)表示是明显的赢家,只有在有很多列(100、10000,但不能证明其普遍使用)的情况下,性能才开始恶化。 test9修改了尝试在numpy中使用index的功能,但通常来说性能较差。

  2. 在10000x10的情况下,最好分别调用minidxmin,甚至比Dataframe.lookup更好(尽管Dataframe.lookup的结果在100x10000大小写)。尽管数据的形状会影响结果,但我认为拥有10000列有点不现实。

  3. @Wen提供的解决方案遵循性能,但它并不比分别调用idxminmin或使用Dataframe.lookup更好。我做了一个额外的测试(请参阅test7()),因为我觉得相加操作(reset_indexzip可能会干扰结果,但它仍然比test1更糟和test2,即使它没有执行任务(我不知道如何使用head(1)进行任务。)@ Wen,您介意帮我吗?

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

  5. 任何其他具有lambda表达式或使用转置(T)的解决方案都落后。我建议的lambda表达式花了大约1秒,比使用@piRSquared和@RafaelC建议的转置T的〜11秒要好。

TimeIt结果为10000行x 10列(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. 分别两次调用两列:

    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. 调用查找(在这种情况下,它比较慢!):

    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. 使用applymin_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. 使用@piRSquared的agg['min', 'idxmin']

    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. 使用@RafaelC的agg['min', 'idxmin']

    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. 按@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. 由于修改操作过多,我修改了@Wen的值以使比较更合理(我在开头的摘要中解释了原因):

    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. 使用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. 使用numpy,但避免搜索(改为建立索引):

    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)
    

TimeIt结果为1000行x 1000列:

我以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

TimeIt结果为100行x 10000列:

我对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

答案 2 :(得分: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