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

tot*_*ico 6 python performance pandas

我在想,如果有调用的可能性idxmin,并min在同一时间(在相同的呼叫/循环)。

假设以下数据框:

    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
Run Code Online (Sandbox Code Playgroud)

我想计算该系列的最小值(min)和包含最小值的列(idxminoption_

    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
Run Code Online (Sandbox Code Playgroud)

显然,我可以单独调用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)
Run Code Online (Sandbox Code Playgroud)

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

piR*_*red 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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)]
))
Run Code Online (Sandbox Code Playgroud)

全面模拟

结论

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
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

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
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

功能

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)
  )
Run Code Online (Sandbox Code Playgroud)

Sim设置

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)
Run Code Online (Sandbox Code Playgroud)


tot*_*ico 2

更新2:

\n\n

@piRSquared 的 numpy 解决方案是我认为最常见情况的获胜者。这是他的答案,经过最小修改以将列分配给原始数据框(我在所有测试中都这样做了,以便与原始问题的示例保持一致)

\n\n
col_mask = df.columns.str.startswith(\'option\')\noptions = df.columns[col_mask]\nv = np.column_stack([*map(df.get, options)])\ndf.assign(min_value = np.nanmin(v, axis=1),\n          min_column = options[np.nanargmin(v, axis=1)])\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果您有很多列(超过 10000 个),则应该小心,因为在这些极端情况下,结果可能会开始发生显着变化。

\n\n

更新1:

\n\n

根据我的测试,根据所有建议的答案,致电min和分开是您可以做的最快的事情。idxmin

\n\n
\n\n

虽然不是同时(请参见下面的直接答案),但您应该更好地使用DataFrame.lookup列索引(min_columncolum),以避免搜索值(min_values)。

\n\n

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

\n\n
df = pd.DataFrame({\n    \'id\': [0,1,2,3,4], \n    \'option_1\': [10,     np.nan, np.nan, 400,    600], \n    \'option_2\': [np.nan, 20,     300,    np.nan, 700], \n    \'option_3\': [np.nan, 200,    30,     np.nan, 50],\n    \'option_4\': [110,    np.nan, np.nan, 40,     50], \n})\n\ndf[\'min_column\'] = df.filter(like=\'option\').idxmin(1)\ndf[\'min_value\'] = df.lookup(df.index, df[\'min_column\'])\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

直接回答(效率不高

\n\n

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

\n\n
def min_idxmin(x):\n    _idx = x.idxmin()\n    return _idx, x[_idx]\n\ndf[\'min_column\'], df[\'min_value\'] = zip(*df.filter(like=\'option\').apply(\n    lambda x: min_idxmin(x), axis=1))\n
Run Code Online (Sandbox Code Playgroud)\n\n

需要明确的是,虽然这里删除了第二个搜索(由 中的直接访问代替x[_idx]),但这很可能需要更长的时间,因为您没有利用 pandas/numpy 的矢量化属性。

\n\n

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

\n\n
\n\n
\n\n

总结总结:

\n\n

df.lookup使用、调用min分开似乎没有任何优势idxmin,比使用查找更好,这是令人兴奋的,并且本身就值得一个问题

\n\n

时间总结:

\n\n

我测试了一个包含 10000 行和 10 列的数据框(option_初始示例中的序列)。因为,我得到了一些意想不到的结果,所以我还用 1000x1000 和 100x10000 进行了测试。根据结果​​:

\n\n
    \n
  1. 按照 @piRSquared (test8) 的建议使用 numpy 是明显的赢家,只有当列很多时(100、10000,但并不能证明一般使用它是合理的),性能才会开始变差。test9 修改了在 numpy 中使用索引的尝试,但总体来说性能较差。

  2. \n
  3. 对于 10000x10 的情况,单独调用minidxmin是最好的,甚至比 更好Dataframe.lookup(尽管Dataframe.lookup在 100x10000 的情况下结果表现更好)。尽管数据的形状会影响结果,但我认为拥有 10000 列有点不现实。

  4. \n
  5. @Wen 提供的解决方案在性能上也是不错的,虽然并不比单独调用idxminand min,或者使用Dataframe.lookup. 我做了一个额外的测试(参见test7()),因为我觉得添加操作 (reset_indexzip可能会干扰结果。它仍然比test1和更糟糕test2,即使它没有进行分配(我不知道如何使用 ) 进行分配head(1)。@Wen,你介意帮我一下吗?

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

  8. \n
  9. 任何其他带有 lambda 表达式或使用转置 ( T) 的解决方案都会落后。我建议的 lambda 表达式大约需要 1 秒,比使用 @piRSquared 和 @RafaelC 建议的转置 T 的约 11 秒要好。

  10. \n
\n\n

TimeIt 结果为 10000 行 x 10 列(pandas 0.23.4):

\n\n

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

\n\n
import pandas as pd\nimport numpy as np\n\ndf = pd.DataFrame(np.random.randint(0,100,size=(10000, 10)), columns=[f\'option_{x}\' for x in range(1,11)]).reset_index()\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  1. 分别调用两列两次:

    \n\n
    def test1():\n    df[\'min_column\'] = df.filter(like=\'option\').idxmin(1)\n    df[\'min_value\'] = df.filter(like=\'option\').min(1)\n%timeit -n 100 test1()\n13 ms \xc2\xb1 580 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
    Run Code Online (Sandbox Code Playgroud)
  2. \n
  3. 调用查找(在这种情况下速度较慢!):

    \n\n
    def test2():\n    df[\'min_column\'] = df.filter(like=\'option\').idxmin(1)\n    df[\'min_value\'] = df.lookup(df.index, df[\'min_column\'])    \n%timeit -n 100 test2()\n# 15.7 ms \xc2\xb1 399 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
    Run Code Online (Sandbox Code Playgroud)
  4. \n
  5. 使用applymin_idxmin(x)

    \n\n
    def min_idxmin(x):\n    _idx = x.idxmin()\n    return _idx, x[_idx]\n\ndef test3():\n    df[\'min_column\'], df[\'min_value\'] = zip(*df.filter(like=\'option\').apply(\n        lambda x: min_idxmin(x), axis=1))\n%timeit -n 10 test3()\n# 968 ms \xc2\xb1 32.5 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n
    Run Code Online (Sandbox Code Playgroud)
  6. \n
  7. agg[\'min\', \'idxmin\']@piRSquared使用:

    \n\n
    def test4():\n    df[\'min_column\'], df[\'min_value\'] = zip(*df.set_index(\'index\').filter(like=\'option\').T.agg([\'min\', \'idxmin\']).T.values)\n\n%timeit -n 1 test4()\n# 11.2 s \xc2\xb1 850 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n
    Run Code Online (Sandbox Code Playgroud)
  8. \n
  9. agg[\'min\', \'idxmin\']@RafaelC使用:

    \n\n
    def test5():\n\n    df[\'min_column\'], df[\'min_value\'] = zip(*df.filter(like=\'option\').agg(lambda x: x.agg([\'min\', \'idxmin\']), axis=1).values)\n    %timeit -n 1 test5()\n    # 11.7 s \xc2\xb1 597 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n
    Run Code Online (Sandbox Code Playgroud)
  10. \n
  11. 按@Wen 对值进行排序:

    \n\n
    def test6():\n    df[\'min_column\'], df[\'min_value\'] = zip(*df.filter(like=\'option\').stack().sort_values().groupby(level=[0]).head(1).reset_index(level=1).values)\n\n%timeit -n 100 test6()\n# 33.6 ms \xc2\xb1 1.72 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
    Run Code Online (Sandbox Code Playgroud)
  12. \n
  13. 由于赋值操作的过载,我修改了@Wen对值进行排序,以使比较更公平(我在开头的摘要中解释了原因):

    \n\n
    def test7():\n    df.filter(like=\'option\').stack().sort_values().groupby(level=[0]).head(1)\n\n%timeit -n 100 test7()\n# 25 ms \xc2\xb1 937 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
    Run Code Online (Sandbox Code Playgroud)
  14. \n
  15. 使用numpy:

    \n\n
    def test8():\n    col_mask = df.columns.str.startswith(\'option\')\n    options = df.columns[col_mask]\n    v = np.column_stack([*map(df.get, options)])\n    df.assign(min_value = np.nanmin(v, axis=1),\n              min_column = options[np.nanargmin(v, axis=1)])\n\n%timeit -n 100 test8()\n# 2.76 ms \xc2\xb1 248 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
    Run Code Online (Sandbox Code Playgroud)
  16. \n
  17. 使用 numpy 但避免搜索(改为索引):

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

TimeIt 结果为 1000 行 x 1000 列:

\n\n

我使用 1000x1000 形状执行更多测试:

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

尽管结果发生了变化:

\n\n
test1    ~27.6ms\ntest2    ~29.4ms\ntest3    ~135ms\ntest4    ~1.18s\ntest5    ~1.29s\ntest6    ~287ms\ntest7    ~290ms\ntest8    ~25.7\ntest9    ~26.1\n
Run Code Online (Sandbox Code Playgroud)\n\n

TimeIt 结果为 100 行 x 10000 列:

\n\n

我使用 100x10000 形状执行更多测试:

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

尽管结果发生了变化:

\n\n
test1    ~46.8ms\ntest2    ~25.6ms\ntest3    ~101ms\ntest4    ~289ms\ntest5    ~276ms\ntest6    ~349ms\ntest7    ~301ms\ntest8    ~121ms\ntest9    ~122ms\n
Run Code Online (Sandbox Code Playgroud)\n