如何在pandas DataFrame中获取第二大行值的列名

11

我有一个非常简单的问题 - 我认为 - 但似乎我无法理解这个问题。我是Python和Pandas的初学者。我在论坛上搜索了一下,但找不到符合我的需求的(最近的)答案。

我有一个像这样的数据框:

df = pd.DataFrame({'A': [1.1, 2.7, 5.3], 'B': [2, 10, 9], 'C': [3.3, 5.4, 1.5], 'D': [4, 7, 15]}, index = ['a1', 'a2', 'a3'])

这将给出:

          A   B    C   D
    a1  1.1   2  3.3   4
    a2  2.7  10  5.4   7
    a3  5.3   9  1.5  15

我的问题很简单:我想添加一列,该列给出每行的第二大值的列名。

我编写了一个简单的函数,为每行返回第二大的值

def get_second_best(x):
    return sorted(x)[-2]

df['value'] = df.apply(lambda row: get_second_best(row), axis=1)

这将给出:

      A   B    C   D  value
a1  1.1   2  3.3   4    3.3
a2  2.7  10  5.4   7    7.0
a3  5.3   9  1.5  15    9.0

但我找不到如何在“value”列中显示列名称,而不是值...我考虑使用布尔索引(将“value”列的值与每行进行比较),但我还没有弄清楚如何做到这一点。

更明确地说,我希望它是:

      A   B    C   D  value
a1  1.1   2  3.3   4    C
a2  2.7  10  5.4   7    D
a3  5.3   9  1.5  15    B

感谢任何帮助(和解释)!

2个回答

14

一种方法是使用Series.nlargest选出每行中最大的两个元素,并使用Series.idxmin找到对应最小元素所在的列:

In [45]: df['value'] = df.T.apply(lambda x: x.nlargest(2).idxmin())

In [46]: df
Out[46]:
      A   B    C   D value
a1  1.1   2  3.3   4     C
a2  2.7  10  5.4   7     D
a3  5.3   9  1.5  15     B

值得注意的是,与使用DataFrame.idxmin相比,选择使用Series.idxmin在性能方面可能会有所不同:

df = pd.DataFrame(np.random.normal(size=(100, 4)), columns=['A', 'B', 'C', 'D'])
%timeit df.T.apply(lambda x: x.nlargest(2).idxmin()) # 39.8 ms ± 2.66 ms
%timeit df.T.apply(lambda x: x.nlargest(2)).idxmin() # 53.6 ms ± 362 µs

编辑:在@jpp的回答上补充,如果性能很重要,您可以通过使用Numba,将代码编写得像C语言并进行编译,从而获得显着加速:

from numba import njit, prange

@njit
def arg_second_largest(arr):
    args = np.empty(len(arr), dtype=np.int_)
    for k in range(len(arr)):
        a = arr[k]
        second = np.NINF
        arg_second = 0
        first = np.NINF
        arg_first = 0
        for i in range(len(a)):
            x = a[i]
            if x >= first:
                second = first
                first = x
                arg_second = arg_first
                arg_first = i
            elif x >= second:
                second = x
                arg_second = i
        args[k] = arg_second
    return args

我们来比较一下针对两组数据形状分别为 (1000, 4)(1000, 1000) 的不同解决方案:

df = pd.DataFrame(np.random.normal(size=(1000, 4)))
%timeit df.T.apply(lambda x: x.nlargest(2).idxmin())     # 429 ms ± 5.1 ms
%timeit df.columns[df.values.argsort(1)[:, -2]]          # 94.7 µs ± 2.15 µs
%timeit df.columns[np.argpartition(df.values, -2)[:,-2]] # 101 µs ± 1.07 µs
%timeit df.columns[arg_second_largest(df.values)]        # 74.1 µs ± 775 ns

df = pd.DataFrame(np.random.normal(size=(1000, 1000)))
%timeit df.T.apply(lambda x: x.nlargest(2).idxmin())     # 1.8 s ± 49.7 ms
%timeit df.columns[df.values.argsort(1)[:, -2]]          # 52.1 ms ± 1.44 ms
%timeit df.columns[np.argpartition(df.values, -2)[:,-2]] # 14.6 ms ± 145 µs
%timeit df.columns[arg_second_largest(df.values)]        # 1.11 ms ± 22.6 µs

在最后一种情况下,我能够通过使用@njit(parallel=True)并用for k in prange(len(arr))替换外部循环来挤出更多性能,并将基准测试降至852微秒。


太好了,运行得非常顺畅!真希望我自己能想到这个。非常感谢你! - prcbnt

5

这里提供一个使用NumPy的解决方案。思路是对数据框中的值进行argsort排序,选择倒数第二列,最后使用该列作为索引来访问df.column

df['value'] = df.columns[df.values.argsort(1)[:, -2]]

print(df)

      A   B    C   D value
a1  1.1   2  3.3   4     C
a2  2.7  10  5.4   7     D
a3  5.3   9  1.5  15     B

相比于基于Pandas的解决方案,您应该会发现这更加高效:

# Python 3.6, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)

df = pd.DataFrame(np.random.normal(size=(100, 4)), columns=['A', 'B', 'C', 'D'])

%timeit df.T.apply(lambda x: x.nlargest(2).idxmin())  # 49.6 ms
%timeit df.T.apply(lambda x: x.nlargest(2)).idxmin()  # 73.2 ms
%timeit df.columns[df.values.argsort(1)[:, -2]]       # 36.3 µs

1
+1;拥有NumPy等效工具总是不错的。值得注意的是,如果数据框架较宽,则“df.columns [np.argpartition(df.values,-2)[:,-2]]”可能成为一个可行的替代方案。对于大小为(100, 100)的数据框架,基于argsort的解决方案需要364微秒,而argpartition将其降低到168微秒。 - fuglede

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