获取多索引中级别的最后一个元素

Getting the last element of a level in a multiindex

我有一个这种格式的数据框:

a   b   x
1   1   31
1   2   1
1   3   42
1   4   423
1   5   42
1   6   3
1   7   44
1   8   65437
1   9   73
2   1   5656
2   2   7
2   3   5
2   4   5
2   5   34

ab是索引,x是值。

我想要获取行 1 9 732 5 34,换句话说,该级别的最后一行。

我已经弄乱了 .loc.iloc.xs 一个小时,但我无法让它工作。我该怎么做?

使用 df 作为数据框并且列 a 已经排序,这是一种方法 -

df[np.append(np.diff(df['a'])>0,True)]

基本思想是我们沿着排序列 a 执行微分,并使用 (>0) 寻找正变化,给我们一个布尔数组。布尔数组中的 true 元素表示该列中 "group" 的结尾。由于最后一组的最后一个元素没有变化,我们需要在末尾向该布尔数组附加一个 True 元素。最后,用这样一个布尔数组索引 df 到 select 行并给我们想要的输出。

另一种方法可以建议 np.unique 使用其可选参数 return_index,这将为我们提供每个组的第一个出现的元素的索引。因此,要使其适用于最后一个元素,只需翻转列 a,使用 np.unique 并获取第一个出现的索引,然后从 df 中的总行数中减去它们。最后,将 df 与最终输出的索引一起索引。因此,实现将是 -

df.iloc[df.shape[0] - np.unique(df['a'][::-1],return_index=True)[1] - 1]

样本运行-

>>> df
    a   b   x
0   1  26  46
1   1  17  32
2   1  12  65
3   1  31  96
4   1  34  10
5   1   7  80
6   1  64  50
7   1   0  34
8   1  93  28
9   2  18  92
10  2  59  22
11  2  87  31
>>> df[np.append(np.diff(df['a'])>0,True)]
    a   b   x
8   1  93  28
11  2  87  31
>>> df.iloc[df.shape[0] - np.unique(df['a'][::-1],return_index=True)[1] - 1]
    a   b   x
8   1  93  28
11  2  87  31

您可以使用 groupby with last:

print (df.groupby('a', as_index=False).last())
   a  b   x
0  1  9  73
1  2  5  34

如果abMultiIndex, first call reset_index的水平:

print (df.reset_index().groupby('a', as_index=False).last())
   a  b   x
0  1  9  73
1  2  5  34

特例

jezrael 提出的 groupby 解决方案是 high-level 通用解决方案。但是当 groupby 生成很多不同的组时(在 OP 提供的示例中,这将由 a 的很多不同值引起),它的性能很差。在这里,我提出了一个针对特殊情况(与 OP 的情况相匹配)的优化解决方案。

假设您有一个由 MultiIndex 索引的具有多个级别的数据框,并且这些级别的 last 的值始终在每个组中开始于相同的值;例如,假设值总是从 1 开始并向上计数。在以下示例中,这将是 number 级别。

                value
name number          
a    1       0.548126
b    1       0.774775
     2       0.483701
     3       0.820758
c    1       0.696832
     2       0.905071
d    1       0.750546
     2       0.761081
e    1       0.944682
     2       0.336210

然后,要获取 maximum/last number 值的行的横截面,每个唯一值 name (或任何其他级别的值的组合),你可以这样做:

df[np.roll(df.index.get_level_values('number') == 1, -1)]

你得到:

                value
name number          
a    1       0.548126
b    3       0.820758
c    2       0.905071
d    2       0.761081
e    2       0.336210

说明

Piece-by-piece:

  • df.index.get_level_values('number'):获取每行 number 级别值的数组
  • df.index.get_level_values('number') == 1:布尔数组,对于其中 number 为 1
  • 的那些行,它们是 True
  • np.roll(df.index.get_level_values('number') == 1, -1):将前一个数组的所有值以循环方式向后移动一个位置(即第一个元素成为最后一个,第二个,第一个,依此类推)。

这个想法是,一个组的 last 值总是紧接在该组的 first 值之前,它总是1。因此,如果我们得到一个 number 值为 1 的行的布尔掩码,我们可以将所有这些布尔值 向后 移动一个,我们得到一个掩码number.

的最后一个值

通过循环移动 来考虑最后一行的特殊情况,以便第一个布尔值最后结束——第一行总是 number 等于1,因此布尔值将始终为 True,因此最后一行总是被选中(如预期的那样)。

泛型函数

def innermost_level_max(df, start_value=1, drop_level=False):
    assert df.index.is_lexsorted()
    level_values = df.index.get_level_values(-1)
    result = df[np.roll(level_values == start_value, -1)]
    if drop_level:
        result = result.droplevel(-1)
    return result

要玩的设置代码

import itertools as itt

import numpy as np
import pandas as pd
import perfplot

rng = np.random.default_rng(42)


def generate_names():
    alphabet = [chr(i) for i in range(ord('a'), ord('z') + 1)]
    for length in itt.count(1):
        for tup in itt.product(*([alphabet]*length)):
            yield ''.join(tup)


def make_ragged_df(n):
    lengths = rng.integers(1, 3, endpoint=True, size=n)
    names = np.fromiter(
        itt.chain.from_iterable(itt.repeat(n, times=r) for n, r in zip(generate_names(), lengths)),
        dtype='U100',
        count=n
    )
    numbers = np.fromiter(itt.chain.from_iterable(map(range, lengths)), int, count=n) + 1
    index = pd.MultiIndex.from_arrays([names, numbers], names=['name', 'number'])
    data = np.random.rand(n)
    df = pd.DataFrame({'value': data}, index=index)
    return df

这允许您创建示例数据框:

>>> make_ragged_df(10)
                value
name number          
a    1       0.548126
b    1       0.774775
     2       0.483701
     3       0.820758
c    1       0.696832
     2       0.905071
d    1       0.750546
     2       0.761081
e    1       0.944682
     2       0.336210

性能

使用perfplot:

import perfplot

benchmarks = perfplot.bench(
    setup=lambda n: make_ragged_df(n),
    kernels=[
        lambda df: df.groupby('name', sort=False).tail(1),
        lambda df: df[np.roll(df.index.get_level_values('number') == 1, -1)],
    ],
    labels=["with groupby", "with np.roll on == 1"],
    n_range=range(50, 10000, 500),
    xlabel="total number of rows",
)

benchmarks.show()


更特殊的情况

如果您知道 number always 的最后一个值是什么,例如3,你只需要一个索引切片:

df.loc[pd.IndexSlice[:, 3], :]

或 cross-section:

df.xs(3, level='number')

但如果是这种情况,您可能不会一开始就阅读这个问题。