【问题标题】:Getting the last element of a level in a multiindex获取多索引中关卡的最后一个元素
【发布时间】:2016-10-20 16:16:54
【问题描述】:

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

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 搞砸了一个小时,但我无法让它工作。我该怎么做?

【问题讨论】:

  • a 是否总是排序?
  • @Divakar 在我的情况下,是的。

标签: python numpy pandas dataframe multi-index


【解决方案1】:

特殊情况

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

假设您有一个由具有多个级别的MultiIndex 索引的数据框,并且这些级别的最后一个 的值总是在每个组中以相同的值开始;例如,假设值总是从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

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

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

说明

逐个:

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

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

通过循环移动考虑最后一行的特殊情况,因此第一个布尔值最后结束 - 第一行总是有number等于1,因此boolean 将始终为 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], :]

或横截面:

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

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

【讨论】:

    【解决方案2】:

    您可以将groupbylast 一起使用:

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

    如果abMultiIndex 的级别,首先调用reset_index

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

    【讨论】:

      【解决方案3】:

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

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

      基本思想是我们沿着排好序的列a 进行微分,并用(>0) 寻找正变化,给我们一个布尔数组。布尔数组中的true 元素将表示该列中“组”的结束。由于最后一组的最后一个元素没有变化,我们需要在最后的布尔数组中附加一个True 元素。最后,用这样一个布尔数组索引df 以从中选择行并为我们提供所需的输出。

      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
      

      【讨论】:

      • 使用np.diff 是个好主意(+1),当然可以用于其他问题,但我认为这有点太复杂了。
      • np.diff 只是使用了a 的排序特性。因此,可读性较差,但有一点性能值得夸耀:)
      • 特别是在 groupby 会导致很多组的情况下(例如,ab 的很多不同值仅在 1 和 2 之间交替),这种方法性能会更好。
      • 如果ab 是索引(MultiIndex)的一部分,并且索引是唯一的并且按字典顺序排序,并且b 总是采用从1 开始的连续值,可以在b 上使用diff 代替:df[np.append(np.diff(df.index.get_level_values('b')) <= 0, True)]。当对应元素大于或等于下一个(即 diff
      猜你喜欢
      • 2021-12-27
      • 1970-01-01
      • 1970-01-01
      • 2023-04-09
      • 1970-01-01
      • 1970-01-01
      • 2016-07-14
      • 2013-06-05
      • 2012-04-02
      相关资源
      最近更新 更多