特殊情况
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') == 1:True 的布尔数组,用于 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')
但如果是这种情况,您可能一开始就不会阅读这个问题。