在熊猫 MultiIndex 前面添加一个级别

我有一个 DataFrame,在分组之后创建了一个 MultiIndex:

import numpy as np
import pandas as pd
from numpy.random import randn


df = pd.DataFrame({'A' : ['a1', 'a1', 'a2', 'a3'],
'B' : ['b1', 'b2', 'b3', 'b4'],
'Vals' : randn(4)}
).groupby(['A', 'B']).sum()


#            Vals
# A  B
# a1 b1 -1.632460
#    b2  0.596027
# a2 b3 -0.619130
# a3 b4 -0.002009

如何为 MultiIndex 预先设置一个级别,以便将其转换为以下内容:

#                       Vals
# FirstLevel A  B
# Foo        a1 b1 -1.632460
#               b2  0.596027
#            a2 b3 -0.619130
#            a3 b4 -0.002009
117148 次浏览

您可以首先将其作为普通列添加,然后将其追加到当前索引,因此:

df['Firstlevel'] = 'Foo'
df.set_index('Firstlevel', append=True, inplace=True)

如果需要,可以通过以下方式更改顺序:

df.reorder_levels(['Firstlevel', 'A', 'B'])

结果是:

                      Vals
Firstlevel A  B
Foo        a1 b1  0.871563
b2  0.494001
a2 b3 -0.167811
a3 b4 -1.353409

使用 pandas.concat()在一行中完成这项工作的一个好方法是:

import pandas as pd


pd.concat([df], keys=['Foo'], names=['Firstlevel'])

更简单的办法是:

pd.concat({'Foo': df}, names=['Firstlevel'])

这可以推广到许多数据帧,参见 医生

我认为这是一个更普遍的解决方案:

# Convert index to dataframe
old_idx = df.index.to_frame()


# Insert new level at specified location
old_idx.insert(0, 'new_level_name', new_level_values)


# Convert back to MultiIndex
df.index = pandas.MultiIndex.from_frame(old_idx)

与其他答案相比,有一些优势:

  • 新级别可以添加到任何位置,而不仅仅是顶部。
  • 它纯粹是对索引的操作,不需要操作数据,就像连接技巧一样。
  • 它不需要添加列作为中间步骤,因为中间步骤可以中断多级列索引。

我用 Cxrodgers 回答做了一个小函数,恕我直言,它是最好的解决方案,因为它纯粹在索引上工作,独立于任何数据框架或序列。

我添加了一个修复程序: to_frame()方法将为没有索引级别的索引创建新名称。因此,新索引的名称将不存在于旧索引中。我添加了一些代码来恢复这个名称更改。

下面是代码,我自己已经使用了一段时间,它似乎工作得很好。如果你发现任何问题或边缘情况,我将不得不调整我的答案。

import pandas as pd


def _handle_insert_loc(loc: int, n: int) -> int:
"""
Computes the insert index from the right if loc is negative for a given size of n.
"""
return n + loc + 1 if loc < 0 else loc




def add_index_level(old_index: pd.Index, value: Any, name: str = None, loc: int = 0) -> pd.MultiIndex:
"""
Expand a (multi)index by adding a level to it.


:param old_index: The index to expand
:param name: The name of the new index level
:param value: Scalar or list-like, the values of the new index level
:param loc: Where to insert the level in the index, 0 is at the front, negative values count back from the rear end
:return: A new multi-index with the new level added
"""
loc = _handle_insert_loc(loc, len(old_index.names))
old_index_df = old_index.to_frame()
old_index_df.insert(loc, name, value)
new_index_names = list(old_index.names)  # sometimes new index level names are invented when converting to a df,
new_index_names.insert(loc, name)        # here the original names are reconstructed
new_index = pd.MultiIndex.from_frame(old_index_df, names=new_index_names)
return new_index

它通过了以下单元测试代码:

import unittest


import numpy as np
import pandas as pd


class TestPandaStuff(unittest.TestCase):


def test_add_index_level(self):
df = pd.DataFrame(data=np.random.normal(size=(6, 3)))
i1 = add_index_level(df.index, "foo")


# it does not invent new index names where there are missing
self.assertEqual([None, None], i1.names)


# the new level values are added
self.assertTrue(np.all(i1.get_level_values(0) == "foo"))
self.assertTrue(np.all(i1.get_level_values(1) == df.index))


# it does not invent new index names where there are missing
i2 = add_index_level(i1, ["x", "y"]*3, name="xy", loc=2)
i3 = add_index_level(i2, ["a", "b", "c"]*2, name="abc", loc=-1)
self.assertEqual([None, None, "xy", "abc"], i3.names)


# the new level values are added
self.assertTrue(np.all(i3.get_level_values(0) == "foo"))
self.assertTrue(np.all(i3.get_level_values(1) == df.index))
self.assertTrue(np.all(i3.get_level_values(2) == ["x", "y"]*3))
self.assertTrue(np.all(i3.get_level_values(3) == ["a", "b", "c"]*2))


# df.index = i3
# print()
# print(df)

MultiIndex.from _ tuple从头开始构建它怎么样?

df.index = p.MultiIndex.from_tuples(
[(nl, A, B) for nl, (A, B) in
zip(['Foo'] * len(df), df.index)],
names=['FirstLevel', 'A', 'B'])

Cxrodger 解类似,这是一种灵活的方法,可以避免修改数据框架的底层数组。

另一个答案使用 from_tuples()。这推广了 这个以前的答案。

key = "Foo"
name = "First"
# If df.index.nlevels > 1:
df.index = pd.MultiIndex.from_tuples(((key, *item) for item in df.index),
names=[name]+df.index.names)
# If df.index.nlevels == 1:
# df.index = pd.MultiIndex.from_tuples(((key, item) for item in df.index),
#                                      names=[name]+df.index.names)

我喜欢这个方法是因为

  • 它只修改索引(没有不必要的主体复制操作)
  • 它适用于两个轴(行和列索引)
  • 它仍然可以写成一行程序

将上面的内容包装在一个函数中可以更容易地在行索引和列索引之间以及在单级索引和多级索引之间进行切换:

def prepend_index_level(index, key, name=None):
names = index.names
if index.nlevels==1:
# Sequence of tuples
index = ((item,) for item in index)


tuples_gen = ((key,)+item for item in index)
return pd.MultiIndex.from_tuples(tuples_gen, names=[name]+names)


df.index = prepend_index_level(df.index, key="Foo", name="First")
df.columns = prepend_index_level(df.columns, key="Bar", name="Top")


# Top               Bar
#                  Vals
# First A  B
# Foo   a1 b1 -0.446066
#          b2 -0.248027
#       a2 b3  0.522357
#       a3 b4  0.404048

最后,可以通过在任何索引级别插入密钥进一步推广上述内容:

def insert_index_level(index, key, name=None, level=0):
def insert_(pos, seq, value):
seq = list(seq)
seq.insert(pos, value)
return tuple(seq)


names = insert_(level, index.names, name)
if index.nlevels==1:
# Sequence of tuples.
index = ((item,) for item in index)
    

tuples_gen = (insert_(level, item, key) for item in index)
return pd.MultiIndex.from_tuples(tuples_gen, names=names)


df.index = insert_index_level(df.index, key="Foo", name="Last", level=2)
df.columns = insert_index_level(df.columns, key="Bar", name="Top", level=0)


# Top              Bar
#                 Vals
# A  B  Last
# a1 b1 Foo  -0.595949
#    b2 Foo  -1.621233
# a2 b3 Foo  -0.748917
# a3 b4 Foo   2.147814