【问题标题】:Prepend a level to a pandas MultiIndex为 pandas MultiIndex 添加一个级别
【发布时间】:2013-01-22 12:52:35
【问题描述】:

我有一个在分组后创建的带有 MultiIndex 的 DataFrame:

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

【问题讨论】:

    标签: python pandas


    【解决方案1】:

    使用from_tuples() 的另一个答案。这概括了this之前的答案。

    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
    

    【讨论】:

      【解决方案2】:

      使用pandas.MultiIndex.from_tuples 从头开始​​构建它怎么样?

      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's solution,这是一种灵活的方法,避免修改数据帧的底层数组。

      【讨论】:

        【解决方案3】:

        使用pandas.concat() 在一行中执行此操作的好方法:

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

        更短的方法:

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

        这可以推广到许多数据帧,请参阅docs

        【讨论】:

        • 这对于通过添加axis=1 为列添加级别特别好,因为df.columns 没有像索引那样的“set_index”方法,这总是让我感到困惑。跨度>
        • 这很好,因为它也适用于 pd.Series 对象,而当前接受的答案(从 2013 年开始)不适用。
        • 不再工作了。 TypeError: unhashable type: 'list'
        • 对我来说不是这样,它仍然有效。请检查您的版本。我用最新版本 0.23.4 对其进行了测试
        • 我花了一段时间才意识到,如果你有多个FirstLevel 的键,就像['Foo', 'Bar'] 一样,第一个参数也需要有相应的长度,即[df] * len(['Foo', 'Bar'])
        【解决方案4】:

        我用cxrodgers answer 做了一个小功能,恕我直言,这是最好的解决方案,因为它完全适用于索引,独立于任何数据框或系列。

        我添加了一个修复: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)
        

        【讨论】:

          【解决方案5】:

          我认为这是一个更通用的解决方案:

          # 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)
          

          相对于其他答案的一些优势:

          • 可以在任何位置添加新关卡,而不仅仅是顶部。
          • 它纯粹是对索引的操作,不需要像连接技巧那样操作数据。
          • 不需要添加列作为中间步骤,这会破坏多级列索引。

          【讨论】:

          • 我真的很喜欢这个答案。执行任何类型的 MultiIndex 操作都非常灵活和直接。当然,这种方法也可以应用于 MultiIndex 列df.columns.to_frame()
          【解决方案6】:

          你可以先将其添加为普通列,然后将其附加到当前索引,所以:

          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
          

          【讨论】:

          • 如果您使用具有 MultiIndex 列索引的数据框执行此操作,它会添加级别,这在大多数情况下可能无关紧要,但如果您依赖元数据来处理其他事情,则可能会这样做。
          • 这仅适用于行索引,不适用于列索引。
          • 是的,使用列多索引执行此操作的最佳方法是什么 - 只是转置?
          猜你喜欢
          • 2020-09-16
          • 1970-01-01
          • 2017-04-19
          • 2018-11-09
          • 2018-12-20
          • 2017-06-18
          • 2020-02-14
          • 1970-01-01
          • 2018-02-03
          相关资源
          最近更新 更多