【问题标题】:Prepend a level to a pandas MultiIndex在 pandas MultiIndex 中添加一个级别
【发布时间】:2020-09-16 07:31:06
【问题描述】:

我有一个在分组后创建的带有 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】:

    使用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'])
    【解决方案2】:

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

    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 列索引的数据框执行此操作,它会添加级别,这在大多数情况下可能无关紧要,但如果您依赖元数据来处理其他事情,则可能会这样做。
    • 这仅适用于行索引,不适用于列索引。
    • 是的,使用列多索引执行此操作的最佳方法是什么 - 只是转置?
    【解决方案3】:

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

    # 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()
    【解决方案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】:

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

      【讨论】:

        【解决方案6】:

        使用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
        

        【讨论】:

          猜你喜欢
          • 2013-01-22
          • 2017-04-19
          • 1970-01-01
          • 2019-02-16
          • 1970-01-01
          • 2018-11-09
          • 2018-12-20
          • 2017-06-18
          • 2020-02-14
          相关资源
          最近更新 更多