Anteponi un livello a un MultiIndex panda


101

Ho un DataFrame con un MultiIndex creato dopo alcuni raggruppamenti:

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

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

df

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

Come antepongo un livello al MultiIndex in modo da trasformarlo in qualcosa del tipo:

Output>                       Vals
Output> FirstLevel A  B           
Output> Foo        a1 b1 -1.632460
Output>               b2  0.596027
Output>            a2 b3 -0.619130
Output>            a3 b4 -0.002009

Risposte:


138

Un bel modo per farlo in una riga usando pandas.concat():

import pandas as pd

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

Un modo ancora più breve:

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

Questo può essere generalizzato a molti frame di dati, vedere la documentazione .


28
Questo è particolarmente utile per aggiungere un livello alle colonne aggiungendo axis=1, poiché df.columnsnon ha il metodo "set_index" come l'indice, il che mi infastidisce sempre.
Rutger Kassies

2
Questo è bello perché funziona anche per gli pd.Seriesoggetti, mentre la risposta attualmente accettata (dal 2013) no.
John

1
Non funziona più. TypeError: unhashable type: 'list'
cduguet

5
Mi ci è voluto un po 'per capire che se hai più di una chiave FirstLevelcome nel ['Foo', 'Bar']primo argomento, dovrai anche avere la lunghezza corrispondente, cioè [df] * len(['Foo', 'Bar'])!
mrclng

7
E ancora più conciso:pd.concat({'Foo': df}, names=['Firstlevel'])
kadee

123

Puoi prima aggiungerlo come una colonna normale e poi aggiungerlo all'indice corrente, quindi:

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

E modifica l'ordine se necessario con:

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

Che si traduce in:

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

2
Se lo fai con un dataframe con un indice di colonna MultiIndex, vengono aggiunti livelli, che probabilmente non sono importanti nella maggior parte dei casi, ma potrebbero, se ti affidi ai metadati per qualcos'altro.
nought101

16

Penso che questa sia una soluzione più generale:

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

Alcuni vantaggi rispetto alle altre risposte:

  • Il nuovo livello può essere aggiunto in qualsiasi posizione, non solo in alto.
  • È puramente una manipolazione sull'indice e non richiede la manipolazione dei dati, come il trucco della concatenazione.
  • Non richiede l'aggiunta di una colonna come passaggio intermedio, che può rompere gli indici di colonna multilivello.

2

Ho creato una piccola funzione dalla risposta di cxrodgers , che IMHO è la soluzione migliore poiché funziona puramente su un indice, indipendentemente da qualsiasi frame o serie di dati.

C'è una correzione che ho aggiunto: il to_frame() metodo inventerà nuovi nomi per i livelli di indice che non ne hanno uno. In quanto tale, il nuovo indice avrà nomi che non esistono nel vecchio indice. Ho aggiunto del codice per annullare questo cambio di nome.

Di seguito è riportato il codice, l'ho usato io stesso per un po 'e sembra funzionare bene. Se trovi problemi o casi limite, sarei molto obbligato a modificare la mia risposta.

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

Ha passato il seguente codice unittest:

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)

0

Che ne dici di costruirlo da zero con 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'])

Analogamente alla soluzione di cxrodger , questo è un metodo flessibile ed evita di modificare l'array sottostante per il dataframe.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.