Anteponer un nivel a pandas MultiIndex

106

Tengo un DataFrame con un MultiIndex creado después de algunas agrupaciones:

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

¿Cómo antepongo un nivel al MultiIndex para convertirlo en algo como:

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
Yawar
fuente

Respuestas:

139

Una buena forma de hacer esto en una línea usando pandas.concat():

import pandas as pd

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

Una forma aún más corta:

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

Esto se puede generalizar a muchos marcos de datos, consulte los documentos .

okartal
fuente
28
Esto es especialmente bueno para agregar un nivel a las columnas agregando axis=1, ya df.columnsque no tiene el método "set_index" como el índice, lo que siempre me molesta.
Rutger Kassies
2
Esto es bueno porque también funciona para pd.Seriesobjetos, mientras que la respuesta actualmente aceptada (de 2013) no lo hace.
John
1
Ya no funciona. TypeError: unhashable type: 'list'
cduguet
5
Me tomó un tiempo darme cuenta de que si tiene más de una clave, FirstLevelcomo en ['Foo', 'Bar']el primer argumento también necesitará tener la longitud correspondiente, es decir [df] * len(['Foo', 'Bar']),!
mrclng
7
Y aún más conciso:pd.concat({'Foo': df}, names=['Firstlevel'])
kadee
128

Primero puede agregarlo como una columna normal y luego agregarlo al índice actual, así:

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

Y cambie el orden si es necesario con:

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

Lo que resulta en:

                      Vals
Firstlevel A  B           
Foo        a1 b1  0.871563
              b2  0.494001
           a2 b3 -0.167811
           a3 b4 -1.353409
Rutger Kassies
fuente
2
Si hace esto con un marco de datos con un índice de columna MultiIndex, agrega niveles, lo que probablemente no importa en la mayoría de los casos, pero podría, si confía en los metadatos para otra cosa.
naught101
23

Creo que esta es una solución más general:

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

Algunas ventajas sobre las otras respuestas:

  • El nuevo nivel se puede agregar en cualquier ubicación, no solo en la parte superior.
  • Es puramente una manipulación del índice y no requiere manipular los datos, como el truco de concatenación.
  • No requiere agregar una columna como paso intermedio, lo que puede romper los índices de columna de varios niveles.
cxrodgers
fuente
2

Hice una pequeña función de la respuesta de cxrodgers , que en mi humilde opinión es la mejor solución ya que funciona puramente en un índice, independiente de cualquier marco de datos o serie.

Hay una solución que agregué: el to_frame()método inventará nuevos nombres para los niveles de índice que no tienen uno. Como tal, el nuevo índice tendrá nombres que no existen en el índice anterior. Agregué un código para revertir este cambio de nombre.

A continuación se muestra el código, lo he usado por un tiempo y parece funcionar bien. Si encuentra algún problema o casos extremos, estaría muy obligado a ajustar mi respuesta.

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

Pasó el siguiente código de prueba unitaria:

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)
Sam De Meyer
fuente
0

¿Qué tal construirlo desde cero 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'])

De manera similar a la solución de cxrodger , este es un método flexible y evita modificar la matriz subyacente para el marco de datos.

RichieV
fuente