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
axis=1
, yadf.columns
que no tiene el método "set_index" como el índice, lo que siempre me molesta.pd.Series
objetos, mientras que la respuesta actualmente aceptada (de 2013) no lo hace.FirstLevel
como en['Foo', 'Bar']
el primer argumento también necesitará tener la longitud correspondiente, es decir[df] * len(['Foo', 'Bar'])
,!pd.concat({'Foo': df}, names=['Firstlevel'])
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
fuente
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:
fuente
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)
fuente
¿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.
fuente