Ajouter un niveau à un pandas MultiIndex

101

J'ai un DataFrame avec un MultiIndex créé après un certain regroupement:

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

Comment ajouter un niveau au MultiIndex afin de le transformer en quelque chose comme:

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
la source

Réponses:

138

Une belle façon de faire cela en une seule ligne en utilisant pandas.concat():

import pandas as pd

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

Un moyen encore plus court:

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

Cela peut être généralisé à de nombreux blocs de données, voir la documentation .

okartal
la source
28
C'est particulièrement bien pour ajouter un niveau aux colonnes en ajoutant axis=1, car le df.columnsn'a pas la méthode "set_index" comme l'index, ce qui me dérange toujours.
Rutger Kassies
2
C'est bien car cela fonctionne également pour les pd.Seriesobjets, contrairement à la réponse actuellement acceptée (à partir de 2013).
John
1
Ne fonctionne plus. TypeError: unhashable type: 'list'
cduguet
5
Il m'a fallu un certain temps pour réaliser que si vous avez plus d'une clé car FirstLevelcomme dans ['Foo', 'Bar']le premier argument, vous devrez également avoir la longueur correspondante, c'est-à-dire [df] * len(['Foo', 'Bar'])!
mrclng
7
Et encore plus concis:pd.concat({'Foo': df}, names=['Firstlevel'])
kadee
123

Vous pouvez d'abord l'ajouter en tant que colonne normale, puis l'ajouter à l'index actuel, donc:

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

Et modifiez l'ordre si besoin avec:

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

Ce qui se traduit par:

                      Vals
Firstlevel A  B           
Foo        a1 b1  0.871563
              b2  0.494001
           a2 b3 -0.167811
           a3 b4 -1.353409
Rutger Kassies
la source
2
Si vous faites cela avec une trame de données avec un index de colonne MultiIndex, cela ajoute des niveaux, ce qui n'a probablement pas d'importance dans la plupart des cas, mais peut-être, si vous comptez sur les métadonnées pour autre chose.
naught101
16

Je pense que c'est une solution plus générale:

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

Quelques avantages par rapport aux autres réponses:

  • Le nouveau niveau peut être ajouté à n'importe quel endroit, pas seulement au sommet.
  • C'est purement une manipulation sur l'index et ne nécessite pas de manipuler les données, comme l'astuce de concaténation.
  • Il n'est pas nécessaire d'ajouter une colonne comme étape intermédiaire, ce qui peut casser les index de colonne à plusieurs niveaux.
cxrodgers
la source
2

J'ai fait une petite fonction de la réponse de cxrodgers , qui à mon humble avis est la meilleure solution car elle fonctionne uniquement sur un index, indépendant de toute trame ou série de données.

Il y a un correctif que j'ai ajouté: la to_frame()méthode inventera de nouveaux noms pour les niveaux d'index qui n'en ont pas. En tant que tel, le nouvel index aura des noms qui n'existent pas dans l'ancien index. J'ai ajouté du code pour annuler ce changement de nom.

Ci-dessous le code, je l'ai utilisé moi-même pendant un moment et il semble fonctionner correctement. Si vous trouvez des problèmes ou des cas extrêmes, je serais bien obligé d'ajuster ma réponse.

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

Il a passé le code unittest suivant:

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
la source
0

Que diriez-vous de le créer à partir de zéro avec 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 la même manière que la solution de cxrodger , il s'agit d'une méthode flexible et évite de modifier le tableau sous-jacent pour le dataframe.

RichieV
la source