187 votes

Ajout d'un niveau à un MultiIndex pandas

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

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

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

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

247voto

okartal Points 1876

Une bonne 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 chemin encore plus court :

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

Ceci peut être généralisé à de nombreuses trames de données, voir la page docs .

169voto

Rutger Kassies Points 7713

Vous pouvez d'abord l'ajouter comme une colonne normale, puis l'ajouter à l'index actuel, comme suit :

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

Et changez l'ordre si nécessaire avec :

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

Ce qui a pour conséquence :

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

56voto

cxrodgers Points 3167

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.
  • Il s'agit purement d'une manipulation sur l'index et ne nécessite pas de manipuler les données, comme l'astuce de concaténation.
  • Elle ne nécessite pas l'ajout d'une colonne comme étape intermédiaire, ce qui peut casser les index de colonnes à plusieurs niveaux.

6voto

Sam De Meyer Points 537

J'ai fait une petite fonction à partir de réponse de cxrodgers Ce qui, à mon avis, est la meilleure solution puisqu'elle fonctionne uniquement sur un index, indépendamment de tout cadre ou série de données.

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

Voici le code, je l'ai utilisé moi-même pendant un certain temps et il semble fonctionner correctement. Si vous trouvez des problèmes ou des cas limites, je serais très heureux de modifier 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)

4voto

normanius Points 118

Une autre réponse utilisant from_tuples() . Ceci généralise ce réponse précédente.

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)

J'aime cette approche car

  • il ne modifie que l'index (pas d'action de copie inutile du corps)
  • cela fonctionne pour les deux axes (indices de ligne et de colonne)
  • on peut toujours l'écrire en une seule ligne

L'intégration des éléments ci-dessus dans une fonction facilite le passage entre les index de lignes et de colonnes, et entre les index à un ou plusieurs niveaux :

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

Enfin, on peut encore généraliser ce qui précède en insérant la clé à n'importe quel niveau d'index :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X