Tambahkan level ke MultiIndex pandas

105

Saya memiliki DataFrame dengan MultiIndex yang dibuat setelah beberapa pengelompokan:

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

Bagaimana cara menambahkan level ke MultiIndex sehingga saya mengubahnya menjadi seperti:

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
sumber

Jawaban:

139

Cara yang bagus untuk melakukan ini dalam satu baris menggunakan pandas.concat():

import pandas as pd

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

Cara yang lebih pendek lagi:

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

Ini dapat digeneralisasikan ke banyak bingkai data, lihat dokumen .

okartal
sumber
28
Ini sangat bagus untuk menambahkan level ke kolom dengan menambahkan axis=1, karena df.columnstidak memiliki metode "set_index" seperti indeks, yang selalu mengganggu saya.
Rutger Kassies
2
Ini bagus karena ini juga berfungsi untuk pd.Seriesobjek, sedangkan jawaban yang diterima saat ini (dari 2013) tidak.
Yohanes
1
Tidak bekerja lagi. TypeError: tipe tidak dapat dihashable: 'list'
cduguet
5
Butuh beberapa saat untuk menyadari bahwa jika Anda memiliki lebih dari satu kunci untuk FirstLevelseperti pada ['Foo', 'Bar']argumen pertama juga harus memiliki panjang yang sesuai, yaitu [df] * len(['Foo', 'Bar'])!
mrclng
7
Dan bahkan lebih ringkas:pd.concat({'Foo': df}, names=['Firstlevel'])
kadee
128

Anda dapat menambahkannya terlebih dahulu sebagai kolom normal dan kemudian menambahkannya ke indeks saat ini, jadi:

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

Dan ubah urutannya jika diperlukan dengan:

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

Yang mengakibatkan:

                      Vals
Firstlevel A  B           
Foo        a1 b1  0.871563
              b2  0.494001
           a2 b3 -0.167811
           a3 b4 -1.353409
Rutger Kassies
sumber
2
Jika Anda melakukan ini dengan kerangka data dengan indeks kolom MultiIndex, itu menambahkan level, yang mungkin tidak masalah dalam banyak kasus, tetapi mungkin, jika Anda mengandalkan metadata untuk hal lain.
n nothing101
23

Saya pikir ini adalah solusi yang lebih umum:

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

Beberapa keunggulan dibandingkan jawaban lainnya:

  • Level baru dapat ditambahkan di lokasi mana pun, tidak hanya di atas.
  • Ini murni manipulasi pada indeks dan tidak memerlukan manipulasi data, seperti trik penggabungan.
  • Tidak perlu menambahkan kolom sebagai langkah perantara, yang dapat merusak indeks kolom multi-level.
cxrodgers
sumber
2

Saya membuat sedikit fungsi dari jawaban cxrodgers , yang IMHO adalah solusi terbaik karena bekerja murni pada indeks, terlepas dari bingkai atau seri data apa pun.

Ada satu perbaikan yang saya tambahkan: to_frame()metode ini akan menemukan nama baru untuk tingkat indeks yang tidak memilikinya. Dengan demikian indeks baru akan memiliki nama yang tidak ada di indeks lama. Saya menambahkan beberapa kode untuk mengembalikan perubahan nama ini.

Di bawah ini adalah kodenya, saya telah menggunakannya sendiri untuk sementara waktu dan tampaknya berfungsi dengan baik. Jika Anda menemukan masalah atau kasus tepi, saya akan sangat berkewajiban untuk menyesuaikan jawaban saya.

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

Itu melewati kode unittest berikut:

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
sumber
0

Bagaimana jika membangunnya dari awal dengan 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'])

Mirip dengan solusi cxrodger , ini adalah metode yang fleksibel dan menghindari modifikasi larik yang mendasari untuk dataframe.

RichieV
sumber