Сообщить об ошибке.

Объект MultiIndex модуля pandas в Python

Многоуровневый индексный объект для объектов pandas

Синтаксис:

import pandas

midx = pandas.MultiIndex(levels=None, codes=None, sortorder=None, 
                         names=None, copy=False, verify_integrity=True)

Параметры конструктора:

  • levels=None - последовательность уникальных индексных меток для каждого уровня.
  • codes=None - последовательность целых чисел для каждого уровня, обозначающие, какая метка - где находится.
  • sortorder=None - целое число - уровень сортировки (лексикографическая отсортировка по этому уровню).
  • names=None - последовательность имен для каждого из уровней индекса (имя принимается для сопоставления).
  • copy=False - копирует метаданные.
  • verify_integrity=True - проверяет на согласованность аргументы levels и codes.

Возвращаемое значение:

  • объект MultiIndex.

Описание объекта MultiIndex

Класс MultiIndex() представляет собой многоуровневый или иерархический объект индекса для объектов pandas. Объект MultiIndex позволяет хранить и манипулировать данными с помощью произвольного числа измерений в структурах данных с более меньшей размерностью, таких как Series (1D) и DataFrame (2D).

Важность MultiIndex заключается в том, что он позволяет выполнять операции группировки, выбора и изменения формы данных.

Можно работать с иерархически индексированными данными, не создавая самостоятельно MultiIndex. Но иногда, при загрузке данных из файла и подготовки набора данных, может потребоваться создать свой собственный MultiIndex.

Содержание:


Создание объекта MultiIndex

Объект MultiIndex является иерархическим аналогом стандартного объекта Index, который обычно хранит метки осей. Об MultiIndex можно думать как о массиве кортежей, каждый из которых уникален. MultiIndex можно создать из списка массивов (MultiIndex.from_arrays()), массива кортежей (MultiIndex.from_tuples()), декартова произведения нескольких итераций (MultiIndex.from_product()) или DataFrame (MultiIndex.from_frame()). Конструктор Index попытается вернуть MultiIndex, когда ему будет передан список кортежей.

Примеры различных способов инициализации MultiIndex.

>>> import pandas as pd
arrays = [
    ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
    ["one", "two", "one", "two", "one", "two", "one", "two"],
]
>>> tuples = list(zip(*arrays))
>>> tuples
# [('bar', 'one'),
#  ('bar', 'two'),
#  ('baz', 'one'),
#  ('baz', 'two'),
#  ('foo', 'one'),
#  ('foo', 'two'),
#  ('qux', 'one'),
#  ('qux', 'two')]

>>> index = pd.MultiIndex.from_tuples(tuples, names=["first", "second"])
>>> index
# MultiIndex([('bar', 'one'),
#             ('bar', 'two'),
#             ('baz', 'one'),
#             ('baz', 'two'),
#             ('foo', 'one'),
#             ('foo', 'two'),
#             ('qux', 'one'),
#             ('qux', 'two')],
#            names=['first', 'second'])

>>> import numpy as np
>>> s = pd.Series(np.random.randn(8), index=index)
>>> s
# first  second
# bar    one       0.469112
#        two      -0.282863
# baz    one      -1.509059
#        two      -1.135632
# foo    one       1.212112
#        two      -0.173215
# qux    one       0.119209
#        two      -1.044236
# dtype: float64

Если нужно, чтобы каждая пара элементов была в двух итерациях, проще использовать метод MultiIndex.from_product():

>>> iterables = [["bar", "baz", "foo", "qux"], ["one", "two"]]
>>> pd.MultiIndex.from_product(iterables, names=["first", "second"])
# MultiIndex([('bar', 'one'),
#             ('bar', 'two'),
#             ('baz', 'one'),
#             ('baz', 'two'),
#             ('foo', 'one'),
#             ('foo', 'two'),
#             ('qux', 'one'),
#             ('qux', 'two')],
#            names=['first', 'second'])

Можно создать MultiIndex напрямую из DataFrame, используя метод MultiIndex.from_frame() или дополнительного метода MultiIndex.to_frame().

df = pd.DataFrame(
    [["bar", "one"], ["bar", "two"], ["foo", "one"], ["foo", "two"]],
    columns=["first", "second"],
)

>>> pd.MultiIndex.from_frame(df)
# MultiIndex([('bar', 'one'),
#             ('bar', 'two'),
#             ('foo', 'one'),
#             ('foo', 'two')],
#            names=['first', 'second'])

Для автоматического создания MultiIndex можно передать список массивов непосредственно в Series или DataFrame:

arrays = [
    np.array(["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"]),
    np.array(["one", "two", "one", "two", "one", "two", "one", "two"]),
]

>>> s = pd.Series(np.random.randn(8), index=arrays)
>>> s
# bar  one   -0.861849
#      two   -2.104569
# baz  one   -0.494929
#      two    1.071804
# foo  one    0.721555
#      two   -0.706771
# qux  one   -1.039575
#      two    0.271860
# dtype: float64

>>> df = pd.DataFrame(np.random.randn(8, 4), index=arrays)
>>> df
#                 0         1         2         3
# bar one -0.424972  0.567020  0.276232 -1.087401
#     two -0.673690  0.113648 -1.478427  0.524988
# baz one  0.404705  0.577046 -1.715002 -1.039268
#     two -0.370647 -1.157892 -1.344312  0.844885
# foo one  1.075770 -0.109050  1.643563 -1.469388
#     two  0.357021 -0.674600 -1.776904 -0.968914
# qux one -1.294524  0.413738  0.276662 -0.472035
#     two -0.013960 -0.362543 -0.006154 -0.923061

Все конструкторы MultiIndex принимают аргумент names, который хранит имена/метки (в виде строки) для самих уровней. Если имена не указаны, то будет присвоено значение None:

>>> df.index.names
# FrozenList([None, None])

MultiIndex может поддерживать любую ось объекта pandas, а количество уровней индекса зависит пользователя:

>>> df = pd.DataFrame(np.random.randn(3, 8), index=["A", "B", "C"], columns=index)
>>> df
# first        bar                 baz  ...       foo       qux          
# second       one       two       one  ...       two       one       two
# A       0.895717  0.805244 -1.206412  ...  1.340309 -1.170299 -0.226169
# B       0.410835  0.813850  0.132003  ... -1.187678  1.130127 -1.436737
# C      -1.413681  1.607920  1.024180  ... -2.211372  0.974466 -2.006747
# 
# [3 rows x 8 columns]

>>> pd.DataFrame(np.random.randn(6, 6), index=index[:6], columns=index[:6])
# first              bar                 baz                 foo          
# second             one       two       one       two       one       two
# first second                                                            
# bar   one    -0.410001 -0.078638  0.545952 -1.219217 -1.226825  0.769804
#       two    -1.281247 -0.727707 -0.121306 -0.097883  0.695775  0.341734
# baz   one     0.959726 -1.110336 -0.619976  0.149748 -0.732339  0.687738
#       two     0.176444  0.403310 -0.154951  0.301624 -2.179861 -1.369849
# foo   one    -0.954208  1.462696 -1.743161 -0.826591 -0.345352  1.314232
#       two     0.690579  0.995761  2.396780  0.014871  3.357427 -0.317441

В примерах выше верхние уровни индексов "урезаются" , чтобы сделать вывод на консоль более удобным для глаз.

Стоит иметь в виду, что ничто не мешает использовать кортежи в качестве атомарных меток оси:

arrays = [
    ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
    ["one", "two", "one", "two", "one", "two", "one", "two"],
]
>>> tuples = list(zip(*arrays))
>>> pd.Series(np.random.randn(8), index=tuples)
# (bar, one)   -1.236269
# (bar, two)    0.896171
# (baz, one)   -0.487602
# (baz, two)   -0.082240
# (foo, one)   -2.182937
# (foo, two)    0.380396
# (qux, one)    0.084844
# (qux, two)    0.432390
# dtype: float64

Просмотр меток уровней MultiIndex

Метод MultiIndex.get_level_values() вернет вектор меток для каждого положения на определенном уровне:

# принимает уровень в виде числа
>>> index.get_level_values(0)
# Index(['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

# или в виде индексной метки
>>> index.get_level_values("second")
# Index(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'], dtype='object', name='second')

Базовая индексация/выбор данных по оси с помощью MultiIndex

Одной из важных особенностей MultiIndex является то, что можно сделать "частичный выбор" по метке, которая идентифицирует подгруппу в данных. В результате частичного выбора "сбрасываются" уровни MultiIndex, аналогично выбору столбца в обычном DataFrame:

Пример с DataFrame:

# используем `DataFrame`, созданный ранее
>>> df
# first        bar                 baz  ...       foo       qux          
# second       one       two       one  ...       two       one       two
# A       0.895717  0.805244 -1.206412  ...  1.340309 -1.170299 -0.226169
# B       0.410835  0.813850  0.132003  ... -1.187678  1.130127 -1.436737
# C      -1.413681  1.607920  1.024180  ... -2.211372  0.974466 -2.006747
# 
# [3 rows x 8 columns]

>>> df["bar"]
# second       one       two
# A       0.895717  0.805244
# B       0.410835  0.813850
# C      -1.413681  1.607920

>>> df["bar", "one"]
# A    0.895717
# B    0.410835
# C   -1.413681
# Name: (bar, one), dtype: float64

>>> df["bar"]["one"]
# A    0.895717
# B    0.410835
# C   -1.413681
# Name: one, dtype: float64

Пример с Series:

# используем `Series`, созданную ранее
>>> s
# bar  one   -0.861849
#      two   -2.104569
# baz  one   -0.494929
#      two    1.071804
# foo  one    0.721555
#      two   -0.706771
# qux  one   -1.039575
#      two    0.271860
# dtype: float64

>>> s["qux"]
# one   -1.039575
# two    0.271860
# dtype: float64

Ниже, в подразделе "Использование функций slice() и pandas.IndexSlice для выполнения срезов с MultiIndex" объясняется как сделать выбор данных на более глубоком уровне.

Определение уровней MultiIndex

MultiIndex сохраняет все определенные уровни индекса, даже если они фактически не используются. Это можно заметить при создании среза индекса. Например:

# исходный `MultiIndex`
>>> df.columns.levels 
# FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

# срез `MultiIndex`
>>> df[["foo","qux"]].columns.levels
# FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

Это сделано для того, чтобы избежать повторного расчета уровней и повысить производительность срезов. Если нужно посмотреть только использованные уровни, то можно использовать метод MultiIndex.get_level_values().

>>> df[["foo", "qux"]].columns.to_numpy()
# array([('foo', 'one'), ('foo', 'two'), ('qux', 'one'), ('qux', 'two')], dtype=object)

# для определенного уровня
>>> df[["foo", "qux"]].columns.get_level_values(0)
# Index(['foo', 'foo', 'qux', 'qux'], dtype='object', name='first')

Чтобы восстановить MultiIndex только используемых уровней, может быть использован метод MultiIndex.remove_unused_levels().

>>> new_mi = df[["foo", "qux"]].columns.remove_unused_levels()
>>> new_mi.levels
# FrozenList([['foo', 'qux'], ['one', 'two']])

Выравнивание данных с MultiIndex

Операции между объектами с разными индексами, имеющими MultiIndex по осям, будут работать так, как ожидается. Выравнивание данных будет работать так же, как обычный индекс кортежей:

# используем `Series`, созданную ранее
>>> s
# bar  one   -0.861849
#      two   -2.104569
# baz  one   -0.494929
#      two    1.071804
# foo  one    0.721555
#      two   -0.706771
# qux  one   -1.039575
#      two    0.271860
# dtype: float64

>>> s + s[:-2]
# bar  one   -1.723698
#      two   -4.209138
# baz  one   -0.989859
#      two    2.143608
# foo  one    1.443110
#      two   -1.413542
# qux  one         NaN
#      two         NaN
# dtype: float64

>>> s + s[::2]
# bar  one   -1.723698
#      two         NaN
# baz  one   -0.989859
#      two         NaN
# foo  one    1.443110
#      two         NaN
# qux  one   -2.079150
#      two         NaN
# dtype: float64

Использование переиндексации MultiIndex

Метод .reindex() объектов Series/DataFrame можно вызвать с другим MultiIndex или даже со списком или массивом кортежей:

>>> s.reindex(index[:3])
# first  second
# bar    one      -0.861849
#        two      -2.104569
# baz    one      -0.494929
# dtype: float64

>>> s.reindex([("foo", "two"), ("bar", "one"), ("qux", "one"), ("baz", "one")])
# foo  two   -0.706771
# bar  one   -0.861849
# qux  one   -1.039575
# baz  one   -0.494929
# dtype: float64

Расширенная индексация/выбор данных с MultiIndex

Синтаксическая интеграция MultiIndex в расширенное индексирование с помощью DataFrame.loc немного сложна... Как правило, ключи MultiIndex имеют форму кортежей. Например, следующий код работает так, как и ожидается:

# используем `DataFrame`, созданный ранее
>>> df
# first        bar                 baz  ...       foo       qux          
# second       one       two       one  ...       two       one       two
# A       0.895717  0.805244 -1.206412  ...  1.340309 -1.170299 -0.226169
# B       0.410835  0.813850  0.132003  ... -1.187678  1.130127 -1.436737
# C      -1.413681  1.607920  1.024180  ... -2.211372  0.974466 -2.006747
# 
# [3 rows x 8 columns]

# транспонируем `DataFrame`
# (меняем оси местами)
>>> df = df.T
>>> df
#                      A         B         C
# first second                              
# bar   one     0.895717  0.410835 -1.413681
#       two     0.805244  0.813850  1.607920
# baz   one    -1.206412  0.132003  1.024180
#       two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372
# qux   one    -1.170299  1.130127  0.974466
#       two    -0.226169 -1.436737 -2.006747

>>> df.loc[("bar", "two")]
# A    0.805244
# B    0.813850
# C    1.607920
# Name: (bar, two), dtype: float64

Обратите внимание, что в этом примере также будет работать вызов df.loc['bar', 'two'], но такое сокращенное обозначение в целом может привести к неоднозначности.

Если с помощью DataFrame.loc[] нужно выбрать определенный столбец, то необходимо использовать следующий кортеж:

>>> df.loc[("bar", "two"), "A"]
# 0.8052440253863785

Не обязательно указывать все уровни MultiIndex, а передать только первые элементы кортежа. Например, можно использовать "частичную" индексацию, чтобы получить все элементы группы bar на первом уровне:

# полная запись => df.loc['bar',]
>>> df.loc["bar"]
#                A         B         C
# second                              
# one     0.895717  0.410835 -1.413681
# two     0.805244  0.813850  1.607920

Это сокращение для более подробной записи df.loc[('bar',),].

Также неплохо работает "частичный" срез.

>>> df.loc["baz":"foo"]
#                      A         B         C
# first second                              
# baz   one    -1.206412  0.132003  1.024180
#       two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372

Можно выполнить срез по диапазону значений, предоставив фрагмент кортежей.

>>> df.loc[("baz", "two"):("qux", "one")]
#                      A         B         C
# first second                              
# baz   two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372
# qux   one    -1.170299  1.130127  0.974466

>>> df.loc[("baz", "two"):"foo"]
#                      A         B         C
# first second                              
# baz   two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372

Передача списка индексных меток или кортежей работает аналогично переиндексации:

>>> df.loc[[("bar", "two"), ("qux", "one")]]
#                      A         B         C
# first second                              
# bar   two     0.805244  0.813850  1.607920
# qux   one    -1.170299  1.130127  0.974466

Примечание. Важно отметить, что когда дело доходит до индексации, то в pandas кортежи и списки не обрабатываются одинаково. Кортеж интерпретируется как один многоуровневый ключ, а список используется для указания нескольких ключей. Другими словами, кортежи идут горизонтально (обход уровней), списки идут вертикально (уровни сканирования).

Важно отметить, что список кортежей индексирует несколько полных ключей MultiIndex, а кортеж списков относится к нескольким значениям внутри уровня:

s = pd.Series(
    [1, 2, 3, 4, 5, 6],
    index=pd.MultiIndex.from_product([["A", "B"], ["c", "d", "e"]]),
)

# список кортежей
>>> s.loc[[("A", "c"), ("B", "d")]]
# A  c    1
# B  d    5
# dtype: int64

# кортеж списков
>>> s.loc[(["A", "B"], ["c", "d"])]
# A  c    1
#    d    2
# B  c    4
#    d    5
# dtype: int64

Использование функций slice() и pandas.IndexSlice для выполнения срезов с MultiIndex

Вы можете , предоставив несколько индексаторов.

Для выполнения срезов с MultiIndex можно предоставить любой из селекторов, как если бы вы индексировали по метке, включая срезы, списки меток, а так-же метки и логические операторы.

Чтобы выбрать все содержимое какого-то уровня, то для него необходимо использовать вызов slice(None), при этом не нужно указывать более глубокие уровни, они будут подразумеваться.

Предупреждение. В спецификации DataFrame.loc необходимо указать все оси, то есть для индекса и столбцов. Есть некоторые неоднозначные случаи, когда переданный индексатор может быть ошибочно интерпретирован как индексирующий обе оси, а не как, скажем, MultiIndex для строк.

# ВНИМАНИЕ псевдокод
# используется полная спецификация
df.loc[(slice("A1", "A3"), ...), :]

# не следует этого делать:
df.loc[(slice("A1", "A3"), ...)]

Для примеров выбора данных срезом подготовим новый DataFrame c MultiIndex

def mklbl(prefix, n):
    return ["%s%s" % (prefix, i) for i in range(n)]

miindex = pd.MultiIndex.from_product(
    [mklbl("A", 4), mklbl("B", 2), mklbl("C", 4), mklbl("D", 2)]
)

micolumns = pd.MultiIndex.from_tuples(
    [("a", "foo"), ("a", "bar"), ("b", "foo"), ("b", "bah")], names=["lvl0", "lvl1"]
)

dfmi = (
    pd.DataFrame(
        np.arange(len(miindex) * len(micolumns)).reshape(
            (len(miindex), len(micolumns))
        ),
        index=miindex,
        columns=micolumns,
    )
    .sort_index()
    .sort_index(axis=1)
)

>>> dfmi
# lvl0           a         b     
# lvl1         bar  foo  bah  foo
# A0 B0 C0 D0    1    0    3    2
#          D1    5    4    7    6
#       C1 D0    9    8   11   10
#          D1   13   12   15   14
#       C2 D0   17   16   19   18
# ...          ...  ...  ...  ...
# A3 B1 C1 D1  237  236  239  238
#       C2 D0  241  240  243  242
#          D1  245  244  247  246
#       C3 D0  249  248  251  250
#          D1  253  252  255  254
# 
# [64 rows x 4 columns]

Базовый выбор данных с MultiIndex с использованием функции slice(), списков и индексных меток.

>>> dfmi.loc[(slice("A1", "A3"), slice(None), ["C1", "C3"]), :]
# lvl0           a         b     
# lvl1         bar  foo  bah  foo
# A1 B0 C1 D0   73   72   75   74
#          D1   77   76   79   78
#       C3 D0   89   88   91   90
#          D1   93   92   95   94
#    B1 C1 D0  105  104  107  106
# ...          ...  ...  ...  ...
# A3 B0 C3 D1  221  220  223  222
#    B1 C1 D0  233  232  235  234
#          D1  237  236  239  238
#       C3 D0  249  248  251  250
#          D1  253  252  255  254
# 
# [24 rows x 4 columns]

Можно использовать pandas.IndexSlice для обеспечения более естественного синтаксиса (:), вместо использования slice(None).

>>> idx = pd.IndexSlice
>>> dfmi.loc[idx[:, :, ["C1", "C3"]], idx[:, "foo"]]
# lvl0           a    b
# lvl1         foo  foo
# A0 B0 C1 D0    8   10
#          D1   12   14
#       C3 D0   24   26
#          D1   28   30
#    B1 C1 D0   40   42
# ...          ...  ...
# A3 B0 C3 D1  220  222
#    B1 C1 D0  232  234
#          D1  236  238
#       C3 D0  248  250
#          D1  252  254
# 
# [32 rows x 2 columns]

Используя этот метод, можно выполнять довольно сложную выборку по нескольким осям одновременно..

>>> dfmi.loc["A1", (slice(None), "foo")]
# lvl0        a    b
# lvl1      foo  foo
# B0 C0 D0   64   66
#       D1   68   70
#    C1 D0   72   74
#       D1   76   78
#    C2 D0   80   82
# ...       ...  ...
# B1 C1 D1  108  110
#    C2 D0  112  114
#       D1  116  118
#    C3 D0  120  122
#       D1  124  126
# 
# [16 rows x 2 columns]

>>> dfmi.loc[idx[:, :, ["C1", "C3"]], idx[:, "foo"]]
# lvl0           a    b
# lvl1         foo  foo
# A0 B0 C1 D0    8   10
#          D1   12   14
#       C3 D0   24   26
#          D1   28   30
#    B1 C1 D0   40   42
# ...          ...  ...
# A3 B0 C3 D1  220  222
#    B1 C1 D0  232  234
#          D1  236  238
#       C3 D0  248  250
#          D1  252  254
# 
# [32 rows x 2 columns]

Используя логические операторы, можно сделать выборку данных, связанный со значениями.

>>> mask = dfmi[("a", "foo")] > 200
>>> dfmi.loc[idx[mask, :, ["C1", "C3"]], idx[:, "foo"]]
# lvl0           a    b
# lvl1         foo  foo
# A3 B0 C1 D1  204  206
#       C3 D0  216  218
#          D1  220  222
#    B1 C1 D0  232  234
#          D1  236  238
#       C3 D0  248  250
#          D1  252  254

Для DataFrame.loc также можно указать аргумент оси axis .

>>> dfmi.loc(axis=0)[:, :, ["C1", "C3"]]
# lvl0           a         b     
# lvl1         bar  foo  bah  foo
# A0 B0 C1 D0    9    8   11   10
#          D1   13   12   15   14
#       C3 D0   25   24   27   26
#          D1   29   28   31   30
#    B1 C1 D0   41   40   43   42
# ...          ...  ...  ...  ...
# A3 B0 C3 D1  221  220  223  222
#    B1 C1 D0  233  232  235  234
#          D1  237  236  239  238
#       C3 D0  249  248  251  250
#          D1  253  252  255  254
# 
# [32 rows x 4 columns]

Кроме того, можно устанавливать/изменять значения элемента, используя следующие методы.

# сделаем копию `DataFrame`
>>> df2 = dfmi.copy()
>>> df2.loc(axis=0)[:, :, ["C1", "C3"]] = -10
>>> df2
# lvl0           a         b     
# lvl1         bar  foo  bah  foo
# A0 B0 C0 D0    1    0    3    2
#          D1    5    4    7    6
#       C1 D0  -10  -10  -10  -10
#          D1  -10  -10  -10  -10
#       C2 D0   17   16   19   18
# ...          ...  ...  ...  ...
# A3 B1 C1 D1  -10  -10  -10  -10
#       C2 D0  241  240  243  242
#          D1  245  244  247  246
#       C3 D0  -10  -10  -10  -10
#          D1  -10  -10  -10  -10
# 
# [64 rows x 4 columns]

Можно использовать правую часть выравниваемого объекта.

# сделаем копию `DataFrame`
>>> df2 = dfmi.copy()
>>> df2.loc[idx[:, :, ["C1", "C3"]], :] = df2 * 1000
>>> df2
# lvl0              a               b        
# lvl1            bar     foo     bah     foo
# A0 B0 C0 D0       1       0       3       2
#          D1       5       4       7       6
#       C1 D0    9000    8000   11000   10000
#          D1   13000   12000   15000   14000
#       C2 D0      17      16      19      18
# ...             ...     ...     ...     ...
# A3 B1 C1 D1  237000  236000  239000  238000
#       C2 D0     241     240     243     242
#          D1     245     244     247     246
#       C3 D0  249000  248000  251000  250000
#          D1  253000  252000  255000  254000
# 
# [64 rows x 4 columns]

Чтобы упростить выбор данных на определенном уровне MultiIndex можно использовать метод DataFrame.xs(), который дополнительно принимает аргумент уровня level.

# используем ранее созданный `DataFrame`
>>> df
#                      A         B         C
# first second                              
# bar   one     0.895717  0.410835 -1.413681
#       two     0.805244  0.813850  1.607920
# baz   one    -1.206412  0.132003  1.024180
#       two     2.565646 -0.827317  0.569605
# foo   one     1.431256 -0.076467  0.875906
#       two     1.340309 -1.187678 -2.211372
# qux   one    -1.170299  1.130127  0.974466
#       two    -0.226169 -1.436737 -2.006747

>>> df.xs("one", level="second")
#               A         B         C
# first                              
# bar    0.895717  0.410835 -1.413681
# baz   -1.206412  0.132003  1.024180
# foo    1.431256 -0.076467  0.875906
# qux   -1.170299  1.130127  0.974466

Делаем то-же самое, с использованием среза pandas и функции slice().

>>> df.loc[(slice(None), "one"), :]
#                      A         B         C
# first second                              
# bar   one     0.895717  0.410835 -1.413681
# baz   one    -1.206412  0.132003  1.024180
# foo   one     1.431256 -0.076467  0.875906
# qux   one    -1.170299  1.130127  0.974466

Можно выбрать столбцы с помощью метода DataFrame.xs(), указав аргумент axis.

# транспонируем `DataFrame`
>>> df = df.T
>>> df.xs("one", level="second", axis=1)
# first       bar       baz       foo       qux
# A      0.895717 -1.206412  1.431256 -1.170299
# B      0.410835  0.132003 -0.076467  1.130127
# C     -1.413681  1.024180  0.875906  0.974466

Делаем то-же самое, с использованием среза pandas и функции slice(). Обратите внимание, что здесь сохраняется метка выбранного уровня 'one'

>>> df.loc[:, (slice(None), "one")]
# first        bar       baz       foo       qux
# second       one       one       one       one
# A       0.895717 -1.206412  1.431256 -1.170299
# B       0.410835  0.132003 -0.076467  1.130127
# C      -1.413681  1.024180  0.875906  0.974466

Метод DataFrame.xs() позволяет выбирать данные с помощью нескольких ключей.

>>> df.xs(("one", "bar"), level=("second", "first"), axis=1)
# first        bar
# second       one
# A       0.895717
# B       0.410835
# C      -1.413681

Делаем то-же самое, с использованием среза pandas. Обратите внимание, что в этом случае метки уровней пропадают.

>>> df.loc[:, ("bar", "one")]
# A    0.895717
# B    0.410835
# C   -1.413681
# Name: (bar, one), dtype: float64

При использовании метода DataFrame.xs(), для сохранения выбранного уровня, нужно в передать аргумент drop_level=False.

# аргумент `drop_level=True` (по умолчанию)
>>> df.xs("one", level="second", axis=1, drop_level=True)
# first       bar       baz       foo       qux
# A      0.895717 -1.206412  1.431256 -1.170299
# B      0.410835  0.132003 -0.076467  1.130127
# C     -1.413681  1.024180  0.875906  0.974466

# аргумент `drop_level=False`
>>> df.xs("one", level="second", axis=1, drop_level=False)
# first        bar       baz       foo       qux
# second       one       one       one       one
# A       0.895717 -1.206412  1.431256 -1.170299
# B       0.410835  0.132003 -0.076467  1.130127
# C      -1.413681  1.024180  0.875906  0.974466

Расширенное выравнивание и переиндексация с MultiIndex

Использование аргумента level в методах DataFrame.reindex() и DataFrame.align() объектов pandas полезно для передачи значений по уровню. Например:

# создаем `MultiIndex` 
midx = pd.MultiIndex(
    levels=[["zero", "one"], ["x", "y"]], codes=[[1, 1, 0, 0], [1, 0, 1, 0]]
)

# теперь `DataFrame` с `MultiIndex`
>>> df = pd.DataFrame(np.random.randn(4, 2), index=midx)
>>> df
#                0         1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

# создаем `df2` из `df` при помощи группировки по уровню
>>> df2 = df.groupby(level=0).mean()
>>> df2
#              0         1
# one   1.060074 -0.109716
# zero  1.271532  0.713416

# переиндексируем `df2` с индексом `df`
>>> df2.reindex(df.index, level=0)
#                0         1
# one  y  1.060074 -0.109716
#      x  1.060074 -0.109716
# zero y  1.271532  0.713416
#      x  1.271532  0.713416

# выравниваем `df` с `df2` по уровню `level=0`
>>> df_aligned, df2_aligned = df.align(df2, level=0)
>>> df_aligned
#                0         1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

>>> df2_aligned
#                0         1
# one  y  1.060074 -0.109716
#      x  1.060074 -0.109716
# zero y  1.271532  0.713416
#      x  1.271532  0.713416

Смена уровней с помощью метода DataFrame.swaplevel()

Метод DataFrame.swaplevel() может переключать порядок двух уровней:

>>> df
#                0         1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

>>> df.swaplevel(0, 1, axis=0)
#                0         1
# y one   1.519970 -0.493662
# x one   0.600178  0.274230
# y zero  0.132885 -0.023688
# x zero  2.410179  1.450520

Изменение порядка уровней с помощью метода DataFrame.reorder_levels()

Метод DataFrame.reorder_levels() обобщает метод DataFrame.swaplevel(), позволяя переставлять уровни MultiIndex за один шаг:

>>> df.reorder_levels([1, 0], axis=0)
#                0         1
# y one   1.519970 -0.493662
# x one   0.600178  0.274230
# y zero  0.132885 -0.023688
# x zero  2.410179  1.450520

Переименование меток Index или MultiIndex

Метод DataFrame.rename() используется для переименования меток MultiIndex и обычно используется для переименования столбцов DataFrame. Аргумент columns позволяет указать словарь, включающий только те столбцы, которые необходимо переименовать.

>>> df.rename(columns={0: "col0", 1: "col1"})
#             col0      col1
# one  y  1.519970 -0.493662
#      x  0.600178  0.274230
# zero y  0.132885 -0.023688
#      x  2.410179  1.450520

Этот метод также можно использовать для переименования определенных меток основного индекса DataFrame.

>>> df.rename(index={"one": "two", "y": "z"})
#                0         1
# two  z  1.519970 -0.493662
#      x  0.600178  0.274230
# zero z  0.132885 -0.023688
#      x  2.410179  1.450520

Метод DataFrame.rename_axis() используется для переименования меток Index или MultiIndex. В частности, можно указать имена уровней MultiIndex, что полезно, если позже используется функция DataFrame.reset_index() для перемещения значений из MultiIndex в столбец.

>>> df.rename_axis(index=["abc", "def"])
#                  0         1
# abc  def                    
# one  y    1.519970 -0.493662
#      x    0.600178  0.274230
# zero y    0.132885 -0.023688
#      x    2.410179  1.450520

Обратите внимание, что столбцы DataFrame являются индексом, поэтому использование DataFrame.rename_axis() с аргументом columns изменит имя этого индекса.

>>> df.rename_axis(columns="Cols").columns
# RangeIndex(start=0, stop=2, step=1, name='Cols')

Методы DataFrame.rename() и DataFrame.rename_axis() поддерживают передачу словаря, Series или функции для сопоставления меток/имен с новыми значениями.

При работе с объектом Index напрямую, а не через DataFrame, для изменения меток/имен можно использовать метод Index.set_names().

>>> mi = pd.MultiIndex.from_product([[1, 2], ["a", "b"]], names=["x", "y"])
>>> mi
# MultiIndex([(1, 'a'),
#             (1, 'b'),
#             (2, 'a'),
#             (2, 'b')],
#            names=['x', 'y'])

>>> mi2 = mi.rename("new name", level=0)
>>> mi2
# MultiIndex([(1, 'a'),
#             (1, 'b'),
#             (2, 'a'),
#             (2, 'b')],
#            !=> метка изменилась 
#            names=['new name', 'y'])

НЕЛЬЗЯ установить имена меток MultiIndex через уровень.

>>> mi.levels[0].name = "name via level"
# Traceback (most recent call last):
# ...
# RuntimeError: Cannot set name on a level of a MultiIndex. 
# Use 'MultiIndex.set_names' instead.

Сортировка MultiIndex

Чтобы объекты с MultiIndex можно было эффективно индексировать и нарезать, их необходимо отсортировать. Как и в случае с любым индексом, для этого можно использовать DataFrame.sort_index().

>>> import random
>>> random.shuffle(tuples)
>>> s = pd.Series(np.random.randn(8), index=pd.MultiIndex.from_tuples(tuples))
>>> s
# baz  two    0.206053
#      one   -0.251905
# qux  two   -2.213588
# bar  one    1.063327
# foo  two    1.266143
# bar  two    0.299368
# qux  one   -0.863838
# foo  one    0.408204
# dtype: float64

>>> s.sort_index()
# bar  one    1.063327
#      two    0.299368
# baz  one   -0.251905
#      two    0.206053
# foo  one    0.408204
#      two    1.266143
# qux  one   -0.863838
#      two   -2.213588
# dtype: float64

>>> s.sort_index(level=0)
# bar  one    1.063327
#      two    0.299368
# baz  one   -0.251905
#      two    0.206053
# foo  one    0.408204
#      two    1.266143
# qux  one   -0.863838
#      two   -2.213588
# dtype: float64

>>> s.sort_index(level=1)
# bar  one    1.063327
# baz  one   -0.251905
# foo  one    0.408204
# qux  one   -0.863838
# bar  two    0.299368
# baz  two    0.206053
# foo  two    1.266143
# qux  two   -2.213588
# dtype: float64

Аргумент level метода DataFrame.sort_index() также принимает имя метки уровня, если уровни MultiIndex имеют имена.

# дадим имена уровням `MultiIndex` в созданной ранее `Series`
>>> s.index = s.index.set_names(["L1", "L2"])
>>> s.sort_index(level="L1")
# L1   L2 
# bar  one    1.063327
#      two    0.299368
# baz  one   -0.251905
#      two    0.206053
# foo  one    0.408204
#      two    1.266143
# qux  one   -0.863838
#      two   -2.213588
# dtype: float64

>>> s.sort_index(level="L2")
# L1   L2 
# bar  one    1.063327
# baz  one   -0.251905
# foo  one    0.408204
# qux  one   -0.863838
# bar  two    0.299368
# baz  two    0.206053
# foo  two    1.266143
# qux  two   -2.213588
# dtype: float64

На объектах больших объектах с MultiIndex можно сортировать любые другие оси по уровню:

# например, через смену осей
>>> df.T.sort_index(level=1, axis=1)
#         one      zero       one      zero
#           x         x         y         y
# 0  0.600178  2.410179  1.519970  0.132885
# 1  0.274230  1.450520 -0.493662 -0.023688

Индексирование будет работать, даже если данные не отсортированы, но будет довольно неэффективно (будет отображаться предупреждение о производительности). При этом будет возвращаться копия данных, а не представление:

dfm = pd.DataFrame(
    {"jim": [0, 0, 1, 1], "joe": ["x", "x", "z", "y"], "jolie": np.random.rand(4)}
)

>>> dfm = dfm.set_index(["jim", "joe"])
>>> dfm
#             jolie
# jim joe          
# 0   x    0.490671
#     x    0.120248
# 1   z    0.537020
#     y    0.110968

>>> dfm.loc[(1, 'z')]
# PerformanceWarning: indexing past lexsort depth may impact performance.
#            jolie
# jim joe         
# 1   z    0.53702

Более того, если попытаться выбрать какие-то данные, которые не полностью отсортированы, это может привести к:

>>> dfm.loc[(0, 'y'):(1, 'z')]
# Traceback (most recent call last):
# ...
# pandas.errors.UnsortedIndexError: 
# 'Key length (2) was greater than MultiIndex lexsort depth (1)'

Проверить, отсортирован ли MultiIndex поможет метод MultiIndex.is_monotonic_increasing():

>>> dfm.index.is_monotonic_increasing
# False

>>> dfm = dfm.sort_index()
>>> dfm
#             jolie
# jim joe          
# 0   x    0.490671
#     x    0.120248
# 1   y    0.110968
#     z    0.537020

>>> dfm.index.is_monotonic_increasing
# True

И теперь выбор данных работает как положено.

>>> dfm.loc[(0, 'y'):(1, 'z')]
#             jolie
# jim joe          
# 1   y    0.110968
#     z    0.537020