Выберите строки в pandas MultiIndex DataFrame

148

Каковы наиболее распространенные способы выбора / фильтрации строк фрейма данных, индекс которого является MultiIndex ?

  • Нарезка на основе одного значения / метки
  • Нарезка на основе нескольких этикеток с одного или нескольких уровней
  • Фильтрация по логическим условиям и выражениям
  • Какие методы применимы в каких обстоятельствах

Предположения для простоты:

  1. входной фрейм данных не имеет повторяющихся ключей индекса
  2. Фреймворк входных данных ниже имеет только два уровня. (Большинство решений, показанных здесь, обобщаются до N уровней)

Пример ввода:

mux = pd.MultiIndex.from_arrays([
    list('aaaabbbbbccddddd'),
    list('tuvwtuvwtuvwtuvw')
], names=['one', 'two'])

df = pd.DataFrame({'col': np.arange(len(mux))}, mux)

         col
one two     
a   t      0
    u      1
    v      2
    w      3
b   t      4
    u      5
    v      6
    w      7
    t      8
c   u      9
    v     10
d   w     11
    t     12
    u     13
    v     14
    w     15

Вопрос 1: выбор одного предмета

Как выбрать строки со знаком «а» на уровне «один»?

         col
one two     
a   t      0
    u      1
    v      2
    w      3

Кроме того, как я могу сбросить уровень «один» на выходе?

     col
two     
t      0
u      1
v      2
w      3

Вопрос 1b
Как мне нарезать все строки со значением «t» на уровне «два»?

         col
one two     
a   t      0
b   t      4
    t      8
d   t     12

Вопрос 2: выбор нескольких значений на уровне

Как выбрать строки, соответствующие элементам «b» и «d» на уровне «один»?

         col
one two     
b   t      4
    u      5
    v      6
    w      7
    t      8
d   w     11
    t     12
    u     13
    v     14
    w     15

Вопрос 2b
Как мне получить все значения, соответствующие «t» и «w» на уровне «два»?

         col
one two     
a   t      0
    w      3
b   t      4
    w      7
    t      8
d   w     11
    t     12
    w     15

Вопрос 3: разрезание одного поперечного сечения (x, y)

Как мне получить поперечное сечение, т. Е. Одну строку, имеющую определенные значения для индекса df? В частности, как мне получить поперечное сечение ('c', 'u'), заданное

         col
one two     
c   u      9

Вопрос 4: Нарезка нескольких поперечных сечений [(a, b), (c, d), ...]

Как выбрать две строки, соответствующие ('c', 'u'), и ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Вопрос 5: по одному предмету на каждый уровень

Как я могу получить все строки, соответствующие «a» на уровне «один» или «t» на уровне «два»?

         col
one two     
a   t      0
    u      1
    v      2
    w      3
b   t      4
    t      8
d   t     12

Вопрос 6: произвольное нарезание

Как я могу разрезать определенные поперечные сечения? Для «a» и «b» я хотел бы выбрать все строки с подуровнями «u» и «v», а для «d» я хотел бы выбрать строки с подуровнем «w».

         col
one two     
a   u      1
    v      2
b   u      5
    v      6
d   w     11
    w     15

Вопрос 7 будет использовать уникальную настройку, состоящую из числового уровня:

np.random.seed(0)
mux2 = pd.MultiIndex.from_arrays([
    list('aaaabbbbbccddddd'),
    np.random.choice(10, size=16)
], names=['one', 'two'])

df2 = pd.DataFrame({'col': np.arange(len(mux2))}, mux2)

         col
one two     
a   5      0
    0      1
    3      2
    3      3
b   7      4
    9      5
    3      6
    5      7
    2      8
c   4      9
    7     10
d   6     11
    8     12
    8     13
    1     14
    6     15

Вопрос 7: Фильтрация по числовому неравенству на отдельных уровнях мультииндекса

Как получить все строки, в которых значения на уровне «два» больше 5?

         col
one two     
b   7      4
    9      5
c   7     10
d   6     11
    8     12
    8     13
    6     15

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

cs95
источник

Ответы:

168

MultiIndex / Расширенное индексирование

Примечание.
Этот пост будет структурирован следующим образом:

  1. Вопросы, поставленные в OP, будут рассмотрены один за другим.
  2. Для каждого вопроса будет продемонстрирован один или несколько методов, применимых к решению этой проблемы и получению ожидаемого результата.

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

Все примеры кода созданы и протестированы на pandas v0.23.4, python3.7 . Если что-то неясно или фактически неверно, или если вы не нашли решения, применимого к вашему варианту использования, пожалуйста, не стесняйтесь предлагать редактирование, запрашивать разъяснения в комментариях или открывать новый вопрос, .... в зависимости от ситуации ,

Вот введение в некоторые распространенные идиомы (далее именуемые «Четыре идиомы»), которые мы будем часто посещать повторно.

  1. DataFrame.loc- Общее решение для выбора по метке (+ pd.IndexSliceдля более сложных приложений, включающих срезы)

  2. DataFrame.xs - Извлечь конкретное поперечное сечение из серии / фрейма данных.

  3. DataFrame.query- Укажите операции нарезки и / или фильтрации динамически (т. Е. Как выражение, которое оценивается динамически. Более применимо к некоторым сценариям, чем к другим. Также см. Этот раздел документации для запросов по MultiIndexes.

  4. Логическое индексирование с маской, сгенерированной с использованием MultiIndex.get_level_values(часто вместе с Index.isin, особенно при фильтрации с несколькими значениями). Это также весьма полезно в некоторых случаях.

Будет полезно взглянуть на различные проблемы нарезки и фильтрации с точки зрения Четырех идиом, чтобы лучше понять, что можно применить к данной ситуации. Очень важно понимать, что не все идиомы будут работать одинаково хорошо (если вообще будут работать) в любых обстоятельствах. Если идиома не указана в качестве потенциального решения проблемы ниже, это означает, что идиома не может быть эффективно применена к этой проблеме.


Вопрос 1

Как выбрать строки со знаком «а» на уровне «один»?

         col
one two     
a   t      0
    u      1
    v      2
    w      3

В locкачестве универсального решения, применимого в большинстве ситуаций, вы можете использовать :

df.loc[['a']]

На этом этапе, если вы получите

TypeError: Expected tuple, got str

Это означает, что вы используете старую версию pandas. Рассмотрите возможность обновления! В противном случае используйте df.loc[('a', slice(None)), :].

В качестве альтернативы вы можете использовать xsздесь, так как мы извлекаем одно поперечное сечение. Обратите внимание , что levelsи axisаргументы (разумные значения по умолчанию можно считать здесь).

df.xs('a', level=0, axis=0, drop_level=False)
# df.xs('a', drop_level=False)

Здесь drop_level=Falseаргумент необходим, чтобы предотвратить xsпадение уровня «один» в результате (уровень, на который мы нарезали).

Еще один вариант - использовать query:

df.query("one == 'a'")

Если у индекса не было имени, вам нужно было бы изменить строку запроса на "ilevel_0 == 'a'".

Наконец, используя get_level_values:

df[df.index.get_level_values('one') == 'a']
# If your levels are unnamed, or if you need to select by position (not label),
# df[df.index.get_level_values(0) == 'a']

Кроме того, как я могу сбросить уровень «один» на выходе?

     col
two     
t      0
u      1
v      2
w      3

Это легко сделать, используя либо

df.loc['a'] # Notice the single string argument instead the list.

Или,

df.xs('a', level=0, axis=0, drop_level=True)
# df.xs('a')

Обратите внимание, что мы можем опустить drop_levelаргумент (предполагается, что он Trueпо умолчанию).

Примечание.
Вы можете заметить, что отфильтрованный DataFrame может по-прежнему иметь все уровни, даже если они не отображаются при выводе DataFrame. Например,

v = df.loc[['a']]
print(v)
         col
one two     
a   t      0
    u      1
    v      2
    w      3

print(v.index)
MultiIndex(levels=[['a', 'b', 'c', 'd'], ['t', 'u', 'v', 'w']],
           labels=[[0, 0, 0, 0], [0, 1, 2, 3]],
           names=['one', 'two'])

Вы можете избавиться от этих уровней, используя MultiIndex.remove_unused_levels:

v.index = v.index.remove_unused_levels()

print(v.index)
MultiIndex(levels=[['a'], ['t', 'u', 'v', 'w']],
           labels=[[0, 0, 0, 0], [0, 1, 2, 3]],
           names=['one', 'two'])

Вопрос 1b

Как нарезать все строки со значением «t» на уровне «два»?

         col
one two     
a   t      0
b   t      4
    t      8
d   t     12

Интуитивно вам может понадобиться что-то, включающее slice():

df.loc[(slice(None), 't'), :]

Это просто работает! ™ Но это неуклюже. Используя pd.IndexSliceAPI, мы можем облегчить более естественный синтаксис нарезки .

idx = pd.IndexSlice
df.loc[idx[:, 't'], :]

Это намного чище.

Примечание.
Почему :требуется завершающий фрагмент по столбцам? Это потому, что, locможет использоваться для выбора и нарезки по обеим осям ( axis=0или axis=1). Без явного указания на то, по какой оси должна выполняться нарезка, операция становится неоднозначной. Смотрите большую красную рамку в документации по нарезке .

Если вы хотите убрать оттенок двусмысленности, locпринимает axis параметр:

df.loc(axis=0)[pd.IndexSlice[:, 't']]

Без axisпараметра (то есть просто путем выполнения df.loc[pd.IndexSlice[:, 't']]) предполагается, что нарезка выполняется по столбцам, и в этом KeyErrorслучае будет повышен a .

Это задокументировано в слайсерах . Однако для целей этой публикации мы явно укажем все оси.

С xs, это

df.xs('t', axis=0, level=1, drop_level=False)

С query, это

df.query("two == 't'")
# Or, if the first level has no name, 
# df.query("ilevel_1 == 't'") 

И, наконец get_level_values, вы можете сделать

df[df.index.get_level_values('two') == 't']
# Or, to perform selection by position/integer,
# df[df.index.get_level_values(1) == 't']

Все к тому же.


вопрос 2

Как выбрать строки, соответствующие элементам «b» и «d» на уровне «один»?

         col
one two     
b   t      4
    u      5
    v      6
    w      7
    t      8
d   w     11
    t     12
    u     13
    v     14
    w     15

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

df.loc[['b', 'd']]

Чтобы решить указанную выше проблему выбора «b» и «d», вы также можете использовать query:

items = ['b', 'd']
df.query("one in @items")
# df.query("one == @items", parser='pandas')
# df.query("one in ['b', 'd']")
# df.query("one == ['b', 'd']", parser='pandas')

Примечание.
Да, синтаксический анализатор по умолчанию - это 'pandas', но важно подчеркнуть, что этот синтаксис традиционно не является python. Парсер Pandas генерирует дерево синтаксического анализа, немного отличающееся от выражения. Это сделано для того, чтобы сделать некоторые операции более интуитивно понятными. Для получения дополнительной информации прочтите мой пост о динамической оценке выражений в pandas с использованием pd.eval () .

И, с get_level_values+ Index.isin:

df[df.index.get_level_values("one").isin(['b', 'd'])]

Вопрос 2b

Как мне получить все значения, соответствующие «t» и «w» на уровне «два»?

         col
one two     
a   t      0
    w      3
b   t      4
    w      7
    t      8
d   w     11
    t     12
    w     15

С loc, это возможно только в сочетании с pd.IndexSlice.

df.loc[pd.IndexSlice[:, ['t', 'w']], :] 

Первый двоеточие :в pd.IndexSlice[:, ['t', 'w']]средство нарезать поперек первого уровня. По мере увеличения глубины запрашиваемого уровня вам потребуется указать больше срезов, по одному на каждый уровень. Однако вам не нужно указывать дополнительные уровни, помимо того, который нарезается.

С query, это

items = ['t', 'w']
df.query("two in @items")
# df.query("two == @items", parser='pandas') 
# df.query("two in ['t', 'w']")
# df.query("two == ['t', 'w']", parser='pandas')

С помощью get_level_valuesи Index.isin(аналогично приведенному выше):

df[df.index.get_level_values('two').isin(['t', 'w'])]

Вопрос 3

Как мне получить поперечное сечение, т. Е. Одну строку, имеющую определенные значения для индекса df? В частности, как мне получить поперечное сечение ('c', 'u'), заданное

         col
one two     
c   u      9

Используйте loc, указав кортеж ключей:

df.loc[('c', 'u'), :]

Или,

df.loc[pd.IndexSlice[('c', 'u')]]

Примечание.
На этом этапе вы можете столкнуться с PerformanceWarningтаким:

PerformanceWarning: indexing past lexsort depth may impact performance.

Это просто означает, что ваш индекс не отсортирован. pandas зависит от сортируемого индекса (в данном случае лексикографически, поскольку мы имеем дело со строковыми значениями) для оптимального поиска и извлечения. Быстрое решение - заранее отсортировать DataFrame, используя DataFrame.sort_index. Это особенно желательно с точки зрения производительности, если вы планируете выполнять несколько таких запросов в тандеме:

df_sort = df.sort_index()
df_sort.loc[('c', 'u')]

Вы также можете использовать, MultiIndex.is_lexsorted()чтобы проверить, отсортирован ли индекс или нет. Эта функция возвращает Trueили Falseсоответственно. Вы можете вызвать эту функцию, чтобы определить, требуется ли дополнительный этап сортировки.

С xs, это снова просто передача одного кортежа в качестве первого аргумента, при этом для всех остальных аргументов установлены соответствующие значения по умолчанию:

df.xs(('c', 'u'))

С query, все становится немного неуклюже:

df.query("one == 'c' and two == 'u'")

Теперь вы видите, что обобщить это будет относительно сложно. Но все еще подходит для этой конкретной проблемы.

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

m1 = (df.index.get_level_values('one') == 'c')
m2 = (df.index.get_level_values('two') == 'u')
df[m1 & m2]

Вопрос 4

Как выбрать две строки, соответствующие ('c', 'u'), и ('a', 'w')?

         col
one two     
c   u      9
a   w      3

С loc, это все так же просто, как:

df.loc[[('c', 'u'), ('a', 'w')]]
# df.loc[pd.IndexSlice[[('c', 'u'), ('a', 'w')]]]

С query, вам нужно будет динамически генерировать строку запроса, перебирая ваши сечения и уровни:

cses = [('c', 'u'), ('a', 'w')]
levels = ['one', 'two']
# This is a useful check to make in advance.
assert all(len(levels) == len(cs) for cs in cses) 

query = '(' + ') or ('.join([
    ' and '.join([f"({l} == {repr(c)})" for l, c in zip(levels, cs)]) 
    for cs in cses
]) + ')'

print(query)
# ((one == 'c') and (two == 'u')) or ((one == 'a') and (two == 'w'))

df.query(query)

100% НЕ РЕКОМЕНДУЕМ! Но это возможно.


Вопрос 5

Как я могу получить все строки, соответствующие «a» на уровне «один» или «t» на уровне «два»?

         col
one two     
a   t      0
    u      1
    v      2
    w      3
b   t      4
    t      8
d   t     12

На самом деле это очень сложно сделать loc, обеспечивая при этом правильность и при этом сохраняя ясность кода. df.loc[pd.IndexSlice['a', 't']]неверно, это интерпретируется как df.loc[pd.IndexSlice[('a', 't')]](т.е. выбор сечения). Вы можете подумать о решении, pd.concatпозволяющем обрабатывать каждую метку отдельно:

pd.concat([
    df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:]
])

         col
one two     
a   t      0
    u      1
    v      2
    w      3
    t      0   # Does this look right to you? No, it isn't!
b   t      4
    t      8
d   t     12

Но вы заметите, что одна из строк дублируется. Это потому, что эта строка удовлетворяла обоим условиям нарезки и поэтому появлялась дважды. Вместо этого вам нужно будет сделать

v = pd.concat([
        df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:]
])
v[~v.index.duplicated()]

Но если ваш DataFrame по своей сути содержит повторяющиеся индексы (которые вы хотите), то это не сохранит их. Используйте с особой осторожностью .

С query, это глупо просто:

df.query("one == 'a' or two == 't'")

С get_level_values, это все еще просто, но не так элегантно:

m1 = (df.index.get_level_values('one') == 'a')
m2 = (df.index.get_level_values('two') == 't')
df[m1 | m2] 

Вопрос 6

Как я могу разрезать определенные поперечные сечения? Для «a» и «b» я хотел бы выбрать все строки с подуровнями «u» и «v», а для «d» я хотел бы выбрать строки с подуровнем «w».

         col
one two     
a   u      1
    v      2
b   u      5
    v      6
d   w     11
    w     15

Это особый случай, который я добавил, чтобы помочь понять применимость Четырех идиом - это тот случай, когда ни одна из них не будет работать эффективно, поскольку нарезка очень специфична и не соответствует никакому реальному шаблону.

Обычно для решения подобных задач требуется явная передача списка ключей в loc. Один из способов сделать это:

keys = [('a', 'u'), ('a', 'v'), ('b', 'u'), ('b', 'v'), ('d', 'w')]
df.loc[keys, :]

Если вы хотите сэкономить на вводе текста, вы узнаете, что существует шаблон для нарезки «a», «b» и его подуровней, поэтому мы можем разделить задачу нарезки на две части и concatрезультат:

pd.concat([
     df.loc[(('a', 'b'), ('u', 'v')), :], 
     df.loc[('d', 'w'), :]
   ], axis=0)

Спецификация нарезки для «a» и «b» немного чище, (('a', 'b'), ('u', 'v'))потому что одни и те же индексируемые подуровни одинаковы для каждого уровня.


Вопрос 7

Как получить все строки, в которых значения на уровне «два» больше 5?

         col
one two     
b   7      4
    9      5
c   7     10
d   6     11
    8     12
    8     13
    6     15

Это можно сделать, используя query,

df2.query("two > 5")

И get_level_values.

df2[df2.index.get_level_values('two') > 5]

Примечание.
Как и в этом примере, мы можем фильтровать по любому произвольному условию, используя эти конструкции. В общем, полезно помнить, что locи xsпредназначены специально для индексирования на основе меток, а queryи get_level_valuesполезны для создания общих условных масок для фильтрации.


Бонусный вопрос

Что, если мне нужно разрезать MultiIndex столбец ?

Собственно, большинство решений здесь применимо и к столбцам с небольшими изменениями. Рассматривать:

np.random.seed(0)
mux3 = pd.MultiIndex.from_product([
        list('ABCD'), list('efgh')
], names=['one','two'])

df3 = pd.DataFrame(np.random.choice(10, (3, len(mux))), columns=mux3)
print(df3)

one  A           B           C           D         
two  e  f  g  h  e  f  g  h  e  f  g  h  e  f  g  h
0    5  0  3  3  7  9  3  5  2  4  7  6  8  8  1  6
1    7  7  8  1  5  9  8  9  4  3  0  3  5  0  2  3
2    8  1  3  3  3  7  0  1  9  9  0  4  7  3  2  7

Вот следующие изменения, которые вам нужно будет внести в Четыре идиомы, чтобы они работали со столбцами.

  1. Чтобы нарезать loc, используйте

    df3.loc[:, ....] # Notice how we slice across the index with `:`. 

    или,

    df3.loc[:, pd.IndexSlice[...]]
  2. Для использования по xsмере необходимости просто передайте аргумент axis=1.

  3. Вы можете получить доступ к значениям уровня столбца напрямую, используя df.columns.get_level_values. Затем вам нужно будет сделать что-то вроде

    df.loc[:, {condition}] 

    Где {condition}представляет собой некоторое условие, построенное с использованием columns.get_level_values.

  4. Для использования query, единственным вариантом является транспонировать, запрос по индексу, и транспонировать снова:

    df3.T.query(...).T

    Не рекомендуется использовать один из трех других вариантов.

cs95
источник
6

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

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

Это другой способ получить результат, немного отличный от вопроса № 6 выше. (и, вероятно, другие вопросы)

В частности, я искал:

  1. Способ выбора двух + значений из одного уровня индекса и одного значения из другого уровня индекса, и
  2. Способ оставить значения индекса из предыдущей операции в выводе фрейма данных.

Как гаечный ключ в шестернях (однако полностью поправимый):

  1. Индексы были безымянными.

На фрейме данных игрушки ниже:

    index = pd.MultiIndex.from_product([['a','b'],
                               ['stock1','stock2','stock3'],
                               ['price','volume','velocity']])

    df = pd.DataFrame([1,2,3,4,5,6,7,8,9,
                      10,11,12,13,14,15,16,17,18], 
                       index)

                        0
    a stock1 price      1
             volume     2
             velocity   3
      stock2 price      4
             volume     5
             velocity   6
      stock3 price      7
             volume     8
             velocity   9
    b stock1 price     10
             volume    11
             velocity  12
      stock2 price     13
             volume    14
             velocity  15
      stock3 price     16
             volume    17
             velocity  18

Конечно, с помощью приведенных ниже работ:

    df.xs(('stock1', 'velocity'), level=(1,2))

        0
    a   3
    b  12

Но мне нужен был другой результат, поэтому мой метод получения этого результата был следующим:

   df.iloc[df.index.isin(['stock1'], level=1) & 
           df.index.isin(['velocity'], level=2)] 

                        0
    a stock1 velocity   3
    b stock1 velocity  12

И если бы я хотел два + значения с одного уровня и одно (или 2+) значение с другого уровня:

    df.iloc[df.index.isin(['stock1','stock3'], level=1) & 
            df.index.isin(['velocity'], level=2)] 

                        0
    a stock1 velocity   3
      stock3 velocity   9
    b stock1 velocity  12
      stock3 velocity  18

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

ра
источник
2
Приятно, не знал об levelаргументе Index.isin!
cs95