Выделение строк в пандах MultiIndex DataFrame - PullRequest
0 голосов
/ 26 декабря 2018

Цель и мотивация

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

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

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

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

Эти проблемы были разбиты на 6 конкретных вопросов, перечисленных ниже.Для простоты пример DataFrames в приведенной ниже настройке имеет только два уровня и не имеет дублирующих индексных ключей.Большинство представленных решений проблем могут обобщаться до N уровней.

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


Вопросы

Вопрос 1-6 будет задан в контексте нижеприведенной установки.

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: Выбор одного элемента
Как выбрать строки, имеющие «a» на уровне «one»?

         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

1 Ответ

0 голосов
/ 26 декабря 2018

MultiIndex / Advanced Indexing

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

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

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

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

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

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

  2. DataFrame.xs - Извлечение определенного поперечного сечения из серии / кадра данных.

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

  4. Booleanиндексирование с использованием маски, созданной с использованием 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

Это означает, что вы используете старую версию панд.Подумайте об обновлении!В противном случае используйте 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'), :]

Это просто работает! ™ Но это неуклюже.Здесь мы можем упростить более естественный синтаксис секционирования, используя API pd.IndexSlice.

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.

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

С 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 генерирует немного другое дерево разбора из выражения.Это сделано для того, чтобы сделать некоторые операции более понятными для указания.Для получения дополнительной информации, пожалуйста, прочитайте мой пост на Оценка динамических выражений в пандах с использованием 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" на уровне "one" или "t" на уровне "two"?

         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') == 'c')
m2 = (df.index.get_level_values('two') == 'u')
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
    

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

...