Виберіть рядки в пандах MultiIndex DataFrame


146

Які найпоширеніші способи вибору панд для вибору / фільтрування рядків фрейму даних, індекс якого є 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: Вибір одного предмета

Як вибрати рядки, що мають "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" на рівні "one"?

         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" на рівні "one" або "t" на рівні "two"?

         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

Примітка. Ця публікація не розглядає способи створення MultiIndexes, як виконувати операції з присвоєння на них чи будь-які дискусії, що стосуються продуктивності (це окремі теми для іншого разу).

Відповіді:


166

MultiIndex / Розширена індексація

Примітка
Ця публікація буде структурована таким чином:

  1. Питання, висунуті в ОП, будуть вирішені по черзі
  2. Для кожного питання буде продемонстровано один або кілька методів, застосовних для вирішення цієї проблеми та отримання очікуваного результату.

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

Усі зразки коду створені та протестовані на пандах v0.23.4, python3.7 . Якщо щось не зрозуміло, або фактично невірно, або якщо ви не знайшли рішення, застосовне до вашої справи використання, будь ласка, запропонуйте редагувати, запитати роз'яснення в коментарях або відкрити нове запитання .... .

Ось ознайомлення з деякими загальними ідіомами (відтепер їх називаються чотирма ідіомами), які ми будемо часто відвідувати

  1. DataFrame.loc- загальне рішення для вибору за міткою (+ pd.IndexSliceдля більш складних додатків, що включають фрагменти)

  2. DataFrame.xs - Витягніть певний поперечний переріз із серії / DataFrame.

  3. DataFrame.query- Динамічно вказуйте операції зрізання та / або фільтрації (тобто як вираження, яке динамічно оцінюється. Більш застосовне для деяких сценаріїв, ніж інші. Також дивіться цей розділ документів для запитів на MultiIndexes.

  4. Булева індексація за допомогою маски, створеної за допомогою MultiIndex.get_level_values(часто в поєднанні з Index.isin, особливо при фільтрації з кількома значеннями). Це також досить корисно за деяких обставин.

Буде корисно подивитися на різні проблеми з нарізанням та фільтруванням з точки зору чотирьох ідіом, щоб краще зрозуміти, що можна застосувати до даної ситуації. Дуже важливо розуміти, що не всі ідіоми працюватимуть однаково добре (якщо взагалі) за будь-яких обставин. Якщо ідіома не вказана як потенційне рішення проблеми нижче, це означає, що ідіома не може бути ефективно застосована до цієї проблеми.


питання 1

Як вибрати рядки, що мають "a" на рівні "one"?

         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'])

Питання 1б

Як я розрізаю всі рядки зі значенням "t" на рівні "два"?

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

Інтуїтивно, ви хочете щось, що стосується slice():

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

Це просто працює! ™ Але це незграбно. Ми можемо полегшити більш природний синтаксис нарізки, використовуючи pd.IndexSliceтут API.

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" на рівні "one"?

         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

Використовуючи локальний, це робиться аналогічно, вказавши список.

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.

Це просто означає, що ваш індекс не відсортований. панди залежать від сортування індексу (у цьому випадку лексикографічно, оскільки ми маємо справу зі значеннями рядків) для оптимального пошуку та пошуку. Швидке виправлення полягатиме в тому, щоб заздалегідь відсортувати ваш 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') == '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

    Не рекомендується, використовуйте один з інших 3 варіантів.


6

Нещодавно я натрапив на випадок використання, коли у мене був багатоіндексний фрейм даних 3+ рівня, в якому я не міг зробити жодне з вищезазначених рішень, щоб отримати результати, які я шукав. Цілком можливо, що вищезазначені рішення, безумовно, працюють для мого випадку використання, і я спробував декілька, проте мені не вдалося змусити їх працювати з наявним часом.

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

Це інший спосіб отримати дещо інший результат до Питання №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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.