Selecionar linhas no pandas MultiIndex DataFrame

145

Quais são as maneiras mais comuns dos pandas de selecionar / filtrar linhas de um quadro de dados cujo índice é um MultiIndex ?

  • Fatiar com base em um único valor / rótulo
  • Fatiar com base em vários rótulos de um ou mais níveis
  • Filtrando condições e expressões booleanas
  • Quais métodos são aplicáveis ​​em quais circunstâncias

Pressupostos de simplicidade:

  1. dataframe de entrada não possui chaves de índice duplicadas
  2. o quadro de dados de entrada abaixo possui apenas dois níveis. (A maioria das soluções mostradas aqui generaliza para N níveis)

Exemplo de entrada:

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

Pergunta 1: Selecionando um único item

Como seleciono linhas com "a" no nível "um"?

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

Além disso, como eu seria capaz de reduzir o nível "um" na saída?

     col
two     
t      0
u      1
v      2
w      3

Pergunta 1b
Como separo todas as linhas com o valor "t" no nível "dois"?

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

Pergunta 2: Selecionando vários valores em um nível

Como posso selecionar linhas correspondentes aos itens "b" e "d" no nível "um"?

         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

Pergunta 2b
Como obteria todos os valores correspondentes a "t" e "w" no nível "dois"?

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

Pergunta 3: Fatiando uma única seção transversal (x, y)

Como recupero uma seção transversal, ou seja, uma única linha com valores específicos para o índice df? Especificamente, como recupero a seção transversal de ('c', 'u'), dada por

         col
one two     
c   u      9

Pergunta 4: Fatiando várias seções cruzadas [(a, b), (c, d), ...]

Como seleciono as duas linhas correspondentes a ('c', 'u'), e ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Pergunta 5: Um item fatiado por nível

Como recuperar todas as linhas correspondentes a "a" no nível "um" ou "t" no nível "dois"?

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

Pergunta 6: Corte arbitrário

Como posso cortar seções transversais específicas? Para "a" e "b", gostaria de selecionar todas as linhas com os subníveis "u" e "v" e, para "d", gostaria de selecionar as linhas com o subnível "w".

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

A pergunta 7 usará uma configuração exclusiva que consiste em um nível numérico:

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

Pergunta 7: Filtragem por desigualdade numérica em níveis individuais do multi-índice

Como obtenho todas as linhas em que os valores no nível "dois" são maiores que 5?

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

Nota: Esta postagem não mostrará como criar MultiIndexes, como executar operações de atribuição neles ou quaisquer discussões relacionadas ao desempenho (esses são tópicos separados para outra hora).

cs95
fonte

Respostas:

166

Indexação MultiIndex / Avançada

Nota
Esta postagem será estruturada da seguinte maneira:

  1. As questões colocadas no OP serão abordadas, uma a uma
  2. Para cada pergunta, um ou mais métodos aplicáveis ​​à solução desse problema e à obtenção do resultado esperado serão demonstrados.

Nota (muito parecidas com esta) serão incluídas para os leitores interessados ​​em aprender sobre funcionalidades adicionais, detalhes de implementação e outras informações necessárias para o tópico em questão. Essas anotações foram compiladas através da análise dos documentos e da descoberta de vários recursos obscuros e de minha própria experiência (reconhecidamente limitada).

Todas as amostras de código foram criadas e testadas no pandas v0.23.4, python3.7 . Se algo não estiver claro ou factualmente incorreto, ou se você não encontrou uma solução aplicável ao seu caso de uso, sinta-se à vontade para sugerir uma edição, solicitar esclarecimentos nos comentários ou abrir uma nova pergunta, ... conforme aplicável .

Aqui está uma introdução a alguns idiomas comuns (doravante referidos como os quatro idiomas) que visitaremos frequentemente

  1. DataFrame.loc- Uma solução geral para seleção por etiqueta (+ pd.IndexSlicepara aplicações mais complexas envolvendo fatias)

  2. DataFrame.xs - Extraia uma seção transversal específica de um Series / DataFrame.

  3. DataFrame.query- Especifique operações de corte e / ou filtragem dinamicamente (ou seja, como uma expressão avaliada dinamicamente. É mais aplicável a alguns cenários do que outros. Consulte também esta seção dos documentos para consultar em MultiIndexes.

  4. Indexação booleana com uma máscara gerada usando MultiIndex.get_level_values(geralmente em conjunto com Index.isin, especialmente ao filtrar com vários valores). Isso também é bastante útil em algumas circunstâncias.

Será benéfico analisar os vários problemas de fatia e filtragem em termos dos Quatro Idiomas para entender melhor o que pode ser aplicado a uma determinada situação. É muito importante entender que nem todos os idiomas funcionarão igualmente bem (se houver) em todas as circunstâncias. Se um idioma não foi listado como uma solução potencial para um problema abaixo, isso significa que o idioma não pode ser aplicado a esse problema efetivamente.


Questão 1

Como seleciono linhas com "a" no nível "um"?

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

Você pode usar loc, como uma solução de uso geral aplicável à maioria das situações:

df.loc[['a']]

Neste ponto, se você receber

TypeError: Expected tuple, got str

Isso significa que você está usando uma versão mais antiga dos pandas. Considere atualizar! Caso contrário, use df.loc[('a', slice(None)), :].

Como alternativa, você pode usar xsaqui, pois estamos extraindo uma única seção transversal. Observe os argumentos levelse axis(padrões razoáveis ​​podem ser assumidos aqui).

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

Aqui, o drop_level=Falseargumento é necessário para impedir a xsqueda do nível "um" no resultado (o nível em que fatiávamos).

Ainda outra opção aqui está usando query:

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

Se o índice não tivesse um nome, você precisaria alterar sua string de consulta "ilevel_0 == 'a'".

Finalmente, usando 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']

Além disso, como eu seria capaz de reduzir o nível "um" na saída?

     col
two     
t      0
u      1
v      2
w      3

Isso pode ser feito facilmente usando

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

Ou,

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

Observe que podemos omitir o drop_levelargumento (é assumido Truepor padrão).

Nota
Você pode perceber que um DataFrame filtrado ainda pode ter todos os níveis, mesmo se eles não aparecerem ao imprimir o DataFrame. Por exemplo,

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

Você pode se livrar desses níveis usando 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'])

Pergunta 1b

Como faço para dividir todas as linhas com o valor "t" no nível "dois"?

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

Intuitivamente, você gostaria de algo envolvendo slice():

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

Simplesmente funciona! ™ Mas é desajeitado. Podemos facilitar uma sintaxe de fatiamento mais natural usando a pd.IndexSliceAPI aqui.

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

Isso é muito, muito mais limpo.

Nota
Por que a fatia final :nas colunas é necessária? Isso ocorre porque, locpode ser usado para selecionar e cortar ao longo dos dois eixos ( axis=0ou axis=1). Sem explicitamente esclarecer em qual eixo o fatiamento será realizado, a operação se torna ambígua. Veja a grande caixa vermelha na documentação sobre fatiar .

Se você deseja remover qualquer tom de ambiguidade, locaceita um axis parâmetro:

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

Sem o axisparâmetro (ou seja, apenas fazendo df.loc[pd.IndexSlice[:, 't']]), o fatiamento é assumido como estando nas colunas e a KeyErrorserá gerado nessa circunstância.

Isso está documentado em slicers . Para os fins deste post, no entanto, especificaremos explicitamente todos os eixos.

Com xs, é

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

Com query, é

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

E finalmente, com get_level_values, você pode fazer

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

Tudo com o mesmo efeito.


Questão 2

Como posso selecionar linhas correspondentes aos itens "b" e "d" no nível "um"?

         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

Usando loc, isso é feito de maneira semelhante, especificando uma lista.

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

Para resolver o problema acima de selecionar "b" e "d", você também pode usar 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')

Nota
Sim, o analisador padrão é 'pandas', mas é importante destacar que essa sintaxe não é convencionalmente python. O analisador Pandas gera uma árvore de análise ligeiramente diferente da expressão. Isso é feito para tornar algumas operações mais intuitivas para especificar. Para obter mais informações, leia meu post sobre Avaliação de Expressão Dinâmica em pandas usando pd.eval () .

E, com get_level_values+ Index.isin:

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

Pergunta 2b

Como eu obteria todos os valores correspondentes a "t" e "w" no nível "dois"?

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

Com loc, isso é possível apenas em conjunto com pd.IndexSlice.

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

O primeiro cólon :em pd.IndexSlice[:, ['t', 'w']]meios para cortar transversalmente o primeiro nível. À medida que a profundidade do nível que está sendo consultado aumenta, você precisará especificar mais fatias, uma por nível sendo fatiada. Você não precisará especificar mais níveis além do que está sendo fatiado, no entanto.

Com query, isso é

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

Com get_level_valuese Index.isin(semelhante ao acima):

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

Questão 3

Como recupero uma seção transversal, ou seja, uma única linha com valores específicos para o índice df? Especificamente, como recupero a seção transversal ('c', 'u'), dada por

         col
one two     
c   u      9

Use locespecificando uma tupla de chaves:

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

Ou,

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

Nota
Neste ponto, você pode encontrar um PerformanceWarningque se parece com isso:

PerformanceWarning: indexing past lexsort depth may impact performance.

Isso significa apenas que seu índice não está classificado. pandas depende do índice que está sendo classificado (neste caso, lexicograficamente, já que estamos lidando com valores de string) para busca e recuperação ideais. Uma solução rápida seria classificar seu DataFrame com antecedência DataFrame.sort_index. Isso é especialmente desejável do ponto de vista de desempenho, se você planeja fazer várias dessas consultas em conjunto:

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

Você também pode usar MultiIndex.is_lexsorted()para verificar se o índice está classificado ou não. Esta função retorna Trueou de Falseacordo. Você pode chamar esta função para determinar se uma etapa de classificação adicional é necessária ou não.

Com xs, isso é simplesmente passar uma única tupla como o primeiro argumento, com todos os outros argumentos definidos com os padrões apropriados:

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

Com query, as coisas ficam um pouco desajeitadas:

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

Você pode ver agora que isso será relativamente difícil de generalizar. Mas ainda está bom para esse problema em particular.

Com acessos abrangendo vários níveis, get_level_valuesainda pode ser usado, mas não é recomendado:

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

Pergunta 4

Como seleciono as duas linhas correspondentes a ('c', 'u'), e ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Com loc, isso ainda é tão simples quanto:

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

Com query, você precisará gerar dinamicamente uma sequência de consultas iterando sobre suas seções transversais e níveis:

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% NÃO RECOMENDO! Mas é possível.


Questão 5

Como recuperar todas as linhas correspondentes a "a" no nível "um" ou "t" no nível "dois"?

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

Isso é realmente muito difícil loc, garantindo a correção e ainda mantendo a clareza do código. df.loc[pd.IndexSlice['a', 't']]está incorreto, é interpretado como df.loc[pd.IndexSlice[('a', 't')]](por exemplo, selecionando uma seção transversal). Você pode pensar em uma solução pd.concatpara lidar com cada rótulo separadamente:

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

Mas você notará que uma das linhas está duplicada. Isso ocorre porque essa linha atendeu às duas condições de fatiamento e, portanto, apareceu duas vezes. Você precisará fazer

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

Mas se o DataFrame contiver inerentemente índices duplicados (que você deseja), isso não os manterá. Use com extrema cautela .

Com query, isso é estupidamente simples:

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

Com get_level_values, isso ainda é simples, mas não tão elegante:

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

Pergunta 6

Como posso cortar seções transversais específicas? Para "a" e "b", gostaria de selecionar todas as linhas com os subníveis "u" e "v" e, para "d", gostaria de selecionar as linhas com o subnível "w".

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

Esse é um caso especial que eu adicionei para ajudar a entender a aplicabilidade dos Quatro Idiomas - esse é um caso em que nenhum deles funcionará efetivamente, pois a fatia é muito específica e não segue nenhum padrão real.

Normalmente, problemas de fatiamento como esse exigirão a passagem explícita de uma lista de chaves para loc. Uma maneira de fazer isso é com:

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

Se você quiser salvar alguma digitação, reconhecerá que existe um padrão para fatiar "a", "b" e seus subníveis, para que possamos separar a tarefa de fatiar em duas partes e concato resultado:

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

A especificação de fatiamento para "a" e "b" é um pouco mais limpa, (('a', 'b'), ('u', 'v'))porque os mesmos subníveis sendo indexados são os mesmos para cada nível.


Pergunta 7

Como obtenho todas as linhas em que os valores no nível "dois" são maiores que 5?

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

Isso pode ser feito usando query,

df2.query("two > 5")

E get_level_values.

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

Nota
Semelhante a este exemplo, podemos filtrar com base em qualquer condição arbitrária usando essas construções. Em geral, é útil lembrar que loce xssão especificamente para indexação baseado em rótulo, enquanto querye get_level_valuessão úteis para a construção de máscaras condicionais gerais para a filtragem.


Pergunta bônus

E se eu precisar dividir uma MultiIndex coluna ?

Na verdade, a maioria das soluções aqui também é aplicável a colunas, com pequenas alterações. Considerar:

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

Estas são as seguintes alterações que você precisará fazer nos Quatro Idiomas para que eles trabalhem com colunas.

  1. Para fatiar loc, use

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

    ou,

    df3.loc[:, pd.IndexSlice[...]]
  2. Para usar xsconforme apropriado, basta passar um argumento axis=1.

  3. Você pode acessar diretamente os valores no nível da coluna usando df.columns.get_level_values. Você precisará fazer algo como

    df.loc[:, {condition}] 

    Onde {condition}representa alguma condição criada usando columns.get_level_values.

  4. Para usar query, sua única opção é transpor, consultar o índice e transpor novamente:

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

    Não recomendado, use uma das outras 3 opções.

cs95
fonte
5

Recentemente, deparei-me com um caso de uso em que eu tinha um dataframe de vários índices com mais de 3 níveis no qual não conseguia fazer nenhuma das soluções acima produzir os resultados que estava procurando. É bem possível que as soluções acima funcionem para o meu caso de uso e tentei várias, no entanto, não consegui fazê-las funcionar com o tempo disponível.

Estou longe de ser especialista, mas me deparei com uma solução que não estava listada nas respostas abrangentes acima. Não ofereço garantia de que as soluções sejam de qualquer maneira ideais.

Essa é uma maneira diferente de obter um resultado ligeiramente diferente da pergunta nº 6 acima. (e provavelmente outras perguntas também)

Eu estava procurando especificamente:

  1. Uma maneira de escolher dois + valores de um nível do índice e um único valor de outro nível do índice, e
  2. Uma maneira de deixar os valores de índice da operação anterior na saída do quadro de dados.

Como uma chave inglesa nas engrenagens (por mais que seja totalmente corrigível):

  1. Os índices não tinham nome.

No quadro de dados do brinquedo abaixo:

    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

Usando os trabalhos abaixo, é claro:

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

        0
    a   3
    b  12

Mas eu queria um resultado diferente, então meu método para obter esse resultado foi:

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

                        0
    a stock1 velocity   3
    b stock1 velocity  12

E se eu quisesse dois valores + de um nível e um valor único (ou mais de 2) de outro nível:

    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

O método acima é provavelmente um pouco desajeitado, no entanto, achei que preenchia minhas necessidades e, como bônus, era mais fácil para eu entender e ler.

ra
fonte
2
Bom, não sabia sobre o levelargumento Index.isin!
cs95