Seleziona le righe in Panda MultiIndex DataFrame


147

Quali sono i metodi panda più comuni per selezionare / filtrare le righe di un dataframe il cui indice è un MultiIndex ?

  • Affettare in base a un singolo valore / etichetta
  • Affettatura basata su più etichette da uno o più livelli
  • Filtro in base a condizioni ed espressioni booleane
  • Quali metodi sono applicabili in quali circostanze

Presupposti per semplicità:

  1. il dataframe di input non ha chiavi di indice duplicate
  2. il dataframe di input di seguito ha solo due livelli. (La maggior parte delle soluzioni mostrate qui si generalizzano a N livelli)

Input di esempio:

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

Domanda 1: selezione di un singolo elemento

Come faccio a selezionare le righe che hanno "a" nel livello "uno"?

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

Inoltre, come potrei abbassare il livello "uno" nell'output?

     col
two     
t      0
u      1
v      2
w      3

Domanda 1b
Come affetto tutte le righe con valore "t" al livello "due"?

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

Domanda 2: selezione di più valori in un livello

Come posso selezionare le righe corrispondenti agli elementi "b" e "d" nel livello "uno"?

         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

Domanda 2b
Come posso ottenere tutti i valori corrispondenti a "t" e "w" nel livello "due"?

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

Domanda 3: Affettare una singola sezione trasversale (x, y)

Come si recupera una sezione trasversale, ovvero una singola riga con valori specifici per l'indice da df? Nello specifico, come faccio a recuperare la sezione trasversale di ('c', 'u'), data da

         col
one two     
c   u      9

Domanda 4: Affettare più sezioni trasversali [(a, b), (c, d), ...]

Come seleziono le due righe corrispondenti a ('c', 'u'), e ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Domanda 5: un oggetto affettato per livello

Come posso recuperare tutte le righe corrispondenti a "a" nel livello "uno" o "t" nel livello "due"?

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

Domanda 6: Slicing arbitrario

Come posso tagliare sezioni trasversali specifiche? Per "a" e "b", vorrei selezionare tutte le righe con sottolivelli "u" e "v", e per "d", vorrei selezionare le righe con sottolivello "w".

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

La domanda 7 utilizzerà una configurazione unica composta da un livello numerico:

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

Domanda 7: Filtraggio per disuguaglianza numerica sui singoli livelli del multiindice

Come ottengo tutte le righe in cui i valori nel livello "due" sono maggiori di 5?

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

Nota: questo post non spiegherà come creare MultiIndex, come eseguire operazioni di assegnazione su di essi o qualsiasi discussione relativa alle prestazioni (questi sono argomenti separati per un'altra volta).

Risposte:


166

MultiIndex / Indicizzazione avanzata

Nota
Questo post sarà strutturato nel modo seguente:

  1. Le domande poste nel PO verranno affrontate una per una
  2. Per ogni domanda, verranno dimostrati uno o più metodi applicabili per risolvere questo problema e ottenere il risultato atteso.

NotaVerranno incluse (molto simili a questa) per i lettori interessati a conoscere funzionalità aggiuntive, dettagli di implementazione e altre informazioni superficiali sull'argomento in questione. Queste note sono state compilate esaminando i documenti e scoprendo varie caratteristiche oscure e dalla mia esperienza (certamente limitata).

Tutti gli esempi di codice sono stati creati e testati su panda v0.23.4, python3.7 . Se qualcosa non è chiaro, o di fatto non è corretto, o se non hai trovato una soluzione applicabile al tuo caso d'uso, sentiti libero di suggerire una modifica, richiedere chiarimenti nei commenti o aprire una nuova domanda, .... a seconda dei casi .

Ecco un'introduzione ad alcuni idiomi comuni (d'ora in poi denominati i quattro idiomi) che visiteremo frequentemente

  1. DataFrame.loc- Una soluzione generale per la selezione per etichetta (+ pd.IndexSliceper applicazioni più complesse che coinvolgono sezioni)

  2. DataFrame.xs - Estrarre una particolare sezione trasversale da un Series / DataFrame.

  3. DataFrame.query- Specificare dinamicamente le operazioni di affettamento e / o filtro (ovvero, come un'espressione valutata dinamicamente. È più applicabile ad alcuni scenari rispetto ad altri. Vedere anche questa sezione della documentazione per eseguire query su MultiIndex.

  4. Indicizzazione booleana con una maschera generata utilizzando MultiIndex.get_level_values(spesso insieme a Index.isin, soprattutto quando si filtra con più valori). Questo è anche abbastanza utile in alcune circostanze.

Sarà utile esaminare i vari problemi di affettamento e filtraggio in termini di Quattro Idiomi per comprendere meglio cosa può essere applicato a una data situazione. È molto importante capire che non tutti gli idiomi funzioneranno ugualmente bene (se non del tutto) in ogni circostanza. Se un idioma non è stato elencato come una potenziale soluzione a un problema di seguito, significa che il linguaggio non può essere applicato efficacemente a quel problema.


Domanda 1

Come faccio a selezionare le righe che hanno "a" nel livello "uno"?

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

È possibile utilizzare loc, come soluzione generica applicabile alla maggior parte delle situazioni:

df.loc[['a']]

A questo punto, se ottieni

TypeError: Expected tuple, got str

Ciò significa che stai utilizzando una versione precedente dei panda. Considera l'aggiornamento! Altrimenti, usadf.loc[('a', slice(None)), :] .

In alternativa, puoi usare xsqui, poiché stiamo estraendo una singola sezione trasversale. Notare gli argomenti levelse axis(qui si possono assumere valori predefiniti ragionevoli).

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

Qui, l' drop_level=Falseargomento è necessario per evitare xsdi far cadere il livello "uno" nel risultato (il livello su cui abbiamo tagliato).

Ancora un'altra opzione qui sta usando query:

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

Se l'indice non aveva un nome, sarebbe necessario modificare la stringa della query in "ilevel_0 == 'a'".

Infine, utilizzando 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']

Inoltre, come potrei abbassare il livello "uno" nell'output?

     col
two     
t      0
u      1
v      2
w      3

Questo può essere fatto facilmente usando entrambi

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

O,

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

Notare che possiamo omettere l' drop_levelargomento (si presume che sia Truedi default).

Nota
È possibile notare che un DataFrame filtrato potrebbe avere ancora tutti i livelli, anche se non vengono visualizzati durante la stampa del DataFrame. Per esempio,

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

Puoi sbarazzarti di questi livelli 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'])

Domanda 1b

Come affetto tutte le righe con valore "t" al livello "due"?

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

Intuitivamente, vorresti qualcosa che coinvolgesse slice():

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

Funziona e basta! ™ Ma è goffo. Possiamo facilitare una sintassi di slicing più naturale utilizzando l' pd.IndexSliceAPI qui.

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

Questo è molto, molto più pulito.

Nota
Perché è necessaria la sezione finale :tra le colonne? Questo perché, locpuò essere utilizzato per selezionare e tagliare lungo entrambi gli assi ( axis=0o axis=1). Senza chiarire esplicitamente su quale asse deve essere eseguito lo slicing, l'operazione diventa ambigua. Vedere il grande riquadro rosso nella documentazione sull'affettatura .

Se vuoi rimuovere ogni sfumatura di ambiguità, locaccetta un axis parametro:

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

Senza il axisparametro (cioè, semplicemente facendo df.loc[pd.IndexSlice[:, 't']]), si presume che lo slicing si trovi sulle colonne e KeyErrorin questa circostanza verrà sollevato a.

Ciò è documentato nelle affettatrici . Ai fini di questo post, tuttavia, specificheremo esplicitamente tutti gli assi.

Con xs, lo è

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

Con query, lo è

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

E infine, con get_level_values, puoi farlo

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

Tutto allo stesso effetto.


Domanda 2

Come posso selezionare le righe corrispondenti agli elementi "b" e "d" nel livello "uno"?

         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, ciò viene fatto in modo simile specificando un elenco.

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

Per risolvere il problema di cui sopra di selezionare "b" e "d", puoi anche utilizzare 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
Sì, il parser predefinito è 'pandas', ma è importante evidenziare che questa sintassi non è convenzionalmente python. Il parser Pandas genera un albero di analisi leggermente diverso dall'espressione. Questo viene fatto per rendere alcune operazioni più intuitive da specificare. Per ulteriori informazioni, leggi il mio post sulla valutazione dell'espressione dinamica nei panda utilizzando pd.eval () .

E, con get_level_values+ Index.isin:

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

Domanda 2b

Come posso ottenere tutti i valori corrispondenti a "t" e "w" nel livello "due"?

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

Con loc, questo è possibile solo in combinazione con pd.IndexSlice.

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

Il primo due punti :in pd.IndexSlice[:, ['t', 'w']]significa tagliare attraverso il primo livello. Man mano che la profondità del livello su cui viene eseguita la query aumenta, sarà necessario specificare più sezioni, una per livello da suddividere. Tuttavia, non sarà necessario specificare più livelli oltre a quello da tagliare.

Con query, questo è

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

Con get_level_valuese Index.isin(simile a sopra):

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

Domanda 3

Come si recupera una sezione trasversale, ovvero una singola riga con valori specifici per l'indice da df? Nello specifico, come faccio a recuperare la sezione trasversale di ('c', 'u'), data da

         col
one two     
c   u      9

Utilizzare locspecificando una tupla di chiavi:

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

O,

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

Nota
A questo punto, potresti imbatterti in un PerformanceWarningsimile a questo:

PerformanceWarning: indexing past lexsort depth may impact performance.

Questo significa solo che il tuo indice non è ordinato. panda dipende dall'indice che viene ordinato (in questo caso, lessicograficamente, dato che si tratta di valori stringa) per una ricerca e un recupero ottimali. Una soluzione rapida sarebbe ordinare il tuo DataFrame in anticipo utilizzando DataFrame.sort_index. Ciò è particolarmente desiderabile dal punto di vista delle prestazioni se si prevede di eseguire più query di questo tipo in tandem:

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

È inoltre possibile utilizzare MultiIndex.is_lexsorted()per verificare se l'indice è ordinato o meno. Questa funzione restituisce Trueo di Falseconseguenza. È possibile chiamare questa funzione per determinare se è necessario o meno un passaggio di ordinamento aggiuntivo.

Con xs, questo è di nuovo semplicemente passare una singola tupla come primo argomento, con tutti gli altri argomenti impostati sui valori predefiniti appropriati:

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

Con query, le cose diventano un po 'goffe:

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

Ora puoi vedere che sarà relativamente difficile generalizzare. Ma è ancora OK per questo particolare problema.

Con accessi che si estendono su più livelli, get_level_valuespuò ancora essere utilizzato, ma non è consigliato:

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

Domanda 4

Come seleziono le due righe corrispondenti a ('c', 'u'), e ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Con loc, questo è ancora semplice come:

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

Con query, sarà necessario generare dinamicamente una stringa di query ripetendo le sezioni trasversali e i livelli:

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% NON CONSIGLIATO! Ma è possibile.


Domanda 5

Come posso recuperare tutte le righe corrispondenti a "a" nel livello "uno" o "t" nel livello "due"?

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

Questo è in realtà molto difficile da fare locgarantendo la correttezza e mantenendo la chiarezza del codice. df.loc[pd.IndexSlice['a', 't']]non è corretto, viene interpretato come df.loc[pd.IndexSlice[('a', 't')]](ovvero, selezione di una sezione trasversale). Potresti pensare a una soluzione pd.concatper gestire ciascuna etichetta separatamente:

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

Ma noterai che una delle righe è duplicata. Questo perché quella riga soddisfaceva entrambe le condizioni di taglio e quindi è apparsa due volte. Avrai invece bisogno di farlo

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

Ma se il tuo DataFrame contiene intrinsecamente indici duplicati (che desideri), questo non li manterrà. Utilizzare con estrema cautela .

Con query, questo è stupidamente semplice:

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

Con get_level_values, questo è ancora semplice, ma non così elegante:

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

Domanda 6

Come posso tagliare sezioni trasversali specifiche? Per "a" e "b", vorrei selezionare tutte le righe con sottolivelli "u" e "v", e per "d", vorrei selezionare le righe con sottolivello "w".

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

Questo è un caso speciale che ho aggiunto per aiutare a capire l'applicabilità dei Quattro Idiomi: questo è un caso in cui nessuno di essi funzionerà efficacemente, poiché lo slicing è molto specifico e non segue alcun modello reale.

Di solito, problemi di slicing come questo richiedono il passaggio esplicito di un elenco di chiavi a loc. Un modo per farlo è con:

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

Se vuoi salvare un po 'di digitazione, riconoscerai che esiste uno schema per affettare "a", "b" e i suoi sottolivelli, quindi possiamo separare l'attività di divisione in due parti e concatil risultato:

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

La specifica di sezionamento per "a" e "b" è leggermente più pulita (('a', 'b'), ('u', 'v'))perché gli stessi sottolivelli indicizzati sono gli stessi per ogni livello.


Domanda 7

Come ottengo tutte le righe in cui i valori nel livello "due" sono maggiori di 5?

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

Questo può essere fatto usando query,

df2.query("two > 5")

E get_level_values.

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

Nota
Simile a questo esempio, possiamo filtrare in base a qualsiasi condizione arbitraria utilizzando questi costrutti. In generale, è utile ricordarlo loce xssono specifici per l'indicizzazione basata su etichette, mentre querye get_level_valuessono utili per creare maschere condizionali generali per il filtraggio.


Domanda bonus

E se ho bisogno di suddividere una MultiIndex colonna ?

In realtà, la maggior parte delle soluzioni qui sono applicabili anche alle colonne, con piccole modifiche. Tener conto di:

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

Queste sono le seguenti modifiche che dovrai apportare ai Quattro modi di dire per farli funzionare con le colonne.

  1. Per affettare loc, usa

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

    o,

    df3.loc[:, pd.IndexSlice[...]]
  2. Per utilizzare xsin modo appropriato, è sufficiente passare un argomento axis=1.

  3. È possibile accedere direttamente ai valori a livello di colonna utilizzando df.columns.get_level_values. Dovrai quindi fare qualcosa di simile

    df.loc[:, {condition}] 

    Dove {condition}rappresenta una condizione costruita utilizzando columns.get_level_values.

  4. Per utilizzarlo query, l'unica opzione è trasporre, eseguire query sull'indice e trasporre di nuovo:

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

    Non consigliato, usa una delle altre 3 opzioni.


6

Recentemente mi sono imbattuto in un caso d'uso in cui avevo un dataframe multi-indice di livello 3+ in cui non potevo fare in modo che nessuna delle soluzioni sopra producesse i risultati che stavo cercando. È del tutto possibile che le soluzioni di cui sopra funzionino ovviamente per il mio caso d'uso e ne ho provate diverse, tuttavia non sono riuscito a farle funzionare con il tempo che avevo a disposizione.

Sono tutt'altro che esperto, ma mi sono imbattuto in una soluzione che non era elencata nelle risposte complete sopra. Non offro alcuna garanzia che le soluzioni siano in alcun modo ottimali.

Questo è un modo diverso per ottenere un risultato leggermente diverso dalla domanda n. 6 sopra. (e probabilmente anche altre domande)

Nello specifico stavo cercando:

  1. Un modo per scegliere due + valori da un livello dell'indice e un singolo valore da un altro livello dell'indice e
  2. Un modo per lasciare i valori di indice dell'operazione precedente nell'output del dataframe.

Come una chiave inglese negli ingranaggi (comunque totalmente riparabile):

  1. Gli indici erano senza nome.

Sul dataframe del giocattolo di seguito:

    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 i seguenti lavori, ovviamente:

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

        0
    a   3
    b  12

Ma volevo un risultato diverso, quindi il mio metodo per ottenere quel risultato era:

   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 volessi due + valori da un livello e un singolo (o 2+) valore da un altro livello:

    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

Il metodo sopra è probabilmente un po 'goffo, tuttavia ho trovato che soddisfaceva le mie esigenze e come bonus era più facile da capire e leggere.


2
Bello, non sapevo leveldell'argomento con Index.isin!
cs95
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.