Sélectionnez des lignes dans pandas MultiIndex DataFrame

147

Quelles sont les méthodes pandas les plus courantes pour sélectionner / filtrer les lignes d'un dataframe dont l'index est un MultiIndex ?

  • Tranchage basé sur une seule valeur / étiquette
  • Tranchage basé sur plusieurs étiquettes d'un ou plusieurs niveaux
  • Filtrage sur des conditions et expressions booléennes
  • Quelles méthodes sont applicables dans quelles circonstances

Hypothèses de simplicité:

  1. la trame de données d'entrée n'a pas de clés d'index en double
  2. la trame de données d'entrée ci-dessous n'a que deux niveaux. (La plupart des solutions présentées ici se généralisent à N niveaux)

Exemple d'entrée:

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

Question 1: Sélection d'un seul élément

Comment sélectionner des lignes ayant «un» au niveau «un»?

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

De plus, comment pourrais-je supprimer le niveau «un» dans la sortie?

     col
two     
t      0
u      1
v      2
w      3

Question 1b
Comment découper toutes les lignes avec la valeur "t" au niveau "deux"?

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

Question 2: Sélection de plusieurs valeurs dans un niveau

Comment puis-je sélectionner les lignes correspondant aux éléments «b» et «d» du niveau «un»?

         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

Question 2b
Comment obtenir toutes les valeurs correspondant à "t" et "w" au niveau "deux"?

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

Question 3: Trancher une seule section transversale (x, y)

Comment puis-je récupérer une section transversale, c'est-à-dire une seule ligne ayant des valeurs spécifiques pour l'index df? Plus précisément, comment puis-je récupérer la section transversale de ('c', 'u'), donnée par

         col
one two     
c   u      9

Question 4: Découpage de plusieurs sections transversales [(a, b), (c, d), ...]

Comment sélectionner les deux lignes correspondant à ('c', 'u'), et ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Question 5: Un élément découpé par niveau

Comment puis-je récupérer toutes les lignes correspondant à "a" au niveau "un" ou "t" au niveau "deux"?

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

Question 6: Découpage arbitraire

Comment découper des sections transversales spécifiques? Pour "a" et "b", je voudrais sélectionner toutes les lignes avec les sous-niveaux "u" et "v", et pour "d", je voudrais sélectionner les lignes avec le sous-niveau "w".

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

La question 7 utilisera une configuration unique composée d'un niveau numérique:

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

Question 7: Filtrage par inégalité numérique aux niveaux individuels du multiindex

Comment obtenir toutes les lignes dont les valeurs du niveau «deux» sont supérieures à 5?

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

Remarque: Cet article ne vous expliquera pas comment créer des MultiIndex, comment effectuer des opérations d'attribution sur eux, ou des discussions liées aux performances (ce sont des sujets séparés pour une autre fois).

cs95
la source

Réponses:

166

Indexation multi-index / avancée

Remarque
Ce message sera structuré de la manière suivante:

  1. Les questions posées dans le PO seront abordées une par une
  2. Pour chaque question, une ou plusieurs méthodes applicables pour résoudre ce problème et obtenir le résultat attendu seront démontrées.

Des notes (un peu comme celle-ci) seront incluses pour les lecteurs intéressés à en savoir plus sur les fonctionnalités supplémentaires, les détails de mise en œuvre et d'autres informations superficielles sur le sujet en question. Ces notes ont été compilées en parcourant les documents et en découvrant diverses caractéristiques obscures, et à partir de ma propre expérience (certes limitée).

Tous les exemples de code ont été créés et testés sur pandas v0.23.4, python3.7 . Si quelque chose n'est pas clair, ou factuellement incorrect, ou si vous n'avez pas trouvé de solution applicable à votre cas d'utilisation, n'hésitez pas à suggérer une modification, demander des éclaircissements dans les commentaires ou ouvrir une nouvelle question, .... selon le cas .

Voici une introduction à certains idiomes courants (désormais appelés les quatre idiomes) que nous reviendrons fréquemment

  1. DataFrame.loc- Une solution générale de sélection par étiquette (+ pd.IndexSlicepour les applications plus complexes impliquant des tranches)

  2. DataFrame.xs - Extraire une section transversale particulière d'un Series / DataFrame.

  3. DataFrame.query- Spécifiez les opérations de découpage et / ou de filtrage de manière dynamique (c'est-à-dire sous la forme d'une expression évaluée dynamiquement. Est plus applicable à certains scénarios qu'à d'autres. Consultez également cette section de la documentation pour les requêtes sur les MultiIndexes.

  4. Indexation booléenne avec un masque généré à l'aide de MultiIndex.get_level_values(souvent en conjonction avec Index.isin, notamment lors d'un filtrage avec plusieurs valeurs) Ceci est également très utile dans certaines circonstances.

Il sera utile d'examiner les différents problèmes de découpage et de filtrage en termes des quatre idiomes pour mieux comprendre ce qui peut être appliqué à une situation donnée. Il est très important de comprendre que tous les idiomes ne fonctionneront pas aussi bien (voire pas du tout) dans toutes les circonstances. Si un idiome n'a pas été répertorié comme une solution potentielle à un problème ci-dessous, cela signifie que l'idiome ne peut pas être appliqué efficacement à ce problème.


question 1

Comment sélectionner des lignes ayant «un» au niveau «un»?

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

Vous pouvez utiliser loc, comme solution à usage général applicable à la plupart des situations:

df.loc[['a']]

À ce stade, si vous obtenez

TypeError: Expected tuple, got str

Cela signifie que vous utilisez une ancienne version de pandas. Pensez à mettre à niveau! Sinon, utilisez df.loc[('a', slice(None)), :].

Vous pouvez également utiliser xsici, car nous extrayons une seule section transversale. Notez les arguments levelset axis(des valeurs par défaut raisonnables peuvent être prises ici).

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

Ici, l' drop_level=Falseargument est nécessaire pour éviter xsde laisser tomber le niveau "un" dans le résultat (le niveau sur lequel nous avons découpé).

Encore une autre option ici consiste à utiliser query:

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

Si l'index n'avait pas de nom, vous devrez changer votre chaîne de requête en "ilevel_0 == 'a'".

Enfin, en utilisant 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']

De plus, comment pourrais-je supprimer le niveau «un» dans la sortie?

     col
two     
t      0
u      1
v      2
w      3

Cela peut être facilement fait en utilisant soit

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

Notez que nous pouvons omettre l' drop_levelargument (il est supposé être Truepar défaut).

Remarque
Vous pouvez remarquer qu'un DataFrame filtré peut toujours avoir tous les niveaux, même s'ils n'apparaissent pas lors de l'impression du DataFrame. Par exemple,

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

Vous pouvez vous débarrasser de ces niveaux en utilisant 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'])

Question 1b

Comment découper toutes les lignes avec la valeur "t" au niveau "deux"?

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

Intuitivement, vous voudriez quelque chose impliquant slice():

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

It Just Works! ™ Mais c'est maladroit. Nous pouvons faciliter une syntaxe de découpage plus naturelle en utilisant l' pd.IndexSliceAPI ici.

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

C'est beaucoup, beaucoup plus propre.

Remarque
Pourquoi la tranche de fin :dans les colonnes est-elle requise? En effet, locpeut être utilisé pour sélectionner et découper le long des deux axes ( axis=0ou axis=1). Sans préciser explicitement sur quel axe le découpage doit être effectué, l'opération devient ambiguë. Voir la grande boîte rouge dans la documentation sur le tranchage .

Si vous souhaitez supprimer toute nuance d'ambiguïté, locaccepte un axis paramètre:

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

Sans le axisparamètre (c'est-à-dire simplement en faisant df.loc[pd.IndexSlice[:, 't']]), le découpage est supposé être sur les colonnes, et a KeyErrorsera déclenché dans ce cas.

Ceci est documenté dans les slicers . Pour les besoins de cet article, cependant, nous spécifierons explicitement tous les axes.

Avec xs, c'est

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

Avec query, c'est

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

Et enfin, avec get_level_values, tu peux faire

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

Tout cela dans le même sens.


question 2

Comment puis-je sélectionner les lignes correspondant aux éléments «b» et «d» du niveau «un»?

         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

En utilisant loc, cela se fait de la même manière en spécifiant une liste.

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

Pour résoudre le problème ci-dessus de sélection de «b» et «d», vous pouvez également utiliser 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')

Remarque
Oui, l'analyseur par défaut est 'pandas', mais il est important de souligner que cette syntaxe n'est pas conventionnellement python. L'analyseur Pandas génère un arbre d'analyse légèrement différent de l'expression. Ceci est fait pour rendre certaines opérations plus intuitives à spécifier. Pour plus d'informations, veuillez lire mon article sur l'évaluation des expressions dynamiques dans les pandas en utilisant pd.eval () .

Et, avec get_level_values+ Index.isin:

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

Question 2b

Comment obtenir toutes les valeurs correspondant à «t» et «w» au niveau «deux»?

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

Avec loc, cela n'est possible qu'en conjonction avec pd.IndexSlice.

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

Le premier colon :dans les pd.IndexSlice[:, ['t', 'w']]moyens à couper en tranches à travers le premier niveau. À mesure que la profondeur du niveau interrogé augmente, vous devrez spécifier plus de tranches, une par niveau étant découpée. Cependant, vous n'aurez pas besoin de spécifier plus de niveaux au - delà de celui qui est découpé.

Avec query, c'est

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

Avec get_level_valueset Index.isin(similaire à ci-dessus):

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

question 3

Comment puis-je récupérer une section transversale, c'est-à-dire une seule ligne ayant des valeurs spécifiques pour l'index df? Plus précisément, comment puis-je récupérer la section transversale de ('c', 'u'), donnée par

         col
one two     
c   u      9

À utiliser locen spécifiant un tuple de clés:

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

Ou,

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

Remarque
À ce stade, vous pouvez rencontrer un PerformanceWarningqui ressemble à ceci:

PerformanceWarning: indexing past lexsort depth may impact performance.

Cela signifie simplement que votre index n'est pas trié. pandas dépend de l'index en cours de tri (dans ce cas, lexicographiquement, car nous avons affaire à des valeurs de chaîne) pour une recherche et une récupération optimales. Une solution rapide serait de trier votre DataFrame à l'avance en utilisant DataFrame.sort_index. Ceci est particulièrement souhaitable du point de vue des performances si vous prévoyez d'effectuer plusieurs requêtes de ce type en tandem:

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

Vous pouvez également utiliser MultiIndex.is_lexsorted()pour vérifier si l'index est trié ou non. Cette fonction renvoie Trueou en Falseconséquence. Vous pouvez appeler cette fonction pour déterminer si une étape de tri supplémentaire est requise ou non.

Avec xs, il s'agit à nouveau de passer simplement un seul tuple comme premier argument, avec tous les autres arguments définis sur leurs valeurs par défaut appropriées:

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

Avec query, les choses deviennent un peu maladroites:

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

Vous pouvez voir maintenant que cela va être relativement difficile à généraliser. Mais est toujours OK pour ce problème particulier.

Avec des accès couvrant plusieurs niveaux, get_level_valuespeut toujours être utilisé, mais n'est pas recommandé:

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

Question 4

Comment sélectionner les deux lignes correspondant à ('c', 'u'), et ('a', 'w')?

         col
one two     
c   u      9
a   w      3

Avec loc, c'est toujours aussi simple que:

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

Avec query, vous devrez générer dynamiquement une chaîne de requête en itérant sur vos sections et niveaux:

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% NE RECOMMANDEZ PAS! Mais c'est possible.


Question 5

Comment puis-je récupérer toutes les lignes correspondant à "a" au niveau "un" ou "t" au niveau "deux"?

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

C'est en fait très difficile à faire loctout en garantissant l'exactitude et en maintenant la clarté du code. df.loc[pd.IndexSlice['a', 't']]est incorrect, il est interprété comme df.loc[pd.IndexSlice[('a', 't')]](c.-à-d. sélectionner une section transversale). Vous pouvez penser à une solution pd.concatpour gérer chaque étiquette séparément:

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

Mais vous remarquerez qu'une des lignes est dupliquée. En effet, cette ligne remplissait les deux conditions de tranchage et apparaissait ainsi deux fois. Vous devrez plutôt faire

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

Mais si votre DataFrame contient intrinsèquement des index en double (que vous voulez), cela ne les conservera pas. Utilisez avec une extrême prudence .

Avec query, c'est stupidement simple:

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

Avec get_level_values, c'est toujours simple, mais pas aussi élégant:

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

Question 6

Comment découper des sections transversales spécifiques? Pour "a" et "b", je voudrais sélectionner toutes les lignes avec les sous-niveaux "u" et "v", et pour "d", je voudrais sélectionner les lignes avec le sous-niveau "w".

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

C'est un cas spécial que j'ai ajouté pour aider à comprendre l'applicabilité des quatre idiomes - c'est un cas où aucun d'eux ne fonctionnera efficacement, car le découpage est très spécifique et ne suit aucun modèle réel.

Habituellement, des problèmes de découpage comme celui-ci nécessiteront de passer explicitement une liste de clés à loc. Une façon de faire est avec:

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

Si vous souhaitez enregistrer un peu de frappe, vous reconnaîtrez qu'il existe un modèle pour découper "a", "b" et ses sous-niveaux, afin que nous puissions séparer la tâche de découpage en deux parties et concatle résultat:

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

La spécification de découpage pour "a" et "b" est légèrement plus claire (('a', 'b'), ('u', 'v'))car les mêmes sous-niveaux indexés sont les mêmes pour chaque niveau.


Question 7

Comment obtenir toutes les lignes dont les valeurs du niveau «deux» sont supérieures à 5?

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

Cela peut être fait en utilisant query,

df2.query("two > 5")

Et get_level_values.

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

Remarque
Semblable à cet exemple, nous pouvons filtrer en fonction de toute condition arbitraire en utilisant ces constructions. En général, il est utile de s'en souvenir locet xssont spécifiquement pour l'indexation basée sur des étiquettes, tandis que queryet get_level_valuessont utiles pour créer des masques conditionnels généraux pour le filtrage.


Question bonus

Et si j'ai besoin de découper une MultiIndex colonne ?

En fait, la plupart des solutions ici s'appliquent également aux colonnes, avec des modifications mineures. Considérer:

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

Voici les modifications suivantes que vous devrez apporter aux quatre expressions idiomatiques pour qu'elles fonctionnent avec des colonnes.

  1. Pour trancher loc, utilisez

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

    ou,

    df3.loc[:, pd.IndexSlice[...]]
  2. Pour utiliser xsle cas échéant, passez simplement un argument axis=1.

  3. Vous pouvez accéder aux valeurs au niveau de la colonne directement à l'aide de df.columns.get_level_values. Vous devrez alors faire quelque chose comme

    df.loc[:, {condition}] 

    {condition}représente une condition construite en utilisant columns.get_level_values.

  4. Pour l'utiliser query, votre seule option est de transposer, d'interroger sur l'index et de transposer à nouveau:

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

    Non recommandé, utilisez l'une des 3 autres options.

cs95
la source
6

Récemment, je suis tombé sur un cas d'utilisation dans lequel j'avais une trame de données multi-index de niveau 3+ dans laquelle je ne pouvais pas faire en sorte que l'une des solutions ci-dessus produise les résultats que je recherchais. Il est fort possible que les solutions ci-dessus fonctionnent bien sûr pour mon cas d'utilisation, et j'en ai essayé plusieurs, mais je n'ai pas pu les faire fonctionner avec le temps dont je disposais.

Je suis loin d'être un expert, mais je suis tombé sur une solution qui ne figurait pas dans les réponses complètes ci-dessus. Je n'offre aucune garantie que les solutions sont en aucune façon optimales.

C'est une façon différente d'obtenir un résultat légèrement différent de la question 6 ci-dessus. (et probablement d'autres questions aussi)

Plus précisément, je cherchais:

  1. Un moyen de choisir deux + valeurs d'un niveau de l'index et une seule valeur d'un autre niveau de l'index, et
  2. Un moyen de laisser les valeurs d'index de l'opération précédente dans la sortie de la trame de données.

En tant que clé à molette dans les engrenages (cependant totalement réparable):

  1. Les index n'étaient pas nommés.

Sur la base de données du jouet ci-dessous:

    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

En utilisant les travaux ci-dessous, bien sûr:

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

        0
    a   3
    b  12

Mais je voulais un résultat différent, donc ma méthode pour obtenir ce résultat était:

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

                        0
    a stock1 velocity   3
    b stock1 velocity  12

Et si je voulais deux valeurs + d'un niveau et une valeur unique (ou 2+) d'un autre niveau:

    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

La méthode ci-dessus est probablement un peu maladroite, mais j'ai trouvé qu'elle répondait à mes besoins et en prime, elle était plus facile à comprendre et à lire.

ra
la source
2
Nice, je ne savais pas à propos de l' levelargument de Index.isin!
cs95