web-dev-qa-db-fra.com

Sélectionner des lignes dans pandas MultiIndex DataFrame

Objectif et motivation

L'API MultiIndex a gagné en popularité au fil des ans, mais tout n'est pas parfaitement compris en termes de structure, de fonctionnement et d'opérations associées.

Une opération importante est le filtrage . Le filtrage est une exigence courante, mais les cas d'utilisation sont divers. En conséquence, certaines méthodes et fonctions seront plus applicables à certains cas d'utilisation qu'à d'autres.

En résumé, l'objectif de cet article est de traiter de problèmes de filtrage courants et de cas d'utilisation, de démontrer différentes méthodes pour résoudre ces problèmes et de discuter de leur applicabilité. Certaines des questions de haut niveau que cet article cherche à aborder sont:

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

Ces problèmes ont été décomposés en 6 questions concrètes, énumérées ci-dessous. Par souci de simplicité, les exemples de DataFrames dans la configuration ci-dessous n'ont que deux niveaux et n'ont pas de clés d'index dupliquées. La plupart des solutions présentées aux problèmes peuvent généraliser à N niveaux.

Ce message ( non ne vous expliquera pas comment créer des multi-index, comment effectuer des opérations d'affectation sur ceux-ci ou toute discussion liée à la performance (ce sont des sujets séparés pour une autre fois.) ).


Des questions

La question 1-6 sera posée dans le contexte de la configuration ci-dessous.

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" dans le niveau "un"?

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

De plus, comment pourrais-je perdre 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 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 pourrais-je 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: Découper une seule coupe (x, y)
Comment récupérer une section transversale, c’est-à-dire une seule ligne ayant une valeur spécifique pour l’index de df? En particulier, comment récupérer la section transversale de ('c', 'u'), donnée par

         col
one two     
c   u      9

Question 4: Découper plusieurs sections [(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 article en tranches 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 puis-je couper des sections spécifiques? Pour "a" et "b", j'aimerais sélectionner toutes les lignes avec les sous-niveaux "u" et "v", et pour "d", j'aimerais 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 consistant en 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 basé sur les inégalités sur les niveaux numériques
Comment obtenir toutes les lignes où 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
61
cs95

Indexation MultiIndex/Advanced

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 présentées.

Remarque s (un peu comme celui-ci) sera inclus pour les lecteurs intéressés à en apprendre davantage sur les fonctionnalités supplémentaires, les détails de mise en œuvre et d'autres informations utiles au sujet traité . Ces notes ont été compilées en parcourant la documentation 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 à quelques idiomes communs (désormais appelés les quatre idiomes) que nous reverrons fréquemment

  1. DataFrame.loc - Une solution générale pour la sélection par libellé (+ pd.IndexSlice pour les applications plus complexes impliquant des tranches)

  2. DataFrame.xs - Extrait 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 de manière dynamique. S'applique davantage à certains scénarios. Voir aussi cette section de la documentation pour interroger sur MultiIndexes.

  4. Indexation booléenne avec un masque généré à l'aide de MultiIndex.get_level_values (souvent conjointement avec Index.isin, en particulier lors du 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 fonction des quatre expressions idiomatiques afin de 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 solution potentielle à un problème ci-dessous, cela signifie qu'il ne peut pas être appliqué efficacement à ce problème.


Question 1

Comment sélectionner des lignes ayant "un" dans le niveau "un"?

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

Vous pouvez utiliser loc comme solution polyvalente applicable à la plupart des situations:

_df.loc[['a']]
_

À ce stade, si vous obtenez

_TypeError: Expected Tuple, got str
_

Cela signifie que vous utilisez une version plus ancienne des pandas. Pensez à améliorer! Sinon, utilisez df.loc[('a', slice(None)), :].

Sinon, vous pouvez utiliser xs ici, car nous extrayons une seule section. Notez les arguments levels et axis (des valeurs raisonnables par défaut peuvent être supposées ici).

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

Ici, l'argument _drop_level=False_ est nécessaire pour empêcher xs de perdre le niveau "un" dans le résultat (le niveau sur lequel nous avons découpé).

Une autre option consiste à utiliser query:

_df.query("one == 'a'")
_

Si l'index n'avait pas de nom, vous devrez changer votre chaîne de requête pour qu'elle soit _"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 perdre 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’argument _drop_level_ (il est supposé être True par défaut).

Remarque
Vous remarquerez peut-être qu'un DataFrame filtré peut avoir encore tous les niveaux, même s'ils ne s'affichent 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'), :]
_

Ça fonctionne! ™ Mais c'est maladroit. Nous pouvons faciliter une syntaxe de découpage plus naturelle en utilisant l'API _pd.IndexSlice_ ici.

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

C'est beaucoup, beaucoup plus propre.

Remarque
Pourquoi la tranche de fin _:_ sur les colonnes est-elle requise? Cela est dû au fait que loc peut être utilisé pour sélectionner et découper le long des deux axes (_axis=0_ ou _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 le documentation sur le découpage .

Si vous souhaitez supprimer toute ambiguïté, loc accepte un paramètre axis:

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

Sans le paramètre axis (c'est-à-dire simplement en faisant _df.loc[pd.IndexSlice[:, 't']]_), le découpage en tranches est supposé être sur les colonnes et un KeyError sera déclenché dans cette circonstance.

Ceci est documenté dans 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_, vous pouvez faire

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

Tous dans le même sens.


Question 2

Comment puis-je sélectionner les lignes correspondant aux éléments "b" et "d" dans le 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 de sélection "b" et "d" ci-dessus, 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 conventionnelle en 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 Évaluation de l'expression dynamique dans pandas à l'aide de pd.eval () .

Et avec _get_level_values_ + _Index.isin_:

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

Question 2b

Comment pourrais-je 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 est possible seulement en conjonction avec _pd.IndexSlice_.

_df.loc[pd.IndexSlice[:, ['t', 'w']], :] 
_

Les deux premiers points _:_ sur _pd.IndexSlice[:, ['t', 'w']]_ correspondent au premier niveau. Au fur et à mesure que la profondeur du niveau interrogé augmente, vous devez spécifier plus de tranches, une par niveau étant découpées en tranches. Vous n'aurez pas besoin de spécifier plus de niveaux au-delà celui qui est découpé, cependant.

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_values_ et _Index.isin_ (comme 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 une valeur spécifique pour l'index à partir de df? Comment puis-je récupérer la section transversale de _('c', 'u')_ donnée par

_         col
one two     
c   u      9
_

Utilisez loc en 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 PerformanceWarning ) == qui 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, puisqu'il s'agit de valeurs de chaîne) pour une recherche et une récupération optimales. Une solution rapide consisterait à trier votre DataFrame à l’avance à l’aide de DataFrame.sort_index ). Ceci est particulièrement souhaitable du point de vue des performances si vous envisagez 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 True ou False en consé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 un seul tuple en tant que premier argument, tous les autres arguments étant 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 cela reste correct pour ce problème particulier.

Avec des accès couvrant plusieurs niveaux, _get_level_values_ peut 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, cela reste aussi simple que:

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

Avec query, vous devrez générer de manière dynamique une chaîne de requête en effectuant une itération 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 RECOMMANDE 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 avec loc tout en garantissant l'exactitude et tout 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'est-à-dire en sélectionnant une section transversale). Vous pouvez penser à une solution avec _pd.concat_ pour traiter 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 découpage et apparaissait donc deux fois. Vous aurez plutôt besoin de faire

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

Mais si votre DataFrame contient de manière inhérente des index en double (que vous voulez), cela ne les conservera pas. À utiliser avec une extrême prudence .

Avec query, c'est bêtement simple:

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

Avec _get_level_values_, cela reste simple, mais pas aussi élégant:

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

Question 6

Comment puis-je couper des sections spécifiques? Pour "a" et "b", j'aimerais sélectionner toutes les lignes avec les sous-niveaux "u" et "v", et pour "d", j'aimerais 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’entre eux ne fonctionnera efficacement, car le découpage en tranches est very spécifique. et ne suit aucun modèle réel.

Généralement, les problèmes de découpage comme celui-ci nécessitent 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 motif 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 concat le 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 nette _(('a', 'b'), ('u', 'v'))_ parce que les mêmes sous-niveaux indexés sont les mêmes pour chaque niveau.


Question 7

Comment puis-je obtenir toutes les lignes où 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
_

Ceci 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 utilisant ces constructions. En général, il est utile de rappeler que loc et xs conviennent spécifiquement à l'indexation basée sur des étiquettes, tandis que query et _get_level_values_ sont utiles pour la création de masques conditionnels généraux destinés au filtrage.


Question bonus

Que faire si j'ai besoin de découper un MultiIndex column ?

En fait, la plupart des solutions proposées 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
_

Ce sont les modifications suivantes que vous devrez apporter aux quatre idiomes pour les faire fonctionner avec des colonnes.

  1. Pour couper avec loc, utilisez

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

    Ou,

    _df3.loc[:, pd.IndexSlice[...]]
    _
  2. Pour utiliser xs comme il convient, il suffit de passer un argument _axis=1_.

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

    _df.loc[:, {condition}] 
    _

    Où _{condition}_ représente une condition générée à l'aide de _columns.get_level_values_.

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

    _df3.T.query(...).T
    _

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

62
cs95