1. ホーム
  2. パイソン

[解決済み】pandas MultiIndex DataFrameで行を選択する。

2022-04-02 17:11:22

質問

の行を選択/フィルタリングするための最も一般的なpandasの方法は何ですか? データフレームで、インデックスが MultiIndex ?

  • 単一の値/ラベルに基づくスライシング
  • 1つ以上の階層からの複数のラベルに基づくスライシング
  • ブーリアン条件や式によるフィルタリング
  • どのような状況でどのようなメソッドが適用されるか

わかりやすくするための前提条件。

  1. 入力データフレームが重複したインデックスキーを持たない。
  2. 下の入力データフレームは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:単品で選択する

レベルquot;one;quot;に"a"を持つ行を選択するにはどうすればよいですか?

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

さらに、出力でレベルquot;one"を落とすにはどうすればいいでしょうか?

     col
two     
t      0
u      1
v      2
w      3

質問1b

レベル 2 の値 "t" を持つすべての行をスライスするにはどうすればよいですか?

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

質問2:レベル内の複数値の選択

レベルquot;one;の項目quot;bとquot;dに対応する行を選択するにはどうすればよいですか?

         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

レベル2において、"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:1つの断面をスライスする場合 (x, y)

のインデックスに特定の値を持つ1つの行を取得するには、どうすればよいですか? df ? 具体的には、以下のような断面を取得することができます。 ('c', 'u') で与えられる。

         col
one two     
c   u      9

質問4:複数の断面をスライスする場合 [(a, b), (c, d), ...]

に対応する2つの行を選択するには、どうすればよいのでしょうか。 ('c', 'u')('a', 'w') ?

         col
one two     
c   u      9
a   w      3

問題5:1レベルにつき1つのアイテムスライス

レベル 1 の "a" またはレベル 2 の "t" に対応するすべての行を取得するには、どうすればよいですか。

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

質問6:任意のスライシング

特定の断面をスライスするにはどうしたらよいですか?aとbについては、サブレベルquot;uとquot;vを持つ行をすべて選択し、dについては、サブレベルquot;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:マルチインデックスの各レベルの数値不等式によるフィルタリング

レベル "2"の値が 5 より大きいすべての行を取得するにはどうすればよいですか?

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


注:この投稿は ではなく MultiIndexesの作成方法、MultiIndexesに対する割り当て操作の実行方法、およびパフォーマンスに関連する議論について説明します(これらは別のトピックとして別の機会に紹介します)。

解決するには?

マルチインデックス / アドバンストインデックス

備考

この記事は、以下のような構成になります。

  1. OPで提示された質問を一つずつ解決していきます。
  2. 各質問に対して、この問題を解決し、期待される結果を得るために適用できる方法を1つ以上示します。

備考 は、追加機能や実装の詳細について知りたい読者のために掲載されます(このようなものです)。 といった、ざっくりとした情報をお伝えします。これらのノートは ドキュメントを精査し、様々な不明な点を発見することで また、私自身の(確かに限られた)経験からも。

すべてのコードサンプルは、以下の環境で作成され、テストされています。 パンダ v0.23.4, Python3.7 . 何か不明な点、事実と異なる点がある場合、あるいは あなたのユースケースに適用できる解決策が見つかれば、遠慮なく 編集を提案したり、コメントで説明を求めたり、新しい という質問があります。

ここでは、よく使われるイディオム(以下、4つのイディオム)を紹介します。

  1. DataFrame.loc - ラベルによる選択の一般的な解決策 (+) pd.IndexSlice スライスを含むより複雑なアプリケーションの場合)

  2. DataFrame.xs - Series/DataFrameから特定の断面を抽出する。

  3. DataFrame.query - スライスやフィルタリングの操作を動的に指定する(動的に評価される式として指定する)。シナリオによっては、より適用しやすいものもある。以下も参照。 ドキュメントのこのセクション は、MultiIndexes のクエリに関するものです。

  4. を使用して生成されたマスクによるブールインデックス。 MultiIndex.get_level_values (しばしば Index.isin 特に複数の値でフィルタリングする場合)。これも状況によってはかなり有効です。

様々なスライシングとフィルタリングの問題を4つのイディオムで見ていくと、ある状況に何が適用できるかをよりよく理解できるようになります。すべてのイディオムがすべての状況で同じようにうまくいくわけではないことを理解することは非常に重要です。もし、あるイディオムが問題解決のためにリストアップされていない場合、そのイディオムはその問題に効果的に適用できないことを意味します。


<ブロッククオート

質問1

レベルquot;one"に"a"を持つ行を選択するにはどうすればよいですか?

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

を使用することができます。 loc ほとんどの状況に適用できる汎用的なソリューションとして。

df.loc[['a']]

このとき、もし

TypeError: Expected tuple, got str

それは、あなたが古いバージョンのpandasを使っていることを意味します。アップグレードを検討してください。そうでない場合は df.loc[('a', slice(None)), :] .

あるいは xs ここでは、1つの断面を抽出しているので なお levelsaxis 引数を指定します (ここでは妥当なデフォルトを仮定しています)。

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

ここで drop_level=False を防ぐために、引数が必要です。 xs をドロップすると、結果 (スライスしたレベル) のレベル "one" が低下します。

さらに、もう一つの方法として 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']

さらに、出力でレベルquot;1"を落とすにはどうすればいいでしょうか?

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


<ブロッククオート

質問1b

レベル 2 の値 "t" を持つすべての行をスライスするにはどうすればよいですか?

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

直感的には、以下のようなものが欲しいところです。 slice() :

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

It Just Works!™ しかし、これは不格好です。より自然なスライスの構文にするために pd.IndexSlice こちらのAPI

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

この方がずっと、すっきりしていますね。

<ブロッククオート

備考

なぜ、末尾のスライスが : は、列をまたいで必要なのでしょうか?これは、以下の理由からです。 loc は、両軸に沿った選択とスライスに使用することができます ( axis=0 または axis=1 ). どの軸でスライスを行うかを明示しないと を実行すると、操作が曖昧になります。の大きな赤枠をご覧ください。 スライシングに関するドキュメント .

曖昧さの影を消したい場合。 locaxis パラメータを指定します。

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

レベルquot;one;の項目quot;bとquot;dに対応する行を選択するにはどうしたらよいですか?

         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

locを使用して、リストを指定することで同様に行うことができます。

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()を使ったpandasの動的な式評価 .

と、いうことで get_level_values + Index.isin :

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


<ブロッククオート

質問2b

レベル2において、"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']] は、最初のレベルを横切るスライスを意味します。クエリされるレベルの深さが増すにつれて、スライスされるレベルごとに 1 つずつ、より多くのスライスを指定する必要があります。より多くのレベルを指定する必要はありません。 を超えて ただし、スライスされるのは1つだけです。

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_valuesIndex.isin (上記と同様)です。

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


<ブロッククオート

質問3

クロスセクション、つまり特定の値を持つ1行を取り出すには、どうすればよいでしょうか。 からのインデックスに対して 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.

これはインデックスがソートされていないことを意味します。pandasは最適な検索と取得のためにインデックスがソートされていること(この場合、文字列値を扱っているため辞書順)に依存しています。手っ取り早い解決策は を使用して、あらかじめDataFrameを作成します。 DataFrame.sort_index . これは、パフォーマンスの観点から、特に このようなクエリを複数回、同時に実行する。

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

を使用することもできます。 MultiIndex.is_lexsorted() を使用して、インデックスが がソートされているかどうか。この関数は True または False を使用します。 この関数は,追加のソートが必要かどうかを判断するために呼び出すことができます。 が必要である。

xs この場合も、最初の引数として1つのタプルを渡し、他の引数はすべて適切なデフォルトに設定されるだけです。

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

に対応する2行を選択するには、どうすればよいのでしょうか。 ('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%お勧めしません! しかし、それは可能です。

レベルが複数ある場合はどうすればよいですか?

このシナリオでは、1つのオプションとして droplevel を使って、チェックしないレベルを削除し、次に isin でメンバーシップをテストし、最終結果でブーリアンインデックスを行います。

df[df.index.droplevel(unused_level).isin([('c', 'u'), ('a', 'w')])]


<ブロッククオート

質問5

レベル 1 の "a" またはレベル 2 の "b" に対応するすべての行を取得するには、どうすればよいですか。 レベルquot;t"の場合は?

         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

しかし、1つの行が重複していることにお気づきでしょうか。これは、その行が両方のスライス条件を満たしているため、2回表示されているためです。その代わりに

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については、サブレベルquot;uとquot;vを持つ行をすべて選択したいのですが、quot;uとquot;vについては、サブレベルquot;uとquot;vを持つ行をすべて選択したいのですが、どうすればよいでしょうか。 については、サブレベル "w" を持つ行を選択したいと思います。

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

これは、4つのイディオムの適用性を理解するために加えた特殊なケースで、どのイディオムも効果的に機能しない場合です。 非常に があり、パターン化されていない。

通常、このようなスライスの問題では、キーのリストを明示的に loc . これを行う方法の1つは、以下の通りです。

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

入力の手間を省きたいなら、"a", "b" とそのサブレベルをスライスするパターンがあることがわかると思いますので、スライス作業を2つに分けて、次のようにします。 concat その結果

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

a"とb"のスライス指定が少しすっきりしました。 (('a', 'b'), ('u', 'v')) というのも、インデックス化されるサブレベルは各レベルで同じだからです。


<ブロッククオート

質問7

レベル "two" の値が 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]

備考

この例と同様に、これらの構文を使って、任意の条件に基づいてフィルタリングすることができる。一般に、以下のことを覚えておくと便利です。 locxs は特にラベルベースのインデックスを作成するためのもので、一方 queryget_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

4つのイディオムをカラムで動作させるために必要な変更は、以下の通りです。

  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つのオプションのいずれかを使用してください。