pandas MultiIndex DataFrame の行を選択する 質問する

pandas MultiIndex DataFrame の行を選択する 質問する

パンダで行を選択/フィルタリングする最も一般的な方法は何ですか?インデックスが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: 単一のアイテムを選択する

レベル「1」に「a」がある行を選択するにはどうすればよいですか?

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

さらに、出力でレベル「1」を削除するにはどうすればよいでしょうか?

     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: レベル内で複数の値を選択する

レベル「1」の項目「b」と「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: 単一の断面をスライスする(x, y)

から断面、つまりインデックスに特定の値を持つ単一の行を取得するにはどうすればよいですか?具体的には、次のように与えられるdfの断面を取得するにはどうすればよいですか?('c', 'u')

         col
one two     
c   u      9

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

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

         col
one two     
c   u      9
a   w      3

質問5: レベルごとに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」については、サブレベル「u」と「v」を持つすべての行を選択し、「d」については、サブレベル「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

注: この投稿では、MultiIndex の作成方法、MultiIndex に対する割り当て操作の実行方法、パフォーマンス関連の説明については説明しません(これらは別のトピックであり、別の機会に説明します)。

ベストアンサー1

マルチインデックス / 高度なインデックス作成

注:
この投稿は次のように構成されます。

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

追加機能、実装の詳細、および現在のトピックに関するその他の情報について知りたい読者のために、メモ (これとよく似たもの) が含まれます。これらのメモは、ドキュメントを精査してさまざまな不明瞭な機能を発見し、私自身の (確かに限られた) 経験に基づいてまとめられています

すべてのコードサンプルは、pandas v0.23.4、python3.7で作成およびテストされています。不明瞭な点や事実誤認がある場合、またはユースケースに適用可能な解決策が見つからない場合は、お気軽に編集を提案したり、コメントで説明を要求したり、新しい質問を開始したりしてください。

ここでは、私たちが頻繁に使用する一般的な慣用句(以下、4つの慣用句と呼ぶ)を紹介します。

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

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

  3. DataFrame.query- スライスやフィルタリング操作を動的に指定します(つまり、動的に評価される式として指定します)。これは、他のシナリオよりも一部のシナリオに適しています。ドキュメントのこのセクションマルチインデックスのクエリ用。

  4. ブールインデックスとマスク生成MultiIndex.get_level_values(多くの場合、Index.isin(特に複数の値でフィルタリングする場合)。これは、状況によっては非常に便利です。

さまざまなスライスおよびフィルタリングの問題を 4 つのイディオムの観点から見ると、特定の状況に何を適用できるかをより深く理解するのに役立ちます。すべてのイディオムがあらゆる状況で同じように機能するわけではないことを理解することが非常に重要です。以下の問題に対する潜在的な解決策としてイディオムがリストされていない場合、そのイディオムはその問題に効果的に適用できないことを意味します。


質問1

レベル「1」に「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ここでは単一の断面を抽出しているので、 を使用することもできます。levelsおよびaxis引数に注意してください (ここでは適切なデフォルトを想定できます)。

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

ここで、結果のレベル「1」(スライスしたレベル) がドロップされないようdrop_level=Falseにするために引数が必要です。xs

ここでのもう一つのオプションは、以下を使用することです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']

さらに、出力でレベル「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

Note
You may notice that a filtered DataFrame may still have all the levels, even if they do not show when printing the DataFrame out. For example,

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

You can get rid of these levels using 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

How do I slice all rows with value "t" on level "two"?

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

Intuitively, you would want something involving slice():

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

It Just Works!™ But it is clunky. We can facilitate a more natural slicing syntax using the pd.IndexSlice API here.

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

This is much, much cleaner.

Note
Why is the trailing slice : across the columns required? This is because, loc can be used to select and slice along both axes (axis=0 or axis=1). Without explicitly making it clear which axis the slicing is to be done on, the operation becomes ambiguous. See the big red box in the documentation on slicing.

If you want to remove any shade of ambiguity, loc accepts an axis parameter:

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

Without the axis parameter (i.e., just by doing df.loc[pd.IndexSlice[:, 't']]), slicing is assumed to be on the columns, and a KeyError will be raised in this circumstance.

This is documented in slicers. For the purpose of this post, however, we will explicitly specify all axes.

With xs, it is

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

With query, it is

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

And finally, with get_level_values, you may do

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

All to the same effect.


Question 2

How can I select rows corresponding to items "b" and "d" in level "one"?

         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

Using loc, this is done in a similar fashion by specifying a list.

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

To solve the above problem of selecting "b" and "d", you can also use 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')

Note
Yes, the default parser is 'pandas', but it is important to highlight this syntax isn't conventionally python. The Pandas parser generates a slightly different parse tree from the expression. This is done to make some operations more intuitive to specify. For more information, please read my post on Dynamic Expression Evaluation in pandas using pd.eval().

And, with get_level_values + Index.isin:

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

Question 2b

How would I get all values corresponding to "t" and "w" in level "two"?

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

With loc, this is possible only in conjuction with pd.IndexSlice.

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

The first colon : in pd.IndexSlice[:, ['t', 'w']] means to slice across the first level. As the depth of the level being queried increases, you will need to specify more slices, one per level being sliced across. You will not need to specify more levels beyond the one being sliced, however.

With query, this is

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

With get_level_values and Index.isin (similar to above):

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

Question 3

How do I retrieve a cross section, i.e., a single row having a specific values for the index from df? Specifically, how do I retrieve the cross section of ('c', 'u'), given by

         col
one two     
c   u      9

Use loc by specifying a tuple of keys:

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

Or,

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

Note
At this point, you may run into a PerformanceWarning that looks like this:

PerformanceWarning: indexing past lexsort depth may impact performance.

This just means that your index is not sorted. pandas depends on the index being sorted (in this case, lexicographically, since we are dealing with string values) for optimal search and retrieval. A quick fix would be to sort your DataFrame in advance using DataFrame.sort_index. This is especially desirable from a performance standpoint if you plan on doing multiple such queries in tandem:

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

You can also use MultiIndex.is_lexsorted() to check whether the index is sorted or not. This function returns True or False accordingly. You can call this function to determine whether an additional sorting step is required or not.

With xs, this is again simply passing a single tuple as the first argument, with all other arguments set to their appropriate defaults:

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

With query, things become a bit clunky:

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

You can see now that this is going to be relatively difficult to generalize. But is still OK for this particular problem.

With accesses spanning multiple levels, get_level_values can still be used, but is not recommended:

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

Question 4

How do I select the two rows corresponding to ('c', 'u'), and ('a', 'w')?

         col
one two     
c   u      9
a   w      3

With loc, this is still as simple as:

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

With query, you will need to dynamically generate a query string by iterating over your cross sections and levels:

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% DO NOT RECOMMEND! But it is possible.

What if I have multiple levels?
One option in this scenario would be to use droplevel to drop the levels you're not checking, then use isin to test membership, and then boolean index on the final result.

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

Question 5

How can I retrieve all rows corresponding to "a" in level "one" or "t" in level "two"?

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

This is actually very difficult to do with loc while ensuring correctness and still maintaining code clarity. df.loc[pd.IndexSlice['a', 't']] is incorrect, it is interpreted as df.loc[pd.IndexSlice[('a', 't')]] (i.e., selecting a cross section). You may think of a solution with pd.concat to handle each label separately:

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

But you'll notice one of the rows is duplicated. This is because that row satisfied both slicing conditions, and so appeared twice. You will instead need to do

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

But if your DataFrame inherently contains duplicate indices (that you want), then this will not retain them. Use with extreme caution.

With query, this is stupidly simple:

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

With get_level_values, this is still simple, but not as elegant:

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

Question 6

How can I slice specific cross sections? For "a" and "b", I would like to select all rows with sub-levels "u" and "v", and for "d", I would like to select rows with sub-level "w".

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

これは、4 つのイディオムの適用性を理解しやすくするために追加した特殊なケースです。スライスが非常に特殊であり、実際のパターンに従っていないため、4 つのイディオムのいずれも効果的に機能しないケースの 1 つです。

通常、このようなスライスの問題では、キーのリストを に明示的に渡す必要があります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'), ('u', 'v'))「a」と「b」のスライス仕様は、インデックス付けされる同じサブレベルが各レベルで同じであるため、少しきれいになります。


質問7

レベル「2」の値が 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便利queryですget_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 つのオプションのいずれかを使用してください。

おすすめ記事