web-dev-qa-db-ja.com

pandas MultiIndex DataFrameの行を選択します

目的と動機

MultiIndex AP​​Iは長年にわたって人気を集めていますが、構造、動作、および関連する操作に関して、そのすべてが完全に理解されているわけではありません。

1つの重要な操作はfilteringです。フィルタリングは一般的な要件ですが、ユースケースはさまざまです。したがって、特定の方法および機能は、他のユースケースよりもいくつかのユースケースに適用できます。

要約すると、この投稿の目的は、いくつかの一般的なフィルタリングの問題とユースケースに触れ、これらの問題を解決するためのさまざまな方法を示し、それらの適用可能性を議論することです。この投稿が対処しようとしている高レベルの質問のいくつかは

  • 単一の値/ラベルに基づいたスライス
  • 1つ以上のレベルからの複数のラベルに基づいたスライス
  • ブール条件と式でのフィルタリング
  • どの方法がどのような状況に適用可能か

これらの問題は、以下に列挙する6つの具体的な質問に分割されています。簡単にするために、以下のセットアップのサンプルDataFramesには2つのレベルのみがあり、重複するインデックスキーはありません。問題に対して提示されるほとんどのソリューションは、Nレベルに一般化できます。

この投稿では、notMultiIndexの作成方法、割り当て操作の実行方法、またはパフォーマンス関連のディスカッション(これらは別のトピックです) )。


ご質問

質問1-6は、以下の設定に関連して尋ねられます。

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:レベルで複数の値を選択する
レベル「one」の項目「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')('a', 'w')に対応する2つの行を選択するにはどうすればよいですか?

         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
61
cs95

MultiIndex/Advanced Indexing


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

  1. OPに記載された質問は、1つずつ対処されます
  2. 質問ごとに、この問題を解決して期待される結果を得るために適用可能な1つ以上の方法が示されます。

s(このようなもの)は、追加の機能、実装の詳細、その他の手近なトピックに関する情報に興味がある読者のために含まれます。 。これらのノートは、ドキュメントを精査し、さまざまな不明瞭な機能を明らかにすることによって、そして私自身の(明らかに制限された)経験から編集されました。

すべてのコードサンプルは、pandas 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

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

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

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

df.loc[['a']]

この時点で、

TypeError: Expected Tuple, got str

つまり、古いバージョンのパンダを使用しています。アップグレードを検討してください!それ以外の場合は、df.loc[('a', slice(None)), :]を使用します。

代わりに、ここではxsを使用できます。これは、単一の断面を抽出しているためです。 levelsおよびaxis引数に注意してください(ここでは適切なデフォルトを想定できます)。

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

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

ここでのもう1つのオプションは、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

これは、easilyまたは

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

それだけで機能します!™しかし、それは不格好です。ここで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が発生します。

これは slicers で文書化されています。ただし、この投稿では、すべての軸を明示的に指定します。

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

レベル「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

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と組み合わせて可能ですonly

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

:の最初のコロンpd.IndexSlice[:, ['t', 'w']]は、最初のレベルをスライスすることを意味します。照会されるレベルの深さが増すにつれて、スライスをレベルごとに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_valuesおよびIndex.isinを使用(上記と同様):

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

質問3

断面、つまり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.sort_index を使用してDataFrameをソートすることです。このようなクエリを複数同時に実行する予定がある場合、これはパフォーマンスの観点から特に望ましいものです。

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

MultiIndex.is_lexsorted() を使用して、インデックスがソートされているかどうかを確認することもできます。この関数は、それに応じてTrueまたはFalseを返します。この関数を呼び出して、追加の並べ替え手順が必要かどうかを判断できます。

xsを使用する場合、これも最初の引数として単一のTupleを渡し、他のすべての引数を適切なデフォルトに設定するだけです。

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

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

         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%はお勧めしません!しかし、それは可能です。


質問5

レベル「1」の「a」またはレベル「2」の「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') == 'c')
m2 = (df.index.get_level_values('two') == 'u')
df[m1 | m2]

質問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

これは、4つのイディオムの適用可能性を理解するために追加した特別なケースです。これは、スライスがvery固有であるため、どれも効果的に機能しない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」のスライス仕様は、(('a', 'b'), ('u', 'v'))の方がわずかにクリーンです。これは、インデックス付けされる同じサブレベルが各レベルで同じであるためです。


質問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はラベルベースのインデックス作成専用であり、queryget_level_valuesはフィルタリング用の一般的な条件付きマスクの作成に役立つことを覚えておくと便利です。


ボーナス質問

MultiIndexcolumnをスライスする必要がある場合はどうなりますか?

実際、ここでのほとんどのソリューションは列にも適用できますが、わずかな変更が加えられています。考慮してください:

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

62
cs95