5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

pandas.MultiIndex.levelsの挙動の注意点

Posted at

MultiIndexなSeriesDataFrameのオブジェクトは、slicingやgroupbyなどの処理を行いpandas.MultiIndex.levelsにアクセスすると、slicingなどを行う前のオブジェクトが保持している全indexを返すので注意が必要です。

ref: MultiIndex#defined-levels

The MultiIndex keeps all the defined levels of an index, even if they are not actually used. When slicing an index, you may notice this.
This is done to avoid a recomputation of the levels in order to make slicing highly performant. If you want to see only the used levels, you can use the get_level_values() method.

以下では中身についてもう少し掘り下げていきます。

version

Python 3.6.8
pandas: 0.25.1

pandas.MultiIndexの概要

MulltiIndexは階層的インデックスと呼ばれるもので、1つの行または列に対して複数のラベルが階層的に付けられているようなIndex objectです。

MultiIndex / advanced indexing — pandas 0.25.1 documentation

ipythonで実行

In [1]: import pandas as pd
   ...: import numpy as np
   ...:
   ...: arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'],
   ...:           ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]
   ...: tuples = list(zip(*arrays))
   ...: index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])
   ...: s = pd.Series(np.random.randn(8), index=index)
   ...: s
   ...:
Out[1]:
first  second
bar    one      -1.907102
       two      -0.743662
baz    one       1.116687
       two       0.113606
foo    one       0.100706
       two      -0.896215
qux    one       0.385674
       two       0.876058
dtype: float64

階層名がfirstである0番目の階層のindexの要素は(foo, bar, baz, qux)であり、階層名がsecondである1番目の階層のindexの要素は(one, two)であることが見た感じ分かるかと思います。階層的になっているindexを組み合わせて指定することで行(または列)をピックアップすることができます。

In [2]: s.loc[('bar', 'one')]
Out[2]: -1.907101843107428

In [3]: s.loc[('bar')]
Out[3]:
second
one   -1.907102
two   -0.743662
dtype: float64

indexの特定の階層で定義されているユニークなlabelには、pandas.MultiIndex.levelsでアクセスできます。

In [16]: s.index.levels
Out[16]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

。。。が、このlevelsへのアクセスは扱いに注意が必要なので後述するget_level_values()remove_unused_levels()を使ったほうが良さそうです。

pandas.MultiIndex.levelsの注意点

記事の冒頭で引用を貼りましたが、MultiIndexは実際に使われていないindexでも、一度定義されたindexのlevelsは保持されています。

Seriesをslicingして一部分だけを取り出したオブジェクトのindexを見ると、

In [13]: s[0:1]
Out[13]:
first  second
bar    one      -1.907102
dtype: float64

In [7]: s[0:1].index
Out[7]:
MultiIndex([('bar', 'one')],
           names=['first', 'second'])

のように切り出した部分にのみ使われているindexが返りますが、s[0:1].index.levelsにアクセスすると

In [9]: s[0:1].index.levels  # FrozenList([['bar'], ['one']] が帰ってきそうだが違う
Out[9]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

のように元のsが保持しているindexの一覧が返ってしまいます。パフォーマンスのためにこのような仕様になっているようですが、あまり直感的な挙動では無いと思うので気をつけてください。

documentにも書いてありますが、slicingしたオブジェクトの特定の階層のindexを取得したい場合はget_level_values()を、MultiIndexを再構成したい場合は remove_unused_levels()を使えばよいです。

In [53]: s[0:1].index.get_level_values(level='first')
Out[53]: Index(['bar'], dtype='object', name='first')

In [18]: s[0:1].index.remove_unused_levels().levels
Out[18]: FrozenList([['bar'], ['one']])

蛇足?

document内で言及している箇所は確認出来ませんでしたが、groupbyでも同様のことが起こります。

In [40]: group = s.groupby(level='first')

In [41]: group.get_group('baz')
Out[41]:
first  second
baz    one       1.116687
       two       0.113606
dtype: float64

In [42]: group.get_group('baz').index.levels
Out[42]: FrozenList([['bar', 'baz', 'foo', 'qux'], ['one', 'two']])

sliceと同じようなことをやってるので仕様を理解していれば予想できると思いますが、自分はしばらくハマってしまったので残しておきます。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?